PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.83
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.83
51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 All 39 releases
← All changes | includes/Core.php +2508 -0 51.1.251.1.83 View file →
@@ -1,0 +1,2508 @@
1 +<?php
2 +
3 +/**
4 + * Core class do all things at the start of the plugin
5 + */
6 +
7 +namespace King_Addons;
8 +
9 +use Elementor\Plugin;
10 +use Elementor\Widgets_Manager;
11 +use Elementor\Controls_Manager;
12 +use Elementor\Group_Control_Image_Size;
13 +use King_Addons\Wishlist\Wishlist_Module;
14 +
15 +if (!defined('ABSPATH')) {
16 + exit; // Exit if accessed directly.
17 +}
18 +
19 +final class Core
20 +{
21 + /**
22 + * Instance
23 + *
24 + * @var Core|null The single instance of the class.
25 + */
26 + private static ?Core $_instance = null;
27 +
28 + /**
29 + * Wishlist module instance.
30 + *
31 + * @var Wishlist_Module|null
32 + */
33 + private ?Wishlist_Module $wishlist_module = null;
34 +
35 + /**
36 + * Instance
37 + *
38 + * Ensures only one instance of the class is loaded or can be loaded.
39 + *
40 + * @return Core An instance of the class.
41 + * @since 1.0.0
42 + */
43 + public static function instance(): Core
44 + {
45 + if (is_null(self::$_instance)) {
46 + self::$_instance = new self();
47 + }
48 + return self::$_instance;
49 + }
50 +
51 + /**
52 + * Check if an extension is enabled.
53 + *
54 + * Checks database options first, defaults to enabled if not set (new installations).
55 + * Falls back to constant check for backward compatibility.
56 + *
57 + * @param string $extension_id The extension ID (e.g., 'templates-catalog', 'popup-builder').
58 + * @param string $constant_name The constant name to check as fallback (e.g., 'KING_ADDONS_EXT_POPUP_BUILDER').
59 + * @return bool True if extension is enabled, false otherwise.
60 + * @since 1.0.0
61 + */
62 + private function isExtensionEnabled(string $extension_id, string $constant_name): bool
63 + {
64 + // Dependency checks (extensions that require other plugins).
65 + if ($extension_id === 'woo-builder' && (!class_exists('WooCommerce') || !function_exists('WC'))) {
66 + return false;
67 + }
68 +
69 + // Get options from database
70 + $options = get_option('king_addons_options', []);
71 +
72 + // If a constant is defined and explicitly false, treat it as a hard disable.
73 + // This is useful for extensions that are in development and should not be
74 + // available/visible even if the database option is enabled.
75 + if ($constant_name !== '' && defined($constant_name) && constant($constant_name) === false) {
76 + return false;
77 + }
78 +
79 + // Check if option exists in database
80 + $option_key = 'ext_' . $extension_id;
81 + if (isset($options[$option_key])) {
82 + // Option exists, use its value
83 + return $options[$option_key] === 'enabled';
84 + }
85 +
86 + // Option doesn't exist (new installation), default to enabled
87 + // But also check constant as fallback for backward compatibility
88 + if ($constant_name !== '' && defined($constant_name)) {
89 + return constant($constant_name);
90 + }
91 +
92 + // Default to enabled for new installations
93 + return true;
94 + }
95 +
96 + /**
97 + * Constructor
98 + *
99 + * Perform some compatibility checks to make sure basic requirements are meet.
100 + * If all compatibility checks pass, initialize the functionality.
101 + *
102 + * @since 1.0.0
103 + */
104 + public function __construct()
105 + {
106 + require_once(KING_ADDONS_PATH . 'includes/ModulesMap.php');
107 + require_once(KING_ADDONS_PATH . 'includes/LibrariesMap.php');
108 + require_once(KING_ADDONS_PATH . 'includes/helpers/Global/Text_Entities_Migration.php');
109 + Text_Entities_Migration::boot();
110 +
111 + if ($this->hasElementorCompatibility()) {
112 +
113 + // Initial requirements check
114 + require_once(KING_ADDONS_PATH . 'includes/helpers/Check_Requirements/Check_Requirements.php');
115 +
116 + // Templates Catalog
117 + if ($this->isExtensionEnabled('templates-catalog', 'KING_ADDONS_EXT_TEMPLATES_CATALOG')) {
118 + require_once(KING_ADDONS_PATH . 'includes/TemplatesMap.php');
119 + require_once(KING_ADDONS_PATH . 'includes/extensions/Templates/CollectionsMap.php');
120 + require_once(KING_ADDONS_PATH . 'includes/extensions/Templates/Templates.php');
121 +
122 + // Template Catalog Button for Elementor Editor
123 + require_once(KING_ADDONS_PATH . 'includes/extensions/Template_Catalog_Button/Template_Catalog_Button.php');
124 + Template_Catalog_Button::instance();
125 + }
126 +
127 + // Header & Footer Builder
128 + if ($this->isExtensionEnabled('header-footer-builder', 'KING_ADDONS_EXT_HEADER_FOOTER_BUILDER')) {
129 + require_once(KING_ADDONS_PATH . 'includes/extensions/Header_Footer_Builder/Header_Footer_Builder.php');
130 + Header_Footer_Builder::instance();
131 + }
132 +
133 + // Popup Builder
134 + if ($this->isExtensionEnabled('popup-builder', 'KING_ADDONS_EXT_POPUP_BUILDER')) {
135 + require_once(KING_ADDONS_PATH . 'includes/extensions/Popup_Builder/Popup_Builder.php');
136 + Popup_Builder::instance();
137 + }
138 +
139 + // Cookie / Consent Bar
140 + if ($this->isExtensionEnabled('cookie-consent', 'KING_ADDONS_EXT_COOKIE_CONSENT')) {
141 + require_once(KING_ADDONS_PATH . 'includes/extensions/Cookie_Consent/Cookie_Consent.php');
142 + Cookie_Consent::instance();
143 + }
144 +
145 + // Dynamic Tags
146 + // Loaded before the builders so their widgets can offer dynamic
147 + // values as soon as the editor asks Elementor for the tag list.
148 + if ($this->isExtensionEnabled('dynamic-tags', 'KING_ADDONS_EXT_DYNAMIC_TAGS')) {
149 + require_once(KING_ADDONS_PATH . 'includes/extensions/Dynamic_Tags/Dynamic_Tags.php');
150 + if (class_exists('King_Addons\\Dynamic_Tags')) {
151 + new Dynamic_Tags();
152 + }
153 + }
154 +
155 + // Loop Builder
156 + if ($this->isExtensionEnabled('loop-builder', 'KING_ADDONS_EXT_LOOP_BUILDER')) {
157 + require_once(KING_ADDONS_PATH . 'includes/extensions/Loop_Builder/Loop_Builder.php');
158 + if (class_exists('King_Addons\\Loop_Builder')) {
159 + new Loop_Builder();
160 + }
161 + }
162 +
163 + // WooCommerce Builder
164 + if ($this->isExtensionEnabled('woo-builder', 'KING_ADDONS_EXT_WOO_BUILDER')) {
165 + require_once(KING_ADDONS_PATH . 'includes/extensions/Woo_Builder/Woo_Builder.php');
166 + if (class_exists('King_Addons\\Woo_Builder')) {
167 + new Woo_Builder();
168 + }
169 + }
170 +
171 + // Sticky Contact Bar
172 + if ($this->isExtensionEnabled('sticky-contact-bar', 'KING_ADDONS_EXT_STICKY_CONTACT_BAR')) {
173 + require_once(KING_ADDONS_PATH . 'includes/extensions/Sticky_Contact_Bar/Sticky_Contact_Bar.php');
174 +
175 + $pro_loaded = false;
176 + if (
177 + function_exists('king_addons_freemius')
178 + && king_addons_freemius()->can_use_premium_code__premium_only()
179 + && defined('KING_ADDONS_PRO_PATH')
180 + ) {
181 + $pro_file_path = KING_ADDONS_PRO_PATH . 'includes/extensions/Sticky_Contact_Bar_Pro/Sticky_Contact_Bar_Pro.php';
182 + if (file_exists($pro_file_path)) {
183 + require_once $pro_file_path;
184 + if (class_exists('King_Addons\\Sticky_Contact_Bar_Pro')) {
185 + new Sticky_Contact_Bar_Pro();
186 + $pro_loaded = true;
187 + }
188 + }
189 + }
190 +
191 + if (!$pro_loaded && class_exists('King_Addons\\Sticky_Contact_Bar')) {
192 + new Sticky_Contact_Bar();
193 + }
194 + }
195 +
196 + // Theme Builder
197 + if ($this->isExtensionEnabled('theme-builder', 'KING_ADDONS_EXT_THEME_BUILDER')) {
198 + require_once(KING_ADDONS_PATH . 'includes/extensions/Theme_Builder/Theme_Builder.php');
199 +
200 + $pro_loaded = false;
201 + if (
202 + function_exists('king_addons_freemius')
203 + && king_addons_freemius()->can_use_premium_code__premium_only()
204 + && defined('KING_ADDONS_PRO_PATH')
205 + ) {
206 + $pro_file_path = KING_ADDONS_PRO_PATH . 'includes/extensions/Theme_Builder_Pro/Theme_Builder_Pro.php';
207 + if (file_exists($pro_file_path)) {
208 + require_once $pro_file_path;
209 + if (class_exists('King_Addons\\Theme_Builder_Pro')) {
210 + new Theme_Builder_Pro();
211 + $pro_loaded = true;
212 + }
213 + }
214 + }
215 +
216 + if (!$pro_loaded && class_exists('King_Addons\\Theme_Builder')) {
217 + new Theme_Builder();
218 + }
219 + }
220 +
221 + // Custom Cursor
222 + if ($this->isExtensionEnabled('custom-cursor', 'KING_ADDONS_EXT_CUSTOM_CURSOR')) {
223 + require_once(KING_ADDONS_PATH . 'includes/extensions/Custom_Cursor/Custom_Cursor.php');
224 + if (class_exists('King_Addons\\Custom_Cursor')) {
225 + new Custom_Cursor();
226 + }
227 + }
228 +
229 + // Age Gate
230 + if ($this->isExtensionEnabled('age-gate', 'KING_ADDONS_EXT_AGE_GATE')) {
231 + require_once(KING_ADDONS_PATH . 'includes/extensions/Age_Gate/Age_Gate.php');
232 + if (defined('KING_ADDONS_PRO_PATH')) {
233 + $age_gate_pro = KING_ADDONS_PRO_PATH . 'includes/extensions/Age_Gate_Pro/Age_Gate_Pro.php';
234 + if (file_exists($age_gate_pro)) {
235 + require_once $age_gate_pro;
236 + }
237 + }
238 + Age_Gate::instance();
239 + }
240 +
241 + // Live Chat & Support Builder
242 + if ($this->isExtensionEnabled('live-chat', 'KING_ADDONS_EXT_LIVE_CHAT')) {
243 + require_once(KING_ADDONS_PATH . 'includes/extensions/Live_Chat/Live_Chat.php');
244 + Live_Chat::instance();
245 + }
246 +
247 + // Docs & Knowledge Base
248 + if ($this->isExtensionEnabled('docs-kb', 'KING_ADDONS_EXT_DOCS_KB')) {
249 + require_once(KING_ADDONS_PATH . 'includes/extensions/Docs_KB/Docs_KB.php');
250 + Docs_KB::instance();
251 + }
252 +
253 + // Pricing Table Builder
254 + if ($this->isExtensionEnabled('pricing-table-builder', 'KING_ADDONS_EXT_PRICING_TABLE_BUILDER')) {
255 + require_once(KING_ADDONS_PATH . 'includes/extensions/Pricing_Table_Builder/Pricing_Table_Builder.php');
256 + Pricing_Table_Builder::instance();
257 + }
258 +
259 + // Custom Code Manager
260 + if ($this->isExtensionEnabled('custom-code-manager', 'KING_ADDONS_EXT_CUSTOM_CODE_MANAGER')) {
261 + require_once(KING_ADDONS_PATH . 'includes/extensions/Custom_Code_Manager/Custom_Code_Manager.php');
262 + Custom_Code_Manager::getInstance();
263 + }
264 +
265 + // Fomo Notifications
266 + if ($this->isExtensionEnabled('fomo-notifications', 'KING_ADDONS_EXT_FOMO_NOTIFICATIONS')) {
267 + require_once(KING_ADDONS_PATH . 'includes/extensions/Fomo_Notifications/Fomo_Notifications.php');
268 + Fomo_Notifications::instance();
269 + }
270 +
271 + // Smart Links
272 + if ($this->isExtensionEnabled('smart-links', 'KING_ADDONS_EXT_SMART_LINKS')) {
273 + require_once(KING_ADDONS_PATH . 'includes/extensions/Smart_Links/Smart_Links.php');
274 + \King_Addons\Smart_Links\Smart_Links::instance();
275 + }
276 +
277 + // Activity Log
278 + if ($this->isExtensionEnabled('activity-log', 'KING_ADDONS_EXT_ACTIVITY_LOG')) {
279 + require_once(KING_ADDONS_PATH . 'includes/extensions/Activity_Log/Activity_Log.php');
280 + \King_Addons\Activity_Log\Activity_Log::instance();
281 + }
282 +
283 + // Maintenance Mode
284 + if ($this->isExtensionEnabled('maintenance-mode', 'KING_ADDONS_EXT_MAINTENANCE_MODE')) {
285 + require_once(KING_ADDONS_PATH . 'includes/extensions/Maintenance_Mode/Maintenance_Mode.php');
286 + \King_Addons\Maintenance_Mode\Maintenance_Mode::instance();
287 + }
288 +
289 + // Data Table Builder
290 + if ($this->isExtensionEnabled('table-builder', 'KING_ADDONS_EXT_TABLE_BUILDER')) {
291 + require_once(KING_ADDONS_PATH . 'includes/extensions/Data_Table_Builder/Data_Table_Builder.php');
292 + Data_Table_Builder::instance();
293 + }
294 +
295 + // Site Preloader Animation
296 + if ($this->isExtensionEnabled('site-preloader', 'KING_ADDONS_EXT_SITE_PRELOADER')) {
297 + require_once(KING_ADDONS_PATH . 'includes/extensions/Site_Preloader/Site_Preloader.php');
298 + Site_Preloader::instance();
299 + }
300 +
301 + // Free Shipping Bar
302 + if ($this->isExtensionEnabled('free-shipping-bar', 'KING_ADDONS_EXT_FREE_SHIPPING_BAR')) {
303 + require_once(KING_ADDONS_PATH . 'includes/extensions/Free_Shipping_Bar/Free_Shipping_Bar.php');
304 + if (class_exists('King_Addons\\Free_Shipping_Bar')) {
305 + Free_Shipping_Bar::instance();
306 + }
307 + }
308 +
309 + // Sticky Add To Cart
310 + if ($this->isExtensionEnabled('sticky-add-to-cart', 'KING_ADDONS_EXT_STICKY_ADD_TO_CART')) {
311 + require_once(KING_ADDONS_PATH . 'includes/extensions/Sticky_Add_To_Cart/Sticky_Add_To_Cart.php');
312 + if (class_exists('King_Addons\\Sticky_Add_To_Cart')) {
313 + Sticky_Add_To_Cart::instance();
314 + }
315 + }
316 +
317 + // Image Optimizer
318 + if ($this->isExtensionEnabled('image-optimizer', 'KING_ADDONS_EXT_IMAGE_OPTIMIZER')) {
319 + require_once(KING_ADDONS_PATH . 'includes/extensions/Image_Optimizer/Image_Optimizer.php');
320 + \King_Addons\Image_Optimizer\Image_Optimizer::instance();
321 + }
322 +
323 + // Admin
324 + require_once(KING_ADDONS_PATH . 'includes/Admin.php');
325 +
326 + // Rating Notice (admin only)
327 + // Temporarily disabled
328 + // if (is_admin()) {
329 + // require_once(KING_ADDONS_PATH . 'includes/admin/notices/RatingNotice.php');
330 + // \King_Addons\Admin\Notices\RatingNotice::instance();
331 + // }
332 +
333 + // Additional - Controls
334 + require_once(KING_ADDONS_PATH . 'includes/controls/Ajax_Select2/Ajax_Select2.php');
335 + require_once(KING_ADDONS_PATH . 'includes/controls/Ajax_Select2/Ajax_Select2_API.php');
336 + require_once(KING_ADDONS_PATH . 'includes/controls/Animations/Animations.php');
337 + require_once(KING_ADDONS_PATH . 'includes/controls/Animations/Button_Animations.php');
338 +
339 + // Additional - Widgets
340 + require_once(KING_ADDONS_PATH . 'includes/widgets/Search/Search_Ajax.php');
341 + require_once(KING_ADDONS_PATH . 'includes/widgets/MailChimp/MailChimp_Ajax.php');
342 +
343 + // Additional - Grids, Magazine Grid
344 + require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Grid_Ajax_Security.php');
345 + require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Filter_Posts_Ajax.php');
346 + require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Filter_WooCommerce_Products_Ajax.php');
347 + require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Post_Likes_Ajax.php');
348 +
349 + // Additional - Form Builder
350 + if (KING_ADDONS_WGT_FORM_BUILDER) {
351 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Extra_Fields.php');
352 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Conditional_Logic.php');
353 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Spam_Protection.php');
354 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Integrations.php');
355 + new Form_Integrations();
356 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Partial_Entries.php');
357 + new Form_Partial_Entries();
358 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Formula.php');
359 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Payments.php');
360 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Payment_Confirm.php');
361 + new Form_Payments();
362 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Form_Builder_Security.php');
363 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Create_Submission.php');
364 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Send_Email.php');
365 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Send_Webhook.php');
366 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Subscribe_Mailchimp.php');
367 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Update_Action_Meta.php');
368 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Upload_Email_File.php');
369 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Verify_Google_Recaptcha.php');
370 + require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/View_Submissions_Pro.php');
371 + }
372 +
373 + // ADDITIONAL CLASSES
374 +
375 + // AI SEO Tools (Alt Text Generator + Auto Tagging)
376 + if ($this->isExtensionEnabled('ai-seo-tools', 'KING_ADDONS_EXT_AI_SEO_TOOLS')) {
377 + require_once(KING_ADDONS_PATH . 'includes/extensions/AI_SEO_Tools/AI_SEO_Tools.php');
378 + if (class_exists('King_Addons\\AI_SEO_Tools\\AI_SEO_Tools')) {
379 + \King_Addons\AI_SEO_Tools\AI_SEO_Tools::instance();
380 + }
381 + }
382 +
383 + // Wishlist module - check extension toggle
384 + if ($this->isExtensionEnabled('wishlist', 'KING_ADDONS_EXT_WISHLIST')) {
385 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_DB.php';
386 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Session.php';
387 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Settings.php';
388 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Service.php';
389 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Renderer.php';
390 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Frontend.php';
391 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_WooCommerce.php';
392 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Module.php';
393 + $this->wishlist_module = new Wishlist_Module();
394 + }
395 +
396 + // Dynamic Posts Grid AJAX Helper - Initialize regardless of Elementor compatibility
397 + // This is needed for AJAX functionality to work even when PRO version is disabled
398 + require_once(KING_ADDONS_PATH . 'includes/helpers/Dynamic_Posts_Grid_Ajax.php');
399 + \King_Addons\Dynamic_Posts_Grid_Ajax::get_instance();
400 +
401 + // Screenshot Generator
402 + // require_once(KING_ADDONS_PATH . 'includes/extensions/Templates/screenshot-generator.php');
403 + // require_once(KING_ADDONS_PATH . 'includes/extensions/Templates/screenshot-admin-page.php');
404 + // new King_Addons\KingAddons\ScreenshotAdmin();
405 +
406 + // END: ADDITIONAL CLASSES
407 +
408 + self::enableWidgetsByDefault();
409 +
410 + add_action('elementor/init', [$this, 'initElementor']);
411 +
412 + add_action('elementor/elements/categories_registered', [$this, 'addWidgetCategory']);
413 + // Elementor promotes Atomic / Custom Widget / Pro after that hook, so the
414 + // panel order is applied again once categories are fully initialised.
415 + add_action('elementor/init', [$this, 'reorderWidgetCategoriesLate'], 100);
416 + add_action('elementor/editor/before_enqueue_scripts', [$this, 'reorderWidgetCategoriesLate'], 1);
417 + add_filter('elementor/document/config', [$this, 'filterDocumentPanelCategories'], 1, 2);
418 + add_action('elementor/controls/controls_registered', [$this, 'registerControls']);
419 +
420 + self::enableFeatures();
421 +
422 + // Load and register AJAX handlers for Login Register Form widget
423 + require_once(KING_ADDONS_PATH . 'includes/widgets/Login_Register_Form/Login_Register_Form_Ajax.php');
424 + require_once(KING_ADDONS_PATH . 'includes/widgets/Login_Register_Form/User_Profile_Fields.php');
425 + require_once(KING_ADDONS_PATH . 'includes/widgets/Login_Register_Form/Email_Handler.php');
426 + require_once(KING_ADDONS_PATH . 'includes/widgets/Login_Register_Form/Social_Login_Handler.php');
427 + add_action('wp_ajax_nopriv_king_addons_user_login', ['King_Addons\Widgets\Login_Register_Form\Login_Register_Form_Ajax', 'handle_login_ajax']);
428 + add_action('wp_ajax_king_addons_user_login', ['King_Addons\Widgets\Login_Register_Form\Login_Register_Form_Ajax', 'handle_login_ajax']);
429 + add_action('wp_ajax_nopriv_king_addons_user_register', ['King_Addons\Widgets\Login_Register_Form\Login_Register_Form_Ajax', 'handle_register_ajax']);
430 + add_action('wp_ajax_king_addons_user_register', ['King_Addons\Widgets\Login_Register_Form\Login_Register_Form_Ajax', 'handle_register_ajax']);
431 + add_action('wp_ajax_nopriv_king_addons_user_lostpassword', ['King_Addons\Widgets\Login_Register_Form\Login_Register_Form_Ajax', 'handle_lostpassword_ajax']);
432 + add_action('wp_ajax_king_addons_user_lostpassword', ['King_Addons\Widgets\Login_Register_Form\Login_Register_Form_Ajax', 'handle_lostpassword_ajax']);
433 +
434 + // Initialize user profile fields
435 + \King_Addons\Widgets\Login_Register_Form\User_Profile_Fields::init();
436 +
437 + // Initialize social login handler
438 + \King_Addons\Widgets\Login_Register_Form\Social_Login_Handler::init();
439 +
440 + // Initialize Security Dashboard for admins (only if Login Register Form widget is enabled)
441 + if (is_admin()) {
442 + $widget_options = get_option('king_addons_options', []);
443 + $login_form_enabled = !isset($widget_options['login-register-form']) || $widget_options['login-register-form'] === 'enabled';
444 + if ($login_form_enabled) {
445 + require_once(KING_ADDONS_PATH . 'includes/widgets/Login_Register_Form/Security_Dashboard.php');
446 + \King_Addons\Widgets\Login_Register_Form\Security_Dashboard::init();
447 + }
448 + }
449 +
450 + new Admin();
451 +
452 + add_action('wp_enqueue_scripts', [$this, 'enqueueFrontendStyles']);
453 + add_action('wp_enqueue_scripts', [$this, 'enqueueLightboxDynamicStyles']);
454 +
455 + // Notice - Upgrade Suggestion
456 + if (!king_addons_freemius()->can_use_premium_code__premium_only()) {
457 + add_action('wp_ajax_king_addons_premium_notice_dismiss', [$this, 'king_addons_premium_notice_dismiss_callback']);
458 +
459 + /**
460 + * The previous notice is kept untouched in showNoticeUpgrade() as a fallback.
461 + * To go back to it, either define KING_ADDONS_UPGRADE_NOTICE_LEGACY as true
462 + * in wp-config.php, or return true from this filter.
463 + */
464 + $use_legacy_notice = defined('KING_ADDONS_UPGRADE_NOTICE_LEGACY')
465 + ? (bool) KING_ADDONS_UPGRADE_NOTICE_LEGACY
466 + : false;
467 + $use_legacy_notice = (bool) apply_filters('king_addons/upgrade_notice/use_legacy', $use_legacy_notice);
468 +
469 + add_action(
470 + 'admin_notices',
471 + [$this, $use_legacy_notice ? 'showNoticeUpgrade' : 'showNoticeUpgradeV2']
472 + );
473 + }
474 +
475 + // Dashboard UI settings AJAX handler
476 + add_action('wp_ajax_king_addons_save_dashboard_ui', [$this, 'king_addons_save_dashboard_ui_callback']);
477 +
478 + // Conditionally enqueue AI text-field enhancement script and styles in Elementor editor
479 + $ai_options = get_option('king_addons_ai_options', []);
480 + $enable_ai_text_buttons = isset($ai_options['enable_ai_buttons']) ? (bool) $ai_options['enable_ai_buttons'] : true;
481 + if ($enable_ai_text_buttons) {
482 + // Enqueue AI text-field enhancement script
483 + add_action('elementor/editor/after_enqueue_scripts', [$this, 'enqueueAiFieldScript']);
484 + // Enqueue styles for AI prompt UI
485 + add_action('elementor/editor/after_enqueue_styles', [$this, 'enqueueAiFieldStyles']);
486 + // Enqueue AI page translator script
487 + add_action('elementor/editor/after_enqueue_scripts', [$this, 'enqueueAiTranslatorScript']);
488 + }
489 +
490 + $enable_ai_image_generation_button = isset($ai_options['enable_ai_image_generation_button']) ? (bool) $ai_options['enable_ai_image_generation_button'] : true;
491 + if ($enable_ai_image_generation_button) {
492 + add_action('elementor/editor/after_enqueue_scripts', [$this, 'enqueueAiImageGenerationScript']);
493 + // Enqueue styles for AI Image Generation controls
494 + add_action('elementor/editor/after_enqueue_styles', [$this, 'enqueueAiImageFieldStyles']);
495 + }
496 + }
497 + }
498 +
499 + function king_addons_premium_notice_dismiss_callback()
500 + {
501 + // Check user capabilities
502 + if (!current_user_can('manage_options')) {
503 + wp_die();
504 + }
505 +
506 + check_ajax_referer('king_addons_premium_notice_dismiss', 'nonce');
507 +
508 + $user_id = get_current_user_id();
509 + // Save the current time as the last dismissal time for the premium notice
510 + update_user_meta($user_id, 'king_addons_premium_notice_dismissed_time', time());
511 + wp_die(); // End AJAX request
512 + }
513 +
514 + /**
515 + * AJAX callback for saving dashboard UI settings (theme, view toggle)
516 + *
517 + * @since 1.0.0
518 + */
519 + function king_addons_save_dashboard_ui_callback()
520 + {
521 + // Check user capabilities
522 + if (!current_user_can('manage_options')) {
523 + wp_send_json_error(['message' => 'Unauthorized'], 403);
524 + }
525 +
526 + check_ajax_referer('king_addons_dashboard_ui', 'nonce');
527 +
528 + $key = isset($_POST['key']) ? sanitize_text_field($_POST['key']) : '';
529 +
530 + $user_id = get_current_user_id();
531 +
532 + // Validate key
533 + $allowed_keys = ['dark_theme', 'theme_mode', 'show_descriptions'];
534 + if (!in_array($key, $allowed_keys, true)) {
535 + wp_send_json_error(['message' => 'Invalid key'], 400);
536 + }
537 +
538 + // Theme preference is per-user.
539 + if ($key === 'theme_mode') {
540 + $mode = isset($_POST['value']) ? sanitize_key($_POST['value']) : '';
541 + $allowed_modes = ['dark', 'light', 'auto'];
542 + if (!in_array($mode, $allowed_modes, true)) {
543 + wp_send_json_error(['message' => 'Invalid theme mode'], 400);
544 + }
545 +
546 + update_user_meta($user_id, 'king_addons_theme_mode', $mode);
547 +
548 + // Also store as a global option so pages can fall back when user meta isn't set.
549 + update_option('king_addons_theme_mode', $mode);
550 +
551 + wp_send_json_success(['key' => $key, 'value' => $mode]);
552 + }
553 +
554 + // Backward compatibility: old boolean dark_theme maps to theme_mode.
555 + if ($key === 'dark_theme') {
556 + $is_dark = isset($_POST['value']) && $_POST['value'] === '1';
557 + $mode = $is_dark ? 'dark' : 'light';
558 + update_user_meta($user_id, 'king_addons_theme_mode', $mode);
559 + wp_send_json_success(['key' => 'theme_mode', 'value' => $mode]);
560 + }
561 +
562 + // Remaining UI settings are still stored as site option (shared).
563 + $value = isset($_POST['value']) && $_POST['value'] === '1';
564 + $settings = get_option('king_addons_dashboard_ui', []);
565 + $settings[$key] = $value;
566 + update_option('king_addons_dashboard_ui', $settings);
567 +
568 + wp_send_json_success(['key' => $key, 'value' => $value]);
569 + }
570 +
571 + function showNoticeUpgrade()
572 + {
573 + // Check user capabilities; show notice only to administrators as an example
574 + if (!current_user_can('manage_options')) {
575 + return;
576 + }
577 +
578 + $user_id = get_current_user_id();
579 + $now = time();
580 + // Retrieve the last time the premium notice was dismissed by the user
581 + $last_dismissed = get_user_meta($user_id, 'king_addons_premium_notice_dismissed_time', true);
582 +
583 + // If the premium notice was dismissed less than a week ago (604800 seconds), do not show it
584 + if ($last_dismissed && ($now - $last_dismissed) < 604800) {
585 + // if ($last_dismissed && ($now - $last_dismissed) < 60) {
586 + return;
587 + }
588 + ?>
589 + <div class="king-addons-upgrade-notice notice notice-info is-dismissible"
590 + style="border-left: 4px solid #0071e3;padding: 10px 15px;">
591 + <p style="font-size: 15px; margin:0; display: flex; align-items: center;">
592 + <span>
593 + Get <strong style="font-weight: 700;">4,000+</strong> premium templates and sections,
594 + <strong style="font-weight: 700;">80+</strong> widgets,
595 + <strong style="font-weight: 700;">200+</strong> advanced features,
596 + and AI tools for Elementor.
597 + From $<strong style="font-weight: 700;">4</strong>/mo, billed annually.
598 +</span>
599 + </p>
600 + <p style="font-size: 14px; opacity: 0.6;">Trusted by 20,000+ users</p>
601 + <p style="display: flex;">
602 + <a href="https://kingaddons.com/pricing?utm_source=kng-notice-offer&amp;utm_medium=plugin&amp;utm_campaign=kng" target="_blank" class="ka-wb-btn ka-wb-btn-primary" style="
603 + background: #0071e3;
604 + color: #fff;
605 + display: inline-flex;
606 + align-items: center;
607 + justify-content: center;
608 + gap: 6px;
609 + padding: 10px 18px;
610 + font-size: 14px;
611 + font-weight: 500;
612 + text-decoration: none;
613 + border-radius: 980px;
614 + border: none;
615 + cursor: pointer;
616 + transition: all 0.3s cubic-bezier(0.25, 1, 0.5, 1);
617 + white-space: nowrap;
618 + font-family: inherit;
619 +">Upgrade to Pro<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="
620 + width: 16px;
621 + height: 16px;
622 +"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
623 + </a>
624 + <a style="margin-left: 20px;display: flex;align-items: center;font-size: 14px;color: #0071e3;"
625 + href="https://kingaddons.com/pricing?utm_source=kng-notice-offer&utm_medium=plugin&utm_campaign=kng"
626 + class="link">Learn More</a>
627 + </p>
628 + </div>
629 + <script>
630 + (function ($) {
631 + // Wait for the document to be ready
632 + const kingAddonsPremiumNoticeNonce = '<?php echo esc_js(wp_create_nonce('king_addons_premium_notice_dismiss')); ?>';
633 + $(document).ready(function () {
634 + // Attach click handler to the dismiss button of the premium notice
635 + $('.king-addons-upgrade-notice.notice.is-dismissible').on('click', '.notice-dismiss', function () {
636 + $.post(ajaxurl, {
637 + action: 'king_addons_premium_notice_dismiss',
638 + nonce: kingAddonsPremiumNoticeNonce
639 + });
640 + });
641 + });
642 + })(jQuery);
643 + </script>
644 + <?php
645 + }
646 +
647 + /**
648 + * Upgrade notice, current design.
649 + *
650 + * Replaces showNoticeUpgrade(), which is kept above unchanged as a fallback —
651 + * see the KING_ADDONS_UPGRADE_NOTICE_LEGACY constant and the
652 + * king_addons/upgrade_notice/use_legacy filter.
653 + *
654 + * @return void
655 + */
656 + function showNoticeUpgradeV2()
657 + {
658 + if (!current_user_can('manage_options')) {
659 + return;
660 + }
661 +
662 + $user_id = get_current_user_id();
663 + $now = time();
664 + $last_dismissed = get_user_meta($user_id, 'king_addons_premium_notice_dismissed_time', true);
665 +
666 + // Stay hidden for a week after the notice is dismissed.
667 + if ($last_dismissed && ($now - $last_dismissed) < WEEK_IN_SECONDS) {
668 + return;
669 + }
670 +
671 + $pricing_url = 'https://kingaddons.com/pricing/?utm_source=kng-notice-offer&utm_medium=plugin&utm_campaign=kng';
672 + // Same page, different utm_source: the two links stay separately
673 + // measurable, which the previous notice could not do because both
674 + // carried kng-notice-offer.
675 + $explore_url = 'https://kingaddons.com/pricing/?utm_source=kng-notice-explore&utm_medium=plugin&utm_campaign=kng';
676 + ?>
677 + <style>
678 + .king-addons-upgrade-notice-v2 {
679 + border-left: 4px solid #5B03FF;
680 + padding: 18px 20px 18px 22px;
681 + }
682 +
683 + .king-addons-upgrade-notice-v2 .king-addons-un-main {
684 + /* Admin notices run the full width of the screen, so a second
685 + column always ends up marooned in empty space. Everything
686 + stays in one left-aligned stack instead, with the price sitting
687 + in the action row where the decision is made. */
688 + display: flex;
689 + flex-direction: column;
690 + gap: 10px;
691 + }
692 +
693 + .king-addons-upgrade-notice-v2 .king-addons-un-brand {
694 + display: flex;
695 + align-items: center;
696 + gap: 7px;
697 + font-size: 11px;
698 + font-weight: 700;
699 + letter-spacing: .09em;
700 + text-transform: uppercase;
701 + color: #5B03FF;
702 + }
703 +
704 + .king-addons-upgrade-notice-v2 .king-addons-un-brand svg {
705 + width: 14px;
706 + height: 14px;
707 + display: block;
708 + fill: currentColor;
709 + }
710 +
711 + .king-addons-upgrade-notice-v2 .king-addons-un-headline {
712 + margin: 0;
713 + font-size: 17px;
714 + font-weight: 700;
715 + line-height: 1.35;
716 + color: #1d2327;
717 + max-width: 46ch;
718 + }
719 +
720 + .king-addons-upgrade-notice-v2 .king-addons-un-sub {
721 + margin: 0;
722 + font-size: 14px;
723 + line-height: 1.5;
724 + color: #50575e;
725 + max-width: 58ch;
726 + }
727 +
728 + .king-addons-upgrade-notice-v2 .king-addons-un-actions {
729 + display: flex;
730 + align-items: center;
731 + gap: 20px;
732 + flex-wrap: wrap;
733 + margin-top: 2px;
734 + }
735 +
736 + .king-addons-upgrade-notice-v2 .king-addons-un-cta {
737 + background: #5B03FF;
738 + color: #fff;
739 + display: inline-flex;
740 + align-items: center;
741 + gap: 7px;
742 + padding: 9px 18px;
743 + font-size: 14px;
744 + font-weight: 600;
745 + text-decoration: none;
746 + border-radius: 8px;
747 + transition: background-color .15s ease;
748 + }
749 +
750 + .king-addons-upgrade-notice-v2 .king-addons-un-cta:hover, .king-addons-upgrade-notice-v2 .king-addons-un-cta:focus {
751 + background: #3D01B0;
752 + color: #fff;
753 + }
754 +
755 + .king-addons-upgrade-notice-v2 .king-addons-un-cta:focus-visible {
756 + outline: 2px solid #5B03FF;
757 + outline-offset: 2px;
758 + }
759 +
760 + .king-addons-upgrade-notice-v2 .king-addons-un-cta svg {
761 + width: 15px;
762 + height: 15px;
763 + display: block;
764 + }
765 +
766 + .king-addons-upgrade-notice-v2 .king-addons-un-secondary {
767 + font-size: 14px;
768 + font-weight: 500;
769 + color: #50575e;
770 + text-decoration: none;
771 + border-bottom: 1px solid #c9c4d8;
772 + padding-bottom: 1px;
773 + }
774 +
775 + .king-addons-upgrade-notice-v2 .king-addons-un-secondary:hover, .king-addons-upgrade-notice-v2 .king-addons-un-secondary:focus {
776 + color: #5B03FF;
777 + border-bottom-color: #5B03FF;
778 + }
779 +
780 + .king-addons-upgrade-notice-v2 .king-addons-un-trust {
781 + margin: 4px 0 0;
782 + font-size: 13px;
783 + color: #50575e;
784 + display: flex;
785 + align-items: center;
786 + gap: 8px 18px;
787 + flex-wrap: wrap;
788 + }
789 +
790 + .king-addons-upgrade-notice-v2 .king-addons-un-trust-item {
791 + display: inline-flex;
792 + align-items: baseline;
793 + gap: 6px;
794 + }
795 +
796 + .king-addons-upgrade-notice-v2 .king-addons-un-trust strong {
797 + color: #1d2327;
798 + font-weight: 700;
799 + }
800 +
801 + .king-addons-upgrade-notice-v2 .king-addons-un-check {
802 + color: #0a6b45;
803 + font-weight: 700;
804 + font-size: 12px;
805 + line-height: 1;
806 + }
807 +
808 + @media screen and (max-width: 782px) {
809 + .king-addons-upgrade-notice-v2 .king-addons-un-actions {
810 + align-items: flex-start;
811 + gap: 12px;
812 + }
813 + }
814 + </style>
815 + <div class="king-addons-upgrade-notice king-addons-upgrade-notice-v2 notice notice-info is-dismissible">
816 + <div class="king-addons-un-main">
817 + <div class="king-addons-un-brand">
818 + <svg viewBox="0 0 512 512" aria-hidden="true" focusable="false"><path d="M504.981,150.787c-6.048-5.163-14.583-6.251-21.736-2.769l-109.444,53.28L271.109,82.896C267.311,78.516,261.798,76,256,76c-5.798,0-11.31,2.516-15.109,6.896L138.199,201.297l-109.444-53.28c-7.153-3.481-15.687-2.394-21.737,2.769c-6.05,5.163-8.466,13.421-6.153,21.031l76,250C79.426,430.242,87.195,436,96,436h320c8.804,0,16.574-5.758,19.134-14.182l76-250C513.448,164.208,511.032,155.95,504.981,150.787z M401.175,396H110.823L52.472,204.052l82.022,39.931c8.144,3.964,17.93,1.962,23.863-4.878L256,126.525l97.644,112.58c5.932,6.841,15.721,8.841,23.862,4.878l82.022-39.931L401.175,396z"></path></svg>
819 + <?php esc_html_e('King Addons Pro', 'king-addons'); ?>
820 + </div>
821 + <h2 class="king-addons-un-headline">
822 + <?php esc_html_e('Mega Menu, Popup Builder, WooCommerce Builder, Theme Builder &amp; AI Tools', 'king-addons'); ?>
823 + </h2>
824 + <p class="king-addons-un-sub">
825 + <?php esc_html_e('200+ Pro features, premium widgets and 4,000+ templates &amp; sections for Elementor.', 'king-addons'); ?>
826 + </p>
827 + <div class="king-addons-un-actions">
828 + <a class="king-addons-un-cta" href="<?php echo esc_url($pricing_url); ?>" target="_blank" rel="noopener noreferrer">
829 + <?php
830 + printf(
831 + /* translators: %s: monthly price, for example $6.99/mo */
832 + esc_html__('Upgrade to Pro for %s', 'king-addons'),
833 + esc_html__('$6.99/mo', 'king-addons')
834 + );
835 + ?>
836 + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" aria-hidden="true" focusable="false"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
837 + </a>
838 + <a class="king-addons-un-secondary" href="<?php echo esc_url($explore_url); ?>" target="_blank" rel="noopener noreferrer">
839 + <?php esc_html_e('Explore Pro features', 'king-addons'); ?>
840 + </a>
841 + </div>
842 + <p class="king-addons-un-trust">
843 + <span class="king-addons-un-trust-item">
844 + <span class="king-addons-un-check" aria-hidden="true">&#10003;</span>
845 + <?php esc_html_e('30-day money-back guarantee', 'king-addons'); ?>
846 + </span>
847 + <span class="king-addons-un-trust-item">
848 + <span class="king-addons-un-check" aria-hidden="true">&#10003;</span>
849 + <?php esc_html_e('Cancel anytime', 'king-addons'); ?>
850 + </span>
851 + <span class="king-addons-un-trust-item">
852 + <span class="king-addons-un-check" aria-hidden="true">&#10003;</span>
853 + <?php
854 + printf(
855 + /* translators: %s: number of users, wrapped in bold */
856 + esc_html__('Trusted by %s users', 'king-addons'),
857 + '<strong>' . esc_html__('20,000+', 'king-addons') . '</strong>'
858 + );
859 + ?>
860 + </span>
861 + </p>
862 + </div>
863 + </div>
864 + <script>
865 + (function ($) {
866 + const kingAddonsPremiumNoticeNonce = '<?php echo esc_js(wp_create_nonce('king_addons_premium_notice_dismiss')); ?>';
867 + $(document).ready(function () {
868 + $('.king-addons-upgrade-notice.notice.is-dismissible').on('click', '.notice-dismiss', function () {
869 + $.post(ajaxurl, {
870 + action: 'king_addons_premium_notice_dismiss',
871 + nonce: kingAddonsPremiumNoticeNonce
872 + });
873 + });
874 + });
875 + })(jQuery);
876 + </script>
877 + <?php
878 + }
879 +
880 + function enqueueFrontendStyles()
881 + {
882 + /**
883 + * It fixes the default Elementor SVG icon rendering feature (Settings -> Features -> Inline Font Icons)
884 + * because sometimes Elementor still renders Font Awesome icons but doesn't load the corresponding Font Awesome styles.
885 + * Therefore, we have to enqueue the styles.
886 + */
887 + wp_enqueue_style(
888 + 'font-awesome-5-all',
889 + ELEMENTOR_ASSETS_URL . 'lib/font-awesome/css/all' . (defined('SCRIPT_DEBUG') && SCRIPT_DEBUG ? '' : '.min') . '.css',
890 + false,
891 + KING_ADDONS_VERSION
892 + );
893 + }
894 +
895 + function hasElementorCompatibility(): bool
896 + {
897 + // Check if Elementor installed and activated
898 + if (!did_action('elementor/loaded')) {
899 + add_action('admin_notices', [$this, 'showAdminNotice_ElementorRequired']);
900 + return false;
901 + }
902 +
903 + // Check for required Elementor version
904 + if (!version_compare(ELEMENTOR_VERSION, '3.19.0', '>=')) {
905 + add_action('admin_notices', [$this, 'showAdminNotice_ElementorMinimumVersion']);
906 + return false;
907 + }
908 +
909 + return true;
910 + }
911 +
912 + function showAdminNotice_ElementorRequired(): void
913 + {
914 + $screen = get_current_screen();
915 + if (isset($screen->parent_file) && 'plugins.php' === $screen->parent_file && 'update' === $screen->id) {
916 + return;
917 + }
918 +
919 + if (isset(get_plugins()['elementor/elementor.php'])) {
920 + if (!current_user_can('activate_plugins') || is_plugin_active('elementor/elementor.php')) {
921 + return;
922 + }
923 + $plugin = 'elementor/elementor.php';
924 + $activation_url = wp_nonce_url('plugins.php?action=activate&amp;plugin=' . $plugin . '&amp;plugin_status=all&amp;paged=1&amp;s', 'activate-plugin_' . $plugin);
925 + $message = '<div class="error"><p>' . esc_html__('King Addons plugin is not working because you need to activate the Elementor plugin.', 'king-addons') . '</p>';
926 + /** @noinspection HtmlUnknownTarget */
927 + $message .= '<p>' . sprintf('<a href="%s" class="button-primary">%s</a>', $activation_url, esc_html__('Activate Elementor now', 'king-addons')) . '</p></div>';
928 + } else {
929 + if (!current_user_can('install_plugins')) {
930 + return;
931 + }
932 + $install_url = wp_nonce_url(self_admin_url('update.php?action=install-plugin&plugin=elementor'), 'install-plugin_elementor');
933 + $message = '<div class="error"><p>' . esc_html__('King Addons plugin is not working because you need to install the Elementor plugin.', 'king-addons') . '</p>';
934 + /** @noinspection HtmlUnknownTarget */
935 + $message .= '<p>' . sprintf('<a href="%s" class="button-primary">%s</a>', $install_url, esc_html__('Install Elementor now', 'king-addons')) . '</p></div>';
936 + }
937 + echo $message;
938 + }
939 +
940 + function showAdminNotice_ElementorMinimumVersion(): void
941 + {
942 + $message = sprintf(
943 + /* translators: 1: Plugin name 2: Elementor 3: Required Elementor version */
944 + esc_html__('%1$s plugin requires %2$s plugin version %3$s or greater.', 'king-addons'),
945 + esc_html__('King Addons', 'king-addons'),
946 + esc_html__('Elementor', 'king-addons'),
947 + '3.19.0'
948 + );
949 + echo '<div class="notice notice-error"><p>' . esc_html($message) . '</p></div>';
950 + }
951 +
952 + public function initElementor(): void
953 + {
954 + add_action('elementor/widgets/register', [$this, 'registerWidgets']);
955 + add_action('elementor/editor/after_enqueue_styles', [$this, 'enqueueEditorStyles']);
956 + add_action('elementor/editor/after_enqueue_scripts', [$this, 'enqueueEditorScripts']);
957 + add_action('elementor/preview/enqueue_styles', [$this, 'enqueueEditorPreviewStyles']);
958 + }
959 +
960 + function enqueueEditorPreviewStyles(): void
961 + {
962 + wp_enqueue_style(
963 + KING_ADDONS_ASSETS_UNIQUE_KEY . '-elementor-preview',
964 + KING_ADDONS_URL . 'includes/admin/css/elementor-preview.css',
965 + [],
966 + KING_ADDONS_VERSION
967 + );
968 + }
969 +
970 + function addWidgetCategory(): void
971 + {
972 + $elements_manager = Plugin::instance()->elements_manager;
973 +
974 + $elements_manager->add_category(
975 + 'king-addons',
976 + [
977 + 'title' => esc_html__('King Addons', 'king-addons'),
978 + 'icon' => 'fa fa-plug',
979 + ]
980 + );
981 +
982 + if (class_exists('WooCommerce') && function_exists('WC')) {
983 + $elements_manager->add_category(
984 + 'king-addons-woo',
985 + [
986 + 'title' => esc_html__('King Addons WooCommerce', 'king-addons'),
987 + 'icon' => 'fa fa-shopping-cart',
988 + 'hideIfEmpty' => true,
989 + ]
990 + );
991 +
992 + $elements_manager->add_category(
993 + 'king-addons-woo-builder',
994 + [
995 + 'title' => esc_html__('King Addons WooCommerce Builder', 'king-addons'),
996 + 'icon' => 'fa fa-shopping-cart',
997 + 'hideIfEmpty' => true,
998 + ]
999 + );
1000 + }
1001 +
1002 + $elements_manager->add_category(
1003 + 'king-addons-theme-builder',
1004 + [
1005 + 'title' => esc_html__('King Addons Theme Builder', 'king-addons'),
1006 + 'icon' => 'fa fa-plug',
1007 + 'hideIfEmpty' => true,
1008 + ]
1009 + );
1010 + }
1011 +
1012 + /**
1013 + * Apply the panel category order after Elementor has finished registering its own.
1014 + *
1015 + * @return void
1016 + */
1017 + public function reorderWidgetCategoriesLate(): void
1018 + {
1019 + if (!did_action('elementor/loaded')) {
1020 + return;
1021 + }
1022 +
1023 + $elements_manager = Plugin::instance()->elements_manager;
1024 + $elements_manager->get_categories();
1025 + $this->reorderWidgetCategories($elements_manager);
1026 +
1027 + if (!$this->shouldApplyEditorPanelVisibility()) {
1028 + return;
1029 + }
1030 +
1031 + $post_id = $this->getCurrentEditorPostId();
1032 + if ($post_id > 0) {
1033 + $this->applyContextCategoryVisibility($elements_manager, $post_id);
1034 + }
1035 + }
1036 +
1037 + /**
1038 + * Overwrite the editor panel category snapshot after Elementor has built document config.
1039 + *
1040 + * @param array $additional_config
1041 + * @param int $post_id
1042 + * @return array
1043 + */
1044 + public function filterDocumentPanelCategories($additional_config, $post_id = 0)
1045 + {
1046 + $post_id = (int) $post_id;
1047 + if ($post_id <= 0) {
1048 + $post_id = $this->getCurrentEditorPostId();
1049 + }
1050 + if (!is_array($additional_config)) {
1051 + $additional_config = [];
1052 + }
1053 +
1054 + if (!did_action('elementor/loaded')) {
1055 + return $additional_config;
1056 + }
1057 +
1058 + $elements_manager = Plugin::instance()->elements_manager;
1059 + $elements_manager->get_categories();
1060 + $this->reorderWidgetCategories($elements_manager);
1061 + if ($post_id > 0) {
1062 + $this->applyContextCategoryVisibility($elements_manager, $post_id);
1063 + }
1064 +
1065 + $categories = \Elementor\Core\Base\Document::get_filtered_editor_panel_categories();
1066 + foreach ($this->getHiddenPanelCategoryKeys($post_id) as $hidden_key) {
1067 + unset($categories[$hidden_key]);
1068 + }
1069 +
1070 + $additional_config['panel']['elements_categories'] = $categories;
1071 +
1072 + foreach ($this->getHiddenPanelWidgetNames($post_id) as $widget_name) {
1073 + $additional_config['widgets'][$widget_name]['show_in_panel'] = false;
1074 + }
1075 +
1076 + return $additional_config;
1077 + }
1078 +
1079 + /**
1080 + * Reorder widget categories: Layout, then King Addons groups, then the rest of Elementor.
1081 + *
1082 + * @param \Elementor\Elements_Manager $elements_manager
1083 + * @return void
1084 + */
1085 + private function reorderWidgetCategories($elements_manager): void
1086 + {
1087 + try {
1088 + $reflection = new \ReflectionClass($elements_manager);
1089 + $categories_property = $reflection->getProperty('categories');
1090 + if (PHP_VERSION_ID < 80100) {
1091 + $categories_property->setAccessible(true);
1092 + }
1093 +
1094 + $categories = $categories_property->getValue($elements_manager);
1095 + if (!is_array($categories) || $categories === []) {
1096 + return;
1097 + }
1098 +
1099 + $layout = [];
1100 + $ours = [];
1101 + $rest = [];
1102 +
1103 + foreach ($categories as $key => $value) {
1104 + if ('layout' === $key) {
1105 + $layout[$key] = $value;
1106 + } elseif (str_starts_with((string) $key, 'king-addons')) {
1107 + $ours[$key] = $value;
1108 + } else {
1109 + $rest[$key] = $value;
1110 + }
1111 + }
1112 +
1113 + $ours_sorted = [];
1114 + foreach (['king-addons', 'king-addons-woo', 'king-addons-woo-builder', 'king-addons-theme-builder'] as $preferred) {
1115 + if (isset($ours[$preferred])) {
1116 + $ours_sorted[$preferred] = $ours[$preferred];
1117 + unset($ours[$preferred]);
1118 + }
1119 + }
1120 + $ours_sorted += $ours;
1121 +
1122 + $categories_property->setValue($elements_manager, $layout + $ours_sorted + $rest);
1123 + } catch (\ReflectionException $e) {
1124 + // Silently fail if reflection doesn't work (e.g., future Elementor changes)
1125 + }
1126 + }
1127 +
1128 + /**
1129 + * Drop context-only categories from the manager for the current editor document.
1130 + *
1131 + * @param \Elementor\Elements_Manager $elements_manager
1132 + * @param int $post_id
1133 + * @return void
1134 + */
1135 + private function applyContextCategoryVisibility($elements_manager, int $post_id): void
1136 + {
1137 + $hidden = $this->getHiddenPanelCategoryKeys($post_id);
1138 + if ($hidden === []) {
1139 + return;
1140 + }
1141 +
1142 + try {
1143 + $reflection = new \ReflectionClass($elements_manager);
1144 + $categories_property = $reflection->getProperty('categories');
1145 + if (PHP_VERSION_ID < 80100) {
1146 + $categories_property->setAccessible(true);
1147 + }
1148 +
1149 + $categories = $categories_property->getValue($elements_manager);
1150 + if (!is_array($categories) || $categories === []) {
1151 + return;
1152 + }
1153 +
1154 + foreach ($hidden as $key) {
1155 + unset($categories[$key]);
1156 + }
1157 +
1158 + $categories_property->setValue($elements_manager, $categories);
1159 + } catch (\ReflectionException $e) {
1160 + // Silently fail if reflection doesn't work (e.g., future Elementor changes)
1161 + }
1162 + }
1163 +
1164 + /**
1165 + * Category keys that should not appear in the current editor document.
1166 + *
1167 + * @param int $post_id
1168 + * @return array<int,string>
1169 + */
1170 + private function getHiddenPanelCategoryKeys(int $post_id): array
1171 + {
1172 + $hidden = [];
1173 + $is_woo_builder = $this->isWooBuilderDocument($post_id);
1174 + $is_theme_builder = $this->isThemeBuilderDocument($post_id);
1175 + $is_loop_item = $this->isLoopItemDocument($post_id);
1176 + $woocommerce_active = class_exists('WooCommerce') && function_exists('WC');
1177 +
1178 + if (!$woocommerce_active) {
1179 + $hidden[] = 'king-addons-woo';
1180 + $hidden[] = 'king-addons-woo-builder';
1181 + } elseif (!$is_woo_builder && !$is_loop_item) {
1182 + $hidden[] = 'king-addons-woo-builder';
1183 + }
1184 +
1185 + if (!$is_theme_builder && !$is_loop_item) {
1186 + $hidden[] = 'king-addons-theme-builder';
1187 + }
1188 +
1189 + return $hidden;
1190 + }
1191 +
1192 + /**
1193 + * Widget names that belong to hidden context categories.
1194 + *
1195 + * @param int $post_id
1196 + * @return array<int,string>
1197 + */
1198 + private function getHiddenPanelWidgetNames(int $post_id): array
1199 + {
1200 + $hidden_categories = $this->getHiddenPanelCategoryKeys($post_id);
1201 + if ($hidden_categories === [] || !did_action('elementor/loaded')) {
1202 + return [];
1203 + }
1204 +
1205 + $hidden_lookup = array_fill_keys($hidden_categories, true);
1206 + $names = [];
1207 +
1208 + foreach (Plugin::instance()->widgets_manager->get_widget_types() as $widget_name => $widget) {
1209 + if (!is_object($widget) || !method_exists($widget, 'get_categories')) {
1210 + continue;
1211 + }
1212 +
1213 + foreach ((array) $widget->get_categories() as $category) {
1214 + if (isset($hidden_lookup[$category])) {
1215 + $names[] = (string) $widget_name;
1216 + break;
1217 + }
1218 + }
1219 + }
1220 +
1221 + return $names;
1222 + }
1223 +
1224 + /**
1225 + * Whether the current request is the Elementor editor (or its AJAX).
1226 + *
1227 + * @return bool
1228 + */
1229 + private function shouldApplyEditorPanelVisibility(): bool
1230 + {
1231 + if (!empty($_GET['action']) && 'elementor' === $_GET['action']) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1232 + return true;
1233 + }
1234 +
1235 + if (!empty($_REQUEST['editor_post_id'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1236 + return true;
1237 + }
1238 +
1239 + return class_exists('\Elementor\Plugin')
1240 + && Plugin::instance()->editor
1241 + && Plugin::instance()->editor->is_edit_mode();
1242 + }
1243 +
1244 + /**
1245 + * Current Elementor editor post ID, if any.
1246 + *
1247 + * @return int
1248 + */
1249 + private function getCurrentEditorPostId(): int
1250 + {
1251 + foreach (['editor_post_id', 'post_id', 'post'] as $key) {
1252 + if (!empty($_REQUEST[$key])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1253 + $post_id = absint(wp_unslash($_REQUEST[$key])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1254 + if ($post_id > 0) {
1255 + return $post_id;
1256 + }
1257 + }
1258 + }
1259 +
1260 + $post = get_post();
1261 + return $post ? (int) $post->ID : 0;
1262 + }
1263 +
1264 + /**
1265 + * Whether the post is a King Addons Woo Builder template.
1266 + *
1267 + * @param int $post_id
1268 + * @return bool
1269 + */
1270 + private function isWooBuilderDocument(int $post_id): bool
1271 + {
1272 + if ($post_id <= 0) {
1273 + return false;
1274 + }
1275 +
1276 + $elementor_type = (string) get_post_meta($post_id, '_elementor_template_type', true);
1277 + if ('king-addons-woo-builder' === $elementor_type) {
1278 + return true;
1279 + }
1280 +
1281 + return (string) get_post_meta($post_id, 'ka_woo_template_type', true) !== '';
1282 + }
1283 +
1284 + /**
1285 + * Whether the post is a King Addons Theme Builder template.
1286 + *
1287 + * @param int $post_id
1288 + * @return bool
1289 + */
1290 + private function isThemeBuilderDocument(int $post_id): bool
1291 + {
1292 + if ($post_id <= 0) {
1293 + return false;
1294 + }
1295 +
1296 + $location_key = '_ka_theme_builder_location';
1297 + if (class_exists('\King_Addons\Theme_Builder\Meta_Keys')) {
1298 + $location_key = \King_Addons\Theme_Builder\Meta_Keys::LOCATION;
1299 + }
1300 +
1301 + return (string) get_post_meta($post_id, $location_key, true) !== '';
1302 + }
1303 +
1304 + /**
1305 + * Whether the post is a King Addons Loop Item template.
1306 + *
1307 + * @param int $post_id
1308 + * @return bool
1309 + */
1310 + private function isLoopItemDocument(int $post_id): bool
1311 + {
1312 + if ($post_id <= 0) {
1313 + return false;
1314 + }
1315 +
1316 + return 'king-addons-loop-item' === (string) get_post_meta($post_id, '_elementor_template_type', true);
1317 + }
1318 +
1319 + /**
1320 + * Registers Elementor widgets with a mechanism to skip (and remember) broken widgets
1321 + * that caused a fatal error previously, and try them again if the plugin version is updated.
1322 + *
1323 + * @param Widgets_Manager $widgets_manager
1324 + * @return void
1325 + */
1326 + function registerWidgets(Widgets_Manager $widgets_manager): void
1327 + {
1328 + // Used to track which widget is currently being loaded when a fatal error occurs
1329 + static $currentlyLoadingWidgetId = null;
1330 +
1331 + $currentPluginVersion = KING_ADDONS_VERSION;
1332 +
1333 + // Get plugin options to check if a widget is enabled
1334 + $options = get_option('king_addons_options');
1335 + $options = is_array($options) ? $options : [];
1336 +
1337 + // Extension toggles (used to prevent loading dependent widgets when extension is disabled).
1338 + $wishlist_extension_enabled = !isset($options['ext_wishlist']) || $options['ext_wishlist'] === 'enabled';
1339 + if (defined('KING_ADDONS_EXT_WISHLIST') && KING_ADDONS_EXT_WISHLIST === false) {
1340 + $wishlist_extension_enabled = false;
1341 + }
1342 +
1343 + // Ensure Woo Builder base class is available for single product widgets.
1344 + $abstract_single_widget = KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Abstract_Single_Widget.php';
1345 + if (file_exists($abstract_single_widget)) {
1346 + require_once $abstract_single_widget;
1347 + }
1348 +
1349 + // Ensure Woo Builder base class is available for archive widgets.
1350 + $abstract_archive_widget = KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Abstract_Archive_Widget.php';
1351 + if (file_exists($abstract_archive_widget)) {
1352 + require_once $abstract_archive_widget;
1353 + }
1354 +
1355 + // Ensure Woo Builder base class is available for cart widgets.
1356 + $abstract_cart_widget = KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Abstract_Cart_Widget.php';
1357 + if (file_exists($abstract_cart_widget)) {
1358 + require_once $abstract_cart_widget;
1359 + }
1360 +
1361 + // Ensure Woo Builder base class is available for checkout widgets.
1362 + $abstract_checkout_widget = KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Abstract_Checkout_Widget.php';
1363 + if (file_exists($abstract_checkout_widget)) {
1364 + require_once $abstract_checkout_widget;
1365 + }
1366 +
1367 + // Ensure Woo Builder base class is available for My Account widgets.
1368 + $abstract_my_account_widget = KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Abstract_My_Account_Widget.php';
1369 + if (file_exists($abstract_my_account_widget)) {
1370 + require_once $abstract_my_account_widget;
1371 + }
1372 +
1373 + /**
1374 + * Retrieve the array of broken widgets from the WordPress options.
1375 + * The structure is expected to be something like:
1376 + *
1377 + * 'widget_id' => [
1378 + * 'version' => '1.2.0',
1379 + * 'error' => 'Some fatal error message'
1380 + * ],
1381 + * ...
1382 + *
1383 + */
1384 + $brokenWidgets = get_option('king_addons_broken_widgets', []);
1385 +
1386 + /**
1387 + * STEP 1: Clear out any "broken widgets" where the stored version is
1388 + * less than the current plugin version. This gives them a second chance
1389 + * after an update, assuming the issue may have been fixed.
1390 + */
1391 + foreach ($brokenWidgets as $brokenId => $brokenData) {
1392 + if (
1393 + isset($brokenData['version'])
1394 + && version_compare($currentPluginVersion, $brokenData['version'], '>')
1395 + ) {
1396 + // If the plugin version is now higher, we remove the widget from the blacklist
1397 + unset($brokenWidgets[$brokenId]);
1398 + }
1399 + }
1400 +
1401 + // Update the option after cleaning up
1402 + update_option('king_addons_broken_widgets', $brokenWidgets);
1403 +
1404 + /**
1405 + * STEP 2: Use register_shutdown_function to detect any fatal errors (E_ERROR, E_PARSE, etc.)
1406 + * that might occur during the loading of a widget. If an error is detected, store that widget
1407 + * in the "broken" list with the current plugin version and the error message.
1408 + */
1409 + register_shutdown_function(function () use (&$currentlyLoadingWidgetId, $currentPluginVersion) {
1410 + $error = error_get_last();
1411 + if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
1412 + // If a fatal error occurred while loading a specific widget
1413 + if (!empty($currentlyLoadingWidgetId)) {
1414 + $brokenWidgetsLocal = get_option('king_addons_broken_widgets', []);
1415 + $brokenWidgetsLocal[$currentlyLoadingWidgetId] = [
1416 + 'version' => $currentPluginVersion,
1417 + 'error' => $error['message'] ?? ''
1418 + ];
1419 + update_option('king_addons_broken_widgets', $brokenWidgetsLocal);
1420 + }
1421 + }
1422 + });
1423 +
1424 + /**
1425 + * STEP 3: Now we iterate through all widgets in our modules map and try to load them.
1426 + * If a widget is in the broken list, we skip it to avoid repeated fatal errors.
1427 + */
1428 + foreach (ModulesMap::getModulesMapArray()['widgets'] as $widget_id => $widget) {
1429 + // Hard-disable via constant (used to QA/rollout new widgets).
1430 + $widget_constant = 'KING_ADDONS_WGT_' . strtoupper(str_replace('-', '_', (string) $widget_id));
1431 + if (defined($widget_constant) && constant($widget_constant) === false) {
1432 + continue;
1433 + }
1434 +
1435 + // Check if the widget is enabled in the options
1436 + if (!isset($options[$widget_id]) || $options[$widget_id] !== 'enabled') {
1437 + continue;
1438 + }
1439 +
1440 + // Skip Wishlist widgets when Wishlist extension is disabled.
1441 + // This prevents fatals when wishlist classes aren't loaded.
1442 + if (!$wishlist_extension_enabled && strpos((string) $widget_id, 'wishlist-') === 0) {
1443 + continue;
1444 + }
1445 +
1446 + // If this widget is listed as broken, skip it
1447 + if (array_key_exists($widget_id, $brokenWidgets)) {
1448 + // Log something here if needed:
1449 + // error_log("Skipping widget {$widget_id}, it previously caused a fatal error.");
1450 + continue;
1451 + }
1452 +
1453 + // Track which widget we're loading
1454 + $currentlyLoadingWidgetId = $widget_id;
1455 +
1456 + // Include the base widget class
1457 + $widget_class = $widget['php-class'];
1458 + $path_widget_class = "King_Addons\\" . $widget_class;
1459 + $widget_file = KING_ADDONS_PATH . 'includes/widgets/' . $widget_class . '/' . $widget_class . '.php';
1460 + if (!file_exists($widget_file)) {
1461 + // Skip missing widget files to avoid fatal errors if registry is ahead of implementation.
1462 + $currentlyLoadingWidgetId = null;
1463 + continue;
1464 + }
1465 +
1466 + require_once $widget_file;
1467 +
1468 + // Check if we can load the Pro version
1469 + if (
1470 + function_exists('king_addons_freemius')
1471 + && king_addons_freemius()->can_use_premium_code__premium_only()
1472 + && defined('KING_ADDONS_PRO_PATH')
1473 + ) {
1474 + if (!empty($widget['has-pro'])) {
1475 + $pro_file_path = KING_ADDONS_PRO_PATH . 'includes/widgets/' . $widget_class . '_Pro/' . $widget_class . '_Pro.php';
1476 +
1477 + if (file_exists($pro_file_path)) {
1478 + require_once($pro_file_path);
1479 + $path_widget_class_pro = "King_Addons\\" . $widget_class . '_Pro';
1480 + $widgets_manager->register(new $path_widget_class_pro);
1481 + } else {
1482 + // If Pro file doesn't exist, register the base widget
1483 + $widgets_manager->register(new $path_widget_class);
1484 + }
1485 + } else {
1486 + // No 'has-pro', register the base widget
1487 + $widgets_manager->register(new $path_widget_class);
1488 + }
1489 + } else {
1490 + // No Freemius Pro available, register the base widget
1491 + $widgets_manager->register(new $path_widget_class);
1492 + }
1493 +
1494 + // Clear the tracking variable after successful load
1495 + $currentlyLoadingWidgetId = null;
1496 + }
1497 + }
1498 +
1499 + function enableWidgetsByDefault(): void
1500 + {
1501 + $options = get_option('king_addons_options');
1502 +
1503 + foreach (ModulesMap::getModulesMapArray()['widgets'] as $widget_id => $widget) {
1504 +
1505 + // Hard-disable via constant (used to QA/rollout new widgets).
1506 + $widget_constant = 'KING_ADDONS_WGT_' . strtoupper(str_replace('-', '_', (string) $widget_id));
1507 + if (defined($widget_constant) && constant($widget_constant) === false) {
1508 + continue;
1509 + }
1510 +
1511 + if (!($options[$widget_id] ?? null)) {
1512 + $options[$widget_id] = 'enabled';
1513 + update_option('king_addons_options', $options);
1514 + }
1515 + }
1516 + }
1517 +
1518 + /**
1519 + * Enable and bootstrap registered features.
1520 + *
1521 + * Loads free feature classes and, when available and licensed, their Pro counterparts.
1522 + *
1523 + * @return void
1524 + */
1525 + public function enableFeatures(): void
1526 + {
1527 + $options = get_option('king_addons_options');
1528 +
1529 + foreach (ModulesMap::getModulesMapArray()['features'] as $feature_id => $feature) {
1530 + // Hard-disable via constant (used to QA/rollout new features).
1531 + $feature_constant = 'KING_ADDONS_FEAT_' . strtoupper(str_replace('-', '_', (string) $feature_id));
1532 + if (defined($feature_constant) && constant($feature_constant) === false) {
1533 + continue;
1534 + }
1535 +
1536 + if (!($options[$feature_id] ?? null)) {
1537 + $options[$feature_id] = 'enabled';
1538 + update_option('king_addons_options', $options);
1539 + }
1540 +
1541 + if ($options[$feature_id] !== 'enabled') {
1542 + continue;
1543 + }
1544 +
1545 + $feature_class = $feature['php-class'];
1546 + $path_feature_class = "King_Addons\\" . $feature_class;
1547 + $feature_file = KING_ADDONS_PATH . 'includes/features/' . $feature_class . '/' . $feature_class . '.php';
1548 +
1549 + if (file_exists($feature_file)) {
1550 + require_once $feature_file;
1551 + }
1552 +
1553 + $pro_loaded = false;
1554 +
1555 + if (
1556 + !empty($feature['has-pro'])
1557 + && function_exists('king_addons_freemius')
1558 + && king_addons_freemius()->can_use_premium_code__premium_only()
1559 + && defined('KING_ADDONS_PRO_PATH')
1560 + ) {
1561 + $pro_file_path = KING_ADDONS_PRO_PATH . 'includes/features/' . $feature_class . '_Pro/' . $feature_class . '_Pro.php';
1562 +
1563 + if (file_exists($pro_file_path)) {
1564 + require_once $pro_file_path;
1565 +
1566 + $path_feature_class_pro = "King_Addons\\" . $feature_class . '_Pro';
1567 + if (class_exists($path_feature_class_pro)) {
1568 + new $path_feature_class_pro();
1569 + $pro_loaded = true;
1570 + }
1571 + }
1572 + }
1573 +
1574 + if (!$pro_loaded && class_exists($path_feature_class)) {
1575 + new $path_feature_class();
1576 + }
1577 + }
1578 + }
1579 +
1580 + public function registerControls(Controls_Manager $controls_manager): void
1581 + {
1582 + $controls_manager->register(new AJAX_Select2\Ajax_Select2());
1583 + $controls_manager->register(new Animations\Animations());
1584 + $controls_manager->register(new Animations\Animations_Alternative());
1585 + $controls_manager->register(new Button_Animations\Button_Animations());
1586 + }
1587 +
1588 + function enqueueEditorStyles(): void
1589 + {
1590 + wp_enqueue_style(KING_ADDONS_ASSETS_UNIQUE_KEY . '-elementor-editor', KING_ADDONS_URL . 'includes/admin/css/elementor-editor.css', '', KING_ADDONS_VERSION);
1591 + }
1592 +
1593 + function enqueueEditorScripts(): void
1594 + {
1595 + wp_enqueue_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-elementor-editor', KING_ADDONS_URL . 'includes/admin/js/elementor-editor.js', '', KING_ADDONS_VERSION);
1596 +
1597 + // Localize script with PRO status
1598 + wp_localize_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-elementor-editor', 'kingAddonsEditor', [
1599 + 'isPro' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false
1600 + ]);
1601 +
1602 + wp_enqueue_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-data-table-export', KING_ADDONS_URL . 'includes/widgets/Data_Table/preview-handler.js', '', KING_ADDONS_VERSION);
1603 +
1604 + if (KING_ADDONS_WGT_FORM_BUILDER) {
1605 + wp_enqueue_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-form-builder-editor-handler', KING_ADDONS_URL . 'includes/widgets/Form_Builder/editor-handler.js', '', KING_ADDONS_VERSION);
1606 + }
1607 + }
1608 +
1609 + /**
1610 + * Widget settings with dynamic tags resolved, and nothing missing.
1611 + *
1612 + * get_settings() never parses dynamic tags, so a tag placed in such a
1613 + * widget printed its placeholder instead of the value. get_settings_for_display()
1614 + * does parse them, but it also drops every setting whose control condition
1615 + * is currently false - and widgets written against the raw array index into
1616 + * those keys without checking, which turns the switch into a batch of
1617 + * "Trying to access array offset on null" warnings.
1618 + *
1619 + * A hidden control comes back as null rather than missing, so the parsed
1620 + * value is preferred only when there is one, and the raw value fills in
1621 + * everywhere else.
1622 + *
1623 + * @param \Elementor\Controls_Stack $widget Widget being rendered.
1624 + *
1625 + * @return array<string,mixed>
1626 + */
1627 + public static function displaySettings($widget): array
1628 + {
1629 + $raw = (array) $widget->get_settings();
1630 + $display = (array) $widget->get_settings_for_display();
1631 +
1632 + foreach ($display as $key => $value) {
1633 + if (null !== $value) {
1634 + $raw[$key] = $value;
1635 + }
1636 + }
1637 +
1638 + return $raw;
1639 + }
1640 +
1641 + public static function renderProFeaturesSection($module, $section, $type, $widget_name, $features): void
1642 + {
1643 + if (king_addons_freemius()->can_use_premium_code__premium_only()) {
1644 + return;
1645 + }
1646 +
1647 + $module->start_controls_section(
1648 + 'king_addons_pro_features_section',
1649 + [
1650 + 'label' => KING_ADDONS_ELEMENTOR_ICON_PRO . '<span class="king-addons-pro-features-heading">' . esc_html__('Pro Features', 'king-addons') . '</span>',
1651 + 'tab' => $section ?: null,
1652 + ]
1653 + );
1654 +
1655 + $list_html = '<ul>' . implode('', array_map(fn($feature) => "<li>$feature</li>", $features)) . '</ul>';
1656 +
1657 + $module->add_control(
1658 + 'king_addons_pro_features_list',
1659 + [
1660 + 'type' => $type,
1661 + 'raw' => $list_html . '<a class="king-addons-pro-features-cta-btn" href="https://kingaddons.com/pricing/?utm_source=kng-module-' . $widget_name . '-upgrade-pro&utm_medium=plugin&utm_campaign=kng" target="_blank">' . esc_html__('Upgrade Now', 'king-addons') . '</a>',
1662 + 'content_classes' => 'king-addons-pro-features-list',
1663 + ]
1664 + );
1665 +
1666 + $module->end_controls_section();
1667 + }
1668 +
1669 + /**
1670 + * Same upgrade notice as renderUpgradeProNotice(), but not tied to another
1671 + * control's value. Use it where a whole section is Pro-only, so the free
1672 + * build does not show a section header that opens onto nothing.
1673 + *
1674 + * @param mixed $module Widget or repeater the control belongs to.
1675 + * @param string $controls_manager Control type to render the notice with.
1676 + * @param string $widget_name Widget slug, used for the campaign link.
1677 + * @param string $control_id Unique control id for the notice.
1678 + */
1679 + public static function renderUpgradeProSection($module, $controls_manager, string $widget_name, string $control_id): void
1680 + {
1681 + if (king_addons_freemius()->can_use_premium_code__premium_only()) {
1682 + return;
1683 + }
1684 +
1685 + $module->add_control(
1686 + $control_id,
1687 + [
1688 + 'raw' => 'Upgrade to the <strong><a href="https://kingaddons.com/pricing/?utm_source=kng-module-' . $widget_name . '-settings-upgrade-pro&utm_medium=plugin&utm_campaign=kng" target="_blank">Pro version</a></strong> now<br> and unlock this feature!',
1689 + 'type' => $controls_manager,
1690 + 'content_classes' => 'king-addons-pro-notice',
1691 + ]
1692 + );
1693 + }
1694 +
1695 + public static function renderUpgradeProNotice($module, $controls_manager, $widget_name, $option, $condition = []): void
1696 + {
1697 + if (king_addons_freemius()->can_use_premium_code__premium_only()) {
1698 + return;
1699 + }
1700 +
1701 + $module->add_control(
1702 + $option . '_pro_notice_',
1703 + [
1704 + 'raw' => 'Upgrade to the <strong><a href="https://kingaddons.com/pricing/?utm_source=kng-module-' . $widget_name . '-settings-upgrade-pro&utm_medium=plugin&utm_campaign=kng" target="_blank">Pro version</a></strong> now<br> and unlock this feature!',
1705 + 'type' => $controls_manager,
1706 + 'content_classes' => 'king-addons-pro-notice',
1707 + 'condition' => [
1708 + $option => $condition,
1709 + ]
1710 + ]
1711 + );
1712 + }
1713 +
1714 + public static function getCustomTypes($query, $exclude_defaults = true): array
1715 + {
1716 + $custom_types = $query === 'tax'
1717 + ? get_taxonomies(['show_in_nav_menus' => true], 'objects')
1718 + : get_post_types(['show_in_nav_menus' => true], 'objects');
1719 +
1720 + return array_filter(
1721 + array_map(fn($type) => $type->label, $custom_types),
1722 + fn($label, $key) => !$exclude_defaults || !in_array($key, ['post', 'page', 'category', 'post_tag']),
1723 + ARRAY_FILTER_USE_BOTH
1724 + );
1725 + }
1726 +
1727 + public static function getShareIcon($args = []): string
1728 + {
1729 + $args = wp_parse_args($args, [
1730 + 'network' => '',
1731 + 'url' => '',
1732 + 'title' => '',
1733 + 'text' => '',
1734 + 'image' => '',
1735 + 'show_whatsapp_title' => 'no',
1736 + 'show_whatsapp_excerpt' => 'no',
1737 + 'tooltip' => 'no',
1738 + 'icons' => 'no',
1739 + 'labels' => 'no',
1740 + 'custom_label' => '',
1741 + ]);
1742 +
1743 + $url = esc_url($args['url']);
1744 + $title = wp_strip_all_tags($args['title']);
1745 + $text = wp_strip_all_tags($args['text']);
1746 + $image = esc_url($args['image']);
1747 + $network = $args['network'];
1748 +
1749 + $get_whatsapp_url = function ($a) {
1750 + if ('yes' === $a['show_whatsapp_title'] && 'yes' === $a['show_whatsapp_excerpt']) {
1751 + return 'https://api.whatsapp.com/send?text=*' . $a['title'] . '*%0a' . $a['text'] . '%0a' . $a['url'];
1752 + } elseif ('yes' === $a['show_whatsapp_title']) {
1753 + return 'https://api.whatsapp.com/send?text=*' . $a['title'] . '*%0a' . $a['url'];
1754 + } elseif ('yes' === $a['show_whatsapp_excerpt']) {
1755 + return 'https://api.whatsapp.com/send?text=*' . $a['text'] . '%0a' . $a['url'];
1756 + }
1757 + return 'https://api.whatsapp.com/send?text=' . $a['url'];
1758 + };
1759 +
1760 + $networks_map = [
1761 + 'facebook-f' => [
1762 + 'url' => "https://www.facebook.com/sharer.php?u=$url",
1763 + 'title' => esc_html__('Facebook', 'king-addons'),
1764 + 'icon' => 'fab',
1765 + ],
1766 + 'x-twitter' => [
1767 + 'url' => "https://twitter.com/intent/tweet?url=$url",
1768 + 'title' => esc_html__('X (Twitter)', 'king-addons'),
1769 + 'icon' => 'fab',
1770 + ],
1771 + 'linkedin-in' => [
1772 + 'url' => "https://www.linkedin.com/shareArticle?mini=true&url=$url&title=$title&summary=$text&source=$url",
1773 + 'title' => esc_html__('LinkedIn', 'king-addons'),
1774 + 'icon' => 'fab',
1775 + ],
1776 + 'pinterest-p' => [
1777 + 'url' => "https://www.pinterest.com/pin/create/button/?url=$url&media=$image",
1778 + 'title' => esc_html__('Pinterest', 'king-addons'),
1779 + 'icon' => 'fab',
1780 + ],
1781 + 'reddit' => [
1782 + 'url' => "https://reddit.com/submit?url=$url&title=$title",
1783 + 'title' => esc_html__('Reddit', 'king-addons'),
1784 + 'icon' => 'fab',
1785 + ],
1786 + 'tumblr' => [
1787 + 'url' => "https://tumblr.com/share/link?url=$url",
1788 + 'title' => esc_html__('Tumblr', 'king-addons'),
1789 + 'icon' => 'fab',
1790 + ],
1791 + 'digg' => [
1792 + 'url' => "https://digg.com/submit?url=$url",
1793 + 'title' => esc_html__('Digg', 'king-addons'),
1794 + 'icon' => 'fab',
1795 + ],
1796 + 'xing' => [
1797 + 'url' => "https://www.xing.com/app/user?op=share&url=$url",
1798 + 'title' => esc_html__('Xing', 'king-addons'),
1799 + 'icon' => 'fab',
1800 + ],
1801 + 'vk' => [
1802 + 'url' => "https://vk.ru/share.php?url=$url&title=$title&description=" . wp_trim_words($text, 250) . "&image=$image/",
1803 + 'title' => esc_html__('VK', 'king-addons'),
1804 + 'icon' => 'fab',
1805 + ],
1806 + 'odnoklassniki' => [
1807 + 'url' => "https://connect.ok.ru/offer?url=$url",
1808 + 'title' => esc_html__('OK', 'king-addons'),
1809 + 'icon' => 'fab',
1810 + ],
1811 + 'get-pocket' => [
1812 + 'url' => "https://getpocket.com/edit?url=$url",
1813 + 'title' => esc_html__('Pocket', 'king-addons'),
1814 + 'icon' => 'fab',
1815 + ],
1816 + 'skype' => [
1817 + 'url' => "https://web.skype.com/share?url=$url",
1818 + 'title' => esc_html__('Skype', 'king-addons'),
1819 + 'icon' => 'fab',
1820 + ],
1821 + 'whatsapp' => [
1822 + 'url' => $get_whatsapp_url($args),
1823 + 'title' => esc_html__('WhatsApp', 'king-addons'),
1824 + 'icon' => 'fab',
1825 + ],
1826 + 'telegram' => [
1827 + 'url' => "https://telegram.me/share/url?url=$url&text=$text",
1828 + 'title' => esc_html__('Telegram', 'king-addons'),
1829 + 'icon' => 'fab',
1830 + ],
1831 + 'envelope' => [
1832 + 'url' => "mailto:?subject=$title&body=$url",
1833 + 'title' => esc_html__('Email', 'king-addons'),
1834 + 'icon' => 'fas',
1835 + ],
1836 + 'print' => [
1837 + 'url' => "javascript:window.print()",
1838 + 'title' => esc_html__('Print', 'king-addons'),
1839 + 'icon' => 'fas',
1840 + ],
1841 + ];
1842 +
1843 + if (!isset($networks_map[$network])) {
1844 + return '';
1845 + }
1846 +
1847 + $share_url = $networks_map[$network]['url'];
1848 + $network_title = $networks_map[$network]['title'];
1849 + $icon_category = $networks_map[$network]['icon'];
1850 +
1851 + $output = '<a href="' . esc_url($share_url) . '" class="king-addons-share-icon king-addons-share-' . esc_attr($network) . '" target="_blank">';
1852 +
1853 + if ('yes' === $args['tooltip']) {
1854 + $output .= '<span class="king-addons-share-tooltip king-addons-tooltip">' . esc_html($network_title) . '</span>';
1855 + }
1856 +
1857 + if ('yes' === $args['icons']) {
1858 + $output .= '<i class="' . esc_attr($icon_category) . ' fa-' . esc_attr($network) . '"></i>';
1859 + }
1860 +
1861 + if ('yes' === $args['labels']) {
1862 + $label = !empty($args['custom_label']) ? $args['custom_label'] : $network_title;
1863 + $output .= '<span class="king-addons-share-label">' . esc_html($label) . '</span>';
1864 + }
1865 +
1866 + $output .= '</a>';
1867 +
1868 + return $output;
1869 + }
1870 +
1871 + public static function validateHTMLTags($setting, $default, $tags_whitelist)
1872 + {
1873 + $value = $setting;
1874 + if (!in_array($value, $tags_whitelist)) {
1875 + $value = $default;
1876 + }
1877 + return $value;
1878 + }
1879 +
1880 + public static function getIcon($icon, $dir)
1881 + {
1882 + if (empty($icon) || strpos($icon, 'fa-') === false) {
1883 + return '';
1884 + }
1885 +
1886 + $dir = $dir ? "-$dir" : '';
1887 + return wp_kses(
1888 + '<i class="' . esc_attr($icon . $dir) . '"></i>',
1889 + ['i' => ['class' => []]]
1890 + );
1891 + }
1892 +
1893 + public static function getPluginName()
1894 + {
1895 + return 'King Addons';
1896 + }
1897 +
1898 + public static function getAnimationTimings(): array
1899 + {
1900 + /** @noinspection DuplicatedCode */
1901 + $timings = [
1902 + 'ease-default' => 'Default',
1903 + 'linear' => 'Linear',
1904 + 'ease-in' => 'Ease In',
1905 + 'ease-out' => 'Ease Out',
1906 + 'pro-eio' => 'EI Out (Pro)',
1907 + 'pro-eiqd' => 'EI Quad (Pro)',
1908 + 'pro-eicb' => 'EI Cubic (Pro)',
1909 + 'pro-eiqrt' => 'EI Quart (Pro)',
1910 + 'pro-eiqnt' => 'EI Quint (Pro)',
1911 + 'pro-eisn' => 'EI Sine (Pro)',
1912 + 'pro-eiex' => 'EI Expo (Pro)',
1913 + 'pro-eicr' => 'EI Circ (Pro)',
1914 + 'pro-eibk' => 'EI Back (Pro)',
1915 + 'pro-eoqd' => 'EO Quad (Pro)',
1916 + 'pro-eocb' => 'EO Cubic (Pro)',
1917 + 'pro-eoqrt' => 'EO Quart (Pro)',
1918 + 'pro-eoqnt' => 'EO Quint (Pro)',
1919 + 'pro-eosn' => 'EO Sine (Pro)',
1920 + 'pro-eoex' => 'EO Expo (Pro)',
1921 + 'pro-eocr' => 'EO Circ (Pro)',
1922 + 'pro-eobk' => 'EO Back (Pro)',
1923 + 'pro-eioqd' => 'EIO Quad (Pro)',
1924 + 'pro-eiocb' => 'EIO Cubic (Pro)',
1925 + 'pro-eioqrt' => 'EIO Quart (Pro)',
1926 + 'pro-eioqnt' => 'EIO Quint (Pro)',
1927 + 'pro-eiosn' => 'EIO Sine (Pro)',
1928 + 'pro-eioex' => 'EIO Expo (Pro)',
1929 + 'pro-eiocr' => 'EIO Circ (Pro)',
1930 + 'pro-eiobk' => 'EIO Back (Pro)',
1931 + ];
1932 +
1933 + if (king_addons_freemius()->can_use_premium_code__premium_only()) {
1934 + /** @noinspection DuplicatedCode */
1935 + $timings = [
1936 + 'ease-default' => 'Default',
1937 + 'linear' => 'Linear',
1938 + 'ease-in' => 'Ease In',
1939 + 'ease-out' => 'Ease Out',
1940 + 'ease-in-out' => 'Ease In Out',
1941 + 'ease-in-quad' => 'Ease In Quad',
1942 + 'ease-in-cubic' => 'Ease In Cubic',
1943 + 'ease-in-quart' => 'Ease In Quart',
1944 + 'ease-in-quint' => 'Ease In Quint',
1945 + 'ease-in-sine' => 'Ease In Sine',
1946 + 'ease-in-expo' => 'Ease In Expo',
1947 + 'ease-in-circ' => 'Ease In Circ',
1948 + 'ease-in-back' => 'Ease In Back',
1949 + 'ease-out-quad' => 'Ease Out Quad',
1950 + 'ease-out-cubic' => 'Ease Out Cubic',
1951 + 'ease-out-quart' => 'Ease Out Quart',
1952 + 'ease-out-quint' => 'Ease Out Quint',
1953 + 'ease-out-sine' => 'Ease Out Sine',
1954 + 'ease-out-expo' => 'Ease Out Expo',
1955 + 'ease-out-circ' => 'Ease Out Circ',
1956 + 'ease-out-back' => 'Ease Out Back',
1957 + 'ease-in-out-quad' => 'Ease In Out Quad',
1958 + 'ease-in-out-cubic' => 'Ease In Out Cubic',
1959 + 'ease-in-out-quart' => 'Ease In Out Quart',
1960 + 'ease-in-out-quint' => 'Ease In Out Quint',
1961 + 'ease-in-out-sine' => 'Ease In Out Sine',
1962 + 'ease-in-out-expo' => 'Ease In Out Expo',
1963 + 'ease-in-out-circ' => 'Ease In Out Circ',
1964 + 'ease-in-out-back' => 'Ease In Out Back',
1965 + ];
1966 + }
1967 +
1968 + return $timings;
1969 + }
1970 +
1971 + public static function getAnimationTimingsConditionsPro()
1972 + {
1973 + return [
1974 + 'pro-eibk',
1975 + 'pro-eicb',
1976 + 'pro-eicr',
1977 + 'pro-eiex',
1978 + 'pro-eio',
1979 + 'pro-eiobk',
1980 + 'pro-eiocb',
1981 + 'pro-eiocr',
1982 + 'pro-eioex',
1983 + 'pro-eioqd',
1984 + 'pro-eioqnt',
1985 + 'pro-eioqrt',
1986 + 'pro-eiosn',
1987 + 'pro-eiqd',
1988 + 'pro-eiqnt',
1989 + 'pro-eiqrt',
1990 + 'pro-eisn',
1991 + 'pro-eobk',
1992 + 'pro-eocb',
1993 + 'pro-eocr',
1994 + 'pro-eoex',
1995 + 'pro-eoqd',
1996 + 'pro-eoqnt',
1997 + 'pro-eoqrt',
1998 + 'pro-eosn',
1999 + ];
2000 + }
2001 +
2002 + public static function isBlogArchive()
2003 + {
2004 + return (
2005 + is_home()
2006 + && '0' === get_option('page_on_front')
2007 + && '0' === get_option('page_for_posts')
2008 + ) || (
2009 + intval(get_option('page_for_posts')) === get_queried_object_id()
2010 + && !is_404()
2011 + );
2012 + }
2013 +
2014 + public static function filterOembedResults($html)
2015 + {
2016 + preg_match('/src="([^"]+)"/', $html, $m);
2017 + return $m[1] . '&auto_play=true';
2018 + }
2019 +
2020 + public static function getWooCommerceTaxonomies()
2021 + {
2022 + $filtered = array_filter(get_object_taxonomies('product'), fn($t) => get_taxonomy($t)->show_ui);
2023 + return array_combine($filtered, array_map(fn($t) => get_taxonomy($t)->label, $filtered));
2024 + }
2025 +
2026 + public static function getCustomMetaKeysTaxonomies()
2027 + {
2028 + $data = [];
2029 + $tax_types = Core::getCustomTypes('tax', false);
2030 +
2031 + foreach ($tax_types as $taxonomy_slug => $post_type_name) {
2032 + $meta_keys = [];
2033 + foreach (get_terms($taxonomy_slug) as $tax) {
2034 + $keys = array_keys(get_term_meta($tax->term_id));
2035 + $keys = array_filter($keys, fn($key) => '_' !== $key[0]);
2036 + $meta_keys = array_merge($meta_keys, $keys);
2037 + }
2038 + $data[$taxonomy_slug] = array_unique($meta_keys);
2039 + }
2040 +
2041 +
2042 + $merged = call_user_func_array('array_merge', array_values($data));
2043 + $merged_meta_keys = array_values(array_unique($merged));
2044 +
2045 + $options = array_combine($merged_meta_keys, $merged_meta_keys);
2046 +
2047 + return [$data, $options];
2048 + }
2049 +
2050 + public static function getMailchimpLists()
2051 + {
2052 + $api_key = get_option('king_addons_mailchimp_api_key', '');
2053 + $mailchimp_list = ['def' => esc_html__('Select List', 'king-addons')];
2054 +
2055 + if (!$api_key) {
2056 + return $mailchimp_list;
2057 + }
2058 +
2059 + $parts = explode('-', (string) $api_key);
2060 + if (count($parts) < 2 || '' === $parts[1]) {
2061 + return $mailchimp_list;
2062 + }
2063 +
2064 + $url = 'https://' . $parts[1] . '.api.mailchimp.com/3.0/lists/';
2065 + $response = wp_remote_get($url, [
2066 + 'headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $api_key)]
2067 + ]);
2068 +
2069 + $body = json_decode(wp_remote_retrieve_body($response));
2070 + if (!empty($body->lists)) {
2071 + foreach ($body->lists as $list) {
2072 + $mailchimp_list[$list->id] = $list->name . ' (' . $list->stats->member_count . ')';
2073 + }
2074 + }
2075 +
2076 + return $mailchimp_list;
2077 + }
2078 +
2079 + public static function getMailchimpGroups()
2080 + {
2081 + $apiKey = (string) get_option('king_addons_mailchimp_api_key', '');
2082 + $groups = ['def' => 'Select Group'];
2083 +
2084 + if ('' === $apiKey || false === strpos($apiKey, '-')) {
2085 + return $groups;
2086 + }
2087 +
2088 + $dc = substr($apiKey, strpos($apiKey, '-') + 1);
2089 + if ('' === $dc) {
2090 + return $groups;
2091 + }
2092 +
2093 + $domain = 'https://' . $dc . '.api.mailchimp.com/3.0/';
2094 + $authArgs = ['headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $apiKey)]];
2095 + $mailchimpIDs = Core::getMailchimpLists();
2096 +
2097 + foreach ($mailchimpIDs as $audience => $ignore) {
2098 + if ($audience === 'def') {
2099 + continue;
2100 + }
2101 +
2102 + $cats_res = wp_remote_get("{$domain}lists/$audience/interest-categories", $authArgs);
2103 + if (is_wp_error($cats_res)) {
2104 + continue;
2105 + }
2106 + $cats = json_decode((string) wp_remote_retrieve_body($cats_res))->categories ?? [];
2107 +
2108 + foreach ($cats as $cat) {
2109 + $interests_res = wp_remote_get("{$domain}lists/$audience/interest-categories/$cat->id/interests", $authArgs);
2110 + if (is_wp_error($interests_res)) {
2111 + continue;
2112 + }
2113 + $interests = json_decode((string) wp_remote_retrieve_body($interests_res))->interests ?? [];
2114 +
2115 + foreach ($interests as $int) {
2116 + $groups[$int->id] = $int->name;
2117 + }
2118 + }
2119 + }
2120 +
2121 + return $groups;
2122 + }
2123 +
2124 + public static function getShopURL($settings)
2125 + {
2126 + global $wp;
2127 + $url = ('' === get_option('permalink_structure'))
2128 + ? remove_query_arg(['page', 'paged'], add_query_arg($wp->query_string, '', home_url($wp->request)))
2129 + : preg_replace('%/page/[0-9]+%', '', home_url(trailingslashit($wp->request)));
2130 + $url = add_query_arg('kingaddonsfilters', '', $url);
2131 + $single_params = [
2132 + 'min_price' => true,
2133 + 'max_price' => true,
2134 + 'orderby' => false,
2135 + 'psearch' => false,
2136 + 'filter_product_cat' => false,
2137 + 'filter_product_tag' => false,
2138 + 'filter_rating' => false,
2139 + ];
2140 + foreach ($single_params as $param => $needs_clean) {
2141 + if (isset($_GET[$param])) {
2142 + $value = wp_unslash($_GET[$param]);
2143 + $value = $needs_clean ? wc_clean($value) : $value;
2144 + $url = add_query_arg($param, $value, $url);
2145 + }
2146 + }
2147 + /** @noinspection DuplicatedCode */
2148 + if ($chosen_attrs = WC()->query->get_layered_nav_chosen_attributes()) {
2149 + foreach ($chosen_attrs as $name => $data) {
2150 + $filter_name = wc_attribute_taxonomy_slug($name);
2151 + if (!empty($data['terms'])) {
2152 + $url = add_query_arg('filter_' . $filter_name, implode(',', $data['terms']), $url);
2153 + }
2154 + if (!empty($settings)) {
2155 + if ('or' === $settings['tax_query_type'] || isset($_GET['query_type_' . $filter_name])) {
2156 + $url = add_query_arg('query_type_' . $filter_name, 'or', $url);
2157 + }
2158 + }
2159 + }
2160 + }
2161 + return $url;
2162 + }
2163 +
2164 + public static function getClientIP()
2165 + {
2166 + $server_ip_keys = [
2167 + 'HTTP_CLIENT_IP',
2168 + 'HTTP_X_FORWARDED_FOR',
2169 + 'HTTP_X_FORWARDED',
2170 + 'HTTP_X_CLUSTER_CLIENT_IP',
2171 + 'HTTP_FORWARDED_FOR',
2172 + 'HTTP_FORWARDED',
2173 + 'REMOTE_ADDR',
2174 + ];
2175 +
2176 + foreach ($server_ip_keys as $key) {
2177 + if (isset($_SERVER[$key])) {
2178 + $ip = wp_kses_post_deep(wp_unslash($_SERVER[$key]));
2179 + if (filter_var($ip, FILTER_VALIDATE_IP)) {
2180 + return $ip;
2181 + }
2182 + }
2183 + }
2184 +
2185 + return '127.0.0.1';
2186 + }
2187 +
2188 + public static function getCustomMetaKeys()
2189 + {
2190 + // Get all custom post types (slug => name).
2191 + $post_types = Core::getCustomTypes('post', false);
2192 +
2193 + // Build $data with each post type's unique custom meta keys (excluding keys beginning with "_").
2194 + $data = array_combine(
2195 + array_keys($post_types),
2196 + array_map(function ($slug) {
2197 + $keys = [];
2198 + foreach (get_posts(['post_type' => $slug, 'posts_per_page' => -1]) as $post) {
2199 + // get_post_custom_keys can return null, so cast to array:
2200 + foreach ((array) get_post_custom_keys($post->ID) as $meta_key) {
2201 + // Exclude protected keys (those beginning with "_").
2202 + if ($meta_key[0] !== '_') {
2203 + $keys[] = $meta_key;
2204 + }
2205 + }
2206 + }
2207 + return array_values(array_unique($keys));
2208 + }, array_keys($post_types))
2209 + );
2210 +
2211 + // Flatten all meta keys across all post types, remove duplicates, and reindex.
2212 + $merged_meta_keys = array_values(array_unique(array_merge([], ...$data)));
2213 +
2214 + // Create an associative array where key == value (for convenient dropdowns, etc.).
2215 + $options = array_combine($merged_meta_keys, $merged_meta_keys);
2216 +
2217 + // Return both the per-post-type data and the merged, deduplicated options.
2218 + return [$data, $options];
2219 + }
2220 +
2221 + public function enqueueLightboxDynamicStyles()
2222 + {
2223 + wp_register_style('king-addons-lightbox-dynamic-style', false);
2224 + wp_enqueue_style('king-addons-lightbox-dynamic-style');
2225 +
2226 + $bg = esc_html(get_option('king_addons_lightbox_bg_color', 'rgba(0,0,0,0.6)'));
2227 + $toolbar = esc_html(get_option('king_addons_lightbox_toolbar_color', 'rgba(0,0,0,0.8)'));
2228 + $caption = esc_html(get_option('king_addons_lightbox_caption_color', 'rgba(0,0,0,0.8)'));
2229 + $gallery = esc_html(get_option('king_addons_lightbox_gallery_color', '#444444'));
2230 + $progress_bar = esc_html(get_option('king_addons_lightbox_pb_color', '#8a8a8a'));
2231 + $ui_color = esc_html(get_option('king_addons_lightbox_ui_color', '#efefef'));
2232 + $icon_size = floatval(get_option('king_addons_lightbox_icon_size', 20));
2233 + $icon_size_big = $icon_size + 4;
2234 + $ui_hover = esc_html(get_option('king_addons_lightbox_ui_hover_color', '#ffffff'));
2235 + $text_color = esc_html(get_option('king_addons_lightbox_text_color', '#efefef'));
2236 + $text_size = esc_html(get_option('king_addons_lightbox_text_size', 14));
2237 + $arrow_size = esc_html(get_option('king_addons_lightbox_arrow_size', 35));
2238 +
2239 + $custom_css = "#lg-counter { color: $text_color !important; font-size: {$text_size}px !important; opacity: 0.9; } .lg-backdrop { background-color: $bg !important; } .lg-dropdown:after { border-bottom-color: $toolbar !important; } .lg-icon { color: $ui_color !important; font-size: {$icon_size}px !important; background-color: transparent !important; } .lg-icon.lg-toogle-thumb { font-size: {$icon_size_big}px !important; } .lg-icon:hover, .lg-dropdown-text:hover { color: $ui_hover !important; } .lg-prev, .lg-next { font-size: {$arrow_size}px !important; } .lg-progress { background-color: $progress_bar !important; } .lg-sub-html { background-color: $caption !important; } .lg-sub-html, .lg-dropdown-text { color: $text_color !important; font-size: {$text_size}px !important; } .lg-thumb-item { border-radius: 0 !important; border: none !important; opacity: 0.5; } .lg-thumb-item.active { opacity: 1; } .lg-thumb-outer, .lg-progress-bar { background-color: $gallery !important; } .lg-thumb-outer { padding: 0 10px; } .lg-toolbar, .lg-dropdown { background-color: $toolbar !important; }";
2240 +
2241 + wp_add_inline_style('king-addons-lightbox-dynamic-style', $custom_css);
2242 + }
2243 +
2244 + /**
2245 + * Enqueues the AI button injection script in the Elementor editor panel.
2246 + *
2247 + * @return void
2248 + */
2249 + public function enqueueAiFieldScript(): void
2250 + {
2251 + $ai_options = get_option('king_addons_ai_options', []);
2252 +
2253 + wp_enqueue_script(
2254 + 'king-addons-ai-field',
2255 + KING_ADDONS_URL . 'includes/admin/js/ai-textfield.js',
2256 + ['jquery', 'elementor-editor'],
2257 + KING_ADDONS_VERSION,
2258 + true
2259 + );
2260 +
2261 + // Localize for AJAX
2262 + wp_localize_script(
2263 + 'king-addons-ai-field',
2264 + 'KingAddonsAiField',
2265 + [
2266 + 'ajax_url' => admin_url('admin-ajax.php'),
2267 + 'generate_nonce' => wp_create_nonce('king_addons_ai_generate_nonce'),
2268 + 'change_nonce' => wp_create_nonce('king_addons_ai_change_nonce'),
2269 + 'generate_action' => 'king_addons_ai_generate_text',
2270 + 'change_action' => 'king_addons_ai_change_text',
2271 + 'icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai.svg',
2272 + 'rewrite_icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai-refresh.svg',
2273 + 'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
2274 + 'plugin_url' => KING_ADDONS_URL,
2275 + 'is_pro' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
2276 + 'premium_active' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
2277 + 'translator_enabled' => isset($ai_options['enable_ai_page_translator']) ? (bool) $ai_options['enable_ai_page_translator'] : true,
2278 + // Editor prompts name the configured provider rather than always OpenAI.
2279 + 'provider' => \King_Addons\AI_Provider::getProvider(),
2280 + 'provider_label' => \King_Addons\AI_Provider::getLabel(),
2281 + 'api_keys_url' => \King_Addons\AI_Provider::getApiKeysUrl(),
2282 + 'api_keys_label' => \King_Addons\AI_Provider::isOpenRouter()
2283 + ? esc_html__('OpenRouter Keys', 'king-addons')
2284 + : esc_html__('OpenAI Platform', 'king-addons'),
2285 + 'setup_billing_note' => \King_Addons\AI_Provider::isOpenRouter()
2286 + ? esc_html__('Free models work without adding any credit.', 'king-addons')
2287 + : esc_html__('and top up your OpenAI account balance by at least $5', 'king-addons'),
2288 + 'setup_cost_note' => \King_Addons\AI_Provider::isOpenRouter()
2289 + ? esc_html__('Free models cost nothing. Paid models cost pennies (about $0.01 per full page).', 'king-addons')
2290 + : esc_html__('Processing a page costs pennies (about $0.01 per full page).', 'king-addons'),
2291 + /**
2292 + * Before either provider has a key there is no "configured
2293 + * provider" to describe - the setting is just its default. The
2294 + * setup dialog then has to present the choice instead of sending
2295 + * everyone to OpenAI.
2296 + */
2297 + 'has_any_key' => (
2298 + '' !== \King_Addons\AI_Provider::getApiKey(\King_Addons\AI_Provider::OPENAI)
2299 + || '' !== \King_Addons\AI_Provider::getApiKey(\King_Addons\AI_Provider::OPENROUTER)
2300 + ),
2301 + 'setup_providers' => [
2302 + [
2303 + 'name' => esc_html__('OpenRouter', 'king-addons'),
2304 + 'url' => \King_Addons\AI_Provider::getApiKeysUrl(\King_Addons\AI_Provider::OPENROUTER),
2305 + 'note' => esc_html__('has free models, no credit needed to start', 'king-addons'),
2306 + ],
2307 + [
2308 + 'name' => esc_html__('OpenAI', 'king-addons'),
2309 + 'url' => \King_Addons\AI_Provider::getApiKeysUrl(\King_Addons\AI_Provider::OPENAI),
2310 + 'note' => esc_html__('needs at least $5 on your account balance', 'king-addons'),
2311 + ],
2312 + ],
2313 + 'setup_cost_note_neutral' => esc_html__('With a free OpenRouter model, nothing. On a paid model it is pennies (about $0.01 per full page).', 'king-addons'),
2314 + 'missing_key_message' => sprintf(
2315 + /* translators: %s: provider name */
2316 + esc_html__('%s API key is missing or invalid. Please configure your API key in AI Settings.', 'king-addons'),
2317 + \King_Addons\AI_Provider::getLabel()
2318 + ),
2319 + ]
2320 + );
2321 + }
2322 +
2323 + /**
2324 + * Enqueues the AI image generation field script in the Elementor editor panel.
2325 + *
2326 + * @return void
2327 + */
2328 + public function enqueueAiImageGenerationScript(): void
2329 + {
2330 + wp_enqueue_script(
2331 + 'king-addons-ai-image-field',
2332 + KING_ADDONS_URL . 'includes/admin/js/ai-imagefield.js',
2333 + ['jquery', 'elementor-editor'],
2334 + KING_ADDONS_VERSION,
2335 + true
2336 + );
2337 +
2338 + // Localize for AJAX
2339 + wp_localize_script(
2340 + 'king-addons-ai-image-field',
2341 + 'KingAddonsAiImageField',
2342 + [
2343 + 'ajax_url' => admin_url('admin-ajax.php'),
2344 + 'generate_nonce' => wp_create_nonce('king_addons_ai_generate_image_nonce'),
2345 + 'generate_action' => 'king_addons_ai_generate_image',
2346 + 'image_model' => \King_Addons\AI_Provider::getImageModel(),
2347 + // The editor builds its model dropdown from this list, so the
2348 + // options follow whichever provider is configured.
2349 + 'image_models' => array_map(
2350 + static function ($model) {
2351 + return ['value' => $model['id'], 'label' => $model['label']];
2352 + },
2353 + \King_Addons\AI_Provider::getModelsFor('image')
2354 + ),
2355 + 'provider' => \King_Addons\AI_Provider::getProvider(),
2356 + 'icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai.svg',
2357 + 'rewrite_icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai-refresh.svg',
2358 + 'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
2359 + 'plugin_url' => KING_ADDONS_URL,
2360 + 'missing_key_message' => sprintf(
2361 + /* translators: %s: provider name */
2362 + esc_html__('%s API key is missing or invalid. Please configure your API key in AI Settings.', 'king-addons'),
2363 + \King_Addons\AI_Provider::getLabel()
2364 + ),
2365 + ]
2366 + );
2367 + }
2368 +
2369 + /**
2370 + * Enqueues the styles for AI prompt UI in the Elementor editor panel.
2371 + *
2372 + * @return void
2373 + */
2374 + public function enqueueAiFieldStyles(): void
2375 + {
2376 + // Enqueue CSS for the AI prompt UI
2377 + wp_enqueue_style(
2378 + 'king-addons-ai-field-css',
2379 + KING_ADDONS_URL . 'includes/admin/css/ai-textfield.css',
2380 + [],
2381 + KING_ADDONS_VERSION
2382 + );
2383 + }
2384 +
2385 + /**
2386 + * Enqueues styles for AI Image Generation UI in the Elementor editor panel.
2387 + *
2388 + * @return void
2389 + */
2390 + public function enqueueAiImageFieldStyles(): void
2391 + {
2392 + wp_enqueue_style(
2393 + 'king-addons-ai-imagefield',
2394 + KING_ADDONS_URL . 'includes/admin/css/ai-imagefield.css',
2395 + [],
2396 + KING_ADDONS_VERSION
2397 + );
2398 + }
2399 +
2400 + /**
2401 + * Enqueues the AI page translator script in the Elementor editor panel.
2402 + *
2403 + * @return void
2404 + */
2405 + public function enqueueAiTranslatorScript(): void
2406 + {
2407 + // Check if AI Page Translator is enabled in settings
2408 + $ai_options = get_option('king_addons_ai_options', []);
2409 + $translator_enabled = isset($ai_options['enable_ai_page_translator']) ? (bool) $ai_options['enable_ai_page_translator'] : true;
2410 +
2411 + if (!$translator_enabled) {
2412 + return; // Don't load script if translator is disabled
2413 + }
2414 +
2415 + wp_enqueue_script(
2416 + 'king-addons-ai-translator',
2417 + KING_ADDONS_URL . 'includes/admin/js/ai-page-translator.js',
2418 + ['jquery', 'elementor-editor'],
2419 + KING_ADDONS_VERSION,
2420 + true
2421 + );
2422 +
2423 + // Note: Using existing KingAddonsAiField localization
2424 + // The translator script will use the same AJAX endpoints and settings
2425 + // No need for separate localization as it reuses existing AI infrastructure
2426 + }
2427 +
2428 + /**
2429 + * Render an attachment through an Elementor image-size group control.
2430 + *
2431 + * Group_Control_Image_Size::get_attachment_image_html() takes a *key into
2432 + * $settings* as its third argument, not an attachment ID. Handing it an ID
2433 + * makes Elementor read $settings[<id>], which raises two PHP warnings and
2434 + * returns nothing, so callers silently lose the image.
2435 + *
2436 + * @param array<string, mixed> $settings Widget settings; carries "<$size_key>_size" and friends.
2437 + * @param string $size_key Name of the image-size group control.
2438 + * @param int $attachment_id Attachment to render.
2439 + *
2440 + * @return string Image HTML, or an empty string when it cannot be rendered.
2441 + */
2442 + /**
2443 + * Show the person building the page why a widget rendered nothing.
2444 + *
2445 + * Only ever printed inside the Elementor editor, so the public page keeps
2446 + * rendering exactly what it rendered before.
2447 + *
2448 + * @param string $message Plain text explaining what the widget still needs.
2449 + */
2450 + public static function renderEditorHint(string $message): void
2451 + {
2452 + if (!Plugin::$instance->editor->is_edit_mode()) {
2453 + return;
2454 + }
2455 +
2456 + $style = 'display:block;padding:12px 14px;border:1px dashed #c3c4c7;border-radius:4px;'
2457 + . 'background:#f6f7f7;color:#50575e;font-size:13px;line-height:1.5;';
2458 +
2459 + echo '<div class="king-addons-editor-hint" style="' . esc_attr($style) . '">'
2460 + . esc_html($message) . '</div>';
2461 + }
2462 +
2463 + /**
2464 + * A setting written straight into a JavaScript object literal has to be a
2465 + * number. Elementor stores a number field the user cleared as an empty
2466 + * string, which would emit `slidesPerView: ,` - a syntax error that stops
2467 + * the whole carousel from initialising - or blow up in PHP when the value
2468 + * is used in arithmetic.
2469 + *
2470 + * @param array<string, mixed> $settings Widget settings.
2471 + * @param string $key Setting to read.
2472 + * @param int|float $fallback Value to use when the setting is empty or not a number.
2473 + *
2474 + * @return string Numeric string, safe to interpolate into JS.
2475 + */
2476 + public static function jsNumber(array $settings, string $key, $fallback = 0): string
2477 + {
2478 + $value = $settings[$key] ?? null;
2479 +
2480 + // Slider controls keep their number under 'size'.
2481 + if (is_array($value)) {
2482 + $value = $value['size'] ?? null;
2483 + }
2484 +
2485 + if (!is_numeric($value)) {
2486 + $value = $fallback;
2487 + }
2488 +
2489 + return (string) (0 + $value);
2490 + }
2491 +
2492 + public static function getAttachmentImageHTML(array $settings, string $size_key, int $attachment_id): string
2493 + {
2494 + if ($attachment_id < 1) {
2495 + return '';
2496 + }
2497 +
2498 + $image_key = '__king_addons_image';
2499 + $settings[$image_key] = [
2500 + 'id' => $attachment_id,
2501 + 'url' => wp_get_attachment_image_url($attachment_id, 'full') ?: '',
2502 + ];
2503 +
2504 + return (string) Group_Control_Image_Size::get_attachment_image_html($settings, $size_key, $image_key);
2505 + }
2506 +}
2507 +
2508 +Core::instance();