PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.87
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.87
51.1.87 51.1.86 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 All 41 releases
king-addons / includes / Core.php

Core.php in King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder 51.1.87, at includes/Core.php

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