PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.82
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.82
51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 51.1.51 All 36 releases
king-addons / includes / Core.php

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

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