PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.87
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.87
51.1.87 51.1.86 51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 All 41 releases
← All changes | includes/Core.php +1492 -99 51.1.14 → 51.1.87 View file →
@@ -8,8 +8,10 @@
8 8
9 9 use Elementor\Plugin;
10 10 use Elementor\Widgets_Manager;
11 11 use Elementor\Controls_Manager;
12 +use Elementor\Group_Control_Image_Size;
13 +use King_Addons\Wishlist\Wishlist_Module;
12 14
13 15 if (!defined('ABSPATH')) {
14 16 exit; // Exit if accessed directly.
15 17 }
@@ -23,8 +25,15 @@
23 25 */
24 26 private static ?Core $_instance = null;
25 27
26 28 /**
29 + * Wishlist module instance.
30 + *
31 + * @var Wishlist_Module|null
32 + */
33 + private ?Wishlist_Module $wishlist_module = null;
34 +
35 + /**
27 36 * Instance
28 37 *
29 38 * Ensures only one instance of the class is loaded or can be loaded.
30 39 *
@@ -39,8 +48,53 @@
39 48 return self::$_instance;
40 49 }
41 50
42 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 + /**
43 97 * Constructor
44 98 *
45 99 * Perform some compatibility checks to make sure basic requirements are meet.
46 100 * If all compatibility checks pass, initialize the functionality.
@@ -50,8 +104,10 @@
50 104 public function __construct()
51 105 {
52 106 require_once(KING_ADDONS_PATH . 'includes/ModulesMap.php');
53 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();
54 110
55 111 if ($this->hasElementorCompatibility()) {
56 112
57 113 // Initial requirements check
@@ -57,39 +113,224 @@
57 113 // Initial requirements check
58 114 require_once(KING_ADDONS_PATH . 'includes/helpers/Check_Requirements/Check_Requirements.php');
59 115
60 116 // Templates Catalog
61 - if (KING_ADDONS_EXT_TEMPLATES_CATALOG) {
117 + if ($this->isExtensionEnabled('templates-catalog', 'KING_ADDONS_EXT_TEMPLATES_CATALOG')) {
62 118 require_once(KING_ADDONS_PATH . 'includes/TemplatesMap.php');
63 119 require_once(KING_ADDONS_PATH . 'includes/extensions/Templates/CollectionsMap.php');
64 120 require_once(KING_ADDONS_PATH . 'includes/extensions/Templates/Templates.php');
65 -
121 +
66 122 // Template Catalog Button for Elementor Editor
67 123 require_once(KING_ADDONS_PATH . 'includes/extensions/Template_Catalog_Button/Template_Catalog_Button.php');
68 124 Template_Catalog_Button::instance();
69 125 }
70 126
71 - // Sections Catalog
72 - // if (KING_ADDONS_EXT_SECTIONS_CATALOG) {
73 - // require_once(KING_ADDONS_PATH . 'includes/extensions/Sections_Catalog/Sections_Catalog.php');
74 - // Sections_Catalog::instance();
75 - // }
76 -
77 127 // Header & Footer Builder
78 - if (KING_ADDONS_EXT_HEADER_FOOTER_BUILDER) {
128 + if ($this->isExtensionEnabled('header-footer-builder', 'KING_ADDONS_EXT_HEADER_FOOTER_BUILDER')) {
79 129 require_once(KING_ADDONS_PATH . 'includes/extensions/Header_Footer_Builder/Header_Footer_Builder.php');
80 130 Header_Footer_Builder::instance();
81 131 }
82 132
83 133 // Popup Builder
84 - if (KING_ADDONS_EXT_POPUP_BUILDER) {
134 + if ($this->isExtensionEnabled('popup-builder', 'KING_ADDONS_EXT_POPUP_BUILDER')) {
85 135 require_once(KING_ADDONS_PATH . 'includes/extensions/Popup_Builder/Popup_Builder.php');
86 136 Popup_Builder::instance();
87 137 }
88 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 +
89 323 // Admin
90 324 require_once(KING_ADDONS_PATH . 'includes/Admin.php');
91 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 +
92 333 // Additional - Controls
93 334 require_once(KING_ADDONS_PATH . 'includes/controls/Ajax_Select2/Ajax_Select2.php');
94 335 require_once(KING_ADDONS_PATH . 'includes/controls/Ajax_Select2/Ajax_Select2_API.php');
95 336 require_once(KING_ADDONS_PATH . 'includes/controls/Animations/Animations.php');
@@ -99,8 +340,9 @@
99 340 require_once(KING_ADDONS_PATH . 'includes/widgets/Search/Search_Ajax.php');
100 341 require_once(KING_ADDONS_PATH . 'includes/widgets/MailChimp/MailChimp_Ajax.php');
101 342
102 343 // Additional - Grids, Magazine Grid
344 + require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Grid_Ajax_Security.php');
103 345 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Filter_Posts_Ajax.php');
104 346 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Filter_WooCommerce_Products_Ajax.php');
105 347 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Post_Likes_Ajax.php');
106 348
@@ -105,8 +347,20 @@
105 347 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Post_Likes_Ajax.php');
106 348
107 349 // Additional - Form Builder
108 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');
109 363 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Create_Submission.php');
110 364 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Send_Email.php');
111 365 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Send_Webhook.php');
112 366 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Subscribe_Mailchimp.php');
@@ -117,15 +371,42 @@
117 371 }
118 372
119 373 // ADDITIONAL CLASSES
120 374
121 - // Alt Text Generator for Media Library
122 - require_once(KING_ADDONS_PATH . 'includes/extensions/alt-text-generator/Alt_Text_Generator.php');
123 - new Alt_Text_Generator();
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 + }
124 382
125 - // Dynamic Posts Grid AJAX Helper
383 + // Wishlist module - check extension toggle
384 + if ($this->isExtensionEnabled('wishlist', 'KING_ADDONS_EXT_WISHLIST')) {
385 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_DB.php';
386 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Session.php';
387 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Settings.php';
388 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Service.php';
389 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Renderer.php';
390 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Frontend.php';
391 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_WooCommerce.php';
392 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Module.php';
393 + $this->wishlist_module = new Wishlist_Module();
394 + } else {
395 + // Extension off: still expire leftover guest cookies so public caches work.
396 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Session.php';
397 + add_action('send_headers', static function () {
398 + if (is_admin()) {
399 + return;
400 + }
401 + \King_Addons\Wishlist\Wishlist_Session::expire_if_present();
402 + });
403 + }
404 +
405 + // Dynamic Posts Grid AJAX Helper - Initialize regardless of Elementor compatibility
406 + // This is needed for AJAX functionality to work even when PRO version is disabled
126 407 require_once(KING_ADDONS_PATH . 'includes/helpers/Dynamic_Posts_Grid_Ajax.php');
127 - new Dynamic_Posts_Grid_Ajax();
408 + \King_Addons\Dynamic_Posts_Grid_Ajax::get_instance();
128 409
129 410 // Screenshot Generator
130 411 // require_once(KING_ADDONS_PATH . 'includes/extensions/Templates/screenshot-generator.php');
131 412 // require_once(KING_ADDONS_PATH . 'includes/extensions/Templates/screenshot-admin-page.php');
@@ -137,12 +418,17 @@
137 418
138 419 add_action('elementor/init', [$this, 'initElementor']);
139 420
140 421 add_action('elementor/elements/categories_registered', [$this, 'addWidgetCategory']);
422 + // Elementor promotes Atomic / Custom Widget / Pro after that hook, so the
423 + // panel order is applied again once categories are fully initialised.
424 + add_action('elementor/init', [$this, 'reorderWidgetCategoriesLate'], 100);
425 + add_action('elementor/editor/before_enqueue_scripts', [$this, 'reorderWidgetCategoriesLate'], 1);
426 + add_filter('elementor/document/config', [$this, 'filterDocumentPanelCategories'], 1, 2);
141 427 add_action('elementor/controls/controls_registered', [$this, 'registerControls']);
142 428
143 429 self::enableFeatures();
144 -
430 +
145 431 // Load and register AJAX handlers for Login Register Form widget
146 432 require_once(KING_ADDONS_PATH . 'includes/widgets/Login_Register_Form/Login_Register_Form_Ajax.php');
147 433 require_once(KING_ADDONS_PATH . 'includes/widgets/Login_Register_Form/User_Profile_Fields.php');
148 434 require_once(KING_ADDONS_PATH . 'includes/widgets/Login_Register_Form/Email_Handler.php');
@@ -152,19 +438,23 @@
152 438 add_action('wp_ajax_nopriv_king_addons_user_register', ['King_Addons\Widgets\Login_Register_Form\Login_Register_Form_Ajax', 'handle_register_ajax']);
153 439 add_action('wp_ajax_king_addons_user_register', ['King_Addons\Widgets\Login_Register_Form\Login_Register_Form_Ajax', 'handle_register_ajax']);
154 440 add_action('wp_ajax_nopriv_king_addons_user_lostpassword', ['King_Addons\Widgets\Login_Register_Form\Login_Register_Form_Ajax', 'handle_lostpassword_ajax']);
155 441 add_action('wp_ajax_king_addons_user_lostpassword', ['King_Addons\Widgets\Login_Register_Form\Login_Register_Form_Ajax', 'handle_lostpassword_ajax']);
156 -
442 +
157 443 // Initialize user profile fields
158 444 \King_Addons\Widgets\Login_Register_Form\User_Profile_Fields::init();
159 -
445 +
160 446 // Initialize social login handler
161 447 \King_Addons\Widgets\Login_Register_Form\Social_Login_Handler::init();
162 -
163 - // Initialize Security Dashboard for admins
448 +
449 + // Initialize Security Dashboard for admins (only if Login Register Form widget is enabled)
164 450 if (is_admin()) {
165 - require_once(KING_ADDONS_PATH . 'includes/widgets/Login_Register_Form/Security_Dashboard.php');
166 - \King_Addons\Widgets\Login_Register_Form\Security_Dashboard::init();
451 + $widget_options = get_option('king_addons_options', []);
452 + $login_form_enabled = !isset($widget_options['login-register-form']) || $widget_options['login-register-form'] === 'enabled';
453 + if ($login_form_enabled) {
454 + require_once(KING_ADDONS_PATH . 'includes/widgets/Login_Register_Form/Security_Dashboard.php');
455 + \King_Addons\Widgets\Login_Register_Form\Security_Dashboard::init();
456 + }
167 457 }
168 458
169 459 new Admin();
170 460
@@ -173,30 +463,46 @@
173 463
174 464 // Notice - Upgrade Suggestion
175 465 if (!king_addons_freemius()->can_use_premium_code__premium_only()) {
176 466 add_action('wp_ajax_king_addons_premium_notice_dismiss', [$this, 'king_addons_premium_notice_dismiss_callback']);
177 - add_action('admin_notices', [$this, 'showNoticeUpgrade']);
467 +
468 + /**
469 + * The previous notice is kept untouched in showNoticeUpgrade() as a fallback.
470 + * To go back to it, either define KING_ADDONS_UPGRADE_NOTICE_LEGACY as true
471 + * in wp-config.php, or return true from this filter.
472 + */
473 + $use_legacy_notice = defined('KING_ADDONS_UPGRADE_NOTICE_LEGACY')
474 + ? (bool) KING_ADDONS_UPGRADE_NOTICE_LEGACY
475 + : false;
476 + $use_legacy_notice = (bool) apply_filters('king_addons/upgrade_notice/use_legacy', $use_legacy_notice);
477 +
478 + add_action(
479 + 'admin_notices',
480 + [$this, $use_legacy_notice ? 'showNoticeUpgrade' : 'showNoticeUpgradeV2']
481 + );
178 482 }
179 483
484 + // Dashboard UI settings AJAX handler
485 + add_action('wp_ajax_king_addons_save_dashboard_ui', [$this, 'king_addons_save_dashboard_ui_callback']);
486 +
180 487 // Conditionally enqueue AI text-field enhancement script and styles in Elementor editor
181 488 $ai_options = get_option('king_addons_ai_options', []);
182 489 $enable_ai_text_buttons = isset($ai_options['enable_ai_buttons']) ? (bool) $ai_options['enable_ai_buttons'] : true;
183 - if ( $enable_ai_text_buttons ) {
490 + if ($enable_ai_text_buttons) {
184 491 // Enqueue AI text-field enhancement script
185 - add_action('elementor/editor/after_enqueue_scripts', [ $this, 'enqueueAiFieldScript' ]);
492 + add_action('elementor/editor/after_enqueue_scripts', [$this, 'enqueueAiFieldScript']);
186 493 // Enqueue styles for AI prompt UI
187 - add_action('elementor/editor/after_enqueue_styles', [ $this, 'enqueueAiFieldStyles' ]);
494 + add_action('elementor/editor/after_enqueue_styles', [$this, 'enqueueAiFieldStyles']);
188 495 // Enqueue AI page translator script
189 - add_action('elementor/editor/after_enqueue_scripts', [ $this, 'enqueueAiTranslatorScript' ]);
496 + add_action('elementor/editor/after_enqueue_scripts', [$this, 'enqueueAiTranslatorScript']);
190 497 }
191 498
192 499 $enable_ai_image_generation_button = isset($ai_options['enable_ai_image_generation_button']) ? (bool) $ai_options['enable_ai_image_generation_button'] : true;
193 - if ( $enable_ai_image_generation_button ) {
194 - add_action('elementor/editor/after_enqueue_scripts', [ $this, 'enqueueAiImageGenerationScript' ]);
500 + if ($enable_ai_image_generation_button) {
501 + add_action('elementor/editor/after_enqueue_scripts', [$this, 'enqueueAiImageGenerationScript']);
195 502 // Enqueue styles for AI Image Generation controls
196 - add_action('elementor/editor/after_enqueue_styles', [ $this, 'enqueueAiImageFieldStyles' ]);
503 + add_action('elementor/editor/after_enqueue_styles', [$this, 'enqueueAiImageFieldStyles']);
197 504 }
198 -
199 505 }
200 506 }
201 507
202 508 function king_addons_premium_notice_dismiss_callback()
@@ -204,8 +510,11 @@
204 510 // Check user capabilities
205 511 if (!current_user_can('manage_options')) {
206 512 wp_die();
207 513 }
514 +
515 + check_ajax_referer('king_addons_premium_notice_dismiss', 'nonce');
516 +
208 517 $user_id = get_current_user_id();
209 518 // Save the current time as the last dismissal time for the premium notice
210 519 update_user_meta($user_id, 'king_addons_premium_notice_dismissed_time', time());
211 520 wp_die(); // End AJAX request
@@ -210,8 +519,65 @@
210 519 update_user_meta($user_id, 'king_addons_premium_notice_dismissed_time', time());
211 520 wp_die(); // End AJAX request
212 521 }
213 522
523 + /**
524 + * AJAX callback for saving dashboard UI settings (theme, view toggle)
525 + *
526 + * @since 1.0.0
527 + */
528 + function king_addons_save_dashboard_ui_callback()
529 + {
530 + // Check user capabilities
531 + if (!current_user_can('manage_options')) {
532 + wp_send_json_error(['message' => 'Unauthorized'], 403);
533 + }
534 +
535 + check_ajax_referer('king_addons_dashboard_ui', 'nonce');
536 +
537 + $key = isset($_POST['key']) ? sanitize_text_field($_POST['key']) : '';
538 +
539 + $user_id = get_current_user_id();
540 +
541 + // Validate key
542 + $allowed_keys = ['dark_theme', 'theme_mode', 'show_descriptions'];
543 + if (!in_array($key, $allowed_keys, true)) {
544 + wp_send_json_error(['message' => 'Invalid key'], 400);
545 + }
546 +
547 + // Theme preference is per-user.
548 + if ($key === 'theme_mode') {
549 + $mode = isset($_POST['value']) ? sanitize_key($_POST['value']) : '';
550 + $allowed_modes = ['dark', 'light', 'auto'];
551 + if (!in_array($mode, $allowed_modes, true)) {
552 + wp_send_json_error(['message' => 'Invalid theme mode'], 400);
553 + }
554 +
555 + update_user_meta($user_id, 'king_addons_theme_mode', $mode);
556 +
557 + // Also store as a global option so pages can fall back when user meta isn't set.
558 + update_option('king_addons_theme_mode', $mode);
559 +
560 + wp_send_json_success(['key' => $key, 'value' => $mode]);
561 + }
562 +
563 + // Backward compatibility: old boolean dark_theme maps to theme_mode.
564 + if ($key === 'dark_theme') {
565 + $is_dark = isset($_POST['value']) && $_POST['value'] === '1';
566 + $mode = $is_dark ? 'dark' : 'light';
567 + update_user_meta($user_id, 'king_addons_theme_mode', $mode);
568 + wp_send_json_success(['key' => 'theme_mode', 'value' => $mode]);
569 + }
570 +
571 + // Remaining UI settings are still stored as site option (shared).
572 + $value = isset($_POST['value']) && $_POST['value'] === '1';
573 + $settings = get_option('king_addons_dashboard_ui', []);
574 + $settings[$key] = $value;
575 + update_option('king_addons_dashboard_ui', $settings);
576 +
577 + wp_send_json_success(['key' => $key, 'value' => $value]);
578 + }
579 +
214 580 function showNoticeUpgrade()
215 581 {
216 582 // Check user capabilities; show notice only to administrators as an example
217 583 if (!current_user_can('manage_options')) {
@@ -227,44 +593,300 @@
227 593 if ($last_dismissed && ($now - $last_dismissed) < 604800) {
228 594 // if ($last_dismissed && ($now - $last_dismissed) < 60) {
229 595 return;
230 596 }
231 -?>
597 + ?>
232 598 <div class="king-addons-upgrade-notice notice notice-info is-dismissible"
233 - style="border-left: 4px solid #FF4040;padding: 10px 15px;">
599 + style="border-left: 4px solid #0071e3;padding: 10px 15px;">
234 600 <p style="font-size: 15px; margin:0; display: flex; align-items: center;">
235 - <span>Unlock <strong style="font-weight: 700;">650+</strong> premium templates and <strong
236 - style="font-weight: 700;">200+</strong> advanced features for only $<strong
237 - style="font-weight: 700;">4.99</strong>/month. Upgrade now and boost your website's performance!</span>
601 + <span>
602 + Get <strong style="font-weight: 700;">4,000+</strong> premium templates and sections,
603 + <strong style="font-weight: 700;">100+</strong> widgets,
604 + <strong style="font-weight: 700;">200+</strong> advanced features,
605 + and AI tools for Elementor.
606 + From $<strong style="font-weight: 700;">6.99</strong>/mo.
607 +</span>
238 608 </p>
239 609 <p style="font-size: 14px; opacity: 0.6;">Trusted by 20,000+ users</p>
240 610 <p style="display: flex;">
241 - <a style="font-size: 14px;padding: 4px 22px;display: flex;align-items: center;background-image: linear-gradient(120deg, #A20BD8 0%, #FF4040 100%);border: none;"
242 - href="https://kingaddons.com/pricing?utm_source=kng-notice-offer&utm_medium=plugin&utm_campaign=kng"
243 - class="button button-primary"><img style="margin-right: 7px;width: 15px;height: 15px;"
244 - src="<?php echo esc_url(KING_ADDONS_URL) . 'includes/admin/img/icon-for-admin.svg'; ?>"
245 - alt="<?php echo esc_html__('Upgrade Now', 'king-addons'); ?>">Upgrade Now</a>
246 - <a style="margin-left: 20px;display: flex;align-items: center;"
247 - href="https://kingaddons.com/pricing?utm_source=kng-notice-offer&utm_medium=plugin&utm_campaign=kng"
248 - class="link">Learn More</a>
611 + <a href="https://kingaddons.com/pricing?utm_source=kng-notice-offer&amp;utm_medium=plugin&amp;utm_campaign=kng" target="_blank" class="ka-wb-btn ka-wb-btn-primary" style="
612 + background: #0071e3;
613 + color: #fff;
614 + display: inline-flex;
615 + align-items: center;
616 + justify-content: center;
617 + gap: 6px;
618 + padding: 10px 18px;
619 + font-size: 14px;
620 + font-weight: 500;
621 + text-decoration: none;
622 + border-radius: 980px;
623 + border: none;
624 + cursor: pointer;
625 + transition: all 0.3s cubic-bezier(0.25, 1, 0.5, 1);
626 + white-space: nowrap;
627 + font-family: inherit;
628 +">Upgrade to Pro<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="
629 + width: 16px;
630 + height: 16px;
631 +"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
632 + </a>
633 + <a style="margin-left: 20px;display: flex;align-items: center;font-size: 14px;color: #0071e3;"
634 + href="https://kingaddons.com/pricing?utm_source=kng-notice-offer&utm_medium=plugin&utm_campaign=kng"
635 + class="link">Learn More</a>
249 636 </p>
250 637 </div>
251 638 <script>
252 - (function($) {
639 + (function ($) {
253 640 // Wait for the document to be ready
254 - $(document).ready(function() {
641 + const kingAddonsPremiumNoticeNonce = '<?php echo esc_js(wp_create_nonce('king_addons_premium_notice_dismiss')); ?>';
642 + $(document).ready(function () {
255 643 // Attach click handler to the dismiss button of the premium notice
256 - $('.king-addons-upgrade-notice.notice.is-dismissible').on('click', '.notice-dismiss', function() {
644 + $('.king-addons-upgrade-notice.notice.is-dismissible').on('click', '.notice-dismiss', function () {
257 645 $.post(ajaxurl, {
258 - action: 'king_addons_premium_notice_dismiss'
646 + action: 'king_addons_premium_notice_dismiss',
647 + nonce: kingAddonsPremiumNoticeNonce
259 648 });
260 649 });
261 650 });
262 651 })(jQuery);
263 652 </script>
264 -<?php
653 + <?php
265 654 }
266 655
656 + /**
657 + * Upgrade notice, current design.
658 + *
659 + * Replaces showNoticeUpgrade(), which is kept above unchanged as a fallback —
660 + * see the KING_ADDONS_UPGRADE_NOTICE_LEGACY constant and the
661 + * king_addons/upgrade_notice/use_legacy filter.
662 + *
663 + * @return void
664 + */
665 + function showNoticeUpgradeV2()
666 + {
667 + if (!current_user_can('manage_options')) {
668 + return;
669 + }
670 +
671 + $user_id = get_current_user_id();
672 + $now = time();
673 + $last_dismissed = get_user_meta($user_id, 'king_addons_premium_notice_dismissed_time', true);
674 +
675 + // Stay hidden for a week after the notice is dismissed.
676 + if ($last_dismissed && ($now - $last_dismissed) < WEEK_IN_SECONDS) {
677 + return;
678 + }
679 +
680 + $pricing_url = 'https://kingaddons.com/pricing/?utm_source=kng-notice-offer&utm_medium=plugin&utm_campaign=kng';
681 + // Same page, different utm_source: the two links stay separately
682 + // measurable, which the previous notice could not do because both
683 + // carried kng-notice-offer.
684 + $explore_url = 'https://kingaddons.com/pricing/?utm_source=kng-notice-explore&utm_medium=plugin&utm_campaign=kng';
685 + ?>
686 + <style>
687 + .king-addons-upgrade-notice-v2 {
688 + border-left: 4px solid #5B03FF;
689 + padding: 18px 20px 18px 22px;
690 + }
691 +
692 + .king-addons-upgrade-notice-v2 .king-addons-un-main {
693 + /* Admin notices run the full width of the screen, so a second
694 + column always ends up marooned in empty space. Everything
695 + stays in one left-aligned stack instead, with the price sitting
696 + in the action row where the decision is made. */
697 + display: flex;
698 + flex-direction: column;
699 + gap: 10px;
700 + }
701 +
702 + .king-addons-upgrade-notice-v2 .king-addons-un-brand {
703 + display: flex;
704 + align-items: center;
705 + gap: 7px;
706 + font-size: 11px;
707 + font-weight: 700;
708 + letter-spacing: .09em;
709 + text-transform: uppercase;
710 + color: #5B03FF;
711 + }
712 +
713 + .king-addons-upgrade-notice-v2 .king-addons-un-brand svg {
714 + width: 14px;
715 + height: 14px;
716 + display: block;
717 + fill: currentColor;
718 + }
719 +
720 + .king-addons-upgrade-notice-v2 .king-addons-un-headline {
721 + margin: 0;
722 + font-size: 17px;
723 + font-weight: 700;
724 + line-height: 1.35;
725 + color: #1d2327;
726 + max-width: 46ch;
727 + }
728 +
729 + .king-addons-upgrade-notice-v2 .king-addons-un-sub {
730 + margin: 0;
731 + font-size: 14px;
732 + line-height: 1.5;
733 + color: #50575e;
734 + max-width: 58ch;
735 + }
736 +
737 + .king-addons-upgrade-notice-v2 .king-addons-un-actions {
738 + display: flex;
739 + align-items: center;
740 + gap: 20px;
741 + flex-wrap: wrap;
742 + margin-top: 2px;
743 + }
744 +
745 + .king-addons-upgrade-notice-v2 .king-addons-un-cta {
746 + background: #5B03FF;
747 + color: #fff;
748 + display: inline-flex;
749 + align-items: center;
750 + gap: 7px;
751 + padding: 9px 18px;
752 + font-size: 14px;
753 + font-weight: 600;
754 + text-decoration: none;
755 + border-radius: 8px;
756 + transition: background-color .15s ease;
757 + }
758 +
759 + .king-addons-upgrade-notice-v2 .king-addons-un-cta:hover, .king-addons-upgrade-notice-v2 .king-addons-un-cta:focus {
760 + background: #3D01B0;
761 + color: #fff;
762 + }
763 +
764 + .king-addons-upgrade-notice-v2 .king-addons-un-cta:focus-visible {
765 + outline: 2px solid #5B03FF;
766 + outline-offset: 2px;
767 + }
768 +
769 + .king-addons-upgrade-notice-v2 .king-addons-un-cta svg {
770 + width: 15px;
771 + height: 15px;
772 + display: block;
773 + }
774 +
775 + .king-addons-upgrade-notice-v2 .king-addons-un-secondary {
776 + font-size: 14px;
777 + font-weight: 500;
778 + color: #50575e;
779 + text-decoration: none;
780 + border-bottom: 1px solid #c9c4d8;
781 + padding-bottom: 1px;
782 + }
783 +
784 + .king-addons-upgrade-notice-v2 .king-addons-un-secondary:hover, .king-addons-upgrade-notice-v2 .king-addons-un-secondary:focus {
785 + color: #5B03FF;
786 + border-bottom-color: #5B03FF;
787 + }
788 +
789 + .king-addons-upgrade-notice-v2 .king-addons-un-trust {
790 + margin: 4px 0 0;
791 + font-size: 13px;
792 + color: #50575e;
793 + display: flex;
794 + align-items: center;
795 + gap: 8px 18px;
796 + flex-wrap: wrap;
797 + }
798 +
799 + .king-addons-upgrade-notice-v2 .king-addons-un-trust-item {
800 + display: inline-flex;
801 + align-items: baseline;
802 + gap: 6px;
803 + }
804 +
805 + .king-addons-upgrade-notice-v2 .king-addons-un-trust strong {
806 + color: #1d2327;
807 + font-weight: 700;
808 + }
809 +
810 + .king-addons-upgrade-notice-v2 .king-addons-un-check {
811 + color: #0a6b45;
812 + font-weight: 700;
813 + font-size: 12px;
814 + line-height: 1;
815 + }
816 +
817 + @media screen and (max-width: 782px) {
818 + .king-addons-upgrade-notice-v2 .king-addons-un-actions {
819 + align-items: flex-start;
820 + gap: 12px;
821 + }
822 + }
823 + </style>
824 + <div class="king-addons-upgrade-notice king-addons-upgrade-notice-v2 notice notice-info is-dismissible">
825 + <div class="king-addons-un-main">
826 + <div class="king-addons-un-brand">
827 + <svg viewBox="0 0 512 512" aria-hidden="true" focusable="false"><path d="M504.981,150.787c-6.048-5.163-14.583-6.251-21.736-2.769l-109.444,53.28L271.109,82.896C267.311,78.516,261.798,76,256,76c-5.798,0-11.31,2.516-15.109,6.896L138.199,201.297l-109.444-53.28c-7.153-3.481-15.687-2.394-21.737,2.769c-6.05,5.163-8.466,13.421-6.153,21.031l76,250C79.426,430.242,87.195,436,96,436h320c8.804,0,16.574-5.758,19.134-14.182l76-250C513.448,164.208,511.032,155.95,504.981,150.787z M401.175,396H110.823L52.472,204.052l82.022,39.931c8.144,3.964,17.93,1.962,23.863-4.878L256,126.525l97.644,112.58c5.932,6.841,15.721,8.841,23.862,4.878l82.022-39.931L401.175,396z"></path></svg>
828 + <?php esc_html_e('King Addons Pro', 'king-addons'); ?>
829 + </div>
830 + <h2 class="king-addons-un-headline">
831 + <?php esc_html_e('Mega Menu, Popup Builder, WooCommerce Builder, Theme Builder &amp; AI Tools', 'king-addons'); ?>
832 + </h2>
833 + <p class="king-addons-un-sub">
834 + <?php esc_html_e('200+ Pro features, premium widgets and 4,000+ templates &amp; sections for Elementor.', 'king-addons'); ?>
835 + </p>
836 + <div class="king-addons-un-actions">
837 + <a class="king-addons-un-cta" href="<?php echo esc_url($pricing_url); ?>" target="_blank" rel="noopener noreferrer">
838 + <?php
839 + printf(
840 + /* translators: %s: monthly price, for example $6.99/mo */
841 + esc_html__('Upgrade to Pro for %s', 'king-addons'),
842 + esc_html__('$6.99/mo', 'king-addons')
843 + );
844 + ?>
845 + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" aria-hidden="true" focusable="false"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
846 + </a>
847 + <a class="king-addons-un-secondary" href="<?php echo esc_url($explore_url); ?>" target="_blank" rel="noopener noreferrer">
848 + <?php esc_html_e('Explore Pro features', 'king-addons'); ?>
849 + </a>
850 + </div>
851 + <p class="king-addons-un-trust">
852 + <span class="king-addons-un-trust-item">
853 + <span class="king-addons-un-check" aria-hidden="true">&#10003;</span>
854 + <?php esc_html_e('30-day money-back guarantee', 'king-addons'); ?>
855 + </span>
856 + <span class="king-addons-un-trust-item">
857 + <span class="king-addons-un-check" aria-hidden="true">&#10003;</span>
858 + <?php esc_html_e('Cancel anytime', 'king-addons'); ?>
859 + </span>
860 + <span class="king-addons-un-trust-item">
861 + <span class="king-addons-un-check" aria-hidden="true">&#10003;</span>
862 + <?php
863 + printf(
864 + /* translators: %s: number of users, wrapped in bold */
865 + esc_html__('Trusted by %s users', 'king-addons'),
866 + '<strong>' . esc_html__('20,000+', 'king-addons') . '</strong>'
867 + );
868 + ?>
869 + </span>
870 + </p>
871 + </div>
872 + </div>
873 + <script>
874 + (function ($) {
875 + const kingAddonsPremiumNoticeNonce = '<?php echo esc_js(wp_create_nonce('king_addons_premium_notice_dismiss')); ?>';
876 + $(document).ready(function () {
877 + $('.king-addons-upgrade-notice.notice.is-dismissible').on('click', '.notice-dismiss', function () {
878 + $.post(ajaxurl, {
879 + action: 'king_addons_premium_notice_dismiss',
880 + nonce: kingAddonsPremiumNoticeNonce
881 + });
882 + });
883 + });
884 + })(jQuery);
885 + </script>
886 + <?php
887 + }
888 +
267 889 function enqueueFrontendStyles()
268 890 {
269 891 /**
270 892 * It fixes the default Elementor SVG icon rendering feature (Settings -> Features -> Inline Font Icons)
@@ -340,22 +962,371 @@
340 962 {
341 963 add_action('elementor/widgets/register', [$this, 'registerWidgets']);
342 964 add_action('elementor/editor/after_enqueue_styles', [$this, 'enqueueEditorStyles']);
343 965 add_action('elementor/editor/after_enqueue_scripts', [$this, 'enqueueEditorScripts']);
966 + add_action('elementor/preview/enqueue_styles', [$this, 'enqueueEditorPreviewStyles']);
344 967 }
345 968
969 + function enqueueEditorPreviewStyles(): void
970 + {
971 + wp_enqueue_style(
972 + KING_ADDONS_ASSETS_UNIQUE_KEY . '-elementor-preview',
973 + KING_ADDONS_URL . 'includes/admin/css/elementor-preview.css',
974 + [],
975 + KING_ADDONS_VERSION
976 + );
977 + }
978 +
346 979 function addWidgetCategory(): void
347 980 {
348 - Plugin::instance()->elements_manager->add_category(
981 + $elements_manager = Plugin::instance()->elements_manager;
982 +
983 + $elements_manager->add_category(
349 984 'king-addons',
350 985 [
351 986 'title' => esc_html__('King Addons', 'king-addons'),
352 - 'icon' => 'fa fa-plug'
987 + 'icon' => 'fa fa-plug',
353 988 ]
354 989 );
990 +
991 + if (class_exists('WooCommerce') && function_exists('WC')) {
992 + $elements_manager->add_category(
993 + 'king-addons-woo',
994 + [
995 + 'title' => esc_html__('King Addons WooCommerce', 'king-addons'),
996 + 'icon' => 'fa fa-shopping-cart',
997 + 'hideIfEmpty' => true,
998 + ]
999 + );
1000 +
1001 + $elements_manager->add_category(
1002 + 'king-addons-woo-builder',
1003 + [
1004 + 'title' => esc_html__('King Addons WooCommerce Builder', 'king-addons'),
1005 + 'icon' => 'fa fa-shopping-cart',
1006 + 'hideIfEmpty' => true,
1007 + ]
1008 + );
1009 + }
1010 +
1011 + $elements_manager->add_category(
1012 + 'king-addons-theme-builder',
1013 + [
1014 + 'title' => esc_html__('King Addons Theme Builder', 'king-addons'),
1015 + 'icon' => 'fa fa-plug',
1016 + 'hideIfEmpty' => true,
1017 + ]
1018 + );
355 1019 }
356 1020
357 1021 /**
1022 + * Apply the panel category order after Elementor has finished registering its own.
1023 + *
1024 + * @return void
1025 + */
1026 + public function reorderWidgetCategoriesLate(): void
1027 + {
1028 + if (!did_action('elementor/loaded')) {
1029 + return;
1030 + }
1031 +
1032 + $elements_manager = Plugin::instance()->elements_manager;
1033 + $elements_manager->get_categories();
1034 + $this->reorderWidgetCategories($elements_manager);
1035 +
1036 + if (!$this->shouldApplyEditorPanelVisibility()) {
1037 + return;
1038 + }
1039 +
1040 + $post_id = $this->getCurrentEditorPostId();
1041 + if ($post_id > 0) {
1042 + $this->applyContextCategoryVisibility($elements_manager, $post_id);
1043 + }
1044 + }
1045 +
1046 + /**
1047 + * Overwrite the editor panel category snapshot after Elementor has built document config.
1048 + *
1049 + * @param array $additional_config
1050 + * @param int $post_id
1051 + * @return array
1052 + */
1053 + public function filterDocumentPanelCategories($additional_config, $post_id = 0)
1054 + {
1055 + $post_id = (int) $post_id;
1056 + if ($post_id <= 0) {
1057 + $post_id = $this->getCurrentEditorPostId();
1058 + }
1059 + if (!is_array($additional_config)) {
1060 + $additional_config = [];
1061 + }
1062 +
1063 + if (!did_action('elementor/loaded')) {
1064 + return $additional_config;
1065 + }
1066 +
1067 + $elements_manager = Plugin::instance()->elements_manager;
1068 + $elements_manager->get_categories();
1069 + $this->reorderWidgetCategories($elements_manager);
1070 + if ($post_id > 0) {
1071 + $this->applyContextCategoryVisibility($elements_manager, $post_id);
1072 + }
1073 +
1074 + $categories = \Elementor\Core\Base\Document::get_filtered_editor_panel_categories();
1075 + foreach ($this->getHiddenPanelCategoryKeys($post_id) as $hidden_key) {
1076 + unset($categories[$hidden_key]);
1077 + }
1078 +
1079 + $additional_config['panel']['elements_categories'] = $categories;
1080 +
1081 + foreach ($this->getHiddenPanelWidgetNames($post_id) as $widget_name) {
1082 + $additional_config['widgets'][$widget_name]['show_in_panel'] = false;
1083 + }
1084 +
1085 + return $additional_config;
1086 + }
1087 +
1088 + /**
1089 + * Reorder widget categories: Layout, then King Addons groups, then the rest of Elementor.
1090 + *
1091 + * @param \Elementor\Elements_Manager $elements_manager
1092 + * @return void
1093 + */
1094 + private function reorderWidgetCategories($elements_manager): void
1095 + {
1096 + try {
1097 + $reflection = new \ReflectionClass($elements_manager);
1098 + $categories_property = $reflection->getProperty('categories');
1099 + if (PHP_VERSION_ID < 80100) {
1100 + $categories_property->setAccessible(true);
1101 + }
1102 +
1103 + $categories = $categories_property->getValue($elements_manager);
1104 + if (!is_array($categories) || $categories === []) {
1105 + return;
1106 + }
1107 +
1108 + $layout = [];
1109 + $ours = [];
1110 + $rest = [];
1111 +
1112 + foreach ($categories as $key => $value) {
1113 + if ('layout' === $key) {
1114 + $layout[$key] = $value;
1115 + } elseif (str_starts_with((string) $key, 'king-addons')) {
1116 + $ours[$key] = $value;
1117 + } else {
1118 + $rest[$key] = $value;
1119 + }
1120 + }
1121 +
1122 + $ours_sorted = [];
1123 + foreach (['king-addons', 'king-addons-woo', 'king-addons-woo-builder', 'king-addons-theme-builder'] as $preferred) {
1124 + if (isset($ours[$preferred])) {
1125 + $ours_sorted[$preferred] = $ours[$preferred];
1126 + unset($ours[$preferred]);
1127 + }
1128 + }
1129 + $ours_sorted += $ours;
1130 +
1131 + $categories_property->setValue($elements_manager, $layout + $ours_sorted + $rest);
1132 + } catch (\ReflectionException $e) {
1133 + // Silently fail if reflection doesn't work (e.g., future Elementor changes)
1134 + }
1135 + }
1136 +
1137 + /**
1138 + * Drop context-only categories from the manager for the current editor document.
1139 + *
1140 + * @param \Elementor\Elements_Manager $elements_manager
1141 + * @param int $post_id
1142 + * @return void
1143 + */
1144 + private function applyContextCategoryVisibility($elements_manager, int $post_id): void
1145 + {
1146 + $hidden = $this->getHiddenPanelCategoryKeys($post_id);
1147 + if ($hidden === []) {
1148 + return;
1149 + }
1150 +
1151 + try {
1152 + $reflection = new \ReflectionClass($elements_manager);
1153 + $categories_property = $reflection->getProperty('categories');
1154 + if (PHP_VERSION_ID < 80100) {
1155 + $categories_property->setAccessible(true);
1156 + }
1157 +
1158 + $categories = $categories_property->getValue($elements_manager);
1159 + if (!is_array($categories) || $categories === []) {
1160 + return;
1161 + }
1162 +
1163 + foreach ($hidden as $key) {
1164 + unset($categories[$key]);
1165 + }
1166 +
1167 + $categories_property->setValue($elements_manager, $categories);
1168 + } catch (\ReflectionException $e) {
1169 + // Silently fail if reflection doesn't work (e.g., future Elementor changes)
1170 + }
1171 + }
1172 +
1173 + /**
1174 + * Category keys that should not appear in the current editor document.
1175 + *
1176 + * @param int $post_id
1177 + * @return array<int,string>
1178 + */
1179 + private function getHiddenPanelCategoryKeys(int $post_id): array
1180 + {
1181 + $hidden = [];
1182 + $is_woo_builder = $this->isWooBuilderDocument($post_id);
1183 + $is_theme_builder = $this->isThemeBuilderDocument($post_id);
1184 + $is_loop_item = $this->isLoopItemDocument($post_id);
1185 + $woocommerce_active = class_exists('WooCommerce') && function_exists('WC');
1186 +
1187 + if (!$woocommerce_active) {
1188 + $hidden[] = 'king-addons-woo';
1189 + $hidden[] = 'king-addons-woo-builder';
1190 + } elseif (!$is_woo_builder && !$is_loop_item) {
1191 + $hidden[] = 'king-addons-woo-builder';
1192 + }
1193 +
1194 + if (!$is_theme_builder && !$is_loop_item) {
1195 + $hidden[] = 'king-addons-theme-builder';
1196 + }
1197 +
1198 + return $hidden;
1199 + }
1200 +
1201 + /**
1202 + * Widget names that belong to hidden context categories.
1203 + *
1204 + * @param int $post_id
1205 + * @return array<int,string>
1206 + */
1207 + private function getHiddenPanelWidgetNames(int $post_id): array
1208 + {
1209 + $hidden_categories = $this->getHiddenPanelCategoryKeys($post_id);
1210 + if ($hidden_categories === [] || !did_action('elementor/loaded')) {
1211 + return [];
1212 + }
1213 +
1214 + $hidden_lookup = array_fill_keys($hidden_categories, true);
1215 + $names = [];
1216 +
1217 + foreach (Plugin::instance()->widgets_manager->get_widget_types() as $widget_name => $widget) {
1218 + if (!is_object($widget) || !method_exists($widget, 'get_categories')) {
1219 + continue;
1220 + }
1221 +
1222 + foreach ((array) $widget->get_categories() as $category) {
1223 + if (isset($hidden_lookup[$category])) {
1224 + $names[] = (string) $widget_name;
1225 + break;
1226 + }
1227 + }
1228 + }
1229 +
1230 + return $names;
1231 + }
1232 +
1233 + /**
1234 + * Whether the current request is the Elementor editor (or its AJAX).
1235 + *
1236 + * @return bool
1237 + */
1238 + private function shouldApplyEditorPanelVisibility(): bool
1239 + {
1240 + if (!empty($_GET['action']) && 'elementor' === $_GET['action']) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1241 + return true;
1242 + }
1243 +
1244 + if (!empty($_REQUEST['editor_post_id'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1245 + return true;
1246 + }
1247 +
1248 + return class_exists('\Elementor\Plugin')
1249 + && Plugin::instance()->editor
1250 + && Plugin::instance()->editor->is_edit_mode();
1251 + }
1252 +
1253 + /**
1254 + * Current Elementor editor post ID, if any.
1255 + *
1256 + * @return int
1257 + */
1258 + private function getCurrentEditorPostId(): int
1259 + {
1260 + foreach (['editor_post_id', 'post_id', 'post'] as $key) {
1261 + if (!empty($_REQUEST[$key])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1262 + $post_id = absint(wp_unslash($_REQUEST[$key])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1263 + if ($post_id > 0) {
1264 + return $post_id;
1265 + }
1266 + }
1267 + }
1268 +
1269 + $post = get_post();
1270 + return $post ? (int) $post->ID : 0;
1271 + }
1272 +
1273 + /**
1274 + * Whether the post is a King Addons Woo Builder template.
1275 + *
1276 + * @param int $post_id
1277 + * @return bool
1278 + */
1279 + private function isWooBuilderDocument(int $post_id): bool
1280 + {
1281 + if ($post_id <= 0) {
1282 + return false;
1283 + }
1284 +
1285 + $elementor_type = (string) get_post_meta($post_id, '_elementor_template_type', true);
1286 + if ('king-addons-woo-builder' === $elementor_type) {
1287 + return true;
1288 + }
1289 +
1290 + return (string) get_post_meta($post_id, 'ka_woo_template_type', true) !== '';
1291 + }
1292 +
1293 + /**
1294 + * Whether the post is a King Addons Theme Builder template.
1295 + *
1296 + * @param int $post_id
1297 + * @return bool
1298 + */
1299 + private function isThemeBuilderDocument(int $post_id): bool
1300 + {
1301 + if ($post_id <= 0) {
1302 + return false;
1303 + }
1304 +
1305 + $location_key = '_ka_theme_builder_location';
1306 + if (class_exists('\King_Addons\Theme_Builder\Meta_Keys')) {
1307 + $location_key = \King_Addons\Theme_Builder\Meta_Keys::LOCATION;
1308 + }
1309 +
1310 + return (string) get_post_meta($post_id, $location_key, true) !== '';
1311 + }
1312 +
1313 + /**
1314 + * Whether the post is a King Addons Loop Item template.
1315 + *
1316 + * @param int $post_id
1317 + * @return bool
1318 + */
1319 + private function isLoopItemDocument(int $post_id): bool
1320 + {
1321 + if ($post_id <= 0) {
1322 + return false;
1323 + }
1324 +
1325 + return 'king-addons-loop-item' === (string) get_post_meta($post_id, '_elementor_template_type', true);
1326 + }
1327 +
1328 + /**
358 1329 * Registers Elementor widgets with a mechanism to skip (and remember) broken widgets
359 1330 * that caused a fatal error previously, and try them again if the plugin version is updated.
360 1331 *
361 1332 * @param Widgets_Manager $widgets_manager
@@ -369,9 +1340,46 @@
369 1340 $currentPluginVersion = KING_ADDONS_VERSION;
370 1341
371 1342 // Get plugin options to check if a widget is enabled
372 1343 $options = get_option('king_addons_options');
1344 + $options = is_array($options) ? $options : [];
373 1345
1346 + // Extension toggles (used to prevent loading dependent widgets when extension is disabled).
1347 + $wishlist_extension_enabled = !isset($options['ext_wishlist']) || $options['ext_wishlist'] === 'enabled';
1348 + if (defined('KING_ADDONS_EXT_WISHLIST') && KING_ADDONS_EXT_WISHLIST === false) {
1349 + $wishlist_extension_enabled = false;
1350 + }
1351 +
1352 + // Ensure Woo Builder base class is available for single product widgets.
1353 + $abstract_single_widget = KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Abstract_Single_Widget.php';
1354 + if (file_exists($abstract_single_widget)) {
1355 + require_once $abstract_single_widget;
1356 + }
1357 +
1358 + // Ensure Woo Builder base class is available for archive widgets.
1359 + $abstract_archive_widget = KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Abstract_Archive_Widget.php';
1360 + if (file_exists($abstract_archive_widget)) {
1361 + require_once $abstract_archive_widget;
1362 + }
1363 +
1364 + // Ensure Woo Builder base class is available for cart widgets.
1365 + $abstract_cart_widget = KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Abstract_Cart_Widget.php';
1366 + if (file_exists($abstract_cart_widget)) {
1367 + require_once $abstract_cart_widget;
1368 + }
1369 +
1370 + // Ensure Woo Builder base class is available for checkout widgets.
1371 + $abstract_checkout_widget = KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Abstract_Checkout_Widget.php';
1372 + if (file_exists($abstract_checkout_widget)) {
1373 + require_once $abstract_checkout_widget;
1374 + }
1375 +
1376 + // Ensure Woo Builder base class is available for My Account widgets.
1377 + $abstract_my_account_widget = KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Abstract_My_Account_Widget.php';
1378 + if (file_exists($abstract_my_account_widget)) {
1379 + require_once $abstract_my_account_widget;
1380 + }
1381 +
374 1382 /**
375 1383 * Retrieve the array of broken widgets from the WordPress options.
376 1384 * The structure is expected to be something like:
377 1385 *
@@ -426,13 +1434,25 @@
426 1434 * STEP 3: Now we iterate through all widgets in our modules map and try to load them.
427 1435 * If a widget is in the broken list, we skip it to avoid repeated fatal errors.
428 1436 */
429 1437 foreach (ModulesMap::getModulesMapArray()['widgets'] as $widget_id => $widget) {
1438 + // Hard-disable via constant (used to QA/rollout new widgets).
1439 + $widget_constant = 'KING_ADDONS_WGT_' . strtoupper(str_replace('-', '_', (string) $widget_id));
1440 + if (defined($widget_constant) && constant($widget_constant) === false) {
1441 + continue;
1442 + }
1443 +
430 1444 // Check if the widget is enabled in the options
431 1445 if (!isset($options[$widget_id]) || $options[$widget_id] !== 'enabled') {
432 1446 continue;
433 1447 }
434 1448
1449 + // Skip Wishlist widgets when Wishlist extension is disabled.
1450 + // This prevents fatals when wishlist classes aren't loaded.
1451 + if (!$wishlist_extension_enabled && strpos((string) $widget_id, 'wishlist-') === 0) {
1452 + continue;
1453 + }
1454 +
435 1455 // If this widget is listed as broken, skip it
436 1456 if (array_key_exists($widget_id, $brokenWidgets)) {
437 1457 // Log something here if needed:
438 1458 // error_log("Skipping widget {$widget_id}, it previously caused a fatal error.");
@@ -444,10 +1464,17 @@
444 1464
445 1465 // Include the base widget class
446 1466 $widget_class = $widget['php-class'];
447 1467 $path_widget_class = "King_Addons\\" . $widget_class;
448 - require_once(KING_ADDONS_PATH . 'includes/widgets/' . $widget_class . '/' . $widget_class . '.php');
1468 + $widget_file = KING_ADDONS_PATH . 'includes/widgets/' . $widget_class . '/' . $widget_class . '.php';
1469 + if (!file_exists($widget_file)) {
1470 + // Skip missing widget files to avoid fatal errors if registry is ahead of implementation.
1471 + $currentlyLoadingWidgetId = null;
1472 + continue;
1473 + }
449 1474
1475 + require_once $widget_file;
1476 +
450 1477 // Check if we can load the Pro version
451 1478 if (
452 1479 function_exists('king_addons_freemius')
453 1480 && king_addons_freemius()->can_use_premium_code__premium_only()
@@ -483,8 +1510,14 @@
483 1510 $options = get_option('king_addons_options');
484 1511
485 1512 foreach (ModulesMap::getModulesMapArray()['widgets'] as $widget_id => $widget) {
486 1513
1514 + // Hard-disable via constant (used to QA/rollout new widgets).
1515 + $widget_constant = 'KING_ADDONS_WGT_' . strtoupper(str_replace('-', '_', (string) $widget_id));
1516 + if (defined($widget_constant) && constant($widget_constant) === false) {
1517 + continue;
1518 + }
1519 +
487 1520 if (!($options[$widget_id] ?? null)) {
488 1521 $options[$widget_id] = 'enabled';
489 1522 update_option('king_addons_options', $options);
490 1523 }
@@ -490,13 +1523,25 @@
490 1523 }
491 1524 }
492 1525 }
493 1526
494 - function enableFeatures(): void
1527 + /**
1528 + * Enable and bootstrap registered features.
1529 + *
1530 + * Loads free feature classes and, when available and licensed, their Pro counterparts.
1531 + *
1532 + * @return void
1533 + */
1534 + public function enableFeatures(): void
495 1535 {
496 1536 $options = get_option('king_addons_options');
497 1537
498 1538 foreach (ModulesMap::getModulesMapArray()['features'] as $feature_id => $feature) {
1539 + // Hard-disable via constant (used to QA/rollout new features).
1540 + $feature_constant = 'KING_ADDONS_FEAT_' . strtoupper(str_replace('-', '_', (string) $feature_id));
1541 + if (defined($feature_constant) && constant($feature_constant) === false) {
1542 + continue;
1543 + }
499 1544
500 1545 if (!($options[$feature_id] ?? null)) {
501 1546 $options[$feature_id] = 'enabled';
502 1547 update_option('king_addons_options', $options);
@@ -501,14 +1546,44 @@
501 1546 $options[$feature_id] = 'enabled';
502 1547 update_option('king_addons_options', $options);
503 1548 }
504 1549
505 - if ($options[$feature_id] === 'enabled') {
506 - $feature_class = $feature['php-class'];
507 - $path_feature_class = "King_Addons\\" . $feature_class;
508 - require_once(KING_ADDONS_PATH . 'includes/features/' . $feature_class . '/' . $feature_class . '.php');
509 - new $path_feature_class;
1550 + if ($options[$feature_id] !== 'enabled') {
1551 + continue;
510 1552 }
1553 +
1554 + $feature_class = $feature['php-class'];
1555 + $path_feature_class = "King_Addons\\" . $feature_class;
1556 + $feature_file = KING_ADDONS_PATH . 'includes/features/' . $feature_class . '/' . $feature_class . '.php';
1557 +
1558 + if (file_exists($feature_file)) {
1559 + require_once $feature_file;
1560 + }
1561 +
1562 + $pro_loaded = false;
1563 +
1564 + if (
1565 + !empty($feature['has-pro'])
1566 + && function_exists('king_addons_freemius')
1567 + && king_addons_freemius()->can_use_premium_code__premium_only()
1568 + && defined('KING_ADDONS_PRO_PATH')
1569 + ) {
1570 + $pro_file_path = KING_ADDONS_PRO_PATH . 'includes/features/' . $feature_class . '_Pro/' . $feature_class . '_Pro.php';
1571 +
1572 + if (file_exists($pro_file_path)) {
1573 + require_once $pro_file_path;
1574 +
1575 + $path_feature_class_pro = "King_Addons\\" . $feature_class . '_Pro';
1576 + if (class_exists($path_feature_class_pro)) {
1577 + new $path_feature_class_pro();
1578 + $pro_loaded = true;
1579 + }
1580 + }
1581 + }
1582 +
1583 + if (!$pro_loaded && class_exists($path_feature_class)) {
1584 + new $path_feature_class();
1585 + }
511 1586 }
512 1587 }
513 1588
514 1589 public function registerControls(Controls_Manager $controls_manager): void
@@ -526,14 +1601,14 @@
526 1601
527 1602 function enqueueEditorScripts(): void
528 1603 {
529 1604 wp_enqueue_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-elementor-editor', KING_ADDONS_URL . 'includes/admin/js/elementor-editor.js', '', KING_ADDONS_VERSION);
530 -
1605 +
531 1606 // Localize script with PRO status
532 1607 wp_localize_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-elementor-editor', 'kingAddonsEditor', [
533 1608 'isPro' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false
534 1609 ]);
535 -
1610 +
536 1611 wp_enqueue_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-data-table-export', KING_ADDONS_URL . 'includes/widgets/Data_Table/preview-handler.js', '', KING_ADDONS_VERSION);
537 1612
538 1613 if (KING_ADDONS_WGT_FORM_BUILDER) {
539 1614 wp_enqueue_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-form-builder-editor-handler', KING_ADDONS_URL . 'includes/widgets/Form_Builder/editor-handler.js', '', KING_ADDONS_VERSION);
@@ -539,8 +1614,40 @@
539 1614 wp_enqueue_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-form-builder-editor-handler', KING_ADDONS_URL . 'includes/widgets/Form_Builder/editor-handler.js', '', KING_ADDONS_VERSION);
540 1615 }
541 1616 }
542 1617
1618 + /**
1619 + * Widget settings with dynamic tags resolved, and nothing missing.
1620 + *
1621 + * get_settings() never parses dynamic tags, so a tag placed in such a
1622 + * widget printed its placeholder instead of the value. get_settings_for_display()
1623 + * does parse them, but it also drops every setting whose control condition
1624 + * is currently false - and widgets written against the raw array index into
1625 + * those keys without checking, which turns the switch into a batch of
1626 + * "Trying to access array offset on null" warnings.
1627 + *
1628 + * A hidden control comes back as null rather than missing, so the parsed
1629 + * value is preferred only when there is one, and the raw value fills in
1630 + * everywhere else.
1631 + *
1632 + * @param \Elementor\Controls_Stack $widget Widget being rendered.
1633 + *
1634 + * @return array<string,mixed>
1635 + */
1636 + public static function displaySettings($widget): array
1637 + {
1638 + $raw = (array) $widget->get_settings();
1639 + $display = (array) $widget->get_settings_for_display();
1640 +
1641 + foreach ($display as $key => $value) {
1642 + if (null !== $value) {
1643 + $raw[$key] = $value;
1644 + }
1645 + }
1646 +
1647 + return $raw;
1648 + }
1649 +
543 1650 public static function renderProFeaturesSection($module, $section, $type, $widget_name, $features): void
544 1651 {
545 1652 if (king_addons_freemius()->can_use_premium_code__premium_only()) {
546 1653 return;
@@ -567,8 +1674,34 @@
567 1674
568 1675 $module->end_controls_section();
569 1676 }
570 1677
1678 + /**
1679 + * Same upgrade notice as renderUpgradeProNotice(), but not tied to another
1680 + * control's value. Use it where a whole section is Pro-only, so the free
1681 + * build does not show a section header that opens onto nothing.
1682 + *
1683 + * @param mixed $module Widget or repeater the control belongs to.
1684 + * @param string $controls_manager Control type to render the notice with.
1685 + * @param string $widget_name Widget slug, used for the campaign link.
1686 + * @param string $control_id Unique control id for the notice.
1687 + */
1688 + public static function renderUpgradeProSection($module, $controls_manager, string $widget_name, string $control_id): void
1689 + {
1690 + if (king_addons_freemius()->can_use_premium_code__premium_only()) {
1691 + return;
1692 + }
1693 +
1694 + $module->add_control(
1695 + $control_id,
1696 + [
1697 + 'raw' => 'Upgrade to the <strong><a href="https://kingaddons.com/pricing/?utm_source=kng-module-' . $widget_name . '-settings-upgrade-pro&utm_medium=plugin&utm_campaign=kng" target="_blank">Pro version</a></strong> now<br> and unlock this feature!',
1698 + 'type' => $controls_manager,
1699 + 'content_classes' => 'king-addons-pro-notice',
1700 + ]
1701 + );
1702 + }
1703 +
571 1704 public static function renderUpgradeProNotice($module, $controls_manager, $widget_name, $option, $condition = []): void
572 1705 {
573 1706 if (king_addons_freemius()->can_use_premium_code__premium_only()) {
574 1707 return;
@@ -622,16 +1755,19 @@
622 1755 $image = esc_url($args['image']);
623 1756 $network = $args['network'];
624 1757
625 1758 $get_whatsapp_url = function ($a) {
626 - if ('yes' === $a['show_whatsapp_title'] && 'yes' === $a['show_whatsapp_excerpt']) {
627 - return 'https://api.whatsapp.com/send?text=*' . $a['title'] . '*%0a' . $a['text'] . '%0a' . $a['url'];
628 - } elseif ('yes' === $a['show_whatsapp_title']) {
629 - return 'https://api.whatsapp.com/send?text=*' . $a['title'] . '*%0a' . $a['url'];
630 - } elseif ('yes' === $a['show_whatsapp_excerpt']) {
631 - return 'https://api.whatsapp.com/send?text=*' . $a['text'] . '%0a' . $a['url'];
1759 + $parts = [];
1760 + if ('yes' === $a['show_whatsapp_title'] && $a['title'] !== '') {
1761 + $parts[] = '*' . $a['title'] . '*';
632 1762 }
633 - return 'https://api.whatsapp.com/send?text=' . $a['url'];
1763 + if ('yes' === $a['show_whatsapp_excerpt'] && $a['text'] !== '') {
1764 + $parts[] = $a['text'];
1765 + }
1766 + if ($a['url'] !== '') {
1767 + $parts[] = $a['url'];
1768 + }
1769 + return 'https://api.whatsapp.com/send?text=' . rawurlencode(implode("\n", $parts));
634 1770 };
635 1771
636 1772 $networks_map = [
637 1773 'facebook-f' => [
@@ -723,10 +1859,17 @@
723 1859 $share_url = $networks_map[$network]['url'];
724 1860 $network_title = $networks_map[$network]['title'];
725 1861 $icon_category = $networks_map[$network]['icon'];
726 1862
727 - $output = '<a href="' . esc_url($share_url) . '" class="king-addons-share-icon king-addons-share-' . esc_attr($network) . '" target="_blank">';
1863 + // esc_url() strips encoded newlines (%0A), which concatenates WhatsApp title and URL.
1864 + if ('whatsapp' === $network && str_starts_with($share_url, 'https://api.whatsapp.com/send?text=')) {
1865 + $href = esc_attr($share_url);
1866 + } else {
1867 + $href = esc_url($share_url);
1868 + }
728 1869
1870 + $output = '<a href="' . $href . '" class="king-addons-share-icon king-addons-share-' . esc_attr($network) . '" target="_blank">';
1871 +
729 1872 if ('yes' === $args['tooltip']) {
730 1873 $output .= '<span class="king-addons-share-tooltip king-addons-tooltip">' . esc_html($network_title) . '</span>';
731 1874 }
732 1875
@@ -931,9 +2074,14 @@
931 2074 if (!$api_key) {
932 2075 return $mailchimp_list;
933 2076 }
934 2077
935 - $url = 'https://' . explode('-', $api_key)[1] . '.api.mailchimp.com/3.0/lists/';
2078 + $parts = explode('-', (string) $api_key);
2079 + if (count($parts) < 2 || '' === $parts[1]) {
2080 + return $mailchimp_list;
2081 + }
2082 +
2083 + $url = 'https://' . $parts[1] . '.api.mailchimp.com/3.0/lists/';
936 2084 $response = wp_remote_get($url, [
937 2085 'headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $api_key)]
938 2086 ]);
939 2087
@@ -948,12 +2096,22 @@
948 2096 }
949 2097
950 2098 public static function getMailchimpGroups()
951 2099 {
952 - $apiKey = get_option('king_addons_mailchimp_api_key');
953 - $domain = 'https://' . substr($apiKey, strpos($apiKey, '-') + 1) . '.api.mailchimp.com/3.0/';
2100 + $apiKey = (string) get_option('king_addons_mailchimp_api_key', '');
2101 + $groups = ['def' => 'Select Group'];
2102 +
2103 + if ('' === $apiKey || false === strpos($apiKey, '-')) {
2104 + return $groups;
2105 + }
2106 +
2107 + $dc = substr($apiKey, strpos($apiKey, '-') + 1);
2108 + if ('' === $dc) {
2109 + return $groups;
2110 + }
2111 +
2112 + $domain = 'https://' . $dc . '.api.mailchimp.com/3.0/';
954 2113 $authArgs = ['headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $apiKey)]];
955 - $groups = ['def' => 'Select Group'];
956 2114 $mailchimpIDs = Core::getMailchimpLists();
957 2115
958 2116 foreach ($mailchimpIDs as $audience => $ignore) {
959 2117 if ($audience === 'def') {
@@ -959,14 +2117,20 @@
959 2117 if ($audience === 'def') {
960 2118 continue;
961 2119 }
962 2120
963 - $cats = wp_remote_get("{$domain}lists/$audience/interest-categories", $authArgs);
964 - $cats = json_decode($cats['body'])->categories ?? [];
2121 + $cats_res = wp_remote_get("{$domain}lists/$audience/interest-categories", $authArgs);
2122 + if (is_wp_error($cats_res)) {
2123 + continue;
2124 + }
2125 + $cats = json_decode((string) wp_remote_retrieve_body($cats_res))->categories ?? [];
965 2126
966 2127 foreach ($cats as $cat) {
967 - $interests = wp_remote_get("{$domain}lists/$audience/interest-categories/$cat->id/interests", $authArgs);
968 - $interests = json_decode($interests['body'])->interests ?? [];
2128 + $interests_res = wp_remote_get("{$domain}lists/$audience/interest-categories/$cat->id/interests", $authArgs);
2129 + if (is_wp_error($interests_res)) {
2130 + continue;
2131 + }
2132 + $interests = json_decode((string) wp_remote_retrieve_body($interests_res))->interests ?? [];
969 2133
970 2134 foreach ($interests as $int) {
971 2135 $groups[$int->id] = $int->name;
972 2136 }
@@ -1051,9 +2215,9 @@
1051 2215 array_map(function ($slug) {
1052 2216 $keys = [];
1053 2217 foreach (get_posts(['post_type' => $slug, 'posts_per_page' => -1]) as $post) {
1054 2218 // get_post_custom_keys can return null, so cast to array:
1055 - foreach ((array)get_post_custom_keys($post->ID) as $meta_key) {
2219 + foreach ((array) get_post_custom_keys($post->ID) as $meta_key) {
1056 2220 // Exclude protected keys (those beginning with "_").
1057 2221 if ($meta_key[0] !== '_') {
1058 2222 $keys[] = $meta_key;
1059 2223 }
@@ -1102,8 +2266,10 @@
1102 2266 * @return void
1103 2267 */
1104 2268 public function enqueueAiFieldScript(): void
1105 2269 {
2270 + $ai_options = get_option('king_addons_ai_options', []);
2271 +
1106 2272 wp_enqueue_script(
1107 2273 'king-addons-ai-field',
1108 2274 KING_ADDONS_URL . 'includes/admin/js/ai-textfield.js',
1109 2275 ['jquery', 'elementor-editor'],
@@ -1115,20 +2281,61 @@
1115 2281 wp_localize_script(
1116 2282 'king-addons-ai-field',
1117 2283 'KingAddonsAiField',
1118 2284 [
1119 - 'ajax_url' => admin_url('admin-ajax.php'),
1120 - 'generate_nonce' => wp_create_nonce('king_addons_ai_generate_nonce'),
1121 - 'change_nonce' => wp_create_nonce('king_addons_ai_change_nonce'),
2285 + 'ajax_url' => admin_url('admin-ajax.php'),
2286 + 'generate_nonce' => wp_create_nonce('king_addons_ai_generate_nonce'),
2287 + 'change_nonce' => wp_create_nonce('king_addons_ai_change_nonce'),
1122 2288 'generate_action' => 'king_addons_ai_generate_text',
1123 - 'change_action' => 'king_addons_ai_change_text',
1124 - 'icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai.svg',
1125 - 'rewrite_icon_url'=> KING_ADDONS_URL . 'includes/admin/img/ai-refresh.svg',
1126 - 'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
1127 - 'plugin_url' => KING_ADDONS_URL,
1128 - 'is_pro' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
1129 - 'premium_active' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
2289 + 'change_action' => 'king_addons_ai_change_text',
2290 + 'icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai.svg',
2291 + 'rewrite_icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai-refresh.svg',
2292 + 'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
2293 + 'plugin_url' => KING_ADDONS_URL,
2294 + 'is_pro' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
2295 + 'premium_active' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
1130 2296 'translator_enabled' => isset($ai_options['enable_ai_page_translator']) ? (bool) $ai_options['enable_ai_page_translator'] : true,
2297 + // Editor prompts name the configured provider rather than always OpenAI.
2298 + 'provider' => \King_Addons\AI_Provider::getProvider(),
2299 + 'provider_label' => \King_Addons\AI_Provider::getLabel(),
2300 + 'api_keys_url' => \King_Addons\AI_Provider::getApiKeysUrl(),
2301 + 'api_keys_label' => \King_Addons\AI_Provider::isOpenRouter()
2302 + ? esc_html__('OpenRouter Keys', 'king-addons')
2303 + : esc_html__('OpenAI Platform', 'king-addons'),
2304 + 'setup_billing_note' => \King_Addons\AI_Provider::isOpenRouter()
2305 + ? esc_html__('Free models work without adding any credit.', 'king-addons')
2306 + : esc_html__('and top up your OpenAI account balance by at least $5', 'king-addons'),
2307 + 'setup_cost_note' => \King_Addons\AI_Provider::isOpenRouter()
2308 + ? esc_html__('Free models cost nothing. Paid models cost pennies (about $0.01 per full page).', 'king-addons')
2309 + : esc_html__('Processing a page costs pennies (about $0.01 per full page).', 'king-addons'),
2310 + /**
2311 + * Before either provider has a key there is no "configured
2312 + * provider" to describe - the setting is just its default. The
2313 + * setup dialog then has to present the choice instead of sending
2314 + * everyone to OpenAI.
2315 + */
2316 + 'has_any_key' => (
2317 + '' !== \King_Addons\AI_Provider::getApiKey(\King_Addons\AI_Provider::OPENAI)
2318 + || '' !== \King_Addons\AI_Provider::getApiKey(\King_Addons\AI_Provider::OPENROUTER)
2319 + ),
2320 + 'setup_providers' => [
2321 + [
2322 + 'name' => esc_html__('OpenRouter', 'king-addons'),
2323 + 'url' => \King_Addons\AI_Provider::getApiKeysUrl(\King_Addons\AI_Provider::OPENROUTER),
2324 + 'note' => esc_html__('has free models, no credit needed to start', 'king-addons'),
2325 + ],
2326 + [
2327 + 'name' => esc_html__('OpenAI', 'king-addons'),
2328 + 'url' => \King_Addons\AI_Provider::getApiKeysUrl(\King_Addons\AI_Provider::OPENAI),
2329 + 'note' => esc_html__('needs at least $5 on your account balance', 'king-addons'),
2330 + ],
2331 + ],
2332 + 'setup_cost_note_neutral' => esc_html__('With a free OpenRouter model, nothing. On a paid model it is pennies (about $0.01 per full page).', 'king-addons'),
2333 + 'missing_key_message' => sprintf(
2334 + /* translators: %s: provider name */
2335 + esc_html__('%s API key is missing or invalid. Please configure your API key in AI Settings.', 'king-addons'),
2336 + \King_Addons\AI_Provider::getLabel()
2337 + ),
1131 2338 ]
1132 2339 );
1133 2340 }
1134 2341
@@ -1138,10 +2345,8 @@
1138 2345 * @return void
1139 2346 */
1140 2347 public function enqueueAiImageGenerationScript(): void
1141 2348 {
1142 - // Retrieve AI options and ensure it's an array to prevent warnings.
1143 - $ai_options = get_option('king_addons_ai_options', []);
1144 2349 wp_enqueue_script(
1145 2350 'king-addons-ai-image-field',
1146 2351 KING_ADDONS_URL . 'includes/admin/js/ai-imagefield.js',
1147 2352 ['jquery', 'elementor-editor'],
@@ -1147,22 +2352,36 @@
1147 2352 ['jquery', 'elementor-editor'],
1148 2353 KING_ADDONS_VERSION,
1149 2354 true
1150 2355 );
1151 -
2356 +
1152 2357 // Localize for AJAX
1153 2358 wp_localize_script(
1154 2359 'king-addons-ai-image-field',
1155 2360 'KingAddonsAiImageField',
1156 2361 [
1157 - 'ajax_url' => admin_url('admin-ajax.php'),
1158 - 'generate_nonce' => wp_create_nonce('king_addons_ai_generate_image_nonce'),
2362 + 'ajax_url' => admin_url('admin-ajax.php'),
2363 + 'generate_nonce' => wp_create_nonce('king_addons_ai_generate_image_nonce'),
1159 2364 'generate_action' => 'king_addons_ai_generate_image',
1160 - 'image_model' => sanitize_text_field( $ai_options['openai_image_model'] ?? '' ),
1161 - 'icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai.svg',
1162 - 'rewrite_icon_url'=> KING_ADDONS_URL . 'includes/admin/img/ai-refresh.svg',
1163 - 'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
1164 - 'plugin_url' => KING_ADDONS_URL,
2365 + 'image_model' => \King_Addons\AI_Provider::getImageModel(),
2366 + // The editor builds its model dropdown from this list, so the
2367 + // options follow whichever provider is configured.
2368 + 'image_models' => array_map(
2369 + static function ($model) {
2370 + return ['value' => $model['id'], 'label' => $model['label']];
2371 + },
2372 + \King_Addons\AI_Provider::getModelsFor('image')
2373 + ),
2374 + 'provider' => \King_Addons\AI_Provider::getProvider(),
2375 + 'icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai.svg',
2376 + 'rewrite_icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai-refresh.svg',
2377 + 'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
2378 + 'plugin_url' => KING_ADDONS_URL,
2379 + 'missing_key_message' => sprintf(
2380 + /* translators: %s: provider name */
2381 + esc_html__('%s API key is missing or invalid. Please configure your API key in AI Settings.', 'king-addons'),
2382 + \King_Addons\AI_Provider::getLabel()
2383 + ),
1165 2384 ]
1166 2385 );
1167 2386 }
1168 2387
@@ -1206,13 +2425,13 @@
1206 2425 {
1207 2426 // Check if AI Page Translator is enabled in settings
1208 2427 $ai_options = get_option('king_addons_ai_options', []);
1209 2428 $translator_enabled = isset($ai_options['enable_ai_page_translator']) ? (bool) $ai_options['enable_ai_page_translator'] : true;
1210 -
2429 +
1211 2430 if (!$translator_enabled) {
1212 2431 return; // Don't load script if translator is disabled
1213 2432 }
1214 -
2433 +
1215 2434 wp_enqueue_script(
1216 2435 'king-addons-ai-translator',
1217 2436 KING_ADDONS_URL . 'includes/admin/js/ai-page-translator.js',
1218 2437 ['jquery', 'elementor-editor'],
@@ -1222,8 +2441,182 @@
1222 2441
1223 2442 // Note: Using existing KingAddonsAiField localization
1224 2443 // The translator script will use the same AJAX endpoints and settings
1225 2444 // No need for separate localization as it reuses existing AI infrastructure
2445 + }
2446 +
2447 + /**
2448 + * Render an attachment through an Elementor image-size group control.
2449 + *
2450 + * Group_Control_Image_Size::get_attachment_image_html() takes a *key into
2451 + * $settings* as its third argument, not an attachment ID. Handing it an ID
2452 + * makes Elementor read $settings[<id>], which raises two PHP warnings and
2453 + * returns nothing, so callers silently lose the image.
2454 + *
2455 + * @param array<string, mixed> $settings Widget settings; carries "<$size_key>_size" and friends.
2456 + * @param string $size_key Name of the image-size group control.
2457 + * @param int $attachment_id Attachment to render.
2458 + *
2459 + * @return string Image HTML, or an empty string when it cannot be rendered.
2460 + */
2461 + /**
2462 + * Show the person building the page why a widget rendered nothing.
2463 + *
2464 + * Only ever printed inside the Elementor editor, so the public page keeps
2465 + * rendering exactly what it rendered before.
2466 + *
2467 + * @param string $message Plain text explaining what the widget still needs.
2468 + */
2469 + public static function renderEditorHint(string $message): void
2470 + {
2471 + if (!Plugin::$instance->editor->is_edit_mode()) {
2472 + return;
2473 + }
2474 +
2475 + $style = 'display:block;padding:12px 14px;border:1px dashed #c3c4c7;border-radius:4px;'
2476 + . 'background:#f6f7f7;color:#50575e;font-size:13px;line-height:1.5;';
2477 +
2478 + echo '<div class="king-addons-editor-hint" style="' . esc_attr($style) . '">'
2479 + . esc_html($message) . '</div>';
2480 + }
2481 +
2482 + /**
2483 + * A setting written straight into a JavaScript object literal has to be a
2484 + * number. Elementor stores a number field the user cleared as an empty
2485 + * string, which would emit `slidesPerView: ,` - a syntax error that stops
2486 + * the whole carousel from initialising - or blow up in PHP when the value
2487 + * is used in arithmetic.
2488 + *
2489 + * @param array<string, mixed> $settings Widget settings.
2490 + * @param string $key Setting to read.
2491 + * @param int|float $fallback Value to use when the setting is empty or not a number.
2492 + *
2493 + * @return string Numeric string, safe to interpolate into JS.
2494 + */
2495 + public static function jsNumber(array $settings, string $key, $fallback = 0): string
2496 + {
2497 + $value = $settings[$key] ?? null;
2498 +
2499 + // Slider controls keep their number under 'size'.
2500 + if (is_array($value)) {
2501 + $value = $value['size'] ?? null;
2502 + }
2503 +
2504 + if (!is_numeric($value)) {
2505 + $value = $fallback;
2506 + }
2507 +
2508 + return (string) (0 + $value);
2509 + }
2510 +
2511 + /**
2512 + * Swiper breakpoints use viewport width, so a desktop page with a ~600px
2513 + * content column still shows 3–5 cards and crushes the text. Print a helper
2514 + * that clamps slidesPerView to the carousel's own width.
2515 + */
2516 + public static function printFitSlidesHelper(): void
2517 + {
2518 + static $printed = false;
2519 + if ($printed) {
2520 + return;
2521 + }
2522 + $printed = true;
2523 + // Avoid && in this snippet: the_content encodes ampersands and would
2524 + // turn the helper into invalid JavaScript.
2525 + wp_print_inline_script_tag(<<<'JS'
2526 +window.kingAddonsFitSlidesPerView = function (el, desired, minWidth, space) {
2527 + desired = parseFloat(desired) || 1;
2528 + minWidth = parseFloat(minWidth) || 240;
2529 + space = parseFloat(space) || 0;
2530 + if (!el) {
2531 + return Math.max(1, desired);
2532 + }
2533 + var width = el.clientWidth || 0;
2534 + if (width <= 0) {
2535 + return Math.max(1, desired);
2536 + }
2537 + var maxFit = Math.max(1, Math.floor((width + space) / (minWidth + space)));
2538 + return Math.min(desired, maxFit);
2539 +};
2540 +window.kingAddonsBindFitSlides = function (swiper, el, minWidth) {
2541 + if (!swiper) {
2542 + return;
2543 + }
2544 + if (!el) {
2545 + return;
2546 + }
2547 + if (typeof swiper.params === 'undefined') {
2548 + return;
2549 + }
2550 + minWidth = parseFloat(minWidth) || 240;
2551 + var apply = function () {
2552 + var passed = swiper.passedParams || {};
2553 + var bpKey = swiper.currentBreakpoint;
2554 + var fromBp = null;
2555 + if (bpKey) {
2556 + if (passed.breakpoints) {
2557 + fromBp = passed.breakpoints[bpKey] || null;
2558 + }
2559 + }
2560 + var desiredRaw = passed.slidesPerView;
2561 + if (fromBp) {
2562 + if (typeof fromBp.slidesPerView !== 'undefined') {
2563 + desiredRaw = fromBp.slidesPerView;
2564 + }
2565 + }
2566 + var desired = parseFloat(desiredRaw) || 1;
2567 + var spaceRaw = swiper.params.spaceBetween;
2568 + if (fromBp) {
2569 + if (typeof fromBp.spaceBetween !== 'undefined') {
2570 + spaceRaw = fromBp.spaceBetween;
2571 + }
2572 + }
2573 + var space = parseFloat(spaceRaw) || 0;
2574 + var next = window.kingAddonsFitSlidesPerView(el, desired, minWidth, space);
2575 + if (swiper.params.slidesPerView !== next) {
2576 + swiper.params.slidesPerView = next;
2577 + if (typeof swiper.update === 'function') {
2578 + swiper.update();
2579 + }
2580 + }
2581 + };
2582 + apply();
2583 + if (typeof swiper.on === 'function') {
2584 + swiper.on('breakpoint', apply);
2585 + }
2586 + if (window.ResizeObserver) {
2587 + new ResizeObserver(apply).observe(el);
2588 + } else {
2589 + window.addEventListener('resize', apply);
2590 + }
2591 +};
2592 +JS
2593 + );
2594 + }
2595 +
2596 + /**
2597 + * Wrap an inline `new Swiper(...)` so slidesPerView is clamped to the container.
2598 + */
2599 + public static function wrapFitSwiper(string $selector, string $js_swiper, int $min_slide = 240): string
2600 + {
2601 + $sel = wp_json_encode($selector);
2602 + $min = (string) max(1, $min_slide);
2603 + return '(function(){var el=document.querySelector(' . $sel . ');if(!el){return;}if(typeof Swiper==="undefined"){return;}var sw=' . $js_swiper . ';if(typeof kingAddonsBindFitSlides==="function"){kingAddonsBindFitSlides(sw,el,' . $min . ');}})()';
2604 + }
2605 +
2606 + public static function getAttachmentImageHTML(array $settings, string $size_key, int $attachment_id): string
2607 + {
2608 + if ($attachment_id < 1) {
2609 + return '';
2610 + }
2611 +
2612 + $image_key = '__king_addons_image';
2613 + $settings[$image_key] = [
2614 + 'id' => $attachment_id,
2615 + 'url' => wp_get_attachment_image_url($attachment_id, 'full') ?: '',
2616 + ];
2617 +
2618 + return (string) Group_Control_Image_Size::get_attachment_image_html($settings, $size_key, $image_key);
1226 2619 }
1227 2620 }
1228 2621
1229 2622 Core::instance();