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 +1468 -76 51.1.35 → 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,9 +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');
54 - require_once(KING_ADDONS_PATH . 'includes/SectionsMap.php');
108 + require_once(KING_ADDONS_PATH . 'includes/helpers/Global/Text_Entities_Migration.php');
109 + Text_Entities_Migration::boot();
55 110
56 111 if ($this->hasElementorCompatibility()) {
57 112
58 113 // Initial requirements check
@@ -58,9 +113,9 @@
58 113 // Initial requirements check
59 114 require_once(KING_ADDONS_PATH . 'includes/helpers/Check_Requirements/Check_Requirements.php');
60 115
61 116 // Templates Catalog
62 - if (KING_ADDONS_EXT_TEMPLATES_CATALOG) {
117 + if ($this->isExtensionEnabled('templates-catalog', 'KING_ADDONS_EXT_TEMPLATES_CATALOG')) {
63 118 require_once(KING_ADDONS_PATH . 'includes/TemplatesMap.php');
64 119 require_once(KING_ADDONS_PATH . 'includes/extensions/Templates/CollectionsMap.php');
65 120 require_once(KING_ADDONS_PATH . 'includes/extensions/Templates/Templates.php');
66 121
@@ -68,29 +123,214 @@
68 123 require_once(KING_ADDONS_PATH . 'includes/extensions/Template_Catalog_Button/Template_Catalog_Button.php');
69 124 Template_Catalog_Button::instance();
70 125 }
71 126
72 - // Sections Catalog
73 - // if (KING_ADDONS_EXT_SECTIONS_CATALOG) {
74 - // require_once(KING_ADDONS_PATH . 'includes/extensions/Sections_Catalog/Sections_Catalog.php');
75 - // Sections_Catalog::instance();
76 - // }
77 -
78 127 // Header & Footer Builder
79 - if (KING_ADDONS_EXT_HEADER_FOOTER_BUILDER) {
128 + if ($this->isExtensionEnabled('header-footer-builder', 'KING_ADDONS_EXT_HEADER_FOOTER_BUILDER')) {
80 129 require_once(KING_ADDONS_PATH . 'includes/extensions/Header_Footer_Builder/Header_Footer_Builder.php');
81 130 Header_Footer_Builder::instance();
82 131 }
83 132
84 133 // Popup Builder
85 - if (KING_ADDONS_EXT_POPUP_BUILDER) {
134 + if ($this->isExtensionEnabled('popup-builder', 'KING_ADDONS_EXT_POPUP_BUILDER')) {
86 135 require_once(KING_ADDONS_PATH . 'includes/extensions/Popup_Builder/Popup_Builder.php');
87 136 Popup_Builder::instance();
88 137 }
89 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 +
90 323 // Admin
91 324 require_once(KING_ADDONS_PATH . 'includes/Admin.php');
92 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 +
93 333 // Additional - Controls
94 334 require_once(KING_ADDONS_PATH . 'includes/controls/Ajax_Select2/Ajax_Select2.php');
95 335 require_once(KING_ADDONS_PATH . 'includes/controls/Ajax_Select2/Ajax_Select2_API.php');
96 336 require_once(KING_ADDONS_PATH . 'includes/controls/Animations/Animations.php');
@@ -100,8 +340,9 @@
100 340 require_once(KING_ADDONS_PATH . 'includes/widgets/Search/Search_Ajax.php');
101 341 require_once(KING_ADDONS_PATH . 'includes/widgets/MailChimp/MailChimp_Ajax.php');
102 342
103 343 // Additional - Grids, Magazine Grid
344 + require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Grid_Ajax_Security.php');
104 345 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Filter_Posts_Ajax.php');
105 346 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Filter_WooCommerce_Products_Ajax.php');
106 347 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Post_Likes_Ajax.php');
107 348
@@ -106,8 +347,20 @@
106 347 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Post_Likes_Ajax.php');
107 348
108 349 // Additional - Form Builder
109 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');
110 363 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Create_Submission.php');
111 364 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Send_Email.php');
112 365 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Send_Webhook.php');
113 366 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Subscribe_Mailchimp.php');
@@ -118,12 +371,38 @@
118 371 }
119 372
120 373 // ADDITIONAL CLASSES
121 374
122 - // Alt Text Generator for Media Library
123 - require_once(KING_ADDONS_PATH . 'includes/extensions/alt-text-generator/Alt_Text_Generator.php');
124 - 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 + }
125 382
383 + // Wishlist module - check extension toggle
384 + if ($this->isExtensionEnabled('wishlist', 'KING_ADDONS_EXT_WISHLIST')) {
385 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_DB.php';
386 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Session.php';
387 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Settings.php';
388 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Service.php';
389 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Renderer.php';
390 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Frontend.php';
391 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_WooCommerce.php';
392 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Module.php';
393 + $this->wishlist_module = new Wishlist_Module();
394 + } else {
395 + // Extension off: still expire leftover guest cookies so public caches work.
396 + require_once KING_ADDONS_PATH . 'includes/wishlist/Wishlist_Session.php';
397 + add_action('send_headers', static function () {
398 + if (is_admin()) {
399 + return;
400 + }
401 + \King_Addons\Wishlist\Wishlist_Session::expire_if_present();
402 + });
403 + }
404 +
126 405 // Dynamic Posts Grid AJAX Helper - Initialize regardless of Elementor compatibility
127 406 // This is needed for AJAX functionality to work even when PRO version is disabled
128 407 require_once(KING_ADDONS_PATH . 'includes/helpers/Dynamic_Posts_Grid_Ajax.php');
129 408 \King_Addons\Dynamic_Posts_Grid_Ajax::get_instance();
@@ -139,8 +418,13 @@
139 418
140 419 add_action('elementor/init', [$this, 'initElementor']);
141 420
142 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);
143 427 add_action('elementor/controls/controls_registered', [$this, 'registerControls']);
144 428
145 429 self::enableFeatures();
146 430
@@ -161,12 +445,16 @@
161 445
162 446 // Initialize social login handler
163 447 \King_Addons\Widgets\Login_Register_Form\Social_Login_Handler::init();
164 448
165 - // Initialize Security Dashboard for admins
449 + // Initialize Security Dashboard for admins (only if Login Register Form widget is enabled)
166 450 if (is_admin()) {
167 - require_once(KING_ADDONS_PATH . 'includes/widgets/Login_Register_Form/Security_Dashboard.php');
168 - \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 + }
169 457 }
170 458
171 459 new Admin();
172 460
@@ -175,11 +463,28 @@
175 463
176 464 // Notice - Upgrade Suggestion
177 465 if (!king_addons_freemius()->can_use_premium_code__premium_only()) {
178 466 add_action('wp_ajax_king_addons_premium_notice_dismiss', [$this, 'king_addons_premium_notice_dismiss_callback']);
179 - 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 + );
180 482 }
181 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 +
182 487 // Conditionally enqueue AI text-field enhancement script and styles in Elementor editor
183 488 $ai_options = get_option('king_addons_ai_options', []);
184 489 $enable_ai_text_buttons = isset($ai_options['enable_ai_buttons']) ? (bool) $ai_options['enable_ai_buttons'] : true;
185 490 if ($enable_ai_text_buttons) {
@@ -205,8 +510,11 @@
205 510 // Check user capabilities
206 511 if (!current_user_can('manage_options')) {
207 512 wp_die();
208 513 }
514 +
515 + check_ajax_referer('king_addons_premium_notice_dismiss', 'nonce');
516 +
209 517 $user_id = get_current_user_id();
210 518 // Save the current time as the last dismissal time for the premium notice
211 519 update_user_meta($user_id, 'king_addons_premium_notice_dismissed_time', time());
212 520 wp_die(); // End AJAX request
@@ -211,8 +519,65 @@
211 519 update_user_meta($user_id, 'king_addons_premium_notice_dismissed_time', time());
212 520 wp_die(); // End AJAX request
213 521 }
214 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 +
215 580 function showNoticeUpgrade()
216 581 {
217 582 // Check user capabilities; show notice only to administrators as an example
218 583 if (!current_user_can('manage_options')) {
@@ -228,44 +593,300 @@
228 593 if ($last_dismissed && ($now - $last_dismissed) < 604800) {
229 594 // if ($last_dismissed && ($now - $last_dismissed) < 60) {
230 595 return;
231 596 }
232 -?>
597 + ?>
233 598 <div class="king-addons-upgrade-notice notice notice-info is-dismissible"
234 - style="border-left: 4px solid #FF4040;padding: 10px 15px;">
599 + style="border-left: 4px solid #0071e3;padding: 10px 15px;">
235 600 <p style="font-size: 15px; margin:0; display: flex; align-items: center;">
236 - <span>Unlock <strong style="font-weight: 700;">650+</strong> premium templates and <strong
237 - style="font-weight: 700;">200+</strong> advanced features for only $<strong
238 - 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>
239 608 </p>
240 609 <p style="font-size: 14px; opacity: 0.6;">Trusted by 20,000+ users</p>
241 610 <p style="display: flex;">
242 - <a style="font-size: 14px;padding: 4px 22px;display: flex;align-items: center;background-image: linear-gradient(120deg, #A20BD8 0%, #FF4040 100%);border: none;"
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;"
243 634 href="https://kingaddons.com/pricing?utm_source=kng-notice-offer&utm_medium=plugin&utm_campaign=kng"
244 - class="button button-primary"><img style="margin-right: 7px;width: 15px;height: 15px;"
245 - src="<?php echo esc_url(KING_ADDONS_URL) . 'includes/admin/img/icon-for-admin.svg'; ?>"
246 - alt="<?php echo esc_html__('Upgrade Now', 'king-addons'); ?>">Upgrade Now</a>
247 - <a style="margin-left: 20px;display: flex;align-items: center;"
248 - href="https://kingaddons.com/pricing?utm_source=kng-notice-offer&utm_medium=plugin&utm_campaign=kng"
249 635 class="link">Learn More</a>
250 636 </p>
251 637 </div>
252 638 <script>
253 - (function($) {
639 + (function ($) {
254 640 // Wait for the document to be ready
255 - $(document).ready(function() {
641 + const kingAddonsPremiumNoticeNonce = '<?php echo esc_js(wp_create_nonce('king_addons_premium_notice_dismiss')); ?>';
642 + $(document).ready(function () {
256 643 // Attach click handler to the dismiss button of the premium notice
257 - $('.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 () {
258 645 $.post(ajaxurl, {
259 - action: 'king_addons_premium_notice_dismiss'
646 + action: 'king_addons_premium_notice_dismiss',
647 + nonce: kingAddonsPremiumNoticeNonce
260 648 });
261 649 });
262 650 });
263 651 })(jQuery);
264 652 </script>
265 -<?php
653 + <?php
266 654 }
267 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 +
268 889 function enqueueFrontendStyles()
269 890 {
270 891 /**
271 892 * It fixes the default Elementor SVG icon rendering feature (Settings -> Features -> Inline Font Icons)
@@ -341,22 +962,371 @@
341 962 {
342 963 add_action('elementor/widgets/register', [$this, 'registerWidgets']);
343 964 add_action('elementor/editor/after_enqueue_styles', [$this, 'enqueueEditorStyles']);
344 965 add_action('elementor/editor/after_enqueue_scripts', [$this, 'enqueueEditorScripts']);
966 + add_action('elementor/preview/enqueue_styles', [$this, 'enqueueEditorPreviewStyles']);
345 967 }
346 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 +
347 979 function addWidgetCategory(): void
348 980 {
349 - Plugin::instance()->elements_manager->add_category(
981 + $elements_manager = Plugin::instance()->elements_manager;
982 +
983 + $elements_manager->add_category(
350 984 'king-addons',
351 985 [
352 986 'title' => esc_html__('King Addons', 'king-addons'),
353 - 'icon' => 'fa fa-plug'
987 + 'icon' => 'fa fa-plug',
354 988 ]
355 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 + );
356 1019 }
357 1020
358 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 + /**
359 1329 * Registers Elementor widgets with a mechanism to skip (and remember) broken widgets
360 1330 * that caused a fatal error previously, and try them again if the plugin version is updated.
361 1331 *
362 1332 * @param Widgets_Manager $widgets_manager
@@ -370,9 +1340,46 @@
370 1340 $currentPluginVersion = KING_ADDONS_VERSION;
371 1341
372 1342 // Get plugin options to check if a widget is enabled
373 1343 $options = get_option('king_addons_options');
1344 + $options = is_array($options) ? $options : [];
374 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 +
375 1382 /**
376 1383 * Retrieve the array of broken widgets from the WordPress options.
377 1384 * The structure is expected to be something like:
378 1385 *
@@ -427,13 +1434,25 @@
427 1434 * STEP 3: Now we iterate through all widgets in our modules map and try to load them.
428 1435 * If a widget is in the broken list, we skip it to avoid repeated fatal errors.
429 1436 */
430 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 +
431 1444 // Check if the widget is enabled in the options
432 1445 if (!isset($options[$widget_id]) || $options[$widget_id] !== 'enabled') {
433 1446 continue;
434 1447 }
435 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 +
436 1455 // If this widget is listed as broken, skip it
437 1456 if (array_key_exists($widget_id, $brokenWidgets)) {
438 1457 // Log something here if needed:
439 1458 // error_log("Skipping widget {$widget_id}, it previously caused a fatal error.");
@@ -445,10 +1464,17 @@
445 1464
446 1465 // Include the base widget class
447 1466 $widget_class = $widget['php-class'];
448 1467 $path_widget_class = "King_Addons\\" . $widget_class;
449 - 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 + }
450 1474
1475 + require_once $widget_file;
1476 +
451 1477 // Check if we can load the Pro version
452 1478 if (
453 1479 function_exists('king_addons_freemius')
454 1480 && king_addons_freemius()->can_use_premium_code__premium_only()
@@ -484,8 +1510,14 @@
484 1510 $options = get_option('king_addons_options');
485 1511
486 1512 foreach (ModulesMap::getModulesMapArray()['widgets'] as $widget_id => $widget) {
487 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 +
488 1520 if (!($options[$widget_id] ?? null)) {
489 1521 $options[$widget_id] = 'enabled';
490 1522 update_option('king_addons_options', $options);
491 1523 }
@@ -491,13 +1523,25 @@
491 1523 }
492 1524 }
493 1525 }
494 1526
495 - 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
496 1535 {
497 1536 $options = get_option('king_addons_options');
498 1537
499 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 + }
500 1544
501 1545 if (!($options[$feature_id] ?? null)) {
502 1546 $options[$feature_id] = 'enabled';
503 1547 update_option('king_addons_options', $options);
@@ -502,14 +1546,44 @@
502 1546 $options[$feature_id] = 'enabled';
503 1547 update_option('king_addons_options', $options);
504 1548 }
505 1549
506 - if ($options[$feature_id] === 'enabled') {
507 - $feature_class = $feature['php-class'];
508 - $path_feature_class = "King_Addons\\" . $feature_class;
509 - require_once(KING_ADDONS_PATH . 'includes/features/' . $feature_class . '/' . $feature_class . '.php');
510 - new $path_feature_class;
1550 + if ($options[$feature_id] !== 'enabled') {
1551 + continue;
511 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 + }
512 1586 }
513 1587 }
514 1588
515 1589 public function registerControls(Controls_Manager $controls_manager): void
@@ -540,8 +1614,40 @@
540 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);
541 1615 }
542 1616 }
543 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 +
544 1650 public static function renderProFeaturesSection($module, $section, $type, $widget_name, $features): void
545 1651 {
546 1652 if (king_addons_freemius()->can_use_premium_code__premium_only()) {
547 1653 return;
@@ -568,8 +1674,34 @@
568 1674
569 1675 $module->end_controls_section();
570 1676 }
571 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 +
572 1704 public static function renderUpgradeProNotice($module, $controls_manager, $widget_name, $option, $condition = []): void
573 1705 {
574 1706 if (king_addons_freemius()->can_use_premium_code__premium_only()) {
575 1707 return;
@@ -623,16 +1755,19 @@
623 1755 $image = esc_url($args['image']);
624 1756 $network = $args['network'];
625 1757
626 1758 $get_whatsapp_url = function ($a) {
627 - if ('yes' === $a['show_whatsapp_title'] && 'yes' === $a['show_whatsapp_excerpt']) {
628 - return 'https://api.whatsapp.com/send?text=*' . $a['title'] . '*%0a' . $a['text'] . '%0a' . $a['url'];
629 - } elseif ('yes' === $a['show_whatsapp_title']) {
630 - return 'https://api.whatsapp.com/send?text=*' . $a['title'] . '*%0a' . $a['url'];
631 - } elseif ('yes' === $a['show_whatsapp_excerpt']) {
632 - 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'] . '*';
633 1762 }
634 - 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));
635 1770 };
636 1771
637 1772 $networks_map = [
638 1773 'facebook-f' => [
@@ -724,10 +1859,17 @@
724 1859 $share_url = $networks_map[$network]['url'];
725 1860 $network_title = $networks_map[$network]['title'];
726 1861 $icon_category = $networks_map[$network]['icon'];
727 1862
728 - $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 + }
729 1869
1870 + $output = '<a href="' . $href . '" class="king-addons-share-icon king-addons-share-' . esc_attr($network) . '" target="_blank">';
1871 +
730 1872 if ('yes' === $args['tooltip']) {
731 1873 $output .= '<span class="king-addons-share-tooltip king-addons-tooltip">' . esc_html($network_title) . '</span>';
732 1874 }
733 1875
@@ -932,9 +2074,14 @@
932 2074 if (!$api_key) {
933 2075 return $mailchimp_list;
934 2076 }
935 2077
936 - $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/';
937 2084 $response = wp_remote_get($url, [
938 2085 'headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $api_key)]
939 2086 ]);
940 2087
@@ -949,12 +2096,22 @@
949 2096 }
950 2097
951 2098 public static function getMailchimpGroups()
952 2099 {
953 - $apiKey = get_option('king_addons_mailchimp_api_key');
954 - $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/';
955 2113 $authArgs = ['headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $apiKey)]];
956 - $groups = ['def' => 'Select Group'];
957 2114 $mailchimpIDs = Core::getMailchimpLists();
958 2115
959 2116 foreach ($mailchimpIDs as $audience => $ignore) {
960 2117 if ($audience === 'def') {
@@ -960,14 +2117,20 @@
960 2117 if ($audience === 'def') {
961 2118 continue;
962 2119 }
963 2120
964 - $cats = wp_remote_get("{$domain}lists/$audience/interest-categories", $authArgs);
965 - $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 ?? [];
966 2126
967 2127 foreach ($cats as $cat) {
968 - $interests = wp_remote_get("{$domain}lists/$audience/interest-categories/$cat->id/interests", $authArgs);
969 - $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 ?? [];
970 2133
971 2134 foreach ($interests as $int) {
972 2135 $groups[$int->id] = $int->name;
973 2136 }
@@ -1052,9 +2215,9 @@
1052 2215 array_map(function ($slug) {
1053 2216 $keys = [];
1054 2217 foreach (get_posts(['post_type' => $slug, 'posts_per_page' => -1]) as $post) {
1055 2218 // get_post_custom_keys can return null, so cast to array:
1056 - foreach ((array)get_post_custom_keys($post->ID) as $meta_key) {
2219 + foreach ((array) get_post_custom_keys($post->ID) as $meta_key) {
1057 2220 // Exclude protected keys (those beginning with "_").
1058 2221 if ($meta_key[0] !== '_') {
1059 2222 $keys[] = $meta_key;
1060 2223 }
@@ -1103,8 +2266,10 @@
1103 2266 * @return void
1104 2267 */
1105 2268 public function enqueueAiFieldScript(): void
1106 2269 {
2270 + $ai_options = get_option('king_addons_ai_options', []);
2271 +
1107 2272 wp_enqueue_script(
1108 2273 'king-addons-ai-field',
1109 2274 KING_ADDONS_URL . 'includes/admin/js/ai-textfield.js',
1110 2275 ['jquery', 'elementor-editor'],
@@ -1116,20 +2281,61 @@
1116 2281 wp_localize_script(
1117 2282 'king-addons-ai-field',
1118 2283 'KingAddonsAiField',
1119 2284 [
1120 - 'ajax_url' => admin_url('admin-ajax.php'),
1121 - 'generate_nonce' => wp_create_nonce('king_addons_ai_generate_nonce'),
1122 - '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'),
1123 2288 'generate_action' => 'king_addons_ai_generate_text',
1124 - 'change_action' => 'king_addons_ai_change_text',
1125 - 'icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai.svg',
2289 + 'change_action' => 'king_addons_ai_change_text',
2290 + 'icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai.svg',
1126 2291 'rewrite_icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai-refresh.svg',
1127 - 'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
1128 - 'plugin_url' => KING_ADDONS_URL,
1129 - 'is_pro' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
1130 - 'premium_active' => king_addons_freemius()->can_use_premium_code__premium_only() ? true : false,
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,
1131 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 + ),
1132 2338 ]
1133 2339 );
1134 2340 }
1135 2341
@@ -1139,10 +2345,8 @@
1139 2345 * @return void
1140 2346 */
1141 2347 public function enqueueAiImageGenerationScript(): void
1142 2348 {
1143 - // Retrieve AI options and ensure it's an array to prevent warnings.
1144 - $ai_options = get_option('king_addons_ai_options', []);
1145 2349 wp_enqueue_script(
1146 2350 'king-addons-ai-image-field',
1147 2351 KING_ADDONS_URL . 'includes/admin/js/ai-imagefield.js',
1148 2352 ['jquery', 'elementor-editor'],
@@ -1154,16 +2358,30 @@
1154 2358 wp_localize_script(
1155 2359 'king-addons-ai-image-field',
1156 2360 'KingAddonsAiImageField',
1157 2361 [
1158 - 'ajax_url' => admin_url('admin-ajax.php'),
1159 - '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'),
1160 2364 'generate_action' => 'king_addons_ai_generate_image',
1161 - 'image_model' => sanitize_text_field($ai_options['openai_image_model'] ?? ''),
1162 - 'icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai.svg',
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',
1163 2376 'rewrite_icon_url' => KING_ADDONS_URL . 'includes/admin/img/ai-refresh.svg',
1164 - 'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
1165 - 'plugin_url' => KING_ADDONS_URL,
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 + ),
1166 2384 ]
1167 2385 );
1168 2386 }
1169 2387
@@ -1223,8 +2441,182 @@
1223 2441
1224 2442 // Note: Using existing KingAddonsAiField localization
1225 2443 // The translator script will use the same AJAX endpoints and settings
1226 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);
1227 2619 }
1228 2620 }
1229 2621
1230 2622 Core::instance();