PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.83
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.83
51.1.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 51.1.45 All 40 releases
← All changes | includes/Core.php +545 -51 51.1.7951.1.83 View file →
@@ -8,8 +8,9 @@
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;
12 13 use King_Addons\Wishlist\Wishlist_Module;
13 14
14 15 if (!defined('ABSPATH')) {
15 16 exit; // Exit if accessed directly.
@@ -103,8 +104,10 @@
103 104 public function __construct()
104 105 {
105 106 require_once(KING_ADDONS_PATH . 'includes/ModulesMap.php');
106 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();
107 110
108 111 if ($this->hasElementorCompatibility()) {
109 112
110 113 // Initial requirements check
@@ -138,8 +141,26 @@
138 141 require_once(KING_ADDONS_PATH . 'includes/extensions/Cookie_Consent/Cookie_Consent.php');
139 142 Cookie_Consent::instance();
140 143 }
141 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 +
142 163 // WooCommerce Builder
143 164 if ($this->isExtensionEnabled('woo-builder', 'KING_ADDONS_EXT_WOO_BUILDER')) {
144 165 require_once(KING_ADDONS_PATH . 'includes/extensions/Woo_Builder/Woo_Builder.php');
145 166 if (class_exists('King_Addons\\Woo_Builder')) {
@@ -276,8 +297,24 @@
276 297 require_once(KING_ADDONS_PATH . 'includes/extensions/Site_Preloader/Site_Preloader.php');
277 298 Site_Preloader::instance();
278 299 }
279 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 +
280 317 // Image Optimizer
281 318 if ($this->isExtensionEnabled('image-optimizer', 'KING_ADDONS_EXT_IMAGE_OPTIMIZER')) {
282 319 require_once(KING_ADDONS_PATH . 'includes/extensions/Image_Optimizer/Image_Optimizer.php');
283 320 \King_Addons\Image_Optimizer\Image_Optimizer::instance();
@@ -310,8 +347,19 @@
310 347 require_once(KING_ADDONS_PATH . 'includes/helpers/Grid/Post_Likes_Ajax.php');
311 348
312 349 // Additional - Form Builder
313 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();
314 362 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Form_Builder_Security.php');
315 363 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Create_Submission.php');
316 364 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Send_Email.php');
317 365 require_once(KING_ADDONS_PATH . 'includes/widgets/Form_Builder/helpers/Send_Webhook.php');
@@ -361,8 +409,13 @@
361 409
362 410 add_action('elementor/init', [$this, 'initElementor']);
363 411
364 412 add_action('elementor/elements/categories_registered', [$this, 'addWidgetCategory']);
413 + // Elementor promotes Atomic / Custom Widget / Pro after that hook, so the
414 + // panel order is applied again once categories are fully initialised.
415 + add_action('elementor/init', [$this, 'reorderWidgetCategoriesLate'], 100);
416 + add_action('elementor/editor/before_enqueue_scripts', [$this, 'reorderWidgetCategoriesLate'], 1);
417 + add_filter('elementor/document/config', [$this, 'filterDocumentPanelCategories'], 1, 2);
365 418 add_action('elementor/controls/controls_registered', [$this, 'registerControls']);
366 419
367 420 self::enableFeatures();
368 421
@@ -765,9 +818,9 @@
765 818 <svg viewBox="0 0 512 512" aria-hidden="true" focusable="false"><path d="M504.981,150.787c-6.048-5.163-14.583-6.251-21.736-2.769l-109.444,53.28L271.109,82.896C267.311,78.516,261.798,76,256,76c-5.798,0-11.31,2.516-15.109,6.896L138.199,201.297l-109.444-53.28c-7.153-3.481-15.687-2.394-21.737,2.769c-6.05,5.163-8.466,13.421-6.153,21.031l76,250C79.426,430.242,87.195,436,96,436h320c8.804,0,16.574-5.758,19.134-14.182l76-250C513.448,164.208,511.032,155.95,504.981,150.787z M401.175,396H110.823L52.472,204.052l82.022,39.931c8.144,3.964,17.93,1.962,23.863-4.878L256,126.525l97.644,112.58c5.932,6.841,15.721,8.841,23.862,4.878l82.022-39.931L401.175,396z"></path></svg>
766 819 <?php esc_html_e('King Addons Pro', 'king-addons'); ?>
767 820 </div>
768 821 <h2 class="king-addons-un-headline">
769 - <?php esc_html_e('Mega Menu, Popup Builder, Theme Builder &amp; AI Tools', 'king-addons'); ?>
822 + <?php esc_html_e('Mega Menu, Popup Builder, WooCommerce Builder, Theme Builder &amp; AI Tools', 'king-addons'); ?>
770 823 </h2>
771 824 <p class="king-addons-un-sub">
772 825 <?php esc_html_e('200+ Pro features, premium widgets and 4,000+ templates &amp; sections for Elementor.', 'king-addons'); ?>
773 826 </p>
@@ -917,32 +970,116 @@
917 970 function addWidgetCategory(): void
918 971 {
919 972 $elements_manager = Plugin::instance()->elements_manager;
920 973
921 - // Add our categories
922 974 $elements_manager->add_category(
923 975 'king-addons',
924 976 [
925 977 'title' => esc_html__('King Addons', 'king-addons'),
926 - 'icon' => 'fa fa-plug'
978 + 'icon' => 'fa fa-plug',
927 979 ]
928 980 );
929 981
982 + if (class_exists('WooCommerce') && function_exists('WC')) {
983 + $elements_manager->add_category(
984 + 'king-addons-woo',
985 + [
986 + 'title' => esc_html__('King Addons WooCommerce', 'king-addons'),
987 + 'icon' => 'fa fa-shopping-cart',
988 + 'hideIfEmpty' => true,
989 + ]
990 + );
991 +
992 + $elements_manager->add_category(
993 + 'king-addons-woo-builder',
994 + [
995 + 'title' => esc_html__('King Addons WooCommerce Builder', 'king-addons'),
996 + 'icon' => 'fa fa-shopping-cart',
997 + 'hideIfEmpty' => true,
998 + ]
999 + );
1000 + }
1001 +
930 1002 $elements_manager->add_category(
931 - 'king-addons-woo-builder',
1003 + 'king-addons-theme-builder',
932 1004 [
933 - 'title' => esc_html__('King Addons Woo Builder', 'king-addons'),
934 - 'icon' => 'fa fa-shopping-cart'
1005 + 'title' => esc_html__('King Addons Theme Builder', 'king-addons'),
1006 + 'icon' => 'fa fa-plug',
1007 + 'hideIfEmpty' => true,
935 1008 ]
936 1009 );
1010 + }
937 1011
938 - // Move our categories to the top of the panel
1012 + /**
1013 + * Apply the panel category order after Elementor has finished registering its own.
1014 + *
1015 + * @return void
1016 + */
1017 + public function reorderWidgetCategoriesLate(): void
1018 + {
1019 + if (!did_action('elementor/loaded')) {
1020 + return;
1021 + }
1022 +
1023 + $elements_manager = Plugin::instance()->elements_manager;
1024 + $elements_manager->get_categories();
939 1025 $this->reorderWidgetCategories($elements_manager);
1026 +
1027 + if (!$this->shouldApplyEditorPanelVisibility()) {
1028 + return;
1029 + }
1030 +
1031 + $post_id = $this->getCurrentEditorPostId();
1032 + if ($post_id > 0) {
1033 + $this->applyContextCategoryVisibility($elements_manager, $post_id);
1034 + }
940 1035 }
941 1036
942 1037 /**
943 - * Reorder widget categories so King Addons categories appear after Layout and Basic.
1038 + * Overwrite the editor panel category snapshot after Elementor has built document config.
944 1039 *
1040 + * @param array $additional_config
1041 + * @param int $post_id
1042 + * @return array
1043 + */
1044 + public function filterDocumentPanelCategories($additional_config, $post_id = 0)
1045 + {
1046 + $post_id = (int) $post_id;
1047 + if ($post_id <= 0) {
1048 + $post_id = $this->getCurrentEditorPostId();
1049 + }
1050 + if (!is_array($additional_config)) {
1051 + $additional_config = [];
1052 + }
1053 +
1054 + if (!did_action('elementor/loaded')) {
1055 + return $additional_config;
1056 + }
1057 +
1058 + $elements_manager = Plugin::instance()->elements_manager;
1059 + $elements_manager->get_categories();
1060 + $this->reorderWidgetCategories($elements_manager);
1061 + if ($post_id > 0) {
1062 + $this->applyContextCategoryVisibility($elements_manager, $post_id);
1063 + }
1064 +
1065 + $categories = \Elementor\Core\Base\Document::get_filtered_editor_panel_categories();
1066 + foreach ($this->getHiddenPanelCategoryKeys($post_id) as $hidden_key) {
1067 + unset($categories[$hidden_key]);
1068 + }
1069 +
1070 + $additional_config['panel']['elements_categories'] = $categories;
1071 +
1072 + foreach ($this->getHiddenPanelWidgetNames($post_id) as $widget_name) {
1073 + $additional_config['widgets'][$widget_name]['show_in_panel'] = false;
1074 + }
1075 +
1076 + return $additional_config;
1077 + }
1078 +
1079 + /**
1080 + * Reorder widget categories: Layout, then King Addons groups, then the rest of Elementor.
1081 + *
945 1082 * @param \Elementor\Elements_Manager $elements_manager
946 1083 * @return void
947 1084 */
948 1085 private function reorderWidgetCategories($elements_manager): void
@@ -949,56 +1086,77 @@
949 1086 {
950 1087 try {
951 1088 $reflection = new \ReflectionClass($elements_manager);
952 1089 $categories_property = $reflection->getProperty('categories');
953 - $categories_property->setAccessible(true);
1090 + if (PHP_VERSION_ID < 80100) {
1091 + $categories_property->setAccessible(true);
1092 + }
954 1093
955 1094 $categories = $categories_property->getValue($elements_manager);
956 - if (!is_array($categories)) {
1095 + if (!is_array($categories) || $categories === []) {
957 1096 return;
958 1097 }
959 1098
960 - // Extract our categories
961 - $our_categories = [];
962 - if (isset($categories['king-addons'])) {
963 - $our_categories['king-addons'] = $categories['king-addons'];
964 - unset($categories['king-addons']);
1099 + $layout = [];
1100 + $ours = [];
1101 + $rest = [];
1102 +
1103 + foreach ($categories as $key => $value) {
1104 + if ('layout' === $key) {
1105 + $layout[$key] = $value;
1106 + } elseif (str_starts_with((string) $key, 'king-addons')) {
1107 + $ours[$key] = $value;
1108 + } else {
1109 + $rest[$key] = $value;
1110 + }
965 1111 }
966 - if (isset($categories['king-addons-woo-builder'])) {
967 - $our_categories['king-addons-woo-builder'] = $categories['king-addons-woo-builder'];
968 - unset($categories['king-addons-woo-builder']);
1112 +
1113 + $ours_sorted = [];
1114 + foreach (['king-addons', 'king-addons-woo', 'king-addons-woo-builder', 'king-addons-theme-builder'] as $preferred) {
1115 + if (isset($ours[$preferred])) {
1116 + $ours_sorted[$preferred] = $ours[$preferred];
1117 + unset($ours[$preferred]);
1118 + }
969 1119 }
1120 + $ours_sorted += $ours;
970 1121
971 - // Insert our categories after Layout and Basic
972 - $reordered = [];
973 - $insert_after = ['layout', 'basic']; // Categories after which we insert ours
974 - $inserted = false;
1122 + $categories_property->setValue($elements_manager, $layout + $ours_sorted + $rest);
1123 + } catch (\ReflectionException $e) {
1124 + // Silently fail if reflection doesn't work (e.g., future Elementor changes)
1125 + }
1126 + }
975 1127
976 - foreach ($categories as $key => $value) {
977 - $reordered[$key] = $value;
978 -
979 - // Insert our categories after the last target category
980 - if (!$inserted && in_array($key, $insert_after, true)) {
981 - // Check if next category is also in our target list
982 - $keys = array_keys($categories);
983 - $current_index = array_search($key, $keys, true);
984 - $next_key = $keys[$current_index + 1] ?? null;
985 -
986 - // Only insert if the next category is NOT in our target list
987 - if ($next_key === null || !in_array($next_key, $insert_after, true)) {
988 - $reordered = array_merge($reordered, $our_categories);
989 - $inserted = true;
990 - }
991 - }
1128 + /**
1129 + * Drop context-only categories from the manager for the current editor document.
1130 + *
1131 + * @param \Elementor\Elements_Manager $elements_manager
1132 + * @param int $post_id
1133 + * @return void
1134 + */
1135 + private function applyContextCategoryVisibility($elements_manager, int $post_id): void
1136 + {
1137 + $hidden = $this->getHiddenPanelCategoryKeys($post_id);
1138 + if ($hidden === []) {
1139 + return;
1140 + }
1141 +
1142 + try {
1143 + $reflection = new \ReflectionClass($elements_manager);
1144 + $categories_property = $reflection->getProperty('categories');
1145 + if (PHP_VERSION_ID < 80100) {
1146 + $categories_property->setAccessible(true);
992 1147 }
993 1148
994 - // If target categories weren't found, append at the end
995 - if (!$inserted) {
996 - $reordered = array_merge($reordered, $our_categories);
1149 + $categories = $categories_property->getValue($elements_manager);
1150 + if (!is_array($categories) || $categories === []) {
1151 + return;
997 1152 }
998 1153
999 - // Set back the reordered array
1000 - $categories_property->setValue($elements_manager, $reordered);
1154 + foreach ($hidden as $key) {
1155 + unset($categories[$key]);
1156 + }
1157 +
1158 + $categories_property->setValue($elements_manager, $categories);
1001 1159 } catch (\ReflectionException $e) {
1002 1160 // Silently fail if reflection doesn't work (e.g., future Elementor changes)
1003 1161 }
1004 1162 }
@@ -1003,8 +1161,163 @@
1003 1161 }
1004 1162 }
1005 1163
1006 1164 /**
1165 + * Category keys that should not appear in the current editor document.
1166 + *
1167 + * @param int $post_id
1168 + * @return array<int,string>
1169 + */
1170 + private function getHiddenPanelCategoryKeys(int $post_id): array
1171 + {
1172 + $hidden = [];
1173 + $is_woo_builder = $this->isWooBuilderDocument($post_id);
1174 + $is_theme_builder = $this->isThemeBuilderDocument($post_id);
1175 + $is_loop_item = $this->isLoopItemDocument($post_id);
1176 + $woocommerce_active = class_exists('WooCommerce') && function_exists('WC');
1177 +
1178 + if (!$woocommerce_active) {
1179 + $hidden[] = 'king-addons-woo';
1180 + $hidden[] = 'king-addons-woo-builder';
1181 + } elseif (!$is_woo_builder && !$is_loop_item) {
1182 + $hidden[] = 'king-addons-woo-builder';
1183 + }
1184 +
1185 + if (!$is_theme_builder && !$is_loop_item) {
1186 + $hidden[] = 'king-addons-theme-builder';
1187 + }
1188 +
1189 + return $hidden;
1190 + }
1191 +
1192 + /**
1193 + * Widget names that belong to hidden context categories.
1194 + *
1195 + * @param int $post_id
1196 + * @return array<int,string>
1197 + */
1198 + private function getHiddenPanelWidgetNames(int $post_id): array
1199 + {
1200 + $hidden_categories = $this->getHiddenPanelCategoryKeys($post_id);
1201 + if ($hidden_categories === [] || !did_action('elementor/loaded')) {
1202 + return [];
1203 + }
1204 +
1205 + $hidden_lookup = array_fill_keys($hidden_categories, true);
1206 + $names = [];
1207 +
1208 + foreach (Plugin::instance()->widgets_manager->get_widget_types() as $widget_name => $widget) {
1209 + if (!is_object($widget) || !method_exists($widget, 'get_categories')) {
1210 + continue;
1211 + }
1212 +
1213 + foreach ((array) $widget->get_categories() as $category) {
1214 + if (isset($hidden_lookup[$category])) {
1215 + $names[] = (string) $widget_name;
1216 + break;
1217 + }
1218 + }
1219 + }
1220 +
1221 + return $names;
1222 + }
1223 +
1224 + /**
1225 + * Whether the current request is the Elementor editor (or its AJAX).
1226 + *
1227 + * @return bool
1228 + */
1229 + private function shouldApplyEditorPanelVisibility(): bool
1230 + {
1231 + if (!empty($_GET['action']) && 'elementor' === $_GET['action']) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1232 + return true;
1233 + }
1234 +
1235 + if (!empty($_REQUEST['editor_post_id'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1236 + return true;
1237 + }
1238 +
1239 + return class_exists('\Elementor\Plugin')
1240 + && Plugin::instance()->editor
1241 + && Plugin::instance()->editor->is_edit_mode();
1242 + }
1243 +
1244 + /**
1245 + * Current Elementor editor post ID, if any.
1246 + *
1247 + * @return int
1248 + */
1249 + private function getCurrentEditorPostId(): int
1250 + {
1251 + foreach (['editor_post_id', 'post_id', 'post'] as $key) {
1252 + if (!empty($_REQUEST[$key])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1253 + $post_id = absint(wp_unslash($_REQUEST[$key])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1254 + if ($post_id > 0) {
1255 + return $post_id;
1256 + }
1257 + }
1258 + }
1259 +
1260 + $post = get_post();
1261 + return $post ? (int) $post->ID : 0;
1262 + }
1263 +
1264 + /**
1265 + * Whether the post is a King Addons Woo Builder template.
1266 + *
1267 + * @param int $post_id
1268 + * @return bool
1269 + */
1270 + private function isWooBuilderDocument(int $post_id): bool
1271 + {
1272 + if ($post_id <= 0) {
1273 + return false;
1274 + }
1275 +
1276 + $elementor_type = (string) get_post_meta($post_id, '_elementor_template_type', true);
1277 + if ('king-addons-woo-builder' === $elementor_type) {
1278 + return true;
1279 + }
1280 +
1281 + return (string) get_post_meta($post_id, 'ka_woo_template_type', true) !== '';
1282 + }
1283 +
1284 + /**
1285 + * Whether the post is a King Addons Theme Builder template.
1286 + *
1287 + * @param int $post_id
1288 + * @return bool
1289 + */
1290 + private function isThemeBuilderDocument(int $post_id): bool
1291 + {
1292 + if ($post_id <= 0) {
1293 + return false;
1294 + }
1295 +
1296 + $location_key = '_ka_theme_builder_location';
1297 + if (class_exists('\King_Addons\Theme_Builder\Meta_Keys')) {
1298 + $location_key = \King_Addons\Theme_Builder\Meta_Keys::LOCATION;
1299 + }
1300 +
1301 + return (string) get_post_meta($post_id, $location_key, true) !== '';
1302 + }
1303 +
1304 + /**
1305 + * Whether the post is a King Addons Loop Item template.
1306 + *
1307 + * @param int $post_id
1308 + * @return bool
1309 + */
1310 + private function isLoopItemDocument(int $post_id): bool
1311 + {
1312 + if ($post_id <= 0) {
1313 + return false;
1314 + }
1315 +
1316 + return 'king-addons-loop-item' === (string) get_post_meta($post_id, '_elementor_template_type', true);
1317 + }
1318 +
1319 + /**
1007 1320 * Registers Elementor widgets with a mechanism to skip (and remember) broken widgets
1008 1321 * that caused a fatal error previously, and try them again if the plugin version is updated.
1009 1322 *
1010 1323 * @param Widgets_Manager $widgets_manager
@@ -1292,8 +1605,40 @@
1292 1605 wp_enqueue_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-form-builder-editor-handler', KING_ADDONS_URL . 'includes/widgets/Form_Builder/editor-handler.js', '', KING_ADDONS_VERSION);
1293 1606 }
1294 1607 }
1295 1608
1609 + /**
1610 + * Widget settings with dynamic tags resolved, and nothing missing.
1611 + *
1612 + * get_settings() never parses dynamic tags, so a tag placed in such a
1613 + * widget printed its placeholder instead of the value. get_settings_for_display()
1614 + * does parse them, but it also drops every setting whose control condition
1615 + * is currently false - and widgets written against the raw array index into
1616 + * those keys without checking, which turns the switch into a batch of
1617 + * "Trying to access array offset on null" warnings.
1618 + *
1619 + * A hidden control comes back as null rather than missing, so the parsed
1620 + * value is preferred only when there is one, and the raw value fills in
1621 + * everywhere else.
1622 + *
1623 + * @param \Elementor\Controls_Stack $widget Widget being rendered.
1624 + *
1625 + * @return array<string,mixed>
1626 + */
1627 + public static function displaySettings($widget): array
1628 + {
1629 + $raw = (array) $widget->get_settings();
1630 + $display = (array) $widget->get_settings_for_display();
1631 +
1632 + foreach ($display as $key => $value) {
1633 + if (null !== $value) {
1634 + $raw[$key] = $value;
1635 + }
1636 + }
1637 +
1638 + return $raw;
1639 + }
1640 +
1296 1641 public static function renderProFeaturesSection($module, $section, $type, $widget_name, $features): void
1297 1642 {
1298 1643 if (king_addons_freemius()->can_use_premium_code__premium_only()) {
1299 1644 return;
@@ -1320,8 +1665,34 @@
1320 1665
1321 1666 $module->end_controls_section();
1322 1667 }
1323 1668
1669 + /**
1670 + * Same upgrade notice as renderUpgradeProNotice(), but not tied to another
1671 + * control's value. Use it where a whole section is Pro-only, so the free
1672 + * build does not show a section header that opens onto nothing.
1673 + *
1674 + * @param mixed $module Widget or repeater the control belongs to.
1675 + * @param string $controls_manager Control type to render the notice with.
1676 + * @param string $widget_name Widget slug, used for the campaign link.
1677 + * @param string $control_id Unique control id for the notice.
1678 + */
1679 + public static function renderUpgradeProSection($module, $controls_manager, string $widget_name, string $control_id): void
1680 + {
1681 + if (king_addons_freemius()->can_use_premium_code__premium_only()) {
1682 + return;
1683 + }
1684 +
1685 + $module->add_control(
1686 + $control_id,
1687 + [
1688 + 'raw' => 'Upgrade to the <strong><a href="https://kingaddons.com/pricing/?utm_source=kng-module-' . $widget_name . '-settings-upgrade-pro&utm_medium=plugin&utm_campaign=kng" target="_blank">Pro version</a></strong> now<br> and unlock this feature!',
1689 + 'type' => $controls_manager,
1690 + 'content_classes' => 'king-addons-pro-notice',
1691 + ]
1692 + );
1693 + }
1694 +
1324 1695 public static function renderUpgradeProNotice($module, $controls_manager, $widget_name, $option, $condition = []): void
1325 1696 {
1326 1697 if (king_addons_freemius()->can_use_premium_code__premium_only()) {
1327 1698 return;
@@ -1684,9 +2055,14 @@
1684 2055 if (!$api_key) {
1685 2056 return $mailchimp_list;
1686 2057 }
1687 2058
1688 - $url = 'https://' . explode('-', $api_key)[1] . '.api.mailchimp.com/3.0/lists/';
2059 + $parts = explode('-', (string) $api_key);
2060 + if (count($parts) < 2 || '' === $parts[1]) {
2061 + return $mailchimp_list;
2062 + }
2063 +
2064 + $url = 'https://' . $parts[1] . '.api.mailchimp.com/3.0/lists/';
1689 2065 $response = wp_remote_get($url, [
1690 2066 'headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $api_key)]
1691 2067 ]);
1692 2068
@@ -1701,12 +2077,22 @@
1701 2077 }
1702 2078
1703 2079 public static function getMailchimpGroups()
1704 2080 {
1705 - $apiKey = get_option('king_addons_mailchimp_api_key');
1706 - $domain = 'https://' . substr($apiKey, strpos($apiKey, '-') + 1) . '.api.mailchimp.com/3.0/';
2081 + $apiKey = (string) get_option('king_addons_mailchimp_api_key', '');
2082 + $groups = ['def' => 'Select Group'];
2083 +
2084 + if ('' === $apiKey || false === strpos($apiKey, '-')) {
2085 + return $groups;
2086 + }
2087 +
2088 + $dc = substr($apiKey, strpos($apiKey, '-') + 1);
2089 + if ('' === $dc) {
2090 + return $groups;
2091 + }
2092 +
2093 + $domain = 'https://' . $dc . '.api.mailchimp.com/3.0/';
1707 2094 $authArgs = ['headers' => ['Authorization' => 'Basic ' . base64_encode('user:' . $apiKey)]];
1708 - $groups = ['def' => 'Select Group'];
1709 2095 $mailchimpIDs = Core::getMailchimpLists();
1710 2096
1711 2097 foreach ($mailchimpIDs as $audience => $ignore) {
1712 2098 if ($audience === 'def') {
@@ -1712,14 +2098,20 @@
1712 2098 if ($audience === 'def') {
1713 2099 continue;
1714 2100 }
1715 2101
1716 - $cats = wp_remote_get("{$domain}lists/$audience/interest-categories", $authArgs);
1717 - $cats = json_decode($cats['body'])->categories ?? [];
2102 + $cats_res = wp_remote_get("{$domain}lists/$audience/interest-categories", $authArgs);
2103 + if (is_wp_error($cats_res)) {
2104 + continue;
2105 + }
2106 + $cats = json_decode((string) wp_remote_retrieve_body($cats_res))->categories ?? [];
1718 2107
1719 2108 foreach ($cats as $cat) {
1720 - $interests = wp_remote_get("{$domain}lists/$audience/interest-categories/$cat->id/interests", $authArgs);
1721 - $interests = json_decode($interests['body'])->interests ?? [];
2109 + $interests_res = wp_remote_get("{$domain}lists/$audience/interest-categories/$cat->id/interests", $authArgs);
2110 + if (is_wp_error($interests_res)) {
2111 + continue;
2112 + }
2113 + $interests = json_decode((string) wp_remote_retrieve_body($interests_res))->interests ?? [];
1722 2114
1723 2115 foreach ($interests as $int) {
1724 2116 $groups[$int->id] = $int->name;
1725 2117 }
@@ -1895,8 +2287,31 @@
1895 2287 : esc_html__('and top up your OpenAI account balance by at least $5', 'king-addons'),
1896 2288 'setup_cost_note' => \King_Addons\AI_Provider::isOpenRouter()
1897 2289 ? esc_html__('Free models cost nothing. Paid models cost pennies (about $0.01 per full page).', 'king-addons')
1898 2290 : esc_html__('Processing a page costs pennies (about $0.01 per full page).', 'king-addons'),
2291 + /**
2292 + * Before either provider has a key there is no "configured
2293 + * provider" to describe - the setting is just its default. The
2294 + * setup dialog then has to present the choice instead of sending
2295 + * everyone to OpenAI.
2296 + */
2297 + 'has_any_key' => (
2298 + '' !== \King_Addons\AI_Provider::getApiKey(\King_Addons\AI_Provider::OPENAI)
2299 + || '' !== \King_Addons\AI_Provider::getApiKey(\King_Addons\AI_Provider::OPENROUTER)
2300 + ),
2301 + 'setup_providers' => [
2302 + [
2303 + 'name' => esc_html__('OpenRouter', 'king-addons'),
2304 + 'url' => \King_Addons\AI_Provider::getApiKeysUrl(\King_Addons\AI_Provider::OPENROUTER),
2305 + 'note' => esc_html__('has free models, no credit needed to start', 'king-addons'),
2306 + ],
2307 + [
2308 + 'name' => esc_html__('OpenAI', 'king-addons'),
2309 + 'url' => \King_Addons\AI_Provider::getApiKeysUrl(\King_Addons\AI_Provider::OPENAI),
2310 + 'note' => esc_html__('needs at least $5 on your account balance', 'king-addons'),
2311 + ],
2312 + ],
2313 + 'setup_cost_note_neutral' => esc_html__('With a free OpenRouter model, nothing. On a paid model it is pennies (about $0.01 per full page).', 'king-addons'),
1899 2314 'missing_key_message' => sprintf(
1900 2315 /* translators: %s: provider name */
1901 2316 esc_html__('%s API key is missing or invalid. Please configure your API key in AI Settings.', 'king-addons'),
1902 2317 \King_Addons\AI_Provider::getLabel()
@@ -2007,8 +2422,87 @@
2007 2422
2008 2423 // Note: Using existing KingAddonsAiField localization
2009 2424 // The translator script will use the same AJAX endpoints and settings
2010 2425 // No need for separate localization as it reuses existing AI infrastructure
2426 + }
2427 +
2428 + /**
2429 + * Render an attachment through an Elementor image-size group control.
2430 + *
2431 + * Group_Control_Image_Size::get_attachment_image_html() takes a *key into
2432 + * $settings* as its third argument, not an attachment ID. Handing it an ID
2433 + * makes Elementor read $settings[<id>], which raises two PHP warnings and
2434 + * returns nothing, so callers silently lose the image.
2435 + *
2436 + * @param array<string, mixed> $settings Widget settings; carries "<$size_key>_size" and friends.
2437 + * @param string $size_key Name of the image-size group control.
2438 + * @param int $attachment_id Attachment to render.
2439 + *
2440 + * @return string Image HTML, or an empty string when it cannot be rendered.
2441 + */
2442 + /**
2443 + * Show the person building the page why a widget rendered nothing.
2444 + *
2445 + * Only ever printed inside the Elementor editor, so the public page keeps
2446 + * rendering exactly what it rendered before.
2447 + *
2448 + * @param string $message Plain text explaining what the widget still needs.
2449 + */
2450 + public static function renderEditorHint(string $message): void
2451 + {
2452 + if (!Plugin::$instance->editor->is_edit_mode()) {
2453 + return;
2454 + }
2455 +
2456 + $style = 'display:block;padding:12px 14px;border:1px dashed #c3c4c7;border-radius:4px;'
2457 + . 'background:#f6f7f7;color:#50575e;font-size:13px;line-height:1.5;';
2458 +
2459 + echo '<div class="king-addons-editor-hint" style="' . esc_attr($style) . '">'
2460 + . esc_html($message) . '</div>';
2461 + }
2462 +
2463 + /**
2464 + * A setting written straight into a JavaScript object literal has to be a
2465 + * number. Elementor stores a number field the user cleared as an empty
2466 + * string, which would emit `slidesPerView: ,` - a syntax error that stops
2467 + * the whole carousel from initialising - or blow up in PHP when the value
2468 + * is used in arithmetic.
2469 + *
2470 + * @param array<string, mixed> $settings Widget settings.
2471 + * @param string $key Setting to read.
2472 + * @param int|float $fallback Value to use when the setting is empty or not a number.
2473 + *
2474 + * @return string Numeric string, safe to interpolate into JS.
2475 + */
2476 + public static function jsNumber(array $settings, string $key, $fallback = 0): string
2477 + {
2478 + $value = $settings[$key] ?? null;
2479 +
2480 + // Slider controls keep their number under 'size'.
2481 + if (is_array($value)) {
2482 + $value = $value['size'] ?? null;
2483 + }
2484 +
2485 + if (!is_numeric($value)) {
2486 + $value = $fallback;
2487 + }
2488 +
2489 + return (string) (0 + $value);
2490 + }
2491 +
2492 + public static function getAttachmentImageHTML(array $settings, string $size_key, int $attachment_id): string
2493 + {
2494 + if ($attachment_id < 1) {
2495 + return '';
2496 + }
2497 +
2498 + $image_key = '__king_addons_image';
2499 + $settings[$image_key] = [
2500 + 'id' => $attachment_id,
2501 + 'url' => wp_get_attachment_image_url($attachment_id, 'full') ?: '',
2502 + ];
2503 +
2504 + return (string) Group_Control_Image_Size::get_attachment_image_html($settings, $size_key, $image_key);
2011 2505 }
2012 2506 }
2013 2507
2014 2508 Core::instance();