PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.63
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.63
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
king-addons / includes / extensions / Woo_Builder / Woo_Builder.php

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

1,655 lines 60.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WooCommerce Builder feature.
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons;
9
10 use Elementor\Controls_Manager;
11 use Elementor\Plugin as Elementor_Plugin;
12 use Elementor\Repeater;
13 use King_Addons\Woo_Builder\Context as Woo_Context;
14 use King_Addons\Woo_Builder\Document as Woo_Document;
15 use King_Addons\Woo_Builder\My_Account_Manager;
16 use WP_Post;
17 use WP_Query;
18
19 if (!defined('ABSPATH')) {
20 exit;
21 }
22
23 /**
24 * Registers and renders Elementor-based WooCommerce templates.
25 */
26 class Woo_Builder
27 {
28 /**
29 * Current template IDs keyed by context.
30 *
31 * @var array<string,int>
32 */
33 private array $current_templates = [];
34
35 /**
36 * Active context for body classes.
37 *
38 * @var string|null
39 */
40 private ?string $active_context = null;
41
42 /**
43 * Flag to ensure default blocks hook only once.
44 *
45 * @var bool
46 */
47 private bool $default_blocks_registered = false;
48
49 /**
50 * Constructor.
51 */
52 public function __construct()
53 {
54 if (!class_exists('WooCommerce') || !did_action('elementor/loaded')) {
55 return;
56 }
57
58 require_once KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Context.php';
59
60 add_action('init', [$this, 'register_template_types']);
61 add_action('wp_enqueue_scripts', [$this, 'register_assets']);
62 add_filter('template_include', [$this, 'override_wc_template'], 99);
63 add_filter('king_addons/woo_builder/current_template_id', [$this, 'filter_current_template_id'], 10, 2);
64 add_filter('body_class', [$this, 'filter_body_class']);
65
66 // My Account endpoint manager (Pro routing).
67 add_action(
68 'plugins_loaded',
69 static function () {
70 if (!class_exists(My_Account_Manager::class)) {
71 require_once KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/My_Account_Manager.php';
72 }
73 new My_Account_Manager();
74 }
75 );
76 add_filter('default_post_metadata', [$this, 'prefill_template_meta'], 10, 5);
77
78 // Save template type meta when post is created via URL parameter
79 add_action('save_post_elementor_library', [$this, 'save_template_type_from_url'], 10, 3);
80
81 // Also hook into wp_insert_post for auto-draft creation
82 add_action('wp_insert_post', [$this, 'save_template_type_on_insert'], 10, 3);
83
84 // Hook into admin to set meta when creating new template from URL
85 add_action('admin_init', [$this, 'maybe_set_woo_template_meta_on_new_post']);
86
87 // All conditions are managed in Elementor document settings (no WP metabox UI).
88
89 add_action('elementor/documents/register_controls', [$this, 'register_document_controls']);
90 add_action('elementor/document/after_save', [$this, 'handle_document_after_save'], 10, 2);
91 }
92
93 /**
94 * Register assets for builder.
95 *
96 * @return void
97 */
98 public function register_assets(): void
99 {
100 $deps = is_admin() ? ['jquery', 'select2', 'wp-api'] : [];
101
102 wp_register_style(
103 KING_ADDONS_ASSETS_UNIQUE_KEY . '-woo-builder-style',
104 KING_ADDONS_URL . 'includes/extensions/Woo_Builder/style.css',
105 [],
106 KING_ADDONS_VERSION
107 );
108
109 wp_register_script(
110 KING_ADDONS_ASSETS_UNIQUE_KEY . '-woo-builder-script',
111 KING_ADDONS_URL . 'includes/extensions/Woo_Builder/script.js',
112 $deps,
113 KING_ADDONS_VERSION,
114 true
115 );
116 }
117
118 /**
119 * Register Elementor document type for Woo Builder.
120 *
121 * @return void
122 */
123 public function register_template_types(): void
124 {
125 if (!class_exists('Elementor\\Plugin')) {
126 return;
127 }
128
129 if (!class_exists('King_Addons\\Woo_Builder\\Document')) {
130 require_once KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Document.php';
131 }
132
133 if (class_exists('King_Addons\\Woo_Builder\\Document')) {
134 Elementor_Plugin::$instance->documents->register_document_type(
135 Woo_Document::get_type(),
136 Woo_Document::class
137 );
138 }
139 }
140
141 /**
142 * Register meta box for Elementor templates.
143 *
144 * @return void
145 */
146 public function register_meta_boxes(): void
147 {
148 add_meta_box(
149 'king-addons-woo-builder',
150 esc_html__('Woo Builder Conditions', 'king-addons'),
151 [$this, 'render_meta_box'],
152 'elementor_library',
153 'side',
154 'default'
155 );
156 }
157
158 /**
159 * Render meta box UI.
160 *
161 * @param WP_Post $post Post object.
162 *
163 * @return void
164 */
165 public function render_meta_box(WP_Post $post): void
166 {
167 wp_nonce_field('king_addons_woo_builder_meta', 'king_addons_woo_builder_meta_nonce');
168
169 $template_type = get_post_meta($post->ID, 'ka_woo_template_type', true);
170 $conditions = get_post_meta($post->ID, 'ka_woo_conditions', true);
171 $enabled = !empty($conditions['enabled']);
172 $priority = isset($conditions['priority']) ? (int) $conditions['priority'] : 10;
173 $rules = [];
174 if (!empty($conditions['rules']) && is_array($conditions['rules'])) {
175 $rules = $conditions['rules'];
176 }
177 foreach ($rules as $idx => $rule) {
178 $rules[$idx]['type'] = $this->normalize_rule_type($rule['type'] ?? '');
179 }
180 if (empty($rules)) {
181 $rules[] = ['type' => 'always', 'values' => []];
182 }
183
184 $types = [
185 '' => esc_html__('Select type', 'king-addons'),
186 'single_product' => esc_html__('Single Product', 'king-addons'),
187 'product_archive' => esc_html__('Product Archive', 'king-addons'),
188 'cart' => esc_html__('Cart', 'king-addons'),
189 'checkout' => esc_html__('Checkout', 'king-addons'),
190 'my_account' => esc_html__('My Account', 'king-addons'),
191 ];
192
193 $rule_types = [
194 'always' => esc_html__('Always', 'king-addons'),
195 'all_products' => esc_html__('All products', 'king-addons'),
196 'product_in' => esc_html__('Specific products', 'king-addons'),
197 'product_cat_in' => esc_html__('Product categories', 'king-addons'),
198 'product_tag_in' => esc_html__('Product tags', 'king-addons'),
199 'product_type_in' => esc_html__('Product types', 'king-addons'),
200 'is_shop' => esc_html__('Shop page', 'king-addons'),
201 'product_cat_archive_in' => esc_html__('Product category archives', 'king-addons'),
202 'product_tag_archive_in' => esc_html__('Product tag archives', 'king-addons'),
203 'cart' => esc_html__('Cart page', 'king-addons'),
204 'checkout' => esc_html__('Checkout page', 'king-addons'),
205 'my_account' => esc_html__('My Account page', 'king-addons'),
206 // Legacy aliases kept for backward compatibility.
207 'products' => esc_html__('Specific products (IDs)', 'king-addons') . ' (legacy)',
208 'product_categories' => esc_html__('Product categories (IDs)', 'king-addons') . ' (legacy)',
209 'product_tags' => esc_html__('Product tags (IDs)', 'king-addons') . ' (legacy)',
210 'product_types' => esc_html__('Product types (slugs)', 'king-addons') . ' (legacy)',
211 'shop' => esc_html__('Shop page', 'king-addons') . ' (legacy)',
212 'product_cat_archives' => esc_html__('Product category archives (IDs)', 'king-addons') . ' (legacy)',
213 ];
214
215 // Admin assets for select2 UI and REST.
216 wp_enqueue_style('select2');
217 wp_enqueue_script('select2');
218 wp_enqueue_script('wp-api');
219 wp_enqueue_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-woo-builder-script');
220 wp_enqueue_style(KING_ADDONS_ASSETS_UNIQUE_KEY . '-woo-builder-style');
221 wp_localize_script(
222 KING_ADDONS_ASSETS_UNIQUE_KEY . '-woo-builder-script',
223 'KingAddonsWooBuilder',
224 [
225 'labels' => [
226 'valuePlaceholder' => esc_html__('Search or select…', 'king-addons'),
227 ],
228 ]
229 );
230 ?>
231 <p>
232 <label>
233 <input type="checkbox" name="ka_woo_enabled" value="1" <?php checked($enabled); ?> />
234 <?php esc_html_e('Enable template', 'king-addons'); ?>
235 </label>
236 </p>
237 <p>
238 <label for="ka_woo_template_type"><strong><?php esc_html_e('Template Type', 'king-addons'); ?></strong></label><br />
239 <select name="ka_woo_template_type" id="ka_woo_template_type" style="width:100%;">
240 <?php foreach ($types as $value => $label) : ?>
241 <option value="<?php echo esc_attr($value); ?>" <?php selected($template_type, $value); ?>><?php echo esc_html($label); ?></option>
242 <?php endforeach; ?>
243 </select>
244 </p>
245 <?php if (!$this->can_use_pro()) : ?>
246 <p class="king-addons-woo-builder-pro-lock">
247 <?php esc_html_e('Cart, Checkout, and My Account templates require the Pro version.', 'king-addons'); ?>
248 </p>
249 <?php endif; ?>
250 <p>
251 <label for="ka_woo_priority"><strong><?php esc_html_e('Priority', 'king-addons'); ?></strong></label><br />
252 <input type="number" min="0" name="ka_woo_priority" id="ka_woo_priority" value="<?php echo esc_attr($priority); ?>" style="width:100%;" />
253 </p>
254 <div id="ka-woo-rules">
255 <strong><?php esc_html_e('Rules (AND)', 'king-addons'); ?></strong>
256 <?php foreach ($rules as $index => $rule) : ?>
257 <div class="ka-woo-rule" style="border:1px solid #e0e0e0;padding:8px;margin-top:8px;border-radius:4px;">
258 <?php
259 $rule_type_value = $rule['type'] ?? '';
260 if ($rule_type_value && !isset($rule_types[$rule_type_value])) {
261 $rule_types[$rule_type_value] = sprintf(
262 /* translators: %s: rule type slug */
263 esc_html__('Unknown rule (%s)', 'king-addons'),
264 esc_html($rule_type_value)
265 );
266 }
267 ?>
268 <select class="ka-woo-rule-type" name="ka_woo_rule_type[<?php echo esc_attr($index); ?>]" style="width:100%;margin-bottom:6px;">
269 <?php foreach ($rule_types as $value => $label) : ?>
270 <option value="<?php echo esc_attr($value); ?>" <?php selected($rule['type'] ?? '', $value); ?>><?php echo esc_html($label); ?></option>
271 <?php endforeach; ?>
272 </select>
273 <?php
274 $rule_values = isset($rule['values']) && is_array($rule['values']) ? $rule['values'] : [];
275 $value_options = $this->get_rule_value_options($rule['type'] ?? '', $rule_values);
276 ?>
277 <select
278 class="ka-woo-rule-values"
279 name="ka_woo_rule_values[<?php echo esc_attr($index); ?>][]"
280 multiple
281 data-rule-type="<?php echo esc_attr($rule['type'] ?? ''); ?>"
282 data-selected-ids="<?php echo esc_attr(implode(',', array_map('strval', $rule_values))); ?>"
283 style="width:100%;"
284 >
285 <?php foreach ($value_options as $option_value => $option_label) : ?>
286 <option value="<?php echo esc_attr($option_value); ?>" <?php selected(true, in_array((string) $option_value, array_map('strval', $rule_values), true)); ?>>
287 <?php echo esc_html($option_label); ?>
288 </option>
289 <?php endforeach; ?>
290 </select>
291 <button type="button" class="button ka-woo-remove-rule" style="margin-top:6px;"><?php esc_html_e('Remove', 'king-addons'); ?></button>
292 </div>
293 <?php endforeach; ?>
294 </div>
295 <p><button type="button" class="button" id="ka-woo-add-rule"><?php esc_html_e('Add rule', 'king-addons'); ?></button></p>
296 <?php
297 }
298
299 /**
300 * Save meta box data.
301 *
302 * @param int $post_id Post ID.
303 * @param WP_Post $post Post.
304 *
305 * @return void
306 */
307 public function save_template_meta(int $post_id, WP_Post $post): void
308 {
309 if ('elementor_library' !== $post->post_type) {
310 return;
311 }
312
313 if (!isset($_POST['king_addons_woo_builder_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['king_addons_woo_builder_meta_nonce'])), 'king_addons_woo_builder_meta')) {
314 return;
315 }
316
317 if (!current_user_can('edit_post', $post_id)) {
318 return;
319 }
320
321 $template_type = sanitize_text_field($_POST['ka_woo_template_type'] ?? '');
322 update_post_meta($post_id, 'ka_woo_template_type', $template_type);
323 if (in_array($template_type, ['single_product', 'product_archive', 'cart', 'checkout', 'my_account'], true)) {
324 $document_type = class_exists(Woo_Document::class) ? Woo_Document::get_type() : 'king-addons-woo-builder';
325 update_post_meta($post_id, '_elementor_template_type', $document_type);
326 }
327
328 $enabled = !empty($_POST['ka_woo_enabled']);
329 $priority = isset($_POST['ka_woo_priority']) ? (int) $_POST['ka_woo_priority'] : 10;
330
331 $types = $_POST['ka_woo_rule_type'] ?? [];
332 $values = $_POST['ka_woo_rule_values'] ?? [];
333
334 $rules = [];
335 if (is_array($types) && is_array($values)) {
336 foreach ($types as $idx => $type) {
337 $type = sanitize_text_field($type);
338 if ('' === $type) {
339 continue;
340 }
341 $val_raw = $values[$idx] ?? '';
342 if (is_array($val_raw)) {
343 $vals = array_filter(array_map('trim', array_map('strval', $val_raw)));
344 } else {
345 $vals = array_filter(array_map('trim', explode(',', (string) $val_raw)));
346 }
347 $parsed_vals = [];
348 foreach ($vals as $v) {
349 if (is_numeric($v)) {
350 $parsed_vals[] = (int) $v;
351 } else {
352 $parsed_vals[] = sanitize_text_field($v);
353 }
354 }
355 $rules[] = [
356 'type' => $this->normalize_rule_type($type),
357 'values' => $parsed_vals,
358 ];
359 }
360 }
361
362 $conditions = [
363 'enabled' => $enabled,
364 'priority' => $priority,
365 'rules' => $rules,
366 ];
367
368 update_post_meta($post_id, 'ka_woo_conditions', $conditions);
369 }
370
371 /**
372 * Register Elementor document controls for Woo Builder templates.
373 *
374 * @param mixed $document Elementor document.
375 *
376 * @return void
377 */
378 public function register_document_controls($document): void
379 {
380 if (!is_object($document) || !method_exists($document, 'get_main_id')) {
381 return;
382 }
383
384 $post_id = (int) $document->get_main_id();
385 if ('elementor_library' !== get_post_type($post_id)) {
386 return;
387 }
388
389 $template_type = (string) get_post_meta($post_id, 'ka_woo_template_type', true);
390 $doc_type = class_exists(Woo_Document::class) ? Woo_Document::get_type() : 'king-addons-woo-builder';
391 $elementor_type = (string) get_post_meta($post_id, '_elementor_template_type', true);
392 if ('' === $template_type && $elementor_type !== $doc_type) {
393 return;
394 }
395
396 $conditions = get_post_meta($post_id, 'ka_woo_conditions', true);
397 $enabled = !empty($conditions['enabled']);
398 $priority = isset($conditions['priority']) ? (int) $conditions['priority'] : 10;
399 $rules = is_array($conditions['rules'] ?? null) ? $conditions['rules'] : [];
400 if (empty($rules)) {
401 $rules = [
402 [
403 'type' => 'always',
404 'values' => [],
405 ],
406 ];
407 }
408
409 $types = [
410 '' => esc_html__('Select type', 'king-addons'),
411 'single_product' => esc_html__('Single Product', 'king-addons'),
412 'product_archive' => esc_html__('Product Archive', 'king-addons'),
413 'cart' => esc_html__('Cart', 'king-addons'),
414 'checkout' => esc_html__('Checkout', 'king-addons'),
415 'my_account' => esc_html__('My Account', 'king-addons'),
416 ];
417
418 $rule_types = [
419 'always' => esc_html__('Always', 'king-addons'),
420 'all_products' => esc_html__('All products', 'king-addons'),
421 'product_in' => esc_html__('Specific products', 'king-addons'),
422 'product_cat_in' => esc_html__('Product categories', 'king-addons'),
423 'product_tag_in' => esc_html__('Product tags', 'king-addons'),
424 'product_type_in' => esc_html__('Product types', 'king-addons'),
425 'is_shop' => esc_html__('Shop page', 'king-addons'),
426 'product_cat_archive_in' => esc_html__('Product category archives', 'king-addons'),
427 'product_tag_archive_in' => esc_html__('Product tag archives', 'king-addons'),
428 'cart' => esc_html__('Cart page', 'king-addons'),
429 'checkout' => esc_html__('Checkout page', 'king-addons'),
430 'my_account' => esc_html__('My Account page', 'king-addons'),
431 ];
432
433 $product_types = function_exists('wc_get_product_types')
434 ? wc_get_product_types()
435 : [
436 'simple' => esc_html__('Simple', 'king-addons'),
437 'variable' => esc_html__('Variable', 'king-addons'),
438 'grouped' => esc_html__('Grouped', 'king-addons'),
439 'external' => esc_html__('External/Affiliate', 'king-addons'),
440 ];
441
442 $repeater = new Repeater();
443 $repeater->add_control(
444 'rule_type',
445 [
446 'label' => esc_html__('Rule Type', 'king-addons'),
447 'type' => Controls_Manager::SELECT,
448 'options' => $rule_types,
449 'default' => 'always',
450 ]
451 );
452 $repeater->add_control(
453 'values_products',
454 [
455 'label' => esc_html__('Products', 'king-addons'),
456 'type' => 'king-addons-ajax-select2',
457 'options' => 'ajaxselect2/getPostsByPostType',
458 'query_slug' => 'product',
459 'multiple' => true,
460 'condition' => [
461 'rule_type' => ['product_in', 'products'],
462 ],
463 ]
464 );
465 $repeater->add_control(
466 'values_product_cats',
467 [
468 'label' => esc_html__('Product categories', 'king-addons'),
469 'type' => 'king-addons-ajax-select2',
470 'options' => 'ajaxselect2/getTaxonomies',
471 'query_slug' => 'product_cat',
472 'multiple' => true,
473 'condition' => [
474 'rule_type' => ['product_cat_in', 'product_categories', 'product_cat_archive_in', 'product_cat_archives'],
475 ],
476 ]
477 );
478 $repeater->add_control(
479 'values_product_tags',
480 [
481 'label' => esc_html__('Product tags', 'king-addons'),
482 'type' => 'king-addons-ajax-select2',
483 'options' => 'ajaxselect2/getTaxonomies',
484 'query_slug' => 'product_tag',
485 'multiple' => true,
486 'condition' => [
487 'rule_type' => ['product_tag_in', 'product_tags', 'product_tag_archive_in', 'product_tag_archives'],
488 ],
489 ]
490 );
491 $repeater->add_control(
492 'values_product_types',
493 [
494 'label' => esc_html__('Product types', 'king-addons'),
495 'type' => Controls_Manager::SELECT2,
496 'options' => $product_types,
497 'multiple' => true,
498 'condition' => [
499 'rule_type' => ['product_type_in', 'product_types'],
500 ],
501 ]
502 );
503
504 $default_rules = [];
505 foreach ($rules as $rule) {
506 $type = $this->normalize_rule_type($rule['type'] ?? '');
507 $values = is_array($rule['values'] ?? null) ? $rule['values'] : [];
508 $item = [
509 'rule_type' => $type ?: 'always',
510 ];
511 switch ($type) {
512 case 'product_in':
513 case 'products':
514 $item['values_products'] = array_map('strval', $values);
515 break;
516 case 'product_cat_in':
517 case 'product_categories':
518 case 'product_cat_archive_in':
519 case 'product_cat_archives':
520 $item['values_product_cats'] = array_map('strval', $values);
521 break;
522 case 'product_tag_in':
523 case 'product_tags':
524 case 'product_tag_archive_in':
525 case 'product_tag_archives':
526 $item['values_product_tags'] = array_map('strval', $values);
527 break;
528 case 'product_type_in':
529 case 'product_types':
530 $item['values_product_types'] = array_map('strval', $values);
531 break;
532 }
533 $default_rules[] = $item;
534 }
535
536 if (empty($default_rules)) {
537 $default_rules[] = ['rule_type' => 'always'];
538 }
539
540 $document->start_controls_section(
541 'ka_woo_builder_settings',
542 [
543 'label' => esc_html__('Woo Builder', 'king-addons'),
544 'tab' => Controls_Manager::TAB_SETTINGS,
545 ]
546 );
547
548 $document->add_control(
549 'ka_woo_template_type',
550 [
551 'label' => esc_html__('Template Type', 'king-addons'),
552 'type' => Controls_Manager::SELECT,
553 'options' => $types,
554 'default' => $template_type ?: '',
555 ]
556 );
557
558 if (!$this->can_use_pro()) {
559 $document->add_control(
560 'ka_woo_pro_notice',
561 [
562 'label' => esc_html__('Pro', 'king-addons'),
563 'type' => Controls_Manager::RAW_HTML,
564 'raw' => esc_html__('Cart, Checkout, and My Account templates require Pro.', 'king-addons'),
565 'content_classes' => 'ka-theme-builder-readonly',
566 ]
567 );
568 }
569
570 $document->add_control(
571 'ka_woo_enabled',
572 [
573 'label' => esc_html__('Enable template', 'king-addons'),
574 'type' => Controls_Manager::SWITCHER,
575 'label_on' => esc_html__('Yes', 'king-addons'),
576 'label_off' => esc_html__('No', 'king-addons'),
577 'return_value' => 'yes',
578 'default' => $enabled ? 'yes' : 'no',
579 ]
580 );
581
582 $document->add_control(
583 'ka_woo_priority',
584 [
585 'label' => esc_html__('Priority', 'king-addons'),
586 'type' => Controls_Manager::NUMBER,
587 'default' => $priority,
588 'min' => 0,
589 ]
590 );
591
592 $document->add_control(
593 'ka_woo_rules',
594 [
595 'label' => esc_html__('Rules (AND)', 'king-addons'),
596 'type' => Controls_Manager::REPEATER,
597 'fields' => $repeater->get_controls(),
598 'default' => $default_rules,
599 'title_field' => '{{ rule_type }}',
600 ]
601 );
602
603 $document->end_controls_section();
604 }
605
606 /**
607 * Persist Woo Builder meta from Elementor document settings.
608 *
609 * @param mixed $document Elementor document.
610 * @param array $data Saved data.
611 *
612 * @return void
613 */
614 public function handle_document_after_save($document, array $data): void
615 {
616 if (!is_object($document) || !method_exists($document, 'get_main_id')) {
617 return;
618 }
619
620 $post_id = (int) $document->get_main_id();
621 if ('elementor_library' !== get_post_type($post_id)) {
622 return;
623 }
624
625 $valid_types = ['single_product', 'product_archive', 'cart', 'checkout', 'my_account'];
626 $template_type = sanitize_text_field((string) $document->get_settings('ka_woo_template_type'));
627 if (!in_array($template_type, $valid_types, true)) {
628 $template_type = '';
629 }
630
631 // If template_type not in settings, check existing meta or URL parameter
632 if ('' === $template_type) {
633 // First check if meta already exists
634 $existing_type = get_post_meta($post_id, 'ka_woo_template_type', true);
635 if (!empty($existing_type) && in_array($existing_type, $valid_types, true)) {
636 $template_type = $existing_type;
637 } else {
638 // Check URL parameter from referrer
639 $template_type = $this->get_template_type_from_request();
640 }
641 }
642
643 if ('' === $template_type && !get_post_meta($post_id, 'ka_woo_template_type', true)) {
644 return;
645 }
646
647 if ('' === $template_type) {
648 delete_post_meta($post_id, 'ka_woo_template_type');
649 } else {
650 update_post_meta($post_id, 'ka_woo_template_type', $template_type);
651 $document_type = class_exists(Woo_Document::class) ? Woo_Document::get_type() : 'king-addons-woo-builder';
652 update_post_meta($post_id, '_elementor_template_type', $document_type);
653 }
654
655 $enabled_setting = $document->get_settings('ka_woo_enabled');
656 $enabled = ('yes' === $enabled_setting || true === $enabled_setting);
657 $priority_setting = $document->get_settings('ka_woo_priority');
658 $priority = is_numeric($priority_setting) ? (int) $priority_setting : 10;
659
660 $raw_rules = $document->get_settings('ka_woo_rules');
661 $rules = [];
662
663 if (is_array($raw_rules)) {
664 foreach ($raw_rules as $rule) {
665 $type = $this->normalize_rule_type(sanitize_text_field($rule['rule_type'] ?? ''));
666 if ('' === $type) {
667 continue;
668 }
669 $values = [];
670 switch ($type) {
671 case 'product_in':
672 case 'products':
673 $values = $this->sanitize_rule_values($rule['values_products'] ?? [], true);
674 break;
675 case 'product_cat_in':
676 case 'product_categories':
677 case 'product_cat_archive_in':
678 case 'product_cat_archives':
679 $values = $this->sanitize_rule_values($rule['values_product_cats'] ?? [], true);
680 break;
681 case 'product_tag_in':
682 case 'product_tags':
683 case 'product_tag_archive_in':
684 case 'product_tag_archives':
685 $values = $this->sanitize_rule_values($rule['values_product_tags'] ?? [], true);
686 break;
687 case 'product_type_in':
688 case 'product_types':
689 $values = $this->sanitize_rule_values($rule['values_product_types'] ?? [], false);
690 break;
691 default:
692 $values = [];
693 break;
694 }
695 $rules[] = [
696 'type' => $type,
697 'values' => $values,
698 ];
699 }
700 }
701
702 if (empty($rules)) {
703 $rules[] = [
704 'type' => 'always',
705 'values' => [],
706 ];
707 }
708
709 $conditions = [
710 'enabled' => $enabled,
711 'priority' => $priority,
712 'rules' => $rules,
713 ];
714
715 update_post_meta($post_id, 'ka_woo_conditions', $conditions);
716 }
717
718 /**
719 * Template include override.
720 *
721 * @param string $template Default template.
722 *
723 * @return string
724 */
725 public function override_wc_template(string $template): string
726 {
727 if (is_admin() || wp_doing_ajax() || (defined('REST_REQUEST') && REST_REQUEST)) {
728 return $template;
729 }
730
731 $context = Woo_Context::detect_context();
732 if (null === $context) {
733 return $template;
734 }
735
736 if (!$this->can_use_pro() && in_array($context, ['cart', 'checkout', 'my_account'], true)) {
737 return $template;
738 }
739
740 $template_id = $this->get_active_template_for_context($context);
741 $this->current_templates[$context] = $template_id ?: 0;
742 $this->active_context = $template_id ? $context : null;
743
744 if (!$template_id) {
745 return $template;
746 }
747
748 $this->maybe_register_default_blocks();
749
750 $wrapper = KING_ADDONS_PATH . 'includes/extensions/Woo_Builder/templates/' . $context . '.php';
751 if (file_exists($wrapper)) {
752 return $wrapper;
753 }
754
755 return $template;
756 }
757
758 /**
759 * Get active template ID for context.
760 *
761 * @param string $context Context.
762 *
763 * @return int|null
764 */
765 public function get_active_template_for_context(string $context): ?int
766 {
767 $query = new WP_Query([
768 'post_type' => 'elementor_library',
769 'posts_per_page' => -1,
770 'post_status' => 'publish', // Only published templates should be applied on frontend
771 'meta_query' => [
772 [
773 'key' => 'ka_woo_template_type',
774 'value' => $context,
775 ],
776 ],
777 ]);
778
779 if (!$query->have_posts()) {
780 return null;
781 }
782
783 $candidates = [];
784
785 foreach ($query->posts as $post) {
786 $conditions = get_post_meta($post->ID, 'ka_woo_conditions', true);
787 if (!is_array($conditions) || empty($conditions['enabled'])) {
788 continue;
789 }
790
791 if (!$this->can_use_pro() && !in_array($context, ['single_product', 'product_archive'], true)) {
792 continue;
793 }
794
795 if (!Woo_Context::match_conditions($context, $conditions)) {
796 continue;
797 }
798
799 $priority = isset($conditions['priority']) ? (int) $conditions['priority'] : 10;
800 $candidates[] = [
801 'id' => (int) $post->ID,
802 'priority' => $priority,
803 'specificity' => $this->get_conditions_specificity_score($context, $conditions),
804 ];
805 }
806
807 if (empty($candidates)) {
808 return null;
809 }
810
811 usort(
812 $candidates,
813 static function ($a, $b) {
814 $spec = ($b['specificity'] ?? 0) <=> ($a['specificity'] ?? 0);
815 if (0 !== $spec) {
816 return $spec;
817 }
818
819 $prio = ($a['priority'] ?? 10) <=> ($b['priority'] ?? 10);
820 if (0 !== $prio) {
821 return $prio;
822 }
823
824 return ($a['id'] ?? 0) <=> ($b['id'] ?? 0);
825 }
826 );
827
828 // Free: limit one template per context by picking top; Pro may have multiple but we still pick top priority.
829 return (int) $candidates[0]['id'];
830 }
831
832 /**
833 * Compute how specific a condition set is for the given context.
834 * Higher score = more specific.
835 *
836 * @param string $context
837 * @param array<string,mixed> $conditions
838 *
839 * @return int
840 */
841 private function get_conditions_specificity_score(string $context, array $conditions): int
842 {
843 if (empty($conditions['rules']) || !is_array($conditions['rules'])) {
844 return 0;
845 }
846
847 $score = 0;
848
849 foreach ($conditions['rules'] as $rule) {
850 $type = (string) ($rule['type'] ?? '');
851 $values = is_array($rule['values'] ?? null) ? $rule['values'] : [];
852 $has_values = !empty(array_filter($values, static fn($v) => '' !== (string) $v));
853
854 if ('single_product' === $context) {
855 if (in_array($type, ['specific_product', 'product_in', 'products'], true) && $has_values) {
856 $score += 300;
857 } elseif (in_array($type, ['product_cat', 'product_cat_in', 'product_categories'], true) && $has_values) {
858 $score += 200;
859 } elseif (in_array($type, ['product_tag', 'product_tag_in', 'product_tags'], true) && $has_values) {
860 $score += 200;
861 } elseif (in_array($type, ['all', 'all_products', 'always'], true)) {
862 $score += 100;
863 }
864 } elseif ('product_archive' === $context) {
865 if (in_array($type, ['shop', 'is_shop'], true)) {
866 $score += 300;
867 } elseif (in_array($type, ['product_cat', 'product_cat_archive_in', 'product_cat_archives', 'product_cat_in', 'product_categories'], true) && $has_values) {
868 $score += 200;
869 } elseif (in_array($type, ['product_tag', 'product_tag_archive_in', 'product_tag_archives', 'product_tag_in', 'product_tags'], true) && $has_values) {
870 $score += 200;
871 } elseif (in_array($type, ['all', 'always'], true)) {
872 $score += 100;
873 }
874 } else {
875 if (in_array($type, ['cart', 'checkout', 'my_account'], true)) {
876 $score += 200;
877 } elseif (in_array($type, ['all', 'always'], true)) {
878 $score += 100;
879 }
880 }
881 }
882
883 return $score;
884 }
885
886 /**
887 * Filter to expose current template id.
888 *
889 * @param int $template_id Template ID.
890 * @param string|null $context Context.
891 *
892 * @return int
893 */
894 public function filter_current_template_id(int $template_id, ?string $context = null): int
895 {
896 if ($context && !empty($this->current_templates[$context])) {
897 return (int) $this->current_templates[$context];
898 }
899 return $template_id;
900 }
901
902 /**
903 * Add body classes for active Woo Builder context.
904 *
905 * @param array<int,string> $classes Existing classes.
906 *
907 * @return array<int,string>
908 */
909 public function filter_body_class(array $classes): array
910 {
911 if (null !== $this->active_context) {
912 $classes[] = 'king-addons-woo-builder';
913 $classes[] = 'king-addons-woo-builder--' . $this->active_context;
914 if ($this->can_use_pro()) {
915 $classes[] = 'king-addons-woo-builder--pro';
916 }
917 }
918
919 return $classes;
920 }
921
922 /**
923 * Register default Pro blocks (upsells, quick links) once.
924 *
925 * @return void
926 */
927 private function maybe_register_default_blocks(): void
928 {
929 if ($this->default_blocks_registered || !$this->can_use_pro()) {
930 return;
931 }
932
933 add_action('king_addons/woo_builder/after_render', [$this, 'render_default_block'], 10, 2);
934 $this->default_blocks_registered = true;
935 }
936
937 /**
938 * Render default Pro blocks for supported contexts.
939 *
940 * @param string $context Context.
941 * @param int $template_id Template id.
942 *
943 * @return void
944 */
945 public function render_default_block(string $context, int $template_id): void
946 {
947 if (!$this->can_use_pro()) {
948 return;
949 }
950
951 $disable_meta = get_post_meta($template_id, 'ka_woo_disable_autoblocks', true);
952 if (!empty($disable_meta)) {
953 return;
954 }
955
956 if ('checkout' === $context) {
957 $this->render_checkout_upsells();
958 } elseif ('cart' === $context) {
959 $this->render_cart_cross_sells();
960 } elseif ('my_account' === $context) {
961 $this->render_account_quick_links();
962 }
963 }
964
965 /**
966 * Render checkout upsells block.
967 *
968 * @return void
969 */
970 private function render_checkout_upsells(): void
971 {
972 if (!function_exists('WC') || !WC()->cart || apply_filters('king_addons/woo_builder/disable_checkout_upsells', false)) {
973 return;
974 }
975
976 $cart_items = WC()->cart->get_cart();
977 $upsell_ids = [];
978 foreach ($cart_items as $item) {
979 $product = $item['data'] ?? null;
980 if ($product && is_object($product) && method_exists($product, 'get_upsell_ids')) {
981 $upsell_ids = array_merge($upsell_ids, $product->get_upsell_ids());
982 }
983 }
984 $upsell_ids = array_values(array_unique(array_filter(array_map('absint', $upsell_ids))));
985
986 /**
987 * Allow custom upsell IDs (e.g., personalization/A/B).
988 *
989 * @param array<int> $upsell_ids Upsell product IDs.
990 * @param array $cart_items Cart contents.
991 */
992 $upsell_ids = apply_filters('king_addons/woo_builder/checkout_upsell_ids', $upsell_ids, $cart_items);
993
994 if (empty($upsell_ids)) {
995 // Fallback: same categories as cart items.
996 $cat_ids = [];
997 $tag_ids = [];
998 foreach ($cart_items as $item) {
999 $product_id = isset($item['product_id']) ? (int) $item['product_id'] : 0;
1000 if ($product_id) {
1001 $terms = wp_get_post_terms($product_id, 'product_cat', ['fields' => 'ids']);
1002 $cat_ids = array_merge($cat_ids, $terms);
1003 $tags = wp_get_post_terms($product_id, 'product_tag', ['fields' => 'ids']);
1004 $tag_ids = array_merge($tag_ids, $tags);
1005 }
1006 }
1007 $cat_ids = array_values(array_unique(array_filter(array_map('absint', $cat_ids))));
1008 $tag_ids = array_values(array_unique(array_filter(array_map('absint', $tag_ids))));
1009 if (!empty($cat_ids)) {
1010 $upsell_ids = get_posts(
1011 [
1012 'post_type' => 'product',
1013 'fields' => 'ids',
1014 'numberposts' => (int) apply_filters('king_addons/woo_builder/checkout_upsells_fallback_limit', 6),
1015 'tax_query' => [
1016 [
1017 'taxonomy' => 'product_cat',
1018 'field' => 'term_id',
1019 'terms' => $cat_ids,
1020 ],
1021 ],
1022 ]
1023 );
1024 } elseif (!empty($tag_ids)) {
1025 $upsell_ids = get_posts(
1026 [
1027 'post_type' => 'product',
1028 'fields' => 'ids',
1029 'numberposts' => (int) apply_filters('king_addons/woo_builder/checkout_upsells_tag_limit', 6),
1030 'tax_query' => [
1031 [
1032 'taxonomy' => 'product_tag',
1033 'field' => 'term_id',
1034 'terms' => $tag_ids,
1035 ],
1036 ],
1037 ]
1038 );
1039 }
1040 }
1041
1042 if (empty($upsell_ids)) {
1043 return;
1044 }
1045
1046 $limit = (int) apply_filters('king_addons/woo_builder/checkout_upsells_limit', 4);
1047 $query = new WP_Query(
1048 apply_filters(
1049 'king_addons/woo_builder/checkout_upsells_query_args',
1050 [
1051 'post_type' => 'product',
1052 'post__in' => $upsell_ids,
1053 'posts_per_page' => $limit,
1054 'orderby' => 'post__in',
1055 ]
1056 )
1057 );
1058
1059 if (!$query->have_posts()) {
1060 wp_reset_postdata();
1061 return;
1062 }
1063
1064 $title = apply_filters('king_addons/woo_builder/checkout_upsells_title', esc_html__('You may also like', 'king-addons'));
1065
1066 echo '<section class="ka-woo-checkout-upsells" aria-label="' . esc_attr($title) . '">';
1067 if ($title) {
1068 echo '<h3 class="ka-woo-checkout-upsells__title">' . esc_html($title) . '</h3>';
1069 }
1070 echo '<div class="ka-woo-checkout-upsells__grid">';
1071 while ($query->have_posts()) {
1072 $query->the_post();
1073 $product = wc_get_product(get_the_ID());
1074 if (!$product) {
1075 continue;
1076 }
1077 $this->render_simple_product_card($product);
1078 }
1079 echo '</div>';
1080 echo '</section>';
1081 wp_reset_postdata();
1082 }
1083
1084 /**
1085 * Render cart cross-sells fallback.
1086 *
1087 * @return void
1088 */
1089 private function render_cart_cross_sells(): void
1090 {
1091 if (!function_exists('woocommerce_cross_sell_display') || apply_filters('king_addons/woo_builder/disable_cart_cross_sells', false)) {
1092 return;
1093 }
1094 $cart_items = (function_exists('WC') && WC()->cart) ? WC()->cart->get_cart() : [];
1095 $suggested_ids = [];
1096 foreach ($cart_items as $item) {
1097 $product_id = isset($item['product_id']) ? (int) $item['product_id'] : 0;
1098 if ($product_id) {
1099 $tags = wp_get_post_terms($product_id, 'product_tag', ['fields' => 'ids']);
1100 $suggested_ids = array_merge($suggested_ids, $tags);
1101 }
1102 }
1103 $suggested_ids = array_values(array_unique(array_filter(array_map('absint', $suggested_ids))));
1104 $suggested_args = [];
1105 if (!empty($suggested_ids)) {
1106 $suggested_args = [
1107 'post_type' => 'product',
1108 'posts_per_page' => (int) apply_filters('king_addons/woo_builder/cart_cross_sells_suggested_limit', 4),
1109 'tax_query' => [
1110 [
1111 'taxonomy' => 'product_tag',
1112 'field' => 'term_id',
1113 'terms' => $suggested_ids,
1114 ],
1115 ],
1116 ];
1117 }
1118
1119 /**
1120 * Allow overriding cross-sell query (e.g., custom tags/meta).
1121 *
1122 * @param array<string,mixed> $args Query args, empty for Woo default.
1123 * @param array $cart_items Cart items.
1124 */
1125 $suggested_args = apply_filters('king_addons/woo_builder/cart_cross_sells_query_args', $suggested_args, $cart_items);
1126
1127 $title = apply_filters('king_addons/woo_builder/cart_cross_sells_title', esc_html__('You may also like', 'king-addons'));
1128 echo '<section class="ka-woo-cart-cross-sells" aria-label="' . esc_attr($title) . '">';
1129 if ($title) {
1130 echo '<h3 class="ka-woo-cart-cross-sells__title">' . esc_html($title) . '</h3>';
1131 }
1132 if (!empty($suggested_args)) {
1133 $loop = new WP_Query($suggested_args);
1134 if ($loop->have_posts()) {
1135 echo '<div class="ka-woo-checkout-upsells__grid">';
1136 while ($loop->have_posts()) {
1137 $loop->the_post();
1138 $product = wc_get_product(get_the_ID());
1139 if ($product) {
1140 $this->render_simple_product_card($product);
1141 }
1142 }
1143 echo '</div>';
1144 }
1145 wp_reset_postdata();
1146 } else {
1147 woocommerce_cross_sell_display(
1148 (int) apply_filters('king_addons/woo_builder/cart_cross_sells_limit', 4),
1149 (int) apply_filters('king_addons/woo_builder/cart_cross_sells_columns', 4)
1150 );
1151 }
1152 echo '</section>';
1153 }
1154
1155 /**
1156 * Render My Account quick links.
1157 *
1158 * @return void
1159 */
1160 private function render_account_quick_links(): void
1161 {
1162 if (!function_exists('wc_get_account_endpoint_url') || apply_filters('king_addons/woo_builder/disable_account_quick_links', false)) {
1163 return;
1164 }
1165
1166 $links = apply_filters(
1167 'king_addons/woo_builder/account_quick_links',
1168 [
1169 'orders' => esc_html__('Orders', 'king-addons'),
1170 'downloads' => esc_html__('Downloads', 'king-addons'),
1171 'edit-address' => esc_html__('Addresses', 'king-addons'),
1172 'payment-methods' => esc_html__('Payment Methods', 'king-addons'),
1173 'edit-account' => esc_html__('Account Details', 'king-addons'),
1174 ]
1175 );
1176
1177 echo '<nav class="ka-woo-account-quick-links" aria-label="' . esc_attr__('Account quick links', 'king-addons') . '">';
1178 echo '<ul class="ka-woo-account-quick-links__list">';
1179 foreach ($links as $endpoint => $label) {
1180 $url = wc_get_account_endpoint_url($endpoint);
1181 if (!$url) {
1182 continue;
1183 }
1184 echo '<li class="ka-woo-account-quick-links__item">';
1185 echo '<a class="ka-woo-account-quick-links__link" href="' . esc_url($url) . '">' . esc_html($label) . '</a>';
1186 echo '</li>';
1187 }
1188 echo '</ul>';
1189 echo '</nav>';
1190
1191 /**
1192 * Hook to output additional custom blocks in My Account (Pro).
1193 *
1194 * @param array<string,string> $links Current quick links.
1195 */
1196 do_action('king_addons/woo_builder/account_after_quick_links', $links);
1197 }
1198
1199 /**
1200 * Render a simple product card for upsells.
1201 *
1202 * @param \WC_Product $product Product instance.
1203 *
1204 * @return void
1205 */
1206 private function render_simple_product_card(\WC_Product $product): void
1207 {
1208 $link = get_permalink($product->get_id());
1209 $image = $product->get_image('woocommerce_thumbnail');
1210 $title = $product->get_name();
1211 $price = $product->get_price_html();
1212
1213 echo '<article class="ka-woo-checkout-upsells__item">';
1214 echo '<a class="ka-woo-checkout-upsells__thumb" href="' . esc_url($link) . '">' . wp_kses_post($image) . '</a>';
1215 echo '<h4 class="ka-woo-checkout-upsells__title"><a href="' . esc_url($link) . '">' . esc_html($title) . '</a></h4>';
1216 if (!empty($price)) {
1217 echo '<div class="ka-woo-checkout-upsells__price">' . wp_kses_post($price) . '</div>';
1218 }
1219 echo $this->render_add_to_cart_button($product); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1220 echo '</article>';
1221 }
1222
1223 /**
1224 * Render a controlled Add to Cart button without relying on Woo shortcode.
1225 *
1226 * @param \WC_Product $product Product instance.
1227 *
1228 * @return string
1229 */
1230 private function render_add_to_cart_button(\WC_Product $product): string
1231 {
1232 if (!$product->is_purchasable() || !$product->is_in_stock()) {
1233 return '<span class="ka-woo-checkout-upsells__add-to-cart button is-disabled">' . esc_html__('Unavailable', 'king-addons') . '</span>';
1234 }
1235
1236 if (function_exists('wp_enqueue_script') && function_exists('wp_script_is') && wp_script_is('wc-add-to-cart', 'registered')) {
1237 wp_enqueue_script('wc-add-to-cart');
1238 }
1239
1240 $classes = [
1241 'ka-woo-checkout-upsells__add-to-cart',
1242 'button',
1243 'product_type_' . $product->get_type(),
1244 ];
1245
1246 if ($product->supports('ajax_add_to_cart')) {
1247 $classes[] = 'ajax_add_to_cart';
1248 $classes[] = 'add_to_cart_button';
1249 }
1250
1251 $attributes = [
1252 'href' => $product->add_to_cart_url(),
1253 'data-quantity' => 1,
1254 'data-product_id' => $product->get_id(),
1255 'data-product_sku' => $product->get_sku(),
1256 'rel' => 'nofollow',
1257 'class' => implode(' ', array_filter($classes)),
1258 'aria-label' => wp_strip_all_tags($product->add_to_cart_description()),
1259 ];
1260
1261 return '<a ' . wc_implode_html_attributes($attributes) . '><span class="ka-woo-checkout-upsells__add-to-cart-label">' . esc_html($product->add_to_cart_text()) . '</span></a>';
1262 }
1263
1264 /**
1265 * Prefill meta values when creating a new Elementor template via custom links.
1266 *
1267 * @param mixed $value Default meta value.
1268 * @param int $object_id Object ID.
1269 * @param string $meta_key Meta key.
1270 * @param bool $single Whether meta is single.
1271 * @param string|null $meta_type Meta type.
1272 *
1273 * @return mixed
1274 */
1275 public function prefill_template_meta($value, int $object_id, string $meta_key, bool $single, ?string $meta_type)
1276 {
1277 if ('post' !== $meta_type) {
1278 return $value;
1279 }
1280
1281 $requested_type = isset($_GET['ka_woo_template_type'])
1282 ? sanitize_text_field(wp_unslash($_GET['ka_woo_template_type']))
1283 : '';
1284
1285 if (empty($requested_type)) {
1286 return $value;
1287 }
1288
1289 $post_type = isset($_GET['post_type']) ? sanitize_text_field(wp_unslash($_GET['post_type'])) : '';
1290
1291 if ('elementor_library' !== $post_type) {
1292 return $value;
1293 }
1294
1295 if ('ka_woo_template_type' === $meta_key) {
1296 return $requested_type;
1297 }
1298
1299 if ('ka_woo_conditions' === $meta_key) {
1300 return [
1301 'enabled' => true,
1302 'priority' => 10,
1303 'rules' => [
1304 [
1305 'type' => 'always',
1306 'values' => [],
1307 ],
1308 ],
1309 ];
1310 }
1311 if ('_elementor_template_type' === $meta_key) {
1312 return class_exists(Woo_Document::class) ? Woo_Document::get_type() : 'king-addons-woo-builder';
1313 }
1314
1315 return $value;
1316 }
1317
1318 /**
1319 * Save template type meta when any post is inserted (catches auto-draft creation).
1320 *
1321 * @param int $post_id Post ID.
1322 * @param \WP_Post $post Post object.
1323 * @param bool $update Whether this is an update.
1324 *
1325 * @return void
1326 */
1327 public function save_template_type_on_insert(int $post_id, \WP_Post $post, bool $update): void
1328 {
1329 // Only for elementor_library posts
1330 if ('elementor_library' !== $post->post_type) {
1331 return;
1332 }
1333
1334 // Skip if meta already exists
1335 if (get_post_meta($post_id, 'ka_woo_template_type', true)) {
1336 return;
1337 }
1338
1339 // Get template type from request or transient
1340 $template_type = $this->get_template_type_from_request();
1341
1342 // If not found in request, check transient
1343 if (empty($template_type)) {
1344 $user_id = get_current_user_id();
1345 $transient_key = 'ka_woo_pending_template_type_' . $user_id;
1346 $template_type = get_transient($transient_key);
1347 if ($template_type) {
1348 delete_transient($transient_key);
1349 }
1350 }
1351
1352 if (empty($template_type)) {
1353 return;
1354 }
1355
1356 // Save the template type meta
1357 update_post_meta($post_id, 'ka_woo_template_type', $template_type);
1358
1359 // Set Elementor template type
1360 $document_type = class_exists(Woo_Document::class) ? Woo_Document::get_type() : 'king-addons-woo-builder';
1361 update_post_meta($post_id, '_elementor_template_type', $document_type);
1362
1363 // Set default conditions (enabled by default)
1364 $default_conditions = [
1365 'enabled' => true,
1366 'priority' => 10,
1367 'rules' => [
1368 [
1369 'type' => 'always',
1370 'values' => [],
1371 ],
1372 ],
1373 ];
1374 update_post_meta($post_id, 'ka_woo_conditions', $default_conditions);
1375 }
1376
1377 /**
1378 * Set WooCommerce template meta when creating a new post from URL parameter.
1379 * This runs on admin_init to catch post creation before Elementor takes over.
1380 *
1381 * @return void
1382 */
1383 public function maybe_set_woo_template_meta_on_new_post(): void
1384 {
1385 global $pagenow;
1386
1387 // Only on post-new.php for elementor_library
1388 if ('post-new.php' !== $pagenow) {
1389 return;
1390 }
1391
1392 $post_type = isset($_GET['post_type']) ? sanitize_text_field(wp_unslash($_GET['post_type'])) : '';
1393 if ('elementor_library' !== $post_type) {
1394 return;
1395 }
1396
1397 // Get template type from URL
1398 $template_type = $this->get_template_type_from_request();
1399 if (empty($template_type)) {
1400 return;
1401 }
1402
1403 // Store in session/transient for later use when the post is actually created
1404 $user_id = get_current_user_id();
1405 set_transient('ka_woo_pending_template_type_' . $user_id, $template_type, 300);
1406 }
1407
1408 /**
1409 * Save template type from URL parameter when post is created.
1410 *
1411 * @param int $post_id Post ID.
1412 * @param \WP_Post $post Post object.
1413 * @param bool $update Whether this is an update.
1414 *
1415 * @return void
1416 */
1417 public function save_template_type_from_url(int $post_id, \WP_Post $post, bool $update): void
1418 {
1419 // Skip if meta already exists
1420 if (get_post_meta($post_id, 'ka_woo_template_type', true)) {
1421 return;
1422 }
1423
1424 // Get template type from request or transient
1425 $template_type = $this->get_template_type_from_request();
1426
1427 // If not found in request, check transient
1428 if (empty($template_type)) {
1429 $user_id = get_current_user_id();
1430 $transient_key = 'ka_woo_pending_template_type_' . $user_id;
1431 $template_type = get_transient($transient_key);
1432 if ($template_type) {
1433 delete_transient($transient_key);
1434 }
1435 }
1436
1437 if (empty($template_type)) {
1438 return;
1439 }
1440
1441 // Save the template type meta
1442 update_post_meta($post_id, 'ka_woo_template_type', $template_type);
1443
1444 // Set Elementor template type
1445 $document_type = class_exists(Woo_Document::class) ? Woo_Document::get_type() : 'king-addons-woo-builder';
1446 update_post_meta($post_id, '_elementor_template_type', $document_type);
1447
1448 // Set default conditions (enabled by default)
1449 $conditions = [
1450 'enabled' => true,
1451 'priority' => 10,
1452 'rules' => [
1453 [
1454 'type' => 'always',
1455 'values' => [],
1456 ],
1457 ],
1458 ];
1459 update_post_meta($post_id, 'ka_woo_conditions', $conditions);
1460 }
1461
1462 /**
1463 * Get template type from current request or referrer URL.
1464 *
1465 * @return string Template type or empty string.
1466 */
1467 private function get_template_type_from_request(): string
1468 {
1469 $valid_types = ['single_product', 'product_archive', 'cart', 'checkout', 'my_account'];
1470 $template_type = '';
1471
1472 // Check current request
1473 if (isset($_GET['ka_woo_template_type'])) {
1474 $template_type = sanitize_text_field(wp_unslash($_GET['ka_woo_template_type']));
1475 }
1476
1477 // Check POST data
1478 if (empty($template_type) && isset($_POST['ka_woo_template_type'])) {
1479 $template_type = sanitize_text_field(wp_unslash($_POST['ka_woo_template_type']));
1480 }
1481
1482 // Check referrer URL
1483 if (empty($template_type) && isset($_SERVER['HTTP_REFERER'])) {
1484 $referrer = wp_unslash($_SERVER['HTTP_REFERER']);
1485 $parsed = wp_parse_url($referrer);
1486 if (!empty($parsed['query'])) {
1487 parse_str($parsed['query'], $query_args);
1488 if (!empty($query_args['ka_woo_template_type'])) {
1489 $template_type = sanitize_text_field($query_args['ka_woo_template_type']);
1490 }
1491 }
1492 }
1493
1494 // Validate
1495 if (!in_array($template_type, $valid_types, true)) {
1496 return '';
1497 }
1498
1499 // Check Pro restriction
1500 if (!$this->can_use_pro() && in_array($template_type, ['cart', 'checkout', 'my_account'], true)) {
1501 return '';
1502 }
1503
1504 return $template_type;
1505 }
1506
1507 /**
1508 * Check Pro availability.
1509 *
1510 * @return bool
1511 */
1512 private function can_use_pro(): bool
1513 {
1514 if (!function_exists('king_addons_freemius')) {
1515 return false;
1516 }
1517
1518 $fs = king_addons_freemius();
1519 if (!is_object($fs) || !method_exists($fs, 'can_use_premium_code')) {
1520 return false;
1521 }
1522
1523 return (bool) $fs->can_use_premium_code();
1524 }
1525
1526 /**
1527 * Map rule values to human-readable labels for select pre-fill.
1528 *
1529 * @param string $type Rule type.
1530 * @param array $values Raw values.
1531 *
1532 * @return array<string,string>
1533 */
1534 private function get_rule_value_options(string $type, array $values): array
1535 {
1536 if (empty($values)) {
1537 return [];
1538 }
1539
1540 $options = [];
1541 $string_values = array_map('strval', $values);
1542
1543 switch ($type) {
1544 case 'product_in':
1545 case 'products':
1546 $products = get_posts([
1547 'post_type' => 'product',
1548 'post__in' => array_map('intval', $values),
1549 'numberposts' => -1,
1550 ]);
1551 foreach ($products as $product) {
1552 $options[(string) $product->ID] = $product->post_title;
1553 }
1554 break;
1555 case 'product_cat_in':
1556 case 'product_cat_archive_in':
1557 case 'product_categories':
1558 case 'product_cat_archives':
1559 $terms = get_terms([
1560 'taxonomy' => 'product_cat',
1561 'include' => array_map('intval', $values),
1562 'hide_empty' => false,
1563 ]);
1564 foreach ($terms as $term) {
1565 $options[(string) $term->term_id] = $term->name;
1566 }
1567 break;
1568 case 'product_tag_in':
1569 case 'product_tag_archive_in':
1570 case 'product_tag_archives':
1571 case 'product_tags':
1572 $terms = get_terms([
1573 'taxonomy' => 'product_tag',
1574 'include' => array_map('intval', $values),
1575 'hide_empty' => false,
1576 ]);
1577 foreach ($terms as $term) {
1578 $options[(string) $term->term_id] = $term->name;
1579 }
1580 break;
1581 case 'product_type_in':
1582 case 'product_types':
1583 $types = [
1584 'simple' => esc_html__('Simple', 'king-addons'),
1585 'variable' => esc_html__('Variable', 'king-addons'),
1586 'grouped' => esc_html__('Grouped', 'king-addons'),
1587 'external' => esc_html__('External/Affiliate', 'king-addons'),
1588 ];
1589 foreach ($string_values as $v) {
1590 if (isset($types[$v])) {
1591 $options[$v] = $types[$v];
1592 }
1593 }
1594 break;
1595 default:
1596 foreach ($string_values as $v) {
1597 $options[$v] = $v;
1598 }
1599 }
1600
1601 return $options;
1602 }
1603
1604 /**
1605 * Sanitize rule values from Elementor document settings.
1606 *
1607 * @param mixed $values Raw values.
1608 * @param bool $numeric Whether values should be numeric IDs.
1609 *
1610 * @return array<int|string>
1611 */
1612 private function sanitize_rule_values($values, bool $numeric): array
1613 {
1614 if (is_string($values)) {
1615 $values = explode(',', $values);
1616 }
1617
1618 if (!is_array($values)) {
1619 return [];
1620 }
1621
1622 $values = array_filter(array_map('strval', $values), 'strlen');
1623
1624 if ($numeric) {
1625 $values = array_values(array_unique(array_filter(array_map('absint', $values))));
1626 } else {
1627 $values = array_values(array_unique(array_filter(array_map('sanitize_text_field', $values), 'strlen')));
1628 }
1629
1630 return $values;
1631 }
1632
1633 /**
1634 * Normalize legacy rule type slugs to current ones.
1635 *
1636 * @param string $type Rule type.
1637 *
1638 * @return string
1639 */
1640 private function normalize_rule_type(string $type): string
1641 {
1642 $map = [
1643 'products' => 'product_in',
1644 'product_categories' => 'product_cat_in',
1645 'product_tags' => 'product_tag_in',
1646 'product_types' => 'product_type_in',
1647 'shop' => 'is_shop',
1648 'product_cat_archives' => 'product_cat_archive_in',
1649 'product_tag_archives' => 'product_tag_archive_in',
1650 ];
1651
1652 return $map[$type] ?? $type;
1653 }
1654 }
1655