PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.78
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.78
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 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / helpers / Woo_Builder / Context.php

Context.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.78, at includes/helpers/Woo_Builder/Context.php

551 lines 18.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Woo Builder context helpers.
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons\Woo_Builder;
9
10 use WC_Product;
11 use Elementor\Plugin as Elementor_Plugin;
12
13 if (!defined('ABSPATH')) {
14 exit;
15 }
16
17 /**
18 * Utility helpers for Woo Builder.
19 */
20 class Context
21 {
22 /**
23 * Cached template type for current editor session.
24 *
25 * @var string|null
26 */
27 private static ?string $cached_editor_template_type = null;
28
29 /**
30 * Cached preview product.
31 *
32 * @var WC_Product|null|false
33 */
34 private static $cached_preview_product = false;
35
36 /**
37 * Check if we are in Elementor editor UI context (including Elementor AJAX render).
38 *
39 * Important: This should never return true on normal frontend requests.
40 *
41 * @return bool
42 */
43 public static function is_editor(): bool
44 {
45 // Check for Elementor AJAX render request first (when widget is added/updated)
46 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
47 if (wp_doing_ajax() && !empty($_REQUEST['action']) && strpos($_REQUEST['action'], 'elementor') !== false) {
48 return true;
49 }
50
51 if (!class_exists('\Elementor\Plugin')) {
52 return false;
53 }
54
55 if (!Elementor_Plugin::$instance) {
56 return false;
57 }
58
59 // Check if editor is active
60 if (isset(Elementor_Plugin::$instance->editor) && Elementor_Plugin::$instance->editor->is_edit_mode()) {
61 return true;
62 }
63
64 // In the Elementor editor iframe, requests are served in preview mode.
65 // Treat it as editor context only when Elementor's preview query var is present.
66 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
67 if (isset(Elementor_Plugin::$instance->preview) && Elementor_Plugin::$instance->preview->is_preview_mode() && !empty($_GET['elementor-preview'])) {
68 return true;
69 }
70
71 return false;
72 }
73
74 /**
75 * Get the template type of the currently edited Woo Builder template.
76 *
77 * @return string|null Template type (single_product, product_archive, cart, checkout, my_account) or null.
78 */
79 public static function get_editor_template_type(): ?string
80 {
81 if (self::$cached_editor_template_type !== null) {
82 return self::$cached_editor_template_type ?: null;
83 }
84
85 self::$cached_editor_template_type = '';
86
87 if (!self::is_editor()) {
88 return null;
89 }
90
91 // Get the post ID being edited
92 $post_id = self::get_editor_post_id();
93 if (!$post_id) {
94 return null;
95 }
96
97 // Check if this is a Woo Builder template
98 $template_type = get_post_meta($post_id, 'ka_woo_template_type', true);
99 if (!empty($template_type)) {
100 self::$cached_editor_template_type = $template_type;
101 return $template_type;
102 }
103
104 // Also check Elementor document type
105 $elementor_type = get_post_meta($post_id, '_elementor_template_type', true);
106 if ('king-addons-woo-builder' === $elementor_type) {
107 // Template type not set yet, but it's a Woo Builder template
108 self::$cached_editor_template_type = 'single_product'; // Default
109 return 'single_product';
110 }
111
112 return null;
113 }
114
115 /**
116 * Get the post ID being edited in Elementor.
117 *
118 * @return int|null
119 */
120 public static function get_editor_post_id(): ?int
121 {
122 // First try $_GET parameters - most reliable and doesn't require Elementor to be fully loaded
123 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
124 if (!empty($_GET['post'])) {
125 return absint($_GET['post']);
126 }
127
128 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
129 if (!empty($_GET['elementor-preview'])) {
130 return absint($_GET['elementor-preview']);
131 }
132
133 // For AJAX requests, check POST data for editor_post_id
134 // phpcs:ignore WordPress.Security.NonceVerification.Missing
135 if (wp_doing_ajax() && !empty($_POST['editor_post_id'])) {
136 return absint($_POST['editor_post_id']);
137 }
138
139 // Try to get from current document if Elementor is available
140 if (class_exists('\Elementor\Plugin') && Elementor_Plugin::$instance && isset(Elementor_Plugin::$instance->documents)) {
141 $document = Elementor_Plugin::$instance->documents->get_current();
142 if ($document) {
143 return $document->get_main_id();
144 }
145 }
146
147 // Fallback to global post
148 global $post;
149 if ($post && $post->ID) {
150 return (int) $post->ID;
151 }
152
153 return null;
154 }
155
156 /**
157 * Check if we're editing a specific Woo Builder template type.
158 *
159 * @param string $expected_type Expected template type.
160 * @return bool
161 */
162 public static function is_editing_template_type(string $expected_type): bool
163 {
164 $current_type = self::get_editor_template_type();
165 return $current_type === $expected_type;
166 }
167
168 /**
169 * Get a preview product for editor mode.
170 *
171 * @return WC_Product|null
172 */
173 public static function get_preview_product(): ?WC_Product
174 {
175 if (self::$cached_preview_product !== false) {
176 return self::$cached_preview_product;
177 }
178
179 self::$cached_preview_product = null;
180
181 if (!function_exists('wc_get_products')) {
182 return null;
183 }
184
185 // Try to get a published product for preview
186 $products = wc_get_products([
187 'status' => 'publish',
188 'limit' => 6,
189 'orderby' => 'date',
190 'order' => 'DESC',
191 ]);
192
193 if (!empty($products)) {
194 foreach ($products as $candidate) {
195 if (!$candidate instanceof WC_Product) {
196 continue;
197 }
198
199 if ($candidate->get_image_id() || $candidate->get_gallery_image_ids()) {
200 self::$cached_preview_product = $candidate;
201 return self::$cached_preview_product;
202 }
203 }
204
205 if ($products[0] instanceof WC_Product) {
206 self::$cached_preview_product = $products[0];
207 return self::$cached_preview_product;
208 }
209 }
210
211 return null;
212 }
213
214 /**
215 * Setup preview product context for editor.
216 * Call this at the start of widget render when in editor mode.
217 *
218 * @return WC_Product|null The preview product if set up, null otherwise.
219 */
220 public static function setup_preview_product(): ?WC_Product
221 {
222 global $product;
223
224 // Only in editor mode and when editing a single_product template
225 if (!self::is_editor()) {
226 return null;
227 }
228
229 $template_type = self::get_editor_template_type();
230 if ('single_product' !== $template_type) {
231 return null;
232 }
233
234 // If product is already set, return it
235 if ($product instanceof WC_Product) {
236 return $product;
237 }
238
239 // Get preview product
240 $preview = self::get_preview_product();
241 if (!$preview) {
242 return null;
243 }
244
245 // Set up global product context only - DO NOT change $post as it breaks Elementor
246 $product = $preview;
247
248 return $preview;
249 }
250
251 /**
252 * Detect current WooCommerce context.
253 *
254 * Cart, Checkout, and My Account pages may not pass is_woocommerce()
255 * so we check them separately.
256 *
257 * @return string|null
258 */
259 public static function detect_context(): ?string
260 {
261 // Note: We do NOT check is_editor() here because this method is also called
262 // by override_wc_template() which has is_admin() check already.
263 // Editor context detection is handled separately in maybe_render_context_notice().
264
265 if (!function_exists('is_woocommerce')) {
266 return null;
267 }
268
269 // Check cart page first (may not pass is_woocommerce())
270 if (function_exists('is_cart') && is_cart()) {
271 return 'cart';
272 }
273
274 // Check checkout page (may not pass is_woocommerce())
275 if (function_exists('is_checkout') && is_checkout() && !is_order_received_page()) {
276 return 'checkout';
277 }
278
279 // Check My Account page (may not pass is_woocommerce())
280 if (function_exists('is_account_page') && is_account_page()) {
281 return 'my_account';
282 }
283
284 // For product and archive pages, is_woocommerce() should be true
285 if (!is_woocommerce()) {
286 return null;
287 }
288
289 if (is_product()) {
290 return 'single_product';
291 }
292
293 if (is_shop() || is_product_category() || is_product_tag() || is_product_taxonomy()) {
294 return 'product_archive';
295 }
296
297 return null;
298 }
299
300 /**
301 * Get current product ID.
302 *
303 * @return int|null
304 */
305 public static function get_product_id(): ?int
306 {
307 global $product, $post;
308
309 if ($product instanceof WC_Product) {
310 return (int) $product->get_id();
311 }
312
313 if (!empty($post->ID) && 'product' === get_post_type($post->ID)) {
314 return (int) $post->ID;
315 }
316
317 return null;
318 }
319
320 /**
321 * Match conditions array against current page context.
322 *
323 * @param string $context Page context.
324 * @param array<string,mixed> $conditions Conditions array.
325 *
326 * @return bool
327 */
328 public static function match_conditions(string $context, array $conditions): bool
329 {
330 if (empty($conditions['rules']) || !is_array($conditions['rules'])) {
331 return true;
332 }
333
334 $product_id = self::get_product_id();
335
336 foreach ($conditions['rules'] as $rule) {
337 $type = $rule['type'] ?? '';
338 $values = is_array($rule['values'] ?? null) ? $rule['values'] : [];
339
340 if (!self::match_rule($context, $type, $values, $product_id)) {
341 return false;
342 }
343 }
344
345 return true;
346 }
347
348 /**
349 * Match a single rule.
350 *
351 * @param string $context Context.
352 * @param string $type Rule type.
353 * @param array $values Rule values.
354 * @param int|null $product_id Product ID.
355 *
356 * @return bool
357 */
358 private static function match_rule(string $context, string $type, array $values, ?int $product_id): bool
359 {
360 switch ($type) {
361 // Universal "all" type - matches everything for the context
362 case 'all':
363 return true;
364
365 // Single Product conditions
366 case 'all_products':
367 return 'single_product' === $context;
368
369 case 'specific_product':
370 case 'product_in':
371 case 'products':
372 $ids = array_filter(array_map('absint', $values));
373 return ('single_product' === $context && $product_id && in_array($product_id, $ids, true));
374
375 case 'product_cat':
376 case 'product_cat_in':
377 case 'product_categories':
378 // For single product - check if product is in category
379 if ('single_product' === $context && $product_id) {
380 $ids = array_filter(array_map('absint', $values));
381 if (empty($ids)) {
382 return true; // No specific categories = all categories
383 }
384 $terms = wp_get_post_terms($product_id, 'product_cat', ['fields' => 'ids']);
385 return !empty(array_intersect($ids, $terms));
386 }
387 // For archive - check if on category archive
388 if ('product_archive' === $context && is_product_category()) {
389 $term = get_queried_object();
390 if (!empty($term->term_id)) {
391 $ids = array_filter(array_map('absint', $values));
392 if (empty($ids)) {
393 return true;
394 }
395 return in_array((int) $term->term_id, $ids, true);
396 }
397 }
398 return false;
399
400 case 'product_tag':
401 case 'product_tag_in':
402 case 'product_tags':
403 // For single product - check if product has tag
404 if ('single_product' === $context && $product_id) {
405 $ids = array_filter(array_map('absint', $values));
406 if (empty($ids)) {
407 return true; // No specific tags = all tags
408 }
409 $terms = wp_get_post_terms($product_id, 'product_tag', ['fields' => 'ids']);
410 return !empty(array_intersect($ids, $terms));
411 }
412 // For archive - check if on tag archive
413 if ('product_archive' === $context && is_product_tag()) {
414 $term = get_queried_object();
415 if (!empty($term->term_id)) {
416 $ids = array_filter(array_map('absint', $values));
417 if (empty($ids)) {
418 return true;
419 }
420 return in_array((int) $term->term_id, $ids, true);
421 }
422 }
423 return false;
424
425 case 'product_type_in':
426 case 'product_types':
427 if ('single_product' !== $context || !$product_id) {
428 return false;
429 }
430 $types = wp_get_object_terms($product_id, 'product_type', ['fields' => 'slugs']);
431 $wanted = array_values(array_filter(array_map('sanitize_text_field', $values)));
432 return !empty(array_intersect($wanted, $types));
433
434 // Archive conditions
435 case 'shop':
436 case 'is_shop':
437 return 'product_archive' === $context && is_shop();
438
439 case 'product_cat_archive_in':
440 case 'product_cat_archives':
441 if ('product_archive' !== $context) {
442 return false;
443 }
444 if (is_product_category()) {
445 $term = get_queried_object();
446 if (!empty($term->term_id)) {
447 $ids = array_filter(array_map('absint', $values));
448 if (empty($values)) {
449 return true;
450 }
451 return in_array((int) $term->term_id, $ids, true);
452 }
453 }
454 return false;
455
456 case 'product_tag_archive_in':
457 case 'product_tag_archives':
458 if ('product_archive' !== $context) {
459 return false;
460 }
461 if (is_product_tag()) {
462 $term = get_queried_object();
463 if (!empty($term->term_id)) {
464 $ids = array_filter(array_map('absint', $values));
465 if (empty($values)) {
466 return true;
467 }
468 return in_array((int) $term->term_id, $ids, true);
469 }
470 }
471 return false;
472 case 'cart':
473 return 'cart' === $context;
474 case 'checkout':
475 return 'checkout' === $context;
476 case 'my_account':
477 return 'my_account' === $context;
478 case 'always':
479 return true;
480 default:
481 return true;
482 }
483 }
484
485 /**
486 * Render an editor-only notice if widget is not in the expected Woo context.
487 *
488 * @param string $expected_context Expected context slug.
489 *
490 * @return bool True when context matches or not in edit mode, false when notice rendered.
491 */
492 public static function maybe_render_context_notice(string $expected_context): bool
493 {
494 // Not in editor mode - allow render (frontend will show appropriate content)
495 if (!class_exists('\Elementor\Plugin') || !Elementor_Plugin::$instance->editor->is_edit_mode()) {
496 return true;
497 }
498
499 // Check if we're editing a Woo Builder template of the matching type
500 $editor_template_type = self::get_editor_template_type();
501 if ($editor_template_type === $expected_context) {
502 // We're editing the correct template type - allow render with preview data
503 return true;
504 }
505
506 // Fallback: check actual page context (for when editing on actual WooCommerce pages)
507 $current = self::detect_context();
508 if ($current === $expected_context) {
509 return true;
510 }
511
512 // Show notice only if we're not editing any Woo Builder template
513 // or editing a different type
514 $labels = [
515 'single_product' => esc_html__('This widget works on Single Product pages.', 'king-addons'),
516 'product_archive' => esc_html__('This widget works on Product Archive pages.', 'king-addons'),
517 'cart' => esc_html__('This widget works on the Cart page.', 'king-addons'),
518 'checkout' => esc_html__('This widget works on the Checkout page.', 'king-addons'),
519 'my_account' => esc_html__('This widget works on My Account pages.', 'king-addons'),
520 ];
521
522 // If editing a Woo Builder template but wrong type, show helpful message
523 if ($editor_template_type) {
524 $type_labels = [
525 'single_product' => esc_html__('Single Product', 'king-addons'),
526 'product_archive' => esc_html__('Product Archive', 'king-addons'),
527 'cart' => esc_html__('Cart', 'king-addons'),
528 'checkout' => esc_html__('Checkout', 'king-addons'),
529 'my_account' => esc_html__('My Account', 'king-addons'),
530 ];
531 $expected_label = $type_labels[$expected_context] ?? $expected_context;
532 $current_label = $type_labels[$editor_template_type] ?? $editor_template_type;
533
534 $message = sprintf(
535 /* translators: 1: expected template type, 2: current template type */
536 esc_html__('This widget is for %1$s templates. You are editing a %2$s template.', 'king-addons'),
537 $expected_label,
538 $current_label
539 );
540 } else {
541 $message = $labels[$expected_context] ?? esc_html__('This widget is only available on WooCommerce pages.', 'king-addons');
542 }
543
544 echo '<div class="king-addons-woo-builder-notice">' . esc_html($message) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
545 return false;
546 }
547 }
548
549
550
551