PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | jetpack_vendor/automattic/woocommerce-analytics/src/class-woo-analytics-trait.php +743 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,743 @@
1 +<?php
2 +/**
3 + * Woo_Analytics_Trait
4 + *
5 + * @package automattic/woocommerce-analytics
6 + */
7 +
8 +namespace Automattic\Woocommerce_Analytics;
9 +
10 +use Automattic\Block_Scanner;
11 +use Automattic\Jetpack\Connection\Manager as Jetpack_Connection;
12 +use WC_Order_Item;
13 +use WC_Order_Item_Product;
14 +use WC_Payment_Gateway;
15 +use WC_Product;
16 +
17 +/**
18 + * Common functionality for WooCommerce Analytics classes.
19 + */
20 +trait Woo_Analytics_Trait {
21 + /**
22 + * Saves whether the cart/checkout templates are in use based on WC Blocks version.
23 + *
24 + * @var bool true if the templates are in use.
25 + */
26 + protected $cart_checkout_templates_in_use;
27 +
28 + /**
29 + * The content of the cart page or where the cart page is ultimately derived from if using a template.
30 + *
31 + * @var string
32 + */
33 + protected $cart_content_source = '';
34 +
35 + /**
36 + * The content of the checkout page or where the cart page is ultimately derived from if using a template.
37 + *
38 + * @var string
39 + */
40 + protected $checkout_content_source = '';
41 +
42 + /**
43 + * Tracks any additional blocks loaded on the Cart page.
44 + *
45 + * @var array
46 + */
47 + protected $additional_blocks_on_cart_page;
48 +
49 + /**
50 + * Tracks any additional blocks loaded on the Checkout page.
51 + *
52 + * @var array
53 + */
54 + protected $additional_blocks_on_checkout_page;
55 +
56 + /**
57 + * Locks Add to Cart Events Tracking in the current request avoiding duplications.
58 + * i.e. If update_cart and add_to_cart actions happens in the same request.
59 + *
60 + * @var bool If true. Cart events are locked for the current request.
61 + */
62 + protected $lock_add_to_cart_events = false;
63 +
64 + /**
65 + * Format Cart Items or Order Items to an array
66 + *
67 + * @param array|WC_Order_Item[] $items Cart Items or Order Items.
68 + */
69 + protected function format_items_to_json( $items ) {
70 + $products = array();
71 +
72 + foreach ( $items as $item ) {
73 + if ( $item instanceof WC_Order_Item_Product ) {
74 + $product = wc_get_product( $item->get_product_id() );
75 + } else {
76 + $product = $item['data'];
77 + }
78 +
79 + if ( ! $product || ! $product instanceof WC_Product ) {
80 + continue;
81 + }
82 +
83 + $data = $this->get_product_details( $product );
84 +
85 + if ( $item instanceof WC_Order_Item_Product ) {
86 + $data['pq'] = $item->get_quantity();
87 + } else {
88 + $data['pq'] = $item['quantity'];
89 + }
90 + $products[] = $data;
91 + }
92 +
93 + return wp_json_encode( $products, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG );
94 + }
95 +
96 + /**
97 + * Get Cart/Checkout page view shared data
98 + */
99 + protected function get_cart_checkout_shared_data() {
100 + $cart = WC()->cart;
101 +
102 + $guest_checkout = ucfirst( get_option( 'woocommerce_enable_guest_checkout', 'No' ) );
103 + $create_account = ucfirst( get_option( 'woocommerce_enable_signup_and_login_from_checkout', 'No' ) );
104 + $delayed_account_creation = ucfirst( get_option( 'woocommerce_enable_delayed_account_creation', 'Yes' ) );
105 +
106 + $coupons = $cart->get_coupons();
107 + $coupon_used = 0;
108 + if ( is_countable( $coupons ) ) {
109 + $coupon_used = count( $coupons ) ? 1 : 0;
110 + }
111 +
112 + $enabled_payment_options = array_filter(
113 + WC()->payment_gateways->get_available_payment_gateways(),
114 + function ( $payment_gateway ) {
115 + if ( ! $payment_gateway instanceof WC_Payment_Gateway ) {
116 + return false;
117 + }
118 +
119 + return $payment_gateway->is_available();
120 + }
121 + );
122 +
123 + $enabled_payment_options = array_keys( $enabled_payment_options );
124 + $shared_data = array(
125 + 'products' => $this->format_items_to_json( $cart->get_cart() ),
126 + 'create_account' => $create_account,
127 + 'guest_checkout' => $guest_checkout,
128 + 'delayed_account_creation' => $delayed_account_creation,
129 + 'express_checkout' => 'null', // TODO: not solved yet.
130 + 'shipping_options_count' => 'null', // TODO: not solved yet.
131 + 'coupon_used' => $coupon_used,
132 + 'payment_options' => $enabled_payment_options,
133 + );
134 +
135 + return $shared_data;
136 + }
137 +
138 + /**
139 + * Gets the content of the cart/checkout page or where the cart/checkout page is ultimately derived from if using a template.
140 + * This method sets the class properties $checkout_content_source and $cart_content_source.
141 + *
142 + * @return void Does not return, but sets class properties.
143 + */
144 + public function find_cart_checkout_content_sources() {
145 +
146 + /**
147 + * The steps we take to find the content are:
148 + * 1. Check the transient, if that contains content and is not expired, return that.
149 + * 2. Check if the cart/checkout templates are in use. If *not in use*, get the content from the pages and
150 + * return it, there is no need to dig further.
151 + * 3. If the templates *are* in use, check if the `page-content-wrapper` block is in use. If so, get the content
152 + * from the pages (same as step 2) and return it.
153 + * 4. If the templates are in use but `page-content-wrapper` is not, then get the content directly from the
154 + * template and return it.
155 + * 5. At the end of each step, assign the found content to the relevant class properties and save them in a
156 + * transient with a 1-day lifespan. This will prevent us from having to do this work on every page load.
157 + */
158 +
159 + $cart_checkout_content_cache_transient_name = 'jetpack_woocommerce_analytics_cart_checkout_content_sources';
160 +
161 + $transient_value = get_transient( $cart_checkout_content_cache_transient_name );
162 +
163 + if (
164 + false !== $transient_value &&
165 + ! empty( $transient_value['checkout_content_source'] ) &&
166 + ! empty( $transient_value['cart_content_source'] )
167 + ) {
168 + $this->cart_content_source = $transient_value['cart_content_source'];
169 + $this->checkout_content_source = $transient_value['checkout_content_source'];
170 + return;
171 + }
172 +
173 + $this->cart_checkout_templates_in_use = wp_is_block_theme()
174 + && class_exists( '\Automattic\WooCommerce\Blocks\Package' )
175 + // @phan-suppress-current-line UnusedPluginSuppression @phan-suppress-next-line PhanUndeclaredClassMethod -- If the class exists (as of WooCommerce 8.5.0), the method exists. See also: https://github.com/phan/phan/issues/1204
176 + && version_compare( \Automattic\WooCommerce\Blocks\Package::get_version(), '10.6.0', '>=' );
177 +
178 + // Cart/Checkout *pages* are in use if the templates are not in use. Return their content and do nothing else.
179 + if ( ! $this->cart_checkout_templates_in_use ) {
180 + $cart_page = get_post( wc_get_page_id( 'cart' ) );
181 + $checkout_page = get_post( wc_get_page_id( 'checkout' ) );
182 +
183 + if ( $cart_page && isset( $cart_page->post_content ) ) {
184 + $this->cart_content_source = $cart_page->post_content;
185 + }
186 +
187 + if ( $checkout_page && isset( $checkout_page->post_content ) ) {
188 + $this->checkout_content_source = $checkout_page->post_content;
189 + }
190 +
191 + set_transient(
192 + $cart_checkout_content_cache_transient_name,
193 + array(
194 + 'cart_content_source' => $this->cart_content_source,
195 + 'checkout_content_source' => $this->checkout_content_source,
196 + ),
197 + DAY_IN_SECONDS
198 + );
199 + return;
200 + }
201 +
202 + // We are in a Block theme - so we need to find out if the templates are being used.
203 + if ( function_exists( 'get_block_template' ) ) {
204 + $checkout_template = get_block_template( 'woocommerce/woocommerce//page-checkout' );
205 + $cart_template = get_block_template( 'woocommerce/woocommerce//page-cart' );
206 +
207 + if ( ! $checkout_template ) {
208 + $checkout_template = get_block_template( 'woocommerce/woocommerce//checkout' );
209 + }
210 +
211 + if ( ! $cart_template ) {
212 + $cart_template = get_block_template( 'woocommerce/woocommerce//cart' );
213 + }
214 + }
215 +
216 + if ( ! empty( $checkout_template->content ) ) {
217 + // Checkout template is in use, but we need to see if the page-content-wrapper is in use, or if the template is being used directly.
218 + $this->checkout_content_source = $checkout_template->content;
219 + $is_using_page_content = str_contains( $checkout_template->content, '<!-- wp:woocommerce/page-content-wrapper {"page":"checkout"}' );
220 +
221 + if ( $is_using_page_content ) {
222 + // The page-content-wrapper is in use, so we need to get the page content.
223 + $checkout_page = get_post( wc_get_page_id( 'checkout' ) );
224 +
225 + if ( $checkout_page && isset( $checkout_page->post_content ) ) {
226 + $this->checkout_content_source = $checkout_page->post_content;
227 + }
228 + }
229 + }
230 +
231 + if ( ! empty( $cart_template->content ) ) {
232 + // Cart template is in use, but we need to see if the page-content-wrapper is in use, or if the template is being used directly.
233 + $this->cart_content_source = $cart_template->content;
234 + $is_using_page_content = str_contains( $cart_template->content, '<!-- wp:woocommerce/page-content-wrapper {"page":"cart"}' );
235 +
236 + if ( $is_using_page_content ) {
237 + // The page-content-wrapper is in use, so we need to get the page content.
238 + $cart_page = get_post( wc_get_page_id( 'cart' ) );
239 +
240 + if ( $cart_page && isset( $cart_page->post_content ) ) {
241 + $this->cart_content_source = $cart_page->post_content;
242 + }
243 + }
244 + }
245 +
246 + set_transient(
247 + $cart_checkout_content_cache_transient_name,
248 + array(
249 + 'cart_content_source' => $this->cart_content_source,
250 + 'checkout_content_source' => $this->checkout_content_source,
251 + ),
252 + DAY_IN_SECONDS
253 + );
254 + }
255 +
256 + /**
257 + * Request-scoped — not for page output, see `get_page_common_properties()`.
258 + *
259 + * Default event properties for events the server fires itself on an uncached
260 + * request. See `WC_Analytics_Tracking::get_common_properties()`.
261 + *
262 + * @return array Array of standard event props.
263 + */
264 + public function get_common_properties() {
265 + $common_properties = WC_Analytics_Tracking::get_common_properties();
266 + /**
267 + * Allow defining custom event properties in WooCommerce Analytics.
268 + *
269 + * See `WC_Analytics_Tracking::get_properties()` for the full contract
270 + * around `$is_client_supplied`.
271 + *
272 + * @module woocommerce-analytics
273 + *
274 + * @since 12.5
275 + * @since 0.18.0 Added the `$is_client_supplied` parameter. This call site
276 + * also began passing `$event_name`, which the hook already had.
277 + *
278 + * @param array $properties Array of event props to be filtered.
279 + * @param string $event_name Event name. Empty string here: this call builds
280 + * common properties, not properties for a specific event.
281 + * @param bool $is_client_supplied Whether the props came from an untrusted client.
282 + */
283 + $properties = apply_filters(
284 + 'jetpack_woocommerce_analytics_event_props',
285 + $common_properties,
286 + '',
287 + false
288 + );
289 +
290 + return $properties;
291 + }
292 +
293 + /**
294 + * Default event properties for the client, excluding anything derived from
295 + * the current request.
296 + *
297 + * Used for the properties embedded in front-end page HTML, which is cached
298 + * and replayed to later visitors. See
299 + * `WC_Analytics_Tracking::get_page_common_properties()`.
300 + *
301 + * @since 0.16.7
302 + *
303 + * @return array Array of standard event props.
304 + */
305 + public function get_page_common_properties() {
306 + $common_properties = WC_Analytics_Tracking::get_page_common_properties();
307 +
308 + /** This filter is documented in src/class-woo-analytics-trait.php */
309 + return apply_filters(
310 + 'jetpack_woocommerce_analytics_event_props',
311 + $common_properties,
312 + '',
313 + false
314 + );
315 + }
316 +
317 + /**
318 + * Enqueue an event with optional product and custom properties.
319 + *
320 + * @param string $event_name The name of the event to record.
321 + * @param array $properties Optional array of (key => value) event properties.
322 + * @param integer|null $product_id The id of the product relating to the event.
323 + *
324 + * @return void
325 + */
326 + public function enqueue_event( $event_name, $properties = array(), $product_id = null ) {
327 + // Only set product details if we have a product id.
328 + if ( $product_id ) {
329 + $product = wc_get_product( $product_id );
330 + if ( ! $product instanceof WC_Product ) {
331 + return;
332 + }
333 + $product_details = $this->get_product_details( $product );
334 + }
335 +
336 + $event_properties = array_merge( $product_details ?? array(), $properties );
337 +
338 + WC_Analytics_Tracking::add_event_to_queue( $event_name, $event_properties );
339 + }
340 +
341 + /**
342 + * Gather relevant product information
343 + *
344 + * @param \WC_Product $product product.
345 + * @return array
346 + */
347 + public function get_product_details( $product ) {
348 + return array(
349 + 'pi' => $product->get_id(),
350 + 'pn' => $product->get_title(),
351 + 'pc' => $this->get_product_categories_concatenated( $product ),
352 + 'pp' => $product->get_price(),
353 + 'pt' => $product->get_type(),
354 + );
355 + }
356 +
357 + /**
358 + * Gets product categories or variation attributes as a formatted concatenated string
359 + *
360 + * @param object $product WC_Product.
361 + * @return string
362 + */
363 + public function get_product_categories_concatenated( $product ) {
364 +
365 + if ( ! $product instanceof WC_Product ) {
366 + return '';
367 + }
368 +
369 + $variation_data = $product->is_type( 'variation' ) ? wc_get_product_variation_attributes( $product->get_id() ) : '';
370 + if ( is_array( $variation_data ) && ! empty( $variation_data ) ) {
371 + $line = wc_get_formatted_variation( $variation_data, true );
372 + } else {
373 + $out = array();
374 + $categories = get_the_terms( $product->get_id(), 'product_cat' );
375 + if ( is_array( $categories ) ) {
376 + foreach ( $categories as $category ) {
377 + $out[] = $category->name;
378 + }
379 + }
380 + $line = implode( '/', $out );
381 + }
382 + return $line;
383 + }
384 +
385 + /**
386 + * Get the current user id
387 + *
388 + * @return string|null
389 + */
390 + public function get_user_id() {
391 + if ( is_user_logged_in() ) {
392 + $blogid = Jetpack_Connection::get_site_id();
393 + $userid = get_current_user_id();
394 + return $blogid . ':' . $userid;
395 + }
396 + return null;
397 + }
398 +
399 + /**
400 + * Gets the IDs of additional blocks on the Cart/Checkout pages or templates.
401 + *
402 + * @param string $cart_or_checkout Whether to get blocks on the cart or checkout page.
403 + * @return array All inner blocks on the page.
404 + */
405 + public function get_additional_blocks_on_page( $cart_or_checkout = 'cart' ) {
406 + $additional_blocks_on_page_transient_name = 'jetpack_woocommerce_analytics_additional_blocks_on_' . $cart_or_checkout . '_page';
407 + $additional_blocks_on_page = get_transient( $additional_blocks_on_page_transient_name );
408 +
409 + if ( false !== $additional_blocks_on_page ) {
410 + return $additional_blocks_on_page;
411 + }
412 +
413 + $content = $this->cart_content_source;
414 +
415 + if ( 'checkout' === $cart_or_checkout ) {
416 + $content = $this->checkout_content_source;
417 + }
418 +
419 + $blocks_to_ignore = array(
420 + 'woocommerce/classic-shortcode',
421 + 'core/shortcode',
422 + 'checkout' === $cart_or_checkout ? 'woocommerce/checkout' : 'woocommerce/cart',
423 + );
424 +
425 + $scanner = Block_Scanner::create( $content );
426 + if ( ! $scanner ) {
427 + return array();
428 + }
429 +
430 + $found_blocks = array();
431 + $ignored_block_depth = 0; // Count how many ignored blocks we're nested inside.
432 +
433 + while ( $scanner->next_delimiter() ) {
434 + $type = $scanner->get_delimiter_type();
435 + $block_type = $scanner->get_block_type();
436 + $is_ignored_block = in_array( $block_type, $blocks_to_ignore, true );
437 +
438 + switch ( $type ) {
439 + case Block_Scanner::OPENER:
440 + // If this is an ignored block, increase our nesting depth.
441 + if ( $is_ignored_block ) {
442 + ++$ignored_block_depth;
443 + }
444 +
445 + // Only collect blocks that are not inside any ignored block.
446 + if ( 0 === $ignored_block_depth ) {
447 + $found_blocks[] = $block_type;
448 + }
449 + break;
450 +
451 + case Block_Scanner::CLOSER:
452 + // If this closes an ignored block, decrease our nesting depth.
453 + if ( $is_ignored_block && $ignored_block_depth > 0 ) {
454 + --$ignored_block_depth;
455 + }
456 + break;
457 +
458 + case Block_Scanner::VOID:
459 + // Void blocks: only collect if we're not inside an ignored block and this isn't an ignored block itself.
460 + if ( 0 === $ignored_block_depth && ! $is_ignored_block ) {
461 + $found_blocks[] = $block_type;
462 + }
463 + break;
464 + }
465 + }
466 +
467 + set_transient( $additional_blocks_on_page_transient_name, $found_blocks, DAY_IN_SECONDS );
468 + return $found_blocks;
469 + }
470 +
471 + /**
472 + * Gets an array containing the block or shortcode use properties for the Cart page.
473 + *
474 + * @return array An array containing the block or shortcode use properties for the Cart page.
475 + */
476 + public function get_cart_page_block_usage() {
477 + $new_info = array();
478 +
479 + $content = $this->cart_content_source;
480 + $block_presence = str_contains( $content, '<!-- wp:woocommerce/cart' );
481 + $shortcode_presence = str_contains( $content, '[woocommerce_cart]' );
482 + $classic_shortcode_presence = str_contains( $content, '<!-- wp:woocommerce/classic-shortcode {"shortcode":"cart"}' );
483 +
484 + $new_info['cart_page_contains_cart_block'] = $block_presence ? '1' : '0';
485 + $new_info['cart_page_contains_cart_shortcode'] = $shortcode_presence || $classic_shortcode_presence ? '1' : '0';
486 + return $new_info;
487 + }
488 +
489 + /**
490 + * Gets an array containing the block or shortcode use properties for the Checkout page.
491 + *
492 + * @return array An array containing the block or shortcode use properties for the Checkout page.
493 + */
494 + public function get_checkout_page_block_usage() {
495 + $new_info = array();
496 +
497 + $content = $this->checkout_content_source;
498 + $block_presence = str_contains( $content, '<!-- wp:woocommerce/checkout' );
499 + $shortcode_presence = str_contains( $content, '[woocommerce_checkout]' );
500 + $classic_shortcode_presence = str_contains( $content, '<!-- wp:woocommerce/classic-shortcode {"shortcode":"checkout"}' );
501 +
502 + $new_info['checkout_page_contains_checkout_block'] = $block_presence ? '1' : '0';
503 + $new_info['checkout_page_contains_checkout_shortcode'] = $shortcode_presence || $classic_shortcode_presence ? '1' : '0';
504 + return $new_info;
505 + }
506 +
507 + /**
508 + * Get info about the cart & checkout pages, in particular
509 + * whether the store is using shortcodes or Gutenberg blocks.
510 + * This info is cached in a transient.
511 + *
512 + * Note: similar code is in a WooCommerce core PR:
513 + * https://github.com/woocommerce/woocommerce/pull/25932
514 + *
515 + * @return array
516 + */
517 + public function get_cart_checkout_info() {
518 + $info = array_merge(
519 + $this->get_cart_page_block_usage(),
520 + $this->get_checkout_page_block_usage()
521 + );
522 + return $info;
523 + }
524 +
525 + /**
526 + * Search a specific post for text content.
527 + *
528 + * Note: similar code is in a WooCommerce core PR:
529 + * https://github.com/woocommerce/woocommerce/pull/25932
530 + *
531 + * @param integer $post_id The id of the post to search.
532 + * @param string $text The text to search for.
533 + * @return integer 1 if post contains $text (otherwise 0).
534 + */
535 + public function post_contains_text( $post_id, $text ) {
536 + global $wpdb;
537 +
538 + // Search for the text anywhere in the post.
539 + $wildcarded = "%{$text}%";
540 +
541 + // No better way to search post content without having filters expanding blocks.
542 + // This is already cached up in the parent function.
543 + $result = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
544 + $wpdb->prepare(
545 + "
546 + SELECT COUNT( * ) FROM {$wpdb->prefix}posts
547 + WHERE ID=%d
548 + AND {$wpdb->prefix}posts.post_content LIKE %s
549 + ",
550 + array( $post_id, $wildcarded )
551 + )
552 + );
553 +
554 + return ( '0' !== $result ) ? 1 : 0;
555 + }
556 +
557 + /**
558 + * Get the cart total
559 + *
560 + * @return float
561 + */
562 + public function get_cart_total() {
563 + $cart = WC()->cart;
564 + if ( $cart === null ) {
565 + return 0;
566 + }
567 + return $cart->get_total( 'tracking' );
568 + }
569 +
570 + /**
571 + * Get the cart subtotal
572 + *
573 + * @return float
574 + */
575 + public function get_cart_subtotal() {
576 + $cart = WC()->cart;
577 + if ( $cart === null ) {
578 + return 0;
579 + }
580 + return $cart->get_subtotal();
581 + }
582 +
583 + /**
584 + * Get the cart shipping total
585 + *
586 + * @return float
587 + */
588 + public function get_cart_shipping_total() {
589 + $cart = WC()->cart;
590 + if ( $cart === null ) {
591 + return 0;
592 + }
593 + return $cart->get_shipping_total();
594 + }
595 +
596 + /**
597 + * Get the cart taxes
598 + *
599 + * @return float
600 + */
601 + public function get_cart_taxes() {
602 + $cart = WC()->cart;
603 + if ( $cart === null ) {
604 + return 0;
605 + }
606 + return $cart->get_taxes_total();
607 + }
608 +
609 + /**
610 + * Get the cart discount total
611 + *
612 + * @return float
613 + */
614 + public function get_total_discounts() {
615 + $cart = WC()->cart;
616 + if ( $cart === null ) {
617 + return 0;
618 + }
619 + return $cart->get_discount_total();
620 + }
621 +
622 + /**
623 + * Get number of items in the cart
624 + *
625 + * @return int
626 + */
627 + public function get_cart_items_count() {
628 + $cart = WC()->cart;
629 + if ( $cart === null ) {
630 + return 0;
631 + }
632 + return $cart->get_cart_contents_count();
633 + }
634 +
635 + /**
636 + * Retrieves the breadcrumb trail as an array of page titles.
637 + *
638 + * This function attempts to generate a hierarchical breadcrumb trail for the current page or post.
639 + * - For the front page, it returns "Home".
640 + * - For WooCommerce product, category, or tag pages, it uses the WooCommerce breadcrumb generator and prepends the shop page title if needed.
641 + * - For regular pages, it builds the breadcrumb from the page's ancestors, ordered from top-level to current.
642 + * - For all other cases, it returns the current page's title.
643 + *
644 + * Titles are capped before being returned; see `cap_page_string()`.
645 + *
646 + * @return array The breadcrumb trail as an array of titles.
647 + */
648 + private function get_breadcrumb_titles() {
649 + return array_map( array( $this, 'cap_page_string' ), $this->build_breadcrumb_titles() );
650 + }
651 +
652 + /**
653 + * Limit the length of a caller-influenced string bound for the page output.
654 + *
655 + * Breadcrumb titles come from `post_title`, which core does not bound, so this
656 + * is their only limit. The search term is already blanked by
657 + * `WP_Query::parse_query()` above 1600 bytes; capping it further keeps the
658 + * search pixel URL, which carries the term twice, from being rejected outright.
659 + *
660 + * Truncated values keep an ellipsis so they stay distinguishable downstream
661 + * from a value that genuinely ended at the limit.
662 + *
663 + * @since 0.16.7
664 + *
665 + * @param string $value The value bound for the page output.
666 + * @return string The value, truncated if it exceeded the limit.
667 + */
668 + private function cap_page_string( $value ) {
669 + $max_length = 200;
670 +
671 + $value = (string) $value;
672 +
673 + if ( mb_strlen( $value ) <= $max_length ) {
674 + return $value;
675 + }
676 +
677 + return mb_substr( $value, 0, $max_length - 1 ) . '…';
678 + }
679 +
680 + /**
681 + * Build the uncapped breadcrumb trail. See `get_breadcrumb_titles()`.
682 + *
683 + * @return array The breadcrumb trail as an array of titles.
684 + */
685 + private function build_breadcrumb_titles() {
686 + if ( is_front_page() ) {
687 + return array( __( 'Home', 'woocommerce-analytics' ) );
688 + }
689 +
690 + if ( class_exists( '\WC_Breadcrumb' ) ) {
691 + $breadcrumb = new \WC_Breadcrumb();
692 + $crumbs = $breadcrumb->generate();
693 + $titles = wp_list_pluck( $crumbs, 0 );
694 +
695 + if ( is_product() || is_product_category() || is_product_tag() ) {
696 + $titles = $this->prepend_shop_page_title( $titles );
697 + }
698 +
699 + if ( ! empty( $titles ) ) {
700 + return $titles;
701 + }
702 + }
703 +
704 + // If it's a page, get the hierarchical title.
705 + if ( is_page() ) {
706 + $titles = array();
707 + $page_id = get_queried_object_id();
708 + $ancestors = get_post_ancestors( $page_id );
709 + // Reverse the ancestors to get the top-level first.
710 + $ancestors = array_reverse( $ancestors );
711 +
712 + foreach ( $ancestors as $ancestor ) {
713 + $titles[] = get_the_title( $ancestor );
714 + }
715 + $titles[] = get_the_title( $page_id );
716 +
717 + return $titles;
718 + }
719 +
720 + return array( get_the_title() );
721 + }
722 +
723 + /**
724 + * Prepend the shop page title if it's not already present.
725 + *
726 + * @param array $titles The titles to prepend the shop page title to.
727 + * @return array The titles with the shop page title prepended.
728 + */
729 + private function prepend_shop_page_title( array $titles ) {
730 + $shop_page_id = wc_get_page_id( 'shop' );
731 + if ( ! $shop_page_id ) {
732 + return $titles;
733 + }
734 +
735 + $shop_page_title = get_the_title( $shop_page_id );
736 +
737 + if ( ! $shop_page_title || ( ! empty( $titles ) && $titles[0] === $shop_page_title ) ) {
738 + return $titles;
739 + }
740 +
741 + return array_merge( array( $shop_page_title ), $titles );
742 + }
743 +}