PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.9.3
WooCommerce v10.9.3
10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / includes / class-wc-structured-data.php
woocommerce / includes Last commit date
abstracts 4 weeks ago admin 4 days ago blocks 11 months ago cli 4 weeks ago customizer 4 months ago data-stores 4 weeks ago emails 4 days ago export 1 year ago gateways 4 weeks ago import 3 months ago integrations 2 months ago interfaces 4 months ago legacy 4 months ago libraries 4 weeks ago log-handlers 1 year ago payment-tokens 5 years ago product-usage 1 year ago queue 4 months ago react-admin 4 weeks ago rest-api 4 weeks ago shipping 3 months ago shortcodes 4 weeks ago theme-support 2 years ago tracks 4 weeks ago traits 5 years ago walkers 5 years ago wccom-site 4 weeks ago widgets 2 months ago class-wc-ajax.php 4 weeks ago class-wc-auth.php 1 year ago class-wc-autoloader.php 9 months ago class-wc-background-emailer.php 2 months ago class-wc-background-updater.php 5 years ago class-wc-brands-brand-settings-manager.php 1 year ago class-wc-brands-coupons.php 1 year ago class-wc-brands.php 4 weeks ago class-wc-breadcrumb.php 4 months ago class-wc-cache-helper.php 4 weeks ago class-wc-cart-fees.php 2 years ago class-wc-cart-session.php 3 months ago class-wc-cart-totals.php 11 months ago class-wc-cart.php 4 weeks ago class-wc-checkout.php 4 weeks ago class-wc-cli.php 10 months ago class-wc-comments.php 4 months ago class-wc-countries.php 2 months ago class-wc-coupon.php 2 months ago class-wc-customer-download-log.php 5 years ago class-wc-customer-download.php 1 year ago class-wc-customer.php 4 weeks ago class-wc-data-exception.php 8 years ago class-wc-data-store.php 3 years ago class-wc-datetime.php 4 years ago class-wc-deprecated-action-hooks.php 2 years ago class-wc-deprecated-filter-hooks.php 3 months ago class-wc-discounts.php 11 months ago class-wc-download-handler.php 4 weeks ago class-wc-emails.php 4 weeks ago class-wc-embed.php 1 year ago class-wc-form-handler.php 3 months ago class-wc-frontend-scripts.php 2 months ago class-wc-geo-ip.php 9 months ago class-wc-geolite-integration.php 6 years ago class-wc-geolocation.php 2 months ago class-wc-https.php 3 years ago class-wc-install.php 4 days ago class-wc-integrations.php 5 years ago class-wc-log-levels.php 2 years ago class-wc-logger.php 4 months ago class-wc-meta-data.php 4 weeks ago class-wc-order-factory.php 2 months ago class-wc-order-item-coupon.php 4 years ago class-wc-order-item-fee.php 5 months ago class-wc-order-item-meta.php 4 years ago class-wc-order-item-product.php 4 weeks ago class-wc-order-item-shipping.php 5 months ago class-wc-order-item-tax.php 4 weeks ago class-wc-order-item.php 4 weeks ago class-wc-order-query.php 4 weeks ago class-wc-order-refund.php 1 year ago class-wc-order.php 4 weeks ago class-wc-payment-gateways.php 4 weeks ago class-wc-payment-tokens.php 3 years ago class-wc-post-data.php 2 months ago class-wc-post-types.php 4 weeks ago class-wc-privacy-background-process.php 1 year ago class-wc-privacy-erasers.php 10 months ago class-wc-privacy-exporters.php 4 years ago class-wc-privacy.php 1 year ago class-wc-product-attribute.php 4 months ago class-wc-product-download.php 4 months ago class-wc-product-external.php 1 year ago class-wc-product-factory.php 3 months ago class-wc-product-grouped.php 3 months ago class-wc-product-query.php 4 months ago class-wc-product-simple.php 11 months ago class-wc-product-variable.php 4 weeks ago class-wc-product-variation.php 4 weeks ago class-wc-query.php 4 weeks ago class-wc-rate-limiter.php 4 years ago class-wc-regenerate-images-request.php 3 years ago class-wc-regenerate-images.php 2 years ago class-wc-register-wp-admin-settings.php 5 years ago class-wc-rest-authentication.php 2 years ago class-wc-rest-exception.php 5 years ago class-wc-session-handler.php 3 months ago class-wc-shipping-rate.php 1 year ago class-wc-shipping-zone.php 5 years ago class-wc-shipping-zones.php 7 months ago class-wc-shipping.php 2 months ago class-wc-shortcodes.php 1 year ago class-wc-structured-data.php 4 weeks ago class-wc-tax.php 2 months ago class-wc-template-loader.php 7 months ago class-wc-tracker.php 4 weeks ago class-wc-validation.php 2 years ago class-wc-webhook.php 4 weeks ago class-woocommerce.php 4 days ago wc-account-functions.php 4 weeks ago wc-attribute-functions.php 4 weeks ago wc-brands-functions.php 1 year ago wc-cart-functions.php 4 weeks ago wc-conditional-functions.php 11 months ago wc-core-functions.php 2 months ago wc-coupon-functions.php 5 months ago wc-deprecated-functions.php 4 weeks ago wc-formatting-functions.php 4 weeks ago wc-interactivity-api-functions.php 2 months ago wc-notice-functions.php 5 months ago wc-order-functions.php 1 month ago wc-order-item-functions.php 3 years ago wc-order-step-logger-functions.php 4 months ago wc-page-functions.php 1 month ago wc-product-functions.php 2 months ago wc-rest-functions.php 7 months ago wc-stock-functions.php 7 months ago wc-template-functions.php 4 weeks ago wc-template-hooks.php 10 months ago wc-term-functions.php 2 months ago wc-update-functions.php 4 days ago wc-user-functions.php 4 weeks ago wc-webhook-functions.php 4 weeks ago wc-widget-functions.php 5 years ago
class-wc-structured-data.php
729 lines
1 <?php
2 /**
3 * Structured data's handler and generator using JSON-LD format.
4 *
5 * When making changes to this file, please make sure to test the generated
6 * markup with Schema Markup Validator and Google Search Console.
7 * * https://validator.schema.org/
8 * * https://search.google.com/test/rich-results
9 *
10 * @package WooCommerce\Classes
11 * @since 3.0.0
12 * @version 3.0.0
13 */
14
15 use Automattic\WooCommerce\Enums\OrderStatus;
16 use Automattic\WooCommerce\Enums\ProductType;
17 use Automattic\WooCommerce\Enums\ProductStockStatus;
18
19 defined( 'ABSPATH' ) || exit;
20
21 /**
22 * Structured data class.
23 */
24 class WC_Structured_Data {
25
26 /**
27 * Stores the structured data.
28 *
29 * @var array $_data Array of structured data.
30 */
31 private $_data = array();
32
33 /**
34 * Constructor.
35 */
36 public function __construct() {
37 // Generate structured data.
38 add_action( 'woocommerce_before_main_content', array( $this, 'generate_website_data' ), 30 );
39 add_action( 'woocommerce_breadcrumb', array( $this, 'generate_breadcrumblist_data' ), 10 );
40 add_action( 'woocommerce_single_product_summary', array( $this, 'generate_product_data' ), 60 );
41 add_action( 'woocommerce_email_order_details', array( $this, 'generate_order_data' ), 20, 3 );
42
43 // Output structured data.
44 add_action( 'woocommerce_email_order_details', array( $this, 'output_email_structured_data' ), 30, 3 );
45 add_action( 'wp_footer', array( $this, 'output_structured_data' ), 10 );
46 }
47
48 /**
49 * Sets data.
50 *
51 * @param array $data Structured data.
52 * @param bool $reset Unset data (default: false).
53 * @return bool
54 */
55 public function set_data( $data, $reset = false ) {
56 if ( ! isset( $data['@type'] ) || ! preg_match( '|^[a-zA-Z]{1,20}$|', $data['@type'] ) ) {
57 return false;
58 }
59
60 if ( $reset && isset( $this->_data ) ) {
61 unset( $this->_data );
62 }
63
64 $this->_data[] = $data;
65
66 return true;
67 }
68
69 /**
70 * Gets data.
71 *
72 * @return array
73 */
74 public function get_data() {
75 return $this->_data;
76 }
77
78 /**
79 * Structures and returns data.
80 *
81 * List of types available by default for specific request:
82 *
83 * 'product',
84 * 'review',
85 * 'breadcrumblist',
86 * 'website',
87 * 'order',
88 *
89 * @param array $types Structured data types.
90 * @return array
91 */
92 public function get_structured_data( $types ) {
93 $data = array();
94
95 // Put together the values of same type of structured data.
96 foreach ( $this->get_data() as $value ) {
97 $data[ strtolower( $value['@type'] ) ][] = $value;
98 }
99
100 // Wrap the multiple values of each type inside a graph... Then add context to each type.
101 foreach ( $data as $type => $value ) {
102 $data[ $type ] = count( $value ) > 1 ? array( '@graph' => $value ) : $value[0];
103 $data[ $type ] = apply_filters( 'woocommerce_structured_data_context', array( '@context' => 'https://schema.org/' ), $data, $type, $value ) + $data[ $type ];
104 }
105
106 // If requested types, pick them up... Finally change the associative array to an indexed one.
107 $data = $types ? array_values( array_intersect_key( $data, array_flip( $types ) ) ) : array_values( $data );
108
109 if ( ! empty( $data ) ) {
110 if ( 1 < count( $data ) ) {
111 $data = apply_filters( 'woocommerce_structured_data_context', array( '@context' => 'https://schema.org/' ), $data, '', '' ) + array( '@graph' => $data );
112 } else {
113 $data = $data[0];
114 }
115 }
116
117 return $data;
118 }
119
120 /**
121 * Get data types for pages.
122 *
123 * @return array
124 */
125 protected function get_data_type_for_page() {
126 $types = array();
127 $types[] = is_shop() || is_product_category() || is_product() ? 'product' : '';
128 $types[] = is_shop() && is_front_page() ? 'website' : '';
129 $types[] = is_product() ? 'review' : '';
130 $types[] = 'breadcrumblist';
131 $types[] = 'order';
132
133 return array_filter( apply_filters( 'woocommerce_structured_data_type_for_page', $types ) );
134 }
135
136 /**
137 * Makes sure email structured data only outputs on non-plain text versions.
138 *
139 * @param WP_Order $order Order data.
140 * @param bool $sent_to_admin Send to admin (default: false).
141 * @param bool $plain_text Plain text email (default: false).
142 */
143 public function output_email_structured_data( $order, $sent_to_admin = false, $plain_text = false ) {
144 if ( $plain_text ) {
145 return;
146 }
147 echo '<div style="display: none; font-size: 0; max-height: 0; line-height: 0; padding: 0; mso-hide: all;">';
148 $this->output_structured_data();
149 echo '</div>';
150 }
151
152 /**
153 * Sanitizes, encodes and outputs structured data.
154 *
155 * Hooked into `wp_footer` action hook.
156 * Hooked into `woocommerce_email_order_details` action hook.
157 */
158 public function output_structured_data() {
159 $types = $this->get_data_type_for_page();
160 $data = $this->get_structured_data( $types );
161
162 if ( $data ) {
163 echo '<script type="application/ld+json">' . wc_esc_json( wp_json_encode( $data, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), true ) . '</script>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
164 }
165 }
166
167 /*
168 |--------------------------------------------------------------------------
169 | Generators
170 |--------------------------------------------------------------------------
171 |
172 | Methods for generating specific structured data types:
173 |
174 | - Product
175 | - Review
176 | - BreadcrumbList
177 | - WebSite
178 | - Order
179 |
180 | The generated data is stored into `$this->_data`.
181 | See the methods above for handling `$this->_data`.
182 |
183 */
184
185 /**
186 * Generates Product structured data.
187 *
188 * Hooked into `woocommerce_single_product_summary` action hook.
189 *
190 * @param WC_Product $product Product data (default: null).
191 */
192 public function generate_product_data( $product = null ) {
193 if ( ! is_object( $product ) ) {
194 global $product;
195 }
196
197 if ( ! is_a( $product, 'WC_Product' ) ) {
198 return;
199 }
200
201 $shop_name = get_bloginfo( 'name' );
202 $shop_url = home_url();
203 $currency = get_woocommerce_currency();
204 $permalink = get_permalink( $product->get_id() );
205 $image = wp_get_attachment_url( $product->get_image_id() );
206
207 $markup = array(
208 '@type' => 'Product',
209 '@id' => $permalink . '#product', // Append '#product' to differentiate between this @id and the @id generated for the Breadcrumblist.
210 'name' => wp_kses_post( $product->get_name() ),
211 'url' => $permalink,
212 'description' => wp_strip_all_tags( do_shortcode( $product->get_short_description() ? $product->get_short_description() : $product->get_description() ) ),
213 );
214
215 if ( $image ) {
216 $markup['image'] = $image;
217 }
218
219 // Declare SKU or fallback to ID.
220 if ( $product->get_sku() ) {
221 $markup['sku'] = $product->get_sku();
222 } else {
223 $markup['sku'] = $product->get_id();
224 }
225
226 // Prepare GTIN and load it if it's valid.
227 $gtin = $this->prepare_gtin( $product->get_global_unique_id() );
228 if ( $this->is_valid_gtin( $gtin ) ) {
229 $markup['gtin'] = $gtin;
230 }
231
232 if ( '' !== $product->get_price() ) {
233 // Assume prices will be valid until the end of next year, unless on sale and there is an end date.
234 $price_valid_until = gmdate( 'Y-12-31', time() + YEAR_IN_SECONDS );
235
236 if ( $product->is_type( ProductType::VARIABLE ) ) {
237 $lowest = $product->get_variation_price( 'min', true );
238 $highest = $product->get_variation_price( 'max', true );
239
240 $variation_prices = $product->get_variation_prices( true );
241
242 if ( $lowest === $highest ) {
243 $unit_price_spec = array(
244 '@type' => 'UnitPriceSpecification',
245 'price' => wc_format_decimal( $lowest, wc_get_price_decimals() ),
246 'priceCurrency' => $currency,
247 'validThrough' => $price_valid_until,
248 );
249 if ( wc_tax_enabled() ) {
250 $unit_price_spec['valueAddedTaxIncluded'] = 'incl' === get_option( 'woocommerce_tax_display_shop' );
251 }
252 $markup_offer = array(
253 '@type' => 'Offer',
254 'priceSpecification' => array( $unit_price_spec ),
255 );
256 } else {
257 $markup_offer = array(
258 '@type' => 'AggregateOffer',
259 'lowPrice' => wc_format_decimal( $lowest, wc_get_price_decimals() ),
260 'highPrice' => wc_format_decimal( $highest, wc_get_price_decimals() ),
261 'offerCount' => count( $variation_prices['price'] ),
262 );
263
264 if ( $product->is_on_sale() ) {
265 $lowest_child_sale_price = $product->get_variation_sale_price( 'min', true );
266 foreach ( $variation_prices['sale_price'] as $variation_id => $variation_price ) {
267 if ( $variation_price === $lowest_child_sale_price ) {
268 break;
269 }
270 }
271 $date_on_sale_to = isset( $variation_id )
272 ? wc_get_product( $variation_id )->get_date_on_sale_to()
273 : null;
274 $sale_price_valid_until = $date_on_sale_to
275 ? gmdate( 'Y-m-d', $date_on_sale_to->getTimestamp() )
276 : null;
277
278 $sale_unit_price_spec = array(
279 '@type' => 'UnitPriceSpecification',
280 'priceType' => 'https://schema.org/SalePrice',
281 'price' => wc_format_decimal( $lowest_child_sale_price, wc_get_price_decimals() ),
282 'priceCurrency' => $currency,
283 'validThrough' => $sale_price_valid_until ?? $price_valid_until,
284 );
285 if ( wc_tax_enabled() ) {
286 $sale_unit_price_spec['valueAddedTaxIncluded'] = 'incl' === get_option( 'woocommerce_tax_display_shop' );
287 }
288 $markup_offer['priceSpecification'] = array( $sale_unit_price_spec );
289 }
290 }
291 } elseif ( $product->is_type( ProductType::GROUPED ) ) {
292 $tax_display_mode = get_option( 'woocommerce_tax_display_shop' );
293 $child_ids = $product->get_children();
294 _prime_post_caches( $child_ids );
295 $children = array_filter( array_map( 'wc_get_product', $child_ids ), 'wc_products_array_filter_visible_grouped' );
296 $price_function = 'incl' === $tax_display_mode ? 'wc_get_price_including_tax' : 'wc_get_price_excluding_tax';
297
298 foreach ( $children as $child ) {
299 if ( '' !== $child->get_regular_price() ) {
300 $child_prices[] = $price_function( $child, array( 'price' => $child->get_regular_price() ) );
301 }
302 if ( '' !== $child->get_sale_price() ) {
303 $child_sale_prices[] = $price_function( $child, array( 'price' => $child->get_sale_price() ) );
304 }
305 }
306 if ( empty( $child_prices ) ) {
307 $min_price = 0;
308 } else {
309 $min_price = min( $child_prices );
310 }
311 if ( empty( $child_sale_prices ) ) {
312 $min_sale_price = 0;
313 } else {
314 $min_sale_price = min( $child_sale_prices );
315 }
316
317 $unit_price_specification = array(
318 '@type' => 'UnitPriceSpecification',
319 'price' => wc_format_decimal( $min_price, wc_get_price_decimals() ),
320 'priceCurrency' => $currency,
321 'validThrough' => $price_valid_until,
322 );
323 if ( wc_tax_enabled() ) {
324 $unit_price_specification['valueAddedTaxIncluded'] = 'incl' === $tax_display_mode;
325 }
326 if ( $product->is_on_sale() && $min_price !== $min_sale_price ) {
327 // `priceType` should only be specified in prices which are not the current offer.
328 // https://developers.google.com/search/docs/appearance/structured-data/merchant-listing#sale-pricing-example
329 $unit_price_specification['priceType'] = 'https://schema.org/ListPrice';
330 }
331 $markup_offer = array(
332 '@type' => 'Offer',
333 'priceSpecification' => array(
334 $unit_price_specification,
335 ),
336 );
337
338 if ( $product->is_on_sale() && $min_price !== $min_sale_price ) {
339 if ( $product->get_date_on_sale_to() ) {
340 $sale_price_valid_until = gmdate( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() );
341 }
342
343 // We add the sale price to the top of the array so it's the first offer.
344 // See https://github.com/woocommerce/woocommerce/issues/55043.
345 $grouped_sale_spec = array(
346 '@type' => 'UnitPriceSpecification',
347 'price' => wc_format_decimal( $min_sale_price, wc_get_price_decimals() ),
348 'priceCurrency' => $currency,
349 'validThrough' => $sale_price_valid_until ?? $price_valid_until,
350 );
351 if ( wc_tax_enabled() ) {
352 $grouped_sale_spec['valueAddedTaxIncluded'] = 'incl' === $tax_display_mode;
353 }
354 array_unshift( $markup_offer['priceSpecification'], $grouped_sale_spec );
355 }
356 } else {
357 $tax_display_mode = get_option( 'woocommerce_tax_display_shop' );
358 $regular_price = 'incl' === $tax_display_mode
359 ? wc_get_price_including_tax( $product, array( 'price' => $product->get_regular_price() ) )
360 : wc_get_price_excluding_tax( $product, array( 'price' => $product->get_regular_price() ) );
361 $unit_price_specification = array(
362 '@type' => 'UnitPriceSpecification',
363 'price' => wc_format_decimal( $regular_price, wc_get_price_decimals() ),
364 'priceCurrency' => $currency,
365 'validThrough' => $price_valid_until,
366 );
367 if ( wc_tax_enabled() ) {
368 $unit_price_specification['valueAddedTaxIncluded'] = 'incl' === $tax_display_mode;
369 }
370 if ( $product->is_on_sale() ) {
371 // `priceType` should only be specified in prices which are not the current offer.
372 // https://developers.google.com/search/docs/appearance/structured-data/merchant-listing#sale-pricing-example
373 $unit_price_specification['priceType'] = 'https://schema.org/ListPrice';
374 }
375 $markup_offer = array(
376 '@type' => 'Offer',
377 'priceSpecification' => array(
378 $unit_price_specification,
379 ),
380 );
381
382 if ( $product->is_on_sale() ) {
383 $sale_price = 'incl' === $tax_display_mode
384 ? wc_get_price_including_tax( $product, array( 'price' => $product->get_sale_price() ) )
385 : wc_get_price_excluding_tax( $product, array( 'price' => $product->get_sale_price() ) );
386 if ( $product->get_date_on_sale_to() ) {
387 $sale_price_valid_until = gmdate( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() );
388 }
389
390 // We add the sale price to the top of the array so it's the first offer.
391 // See https://github.com/woocommerce/woocommerce/issues/55043.
392 $simple_sale_spec = array(
393 '@type' => 'UnitPriceSpecification',
394 'price' => wc_format_decimal( $sale_price, wc_get_price_decimals() ),
395 'priceCurrency' => $currency,
396 'validThrough' => $sale_price_valid_until ?? $price_valid_until,
397 );
398 if ( wc_tax_enabled() ) {
399 $simple_sale_spec['valueAddedTaxIncluded'] = 'incl' === $tax_display_mode;
400 }
401 array_unshift( $markup_offer['priceSpecification'], $simple_sale_spec );
402 }
403 }
404
405 if ( $product->is_in_stock() ) {
406 $stock_status_schema = ( ProductStockStatus::ON_BACKORDER === $product->get_stock_status() ) ? 'BackOrder' : 'InStock';
407 } else {
408 $stock_status_schema = 'OutOfStock';
409 }
410
411 $markup_offer += array(
412 'priceValidUntil' => $sale_price_valid_until ?? $price_valid_until,
413 'availability' => 'https://schema.org/' . $stock_status_schema,
414 'url' => $permalink,
415 'seller' => array(
416 '@type' => 'Organization',
417 'name' => $shop_name,
418 'url' => $shop_url,
419 ),
420 );
421 if ( 'Offer' === $markup_offer['@type'] && empty( $markup_offer['price'] ) && ! empty( $markup_offer['priceSpecification'][0]['price'] ) ) {
422 $markup_offer['price'] = $markup_offer['priceSpecification'][0]['price'];
423 }
424 if (
425 ( ! empty( $markup_offer['price'] ) ||
426 ! empty( $markup_offer['lowPrice'] ) ||
427 ! empty( $markup_offer['highPrice'] )
428 )
429 ) {
430 $markup_offer['priceCurrency'] = $currency;
431 }
432
433 $markup['offers'] = array( apply_filters( 'woocommerce_structured_data_product_offer', $markup_offer, $product ) );
434 }
435
436 if ( $product->get_rating_count() && wc_review_ratings_enabled() ) {
437 $markup['aggregateRating'] = array(
438 '@type' => 'AggregateRating',
439 'ratingValue' => $product->get_average_rating(),
440 'reviewCount' => $product->get_review_count(),
441 );
442
443 // Markup 5 most recent rating/review.
444 $comments = get_comments(
445 array(
446 'number' => 5,
447 'post_id' => $product->get_id(),
448 'status' => 'approve',
449 'post_status' => 'publish',
450 'post_type' => 'product',
451 'parent' => 0,
452 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
453 array(
454 'key' => 'rating',
455 'type' => 'NUMERIC',
456 'compare' => '>',
457 'value' => 0,
458 ),
459 ),
460 )
461 );
462
463 if ( $comments ) {
464 $markup['review'] = array();
465 foreach ( $comments as $comment ) {
466 $markup['review'][] = array(
467 '@type' => 'Review',
468 'reviewRating' => array(
469 '@type' => 'Rating',
470 'bestRating' => '5',
471 'ratingValue' => get_comment_meta( $comment->comment_ID, 'rating', true ),
472 'worstRating' => '1',
473 ),
474 'author' => array(
475 '@type' => 'Person',
476 'name' => get_comment_author( $comment ),
477 ),
478 'reviewBody' => get_comment_text( $comment ),
479 'datePublished' => get_comment_date( 'c', $comment ),
480 );
481 }
482 }
483 }
484
485 // Check we have required data.
486 if ( empty( $markup['aggregateRating'] ) && empty( $markup['offers'] ) && empty( $markup['review'] ) ) {
487 return;
488 }
489
490 $this->set_data( apply_filters( 'woocommerce_structured_data_product', $markup, $product ) );
491 }
492
493 /**
494 * Generates Review structured data.
495 *
496 * Hooked into `woocommerce_review_meta` action hook.
497 *
498 * @param WP_Comment $comment Comment data.
499 */
500 public function generate_review_data( $comment ) {
501 $markup = array();
502 $markup['@type'] = 'Review';
503 $markup['@id'] = get_comment_link( $comment->comment_ID );
504 $markup['datePublished'] = get_comment_date( 'c', $comment->comment_ID );
505 $markup['description'] = get_comment_text( $comment->comment_ID );
506 $markup['itemReviewed'] = array(
507 '@type' => 'Product',
508 'name' => get_the_title( $comment->comment_post_ID ),
509 );
510
511 // Skip replies unless they have a rating.
512 $rating = get_comment_meta( $comment->comment_ID, 'rating', true );
513
514 if ( $rating ) {
515 $markup['reviewRating'] = array(
516 '@type' => 'Rating',
517 'bestRating' => '5',
518 'ratingValue' => $rating,
519 'worstRating' => '1',
520 );
521 } elseif ( $comment->comment_parent ) {
522 return;
523 }
524
525 $markup['author'] = array(
526 '@type' => 'Person',
527 'name' => get_comment_author( $comment->comment_ID ),
528 );
529
530 $this->set_data( apply_filters( 'woocommerce_structured_data_review', $markup, $comment ) );
531 }
532
533 /**
534 * Generates BreadcrumbList structured data.
535 *
536 * Hooked into `woocommerce_breadcrumb` action hook.
537 *
538 * @param WC_Breadcrumb $breadcrumbs Breadcrumb data.
539 */
540 public function generate_breadcrumblist_data( $breadcrumbs ) {
541 $crumbs = $breadcrumbs->get_breadcrumb();
542
543 if ( empty( $crumbs ) || ! is_array( $crumbs ) ) {
544 return;
545 }
546
547 $markup = array();
548 $markup['@type'] = 'BreadcrumbList';
549 $markup['itemListElement'] = array();
550
551 foreach ( $crumbs as $key => $crumb ) {
552 $markup['itemListElement'][ $key ] = array(
553 '@type' => 'ListItem',
554 'position' => $key + 1,
555 'item' => array(
556 'name' => $crumb[0],
557 ),
558 );
559
560 if ( ! empty( $crumb[1] ) ) {
561 $markup['itemListElement'][ $key ]['item'] += array( '@id' => $crumb[1] );
562 } elseif ( isset( $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'] ) ) {
563 $current_url = set_url_scheme( 'http://' . wp_unslash( $_SERVER['HTTP_HOST'] ) . wp_unslash( $_SERVER['REQUEST_URI'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
564
565 $markup['itemListElement'][ $key ]['item'] += array( '@id' => $current_url );
566 }
567 }
568
569 $this->set_data( apply_filters( 'woocommerce_structured_data_breadcrumblist', $markup, $breadcrumbs ) );
570 }
571
572 /**
573 * Generates WebSite structured data.
574 *
575 * Hooked into `woocommerce_before_main_content` action hook.
576 */
577 public function generate_website_data() {
578 $markup = array();
579 $markup['@type'] = 'WebSite';
580 $markup['name'] = get_bloginfo( 'name' );
581 $markup['url'] = home_url();
582 $markup['potentialAction'] = array(
583 '@type' => 'SearchAction',
584 'target' => home_url( '?s={search_term_string}&post_type=product' ),
585 'query-input' => 'required name=search_term_string',
586 );
587
588 $this->set_data( apply_filters( 'woocommerce_structured_data_website', $markup ) );
589 }
590
591 /**
592 * Generates Order structured data.
593 *
594 * Hooked into `woocommerce_email_order_details` action hook.
595 *
596 * @param WP_Order $order Order data.
597 * @param bool $sent_to_admin Send to admin (default: false).
598 * @param bool $plain_text Plain text email (default: false).
599 */
600 public function generate_order_data( $order, $sent_to_admin = false, $plain_text = false ) {
601 if ( $plain_text || ! is_a( $order, 'WC_Order' ) ) {
602 return;
603 }
604
605 $shop_name = get_bloginfo( 'name' );
606 $shop_url = home_url();
607 $order_url = $sent_to_admin ? $order->get_edit_order_url() : $order->get_view_order_url();
608 $order_statuses = array(
609 OrderStatus::PENDING => 'https://schema.org/OrderPaymentDue',
610 OrderStatus::PROCESSING => 'https://schema.org/OrderProcessing',
611 OrderStatus::ON_HOLD => 'https://schema.org/OrderProblem',
612 OrderStatus::COMPLETED => 'https://schema.org/OrderDelivered',
613 OrderStatus::CANCELLED => 'https://schema.org/OrderCancelled',
614 OrderStatus::REFUNDED => 'https://schema.org/OrderReturned',
615 OrderStatus::FAILED => 'https://schema.org/OrderProblem',
616 );
617
618 $markup_offers = array();
619 foreach ( $order->get_items() as $item ) {
620 if ( ! apply_filters( 'woocommerce_order_item_visible', true, $item ) ) {
621 continue;
622 }
623
624 $product = $item->get_product();
625 $product_exists = is_object( $product );
626 $is_visible = $product_exists && $product->is_visible();
627
628 $markup_offers[] = array(
629 '@type' => 'Offer',
630 'price' => $order->get_line_subtotal( $item ),
631 'priceCurrency' => $order->get_currency(),
632 'priceSpecification' => array(
633 'price' => $order->get_line_subtotal( $item ),
634 'priceCurrency' => $order->get_currency(),
635 'eligibleQuantity' => array(
636 '@type' => 'QuantitativeValue',
637 'value' => apply_filters( 'woocommerce_email_order_item_quantity', $item->get_quantity(), $item ),
638 ),
639 ),
640 'itemOffered' => array(
641 '@type' => 'Product',
642 'name' => wp_kses_post( apply_filters( 'woocommerce_order_item_name', $item->get_name(), $item, $is_visible ) ),
643 'sku' => $product_exists ? $product->get_sku() : '',
644 'image' => $product_exists ? wp_get_attachment_image_url( $product->get_image_id() ) : '',
645 'url' => $is_visible ? get_permalink( $product->get_id() ) : get_home_url(),
646 ),
647 'seller' => array(
648 '@type' => 'Organization',
649 'name' => $shop_name,
650 'url' => $shop_url,
651 ),
652 );
653 }
654
655 $markup = array();
656 $markup['@type'] = 'Order';
657 $markup['url'] = $order_url;
658 $markup['orderStatus'] = isset( $order_statuses[ $order->get_status() ] ) ? $order_statuses[ $order->get_status() ] : '';
659 $markup['orderNumber'] = $order->get_order_number();
660 $markup['orderDate'] = $order->get_date_created()->format( 'c' );
661 $markup['acceptedOffer'] = $markup_offers;
662 $markup['discount'] = $order->get_total_discount();
663 $markup['discountCurrency'] = $order->get_currency();
664 $markup['price'] = $order->get_total();
665 $markup['priceCurrency'] = $order->get_currency();
666 $markup['priceSpecification'] = array(
667 'price' => $order->get_total(),
668 'priceCurrency' => $order->get_currency(),
669 );
670 if ( wc_tax_enabled() ) {
671 $markup['priceSpecification']['valueAddedTaxIncluded'] = wc_prices_include_tax();
672 }
673 $markup['billingAddress'] = array(
674 '@type' => 'PostalAddress',
675 'name' => $order->get_formatted_billing_full_name(),
676 'streetAddress' => $order->get_billing_address_1(),
677 'postalCode' => $order->get_billing_postcode(),
678 'addressLocality' => $order->get_billing_city(),
679 'addressRegion' => $order->get_billing_state(),
680 'addressCountry' => $order->get_billing_country(),
681 'email' => $order->get_billing_email(),
682 'telephone' => $order->get_billing_phone(),
683 );
684 $markup['customer'] = array(
685 '@type' => 'Person',
686 'name' => $order->get_formatted_billing_full_name(),
687 );
688 $markup['merchant'] = array(
689 '@type' => 'Organization',
690 'name' => $shop_name,
691 'url' => $shop_url,
692 );
693 $markup['potentialAction'] = array(
694 '@type' => 'ViewAction',
695 'name' => 'View Order',
696 'url' => $order_url,
697 'target' => $order_url,
698 );
699
700 $this->set_data( apply_filters( 'woocommerce_structured_data_order', $markup, $sent_to_admin, $order ), true );
701 }
702
703 /**
704 * Check if a GTIN is valid.
705 * A valid GTIN is a string containing 8,12,13 or 14 digits.
706 *
707 * @see https://schema.org/gtin
708 * @param string $gtin The GTIN to check.
709 * @return bool True if valid. False otherwise.
710 */
711 public function is_valid_gtin( $gtin ) {
712 return is_string( $gtin ) && preg_match( '/^(\d{8}|\d{12,14})$/', $gtin );
713 }
714
715 /**
716 * Prepare a GTIN input removing everything except numbers.
717 *
718 * @param string $gtin The GTIN to prepare.
719 * @return string Empty string if no GTIN is provided or the string with the replacements.
720 */
721 public function prepare_gtin( $gtin ) {
722 if ( ! $gtin || ! is_string( $gtin ) ) {
723 return '';
724 }
725
726 return preg_replace( '/[^0-9]/', '', $gtin );
727 }
728 }
729