PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.0.5
WooCommerce v10.0.5
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 11 months ago admin 10 months ago blocks 1 year ago cli 11 months ago customizer 11 months ago data-stores 11 months ago emails 11 months ago export 1 year ago gateways 11 months ago import 11 months ago integrations 2 years ago interfaces 1 year ago legacy 1 year ago libraries 1 year ago log-handlers 1 year ago payment-tokens 5 years ago product-usage 1 year ago queue 4 years ago react-admin 11 months ago rest-api 10 months ago shipping 1 year ago shortcodes 11 months ago theme-support 2 years ago tracks 1 year ago traits 5 years ago walkers 5 years ago wccom-site 1 year ago widgets 1 year ago class-wc-ajax.php 11 months ago class-wc-auth.php 1 year ago class-wc-autoloader.php 1 year ago class-wc-background-emailer.php 5 years 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 10 months ago class-wc-breadcrumb.php 5 years ago class-wc-cache-helper.php 1 year ago class-wc-cart-fees.php 2 years ago class-wc-cart-session.php 11 months ago class-wc-cart-totals.php 10 months ago class-wc-cart.php 11 months ago class-wc-checkout.php 1 year ago class-wc-cli.php 1 year ago class-wc-comments.php 11 months ago class-wc-countries.php 1 year ago class-wc-coupon.php 11 months ago class-wc-customer-download-log.php 5 years ago class-wc-customer-download.php 1 year ago class-wc-customer.php 11 months 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 years ago class-wc-discounts.php 11 months ago class-wc-download-handler.php 1 year ago class-wc-emails.php 11 months ago class-wc-embed.php 1 year ago class-wc-form-handler.php 11 months ago class-wc-frontend-scripts.php 1 year ago class-wc-geo-ip.php 11 months ago class-wc-geolite-integration.php 6 years ago class-wc-geolocation.php 1 year ago class-wc-https.php 2 years ago class-wc-install.php 11 months ago class-wc-integrations.php 5 years ago class-wc-log-levels.php 2 years ago class-wc-logger.php 1 year ago class-wc-meta-data.php 4 years ago class-wc-order-factory.php 2 years ago class-wc-order-item-coupon.php 4 years ago class-wc-order-item-fee.php 1 year ago class-wc-order-item-meta.php 4 years ago class-wc-order-item-product.php 1 year ago class-wc-order-item-shipping.php 1 year ago class-wc-order-item-tax.php 4 years ago class-wc-order-item.php 1 year ago class-wc-order-query.php 4 years ago class-wc-order-refund.php 1 year ago class-wc-order.php 11 months ago class-wc-payment-gateways.php 11 months ago class-wc-payment-tokens.php 3 years ago class-wc-post-data.php 1 year ago class-wc-post-types.php 1 year ago class-wc-privacy-background-process.php 1 year ago class-wc-privacy-erasers.php 1 year ago class-wc-privacy-exporters.php 4 years ago class-wc-privacy.php 11 months ago class-wc-product-attribute.php 4 years ago class-wc-product-download.php 2 years ago class-wc-product-external.php 1 year ago class-wc-product-factory.php 1 year ago class-wc-product-grouped.php 1 year ago class-wc-product-query.php 1 year ago class-wc-product-simple.php 1 year ago class-wc-product-variable.php 1 year ago class-wc-product-variation.php 1 year ago class-wc-query.php 1 year ago class-wc-rate-limiter.php 4 years ago class-wc-regenerate-images-request.php 3 years ago class-wc-regenerate-images.php 1 year ago class-wc-register-wp-admin-settings.php 4 years ago class-wc-rest-authentication.php 1 year ago class-wc-rest-exception.php 5 years ago class-wc-session-handler.php 10 months ago class-wc-shipping-rate.php 11 months ago class-wc-shipping-zone.php 5 years ago class-wc-shipping-zones.php 5 years ago class-wc-shipping.php 1 year ago class-wc-shortcodes.php 1 year ago class-wc-structured-data.php 1 year ago class-wc-tax.php 2 years ago class-wc-template-loader.php 11 months ago class-wc-tracker.php 11 months ago class-wc-validation.php 2 years ago class-wc-webhook.php 1 year ago class-woocommerce.php 5 months ago wc-account-functions.php 11 months ago wc-attribute-functions.php 1 year ago wc-brands-functions.php 1 year ago wc-cart-functions.php 11 months ago wc-conditional-functions.php 11 months ago wc-core-functions.php 11 months ago wc-coupon-functions.php 1 year ago wc-deprecated-functions.php 1 year ago wc-formatting-functions.php 11 months ago wc-notice-functions.php 11 months ago wc-order-functions.php 11 months ago wc-order-item-functions.php 3 years ago wc-order-step-logger-functions.php 1 year ago wc-page-functions.php 1 year ago wc-product-functions.php 11 months ago wc-rest-functions.php 11 months ago wc-stock-functions.php 1 year ago wc-template-functions.php 11 months ago wc-template-hooks.php 1 year ago wc-term-functions.php 11 months ago wc-update-functions.php 11 months ago wc-user-functions.php 11 months ago wc-webhook-functions.php 1 year ago wc-widget-functions.php 5 years ago
class-wc-structured-data.php
711 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 ), 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', false );
238 $highest = $product->get_variation_price( 'max', false );
239
240 if ( $lowest === $highest ) {
241 $markup_offer = array(
242 '@type' => 'Offer',
243 'priceSpecification' => array(
244 array(
245 '@type' => 'UnitPriceSpecification',
246 'price' => wc_format_decimal( $lowest, wc_get_price_decimals() ),
247 'priceCurrency' => $currency,
248 'valueAddedTaxIncluded' => wc_prices_include_tax(),
249 'validThrough' => $price_valid_until,
250 ),
251 ),
252 );
253 } else {
254 $markup_offer = array(
255 '@type' => 'AggregateOffer',
256 'lowPrice' => wc_format_decimal( $lowest, wc_get_price_decimals() ),
257 'highPrice' => wc_format_decimal( $highest, wc_get_price_decimals() ),
258 'offerCount' => count( $product->get_children() ),
259 );
260
261 if ( $product->is_on_sale() ) {
262 $children = array_map( 'wc_get_product', $product->get_children() );
263 $lowest_child_sale_price = $highest;
264
265 foreach ( $children as $child ) {
266 $child_sale_price = $child->get_sale_price();
267
268 if ( empty( $child_sale_price ) || (int) $child_sale_price > (int) $lowest_child_sale_price ) {
269 continue;
270 }
271
272 $lowest_child_sale_price = $child_sale_price;
273 $date_on_sale_to = $child->get_date_on_sale_to();
274 $sale_price_valid_until = $date_on_sale_to
275 ? gmdate( 'Y-m-d', $date_on_sale_to->getTimestamp() )
276 : null;
277 }
278
279 $markup_offer['priceSpecification'] = array(
280 array(
281 '@type' => 'UnitPriceSpecification',
282 'priceType' => 'https://schema.org/SalePrice',
283 'price' => wc_format_decimal( $lowest_child_sale_price, wc_get_price_decimals() ),
284 'priceCurrency' => $currency,
285 'valueAddedTaxIncluded' => wc_prices_include_tax(),
286 'validThrough' => $sale_price_valid_until ?? $price_valid_until,
287 ),
288 );
289 }
290 }
291 } elseif ( $product->is_type( ProductType::GROUPED ) ) {
292 $tax_display_mode = get_option( 'woocommerce_tax_display_shop' );
293 $children = array_filter( array_map( 'wc_get_product', $product->get_children() ), 'wc_products_array_filter_visible_grouped' );
294 $price_function = 'incl' === $tax_display_mode ? 'wc_get_price_including_tax' : 'wc_get_price_excluding_tax';
295
296 foreach ( $children as $child ) {
297 if ( '' !== $child->get_regular_price() ) {
298 $child_prices[] = $price_function( $child, array( 'price' => $child->get_regular_price() ) );
299 }
300 if ( '' !== $child->get_sale_price() ) {
301 $child_sale_prices[] = $price_function( $child, array( 'price' => $child->get_sale_price() ) );
302 }
303 }
304 if ( empty( $child_prices ) ) {
305 $min_price = 0;
306 } else {
307 $min_price = min( $child_prices );
308 }
309 if ( empty( $child_sale_prices ) ) {
310 $min_sale_price = 0;
311 } else {
312 $min_sale_price = min( $child_sale_prices );
313 }
314
315 $unit_price_specification = array(
316 '@type' => 'UnitPriceSpecification',
317 'price' => wc_format_decimal( $min_price, wc_get_price_decimals() ),
318 'priceCurrency' => $currency,
319 'valueAddedTaxIncluded' => wc_prices_include_tax(),
320 'validThrough' => $price_valid_until,
321 );
322 if ( $product->is_on_sale() && $min_price !== $min_sale_price ) {
323 // `priceType` should only be specified in prices which are not the current offer.
324 // https://developers.google.com/search/docs/appearance/structured-data/merchant-listing#sale-pricing-example
325 $unit_price_specification['priceType'] = 'https://schema.org/ListPrice';
326 }
327 $markup_offer = array(
328 '@type' => 'Offer',
329 'priceSpecification' => array(
330 $unit_price_specification,
331 ),
332 );
333
334 if ( $product->is_on_sale() && $min_price !== $min_sale_price ) {
335 if ( $product->get_date_on_sale_to() ) {
336 $sale_price_valid_until = gmdate( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() );
337 }
338
339 // We add the sale price to the top of the array so it's the first offer.
340 // See https://github.com/woocommerce/woocommerce/issues/55043.
341 array_unshift(
342 $markup_offer['priceSpecification'],
343 array(
344 '@type' => 'UnitPriceSpecification',
345 'price' => wc_format_decimal( $min_sale_price, wc_get_price_decimals() ),
346 'priceCurrency' => $currency,
347 'valueAddedTaxIncluded' => wc_prices_include_tax(),
348 'validThrough' => $sale_price_valid_until ?? $price_valid_until,
349 )
350 );
351 }
352 } else {
353 $unit_price_specification = array(
354 '@type' => 'UnitPriceSpecification',
355 'price' => wc_format_decimal( $product->get_regular_price(), wc_get_price_decimals() ),
356 'priceCurrency' => $currency,
357 'valueAddedTaxIncluded' => wc_prices_include_tax(),
358 'validThrough' => $price_valid_until,
359 );
360 if ( $product->is_on_sale() ) {
361 // `priceType` should only be specified in prices which are not the current offer.
362 // https://developers.google.com/search/docs/appearance/structured-data/merchant-listing#sale-pricing-example
363 $unit_price_specification['priceType'] = 'https://schema.org/ListPrice';
364 }
365 $markup_offer = array(
366 '@type' => 'Offer',
367 'priceSpecification' => array(
368 $unit_price_specification,
369 ),
370 );
371
372 if ( $product->is_on_sale() ) {
373 if ( $product->get_date_on_sale_to() ) {
374 $sale_price_valid_until = gmdate( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() );
375 }
376
377 // We add the sale price to the top of the array so it's the first offer.
378 // See https://github.com/woocommerce/woocommerce/issues/55043.
379 array_unshift(
380 $markup_offer['priceSpecification'],
381 array(
382 '@type' => 'UnitPriceSpecification',
383 'price' => wc_format_decimal( $product->get_sale_price(), wc_get_price_decimals() ),
384 'priceCurrency' => $currency,
385 'valueAddedTaxIncluded' => wc_prices_include_tax(),
386 'validThrough' => $sale_price_valid_until ?? $price_valid_until,
387 )
388 );
389 }
390 }
391
392 if ( $product->is_in_stock() ) {
393 $stock_status_schema = ( ProductStockStatus::ON_BACKORDER === $product->get_stock_status() ) ? 'BackOrder' : 'InStock';
394 } else {
395 $stock_status_schema = 'OutOfStock';
396 }
397
398 $markup_offer += array(
399 'priceValidUntil' => $sale_price_valid_until ?? $price_valid_until,
400 'availability' => 'http://schema.org/' . $stock_status_schema,
401 'url' => $permalink,
402 'seller' => array(
403 '@type' => 'Organization',
404 'name' => $shop_name,
405 'url' => $shop_url,
406 ),
407 );
408 if (
409 ( ! empty( $markup_offer['price'] ) ||
410 ! empty( $markup_offer['lowPrice'] ) ||
411 ! empty( $markup_offer['highPrice'] )
412 ) && empty( $markup_offer['priceCurrency'] )
413 ) {
414 $markup_offer['priceCurrency'] = $currency;
415 }
416
417 $markup['offers'] = array( apply_filters( 'woocommerce_structured_data_product_offer', $markup_offer, $product ) );
418 }
419
420 if ( $product->get_rating_count() && wc_review_ratings_enabled() ) {
421 $markup['aggregateRating'] = array(
422 '@type' => 'AggregateRating',
423 'ratingValue' => $product->get_average_rating(),
424 'reviewCount' => $product->get_review_count(),
425 );
426
427 // Markup 5 most recent rating/review.
428 $comments = get_comments(
429 array(
430 'number' => 5,
431 'post_id' => $product->get_id(),
432 'status' => 'approve',
433 'post_status' => 'publish',
434 'post_type' => 'product',
435 'parent' => 0,
436 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
437 array(
438 'key' => 'rating',
439 'type' => 'NUMERIC',
440 'compare' => '>',
441 'value' => 0,
442 ),
443 ),
444 )
445 );
446
447 if ( $comments ) {
448 $markup['review'] = array();
449 foreach ( $comments as $comment ) {
450 $markup['review'][] = array(
451 '@type' => 'Review',
452 'reviewRating' => array(
453 '@type' => 'Rating',
454 'bestRating' => '5',
455 'ratingValue' => get_comment_meta( $comment->comment_ID, 'rating', true ),
456 'worstRating' => '1',
457 ),
458 'author' => array(
459 '@type' => 'Person',
460 'name' => get_comment_author( $comment ),
461 ),
462 'reviewBody' => get_comment_text( $comment ),
463 'datePublished' => get_comment_date( 'c', $comment ),
464 );
465 }
466 }
467 }
468
469 // Check we have required data.
470 if ( empty( $markup['aggregateRating'] ) && empty( $markup['offers'] ) && empty( $markup['review'] ) ) {
471 return;
472 }
473
474 $this->set_data( apply_filters( 'woocommerce_structured_data_product', $markup, $product ) );
475 }
476
477 /**
478 * Generates Review structured data.
479 *
480 * Hooked into `woocommerce_review_meta` action hook.
481 *
482 * @param WP_Comment $comment Comment data.
483 */
484 public function generate_review_data( $comment ) {
485 $markup = array();
486 $markup['@type'] = 'Review';
487 $markup['@id'] = get_comment_link( $comment->comment_ID );
488 $markup['datePublished'] = get_comment_date( 'c', $comment->comment_ID );
489 $markup['description'] = get_comment_text( $comment->comment_ID );
490 $markup['itemReviewed'] = array(
491 '@type' => 'Product',
492 'name' => get_the_title( $comment->comment_post_ID ),
493 );
494
495 // Skip replies unless they have a rating.
496 $rating = get_comment_meta( $comment->comment_ID, 'rating', true );
497
498 if ( $rating ) {
499 $markup['reviewRating'] = array(
500 '@type' => 'Rating',
501 'bestRating' => '5',
502 'ratingValue' => $rating,
503 'worstRating' => '1',
504 );
505 } elseif ( $comment->comment_parent ) {
506 return;
507 }
508
509 $markup['author'] = array(
510 '@type' => 'Person',
511 'name' => get_comment_author( $comment->comment_ID ),
512 );
513
514 $this->set_data( apply_filters( 'woocommerce_structured_data_review', $markup, $comment ) );
515 }
516
517 /**
518 * Generates BreadcrumbList structured data.
519 *
520 * Hooked into `woocommerce_breadcrumb` action hook.
521 *
522 * @param WC_Breadcrumb $breadcrumbs Breadcrumb data.
523 */
524 public function generate_breadcrumblist_data( $breadcrumbs ) {
525 $crumbs = $breadcrumbs->get_breadcrumb();
526
527 if ( empty( $crumbs ) || ! is_array( $crumbs ) ) {
528 return;
529 }
530
531 $markup = array();
532 $markup['@type'] = 'BreadcrumbList';
533 $markup['itemListElement'] = array();
534
535 foreach ( $crumbs as $key => $crumb ) {
536 $markup['itemListElement'][ $key ] = array(
537 '@type' => 'ListItem',
538 'position' => $key + 1,
539 'item' => array(
540 'name' => $crumb[0],
541 ),
542 );
543
544 if ( ! empty( $crumb[1] ) ) {
545 $markup['itemListElement'][ $key ]['item'] += array( '@id' => $crumb[1] );
546 } elseif ( isset( $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'] ) ) {
547 $current_url = set_url_scheme( 'http://' . wp_unslash( $_SERVER['HTTP_HOST'] ) . wp_unslash( $_SERVER['REQUEST_URI'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
548
549 $markup['itemListElement'][ $key ]['item'] += array( '@id' => $current_url );
550 }
551 }
552
553 $this->set_data( apply_filters( 'woocommerce_structured_data_breadcrumblist', $markup, $breadcrumbs ) );
554 }
555
556 /**
557 * Generates WebSite structured data.
558 *
559 * Hooked into `woocommerce_before_main_content` action hook.
560 */
561 public function generate_website_data() {
562 $markup = array();
563 $markup['@type'] = 'WebSite';
564 $markup['name'] = get_bloginfo( 'name' );
565 $markup['url'] = home_url();
566 $markup['potentialAction'] = array(
567 '@type' => 'SearchAction',
568 'target' => home_url( '?s={search_term_string}&post_type=product' ),
569 'query-input' => 'required name=search_term_string',
570 );
571
572 $this->set_data( apply_filters( 'woocommerce_structured_data_website', $markup ) );
573 }
574
575 /**
576 * Generates Order structured data.
577 *
578 * Hooked into `woocommerce_email_order_details` action hook.
579 *
580 * @param WP_Order $order Order data.
581 * @param bool $sent_to_admin Send to admin (default: false).
582 * @param bool $plain_text Plain text email (default: false).
583 */
584 public function generate_order_data( $order, $sent_to_admin = false, $plain_text = false ) {
585 if ( $plain_text || ! is_a( $order, 'WC_Order' ) ) {
586 return;
587 }
588
589 $shop_name = get_bloginfo( 'name' );
590 $shop_url = home_url();
591 $order_url = $sent_to_admin ? $order->get_edit_order_url() : $order->get_view_order_url();
592 $order_statuses = array(
593 OrderStatus::PENDING => 'https://schema.org/OrderPaymentDue',
594 OrderStatus::PROCESSING => 'https://schema.org/OrderProcessing',
595 OrderStatus::ON_HOLD => 'https://schema.org/OrderProblem',
596 OrderStatus::COMPLETED => 'https://schema.org/OrderDelivered',
597 OrderStatus::CANCELLED => 'https://schema.org/OrderCancelled',
598 OrderStatus::REFUNDED => 'https://schema.org/OrderReturned',
599 OrderStatus::FAILED => 'https://schema.org/OrderProblem',
600 );
601
602 $markup_offers = array();
603 foreach ( $order->get_items() as $item ) {
604 if ( ! apply_filters( 'woocommerce_order_item_visible', true, $item ) ) {
605 continue;
606 }
607
608 $product = $item->get_product();
609 $product_exists = is_object( $product );
610 $is_visible = $product_exists && $product->is_visible();
611
612 $markup_offers[] = array(
613 '@type' => 'Offer',
614 'price' => $order->get_line_subtotal( $item ),
615 'priceCurrency' => $order->get_currency(),
616 'priceSpecification' => array(
617 'price' => $order->get_line_subtotal( $item ),
618 'priceCurrency' => $order->get_currency(),
619 'eligibleQuantity' => array(
620 '@type' => 'QuantitativeValue',
621 'value' => apply_filters( 'woocommerce_email_order_item_quantity', $item->get_quantity(), $item ),
622 ),
623 ),
624 'itemOffered' => array(
625 '@type' => 'Product',
626 'name' => wp_kses_post( apply_filters( 'woocommerce_order_item_name', $item->get_name(), $item, $is_visible ) ),
627 'sku' => $product_exists ? $product->get_sku() : '',
628 'image' => $product_exists ? wp_get_attachment_image_url( $product->get_image_id() ) : '',
629 'url' => $is_visible ? get_permalink( $product->get_id() ) : get_home_url(),
630 ),
631 'seller' => array(
632 '@type' => 'Organization',
633 'name' => $shop_name,
634 'url' => $shop_url,
635 ),
636 );
637 }
638
639 $markup = array();
640 $markup['@type'] = 'Order';
641 $markup['url'] = $order_url;
642 $markup['orderStatus'] = isset( $order_statuses[ $order->get_status() ] ) ? $order_statuses[ $order->get_status() ] : '';
643 $markup['orderNumber'] = $order->get_order_number();
644 $markup['orderDate'] = $order->get_date_created()->format( 'c' );
645 $markup['acceptedOffer'] = $markup_offers;
646 $markup['discount'] = $order->get_total_discount();
647 $markup['discountCurrency'] = $order->get_currency();
648 $markup['price'] = $order->get_total();
649 $markup['priceCurrency'] = $order->get_currency();
650 $markup['priceSpecification'] = array(
651 'price' => $order->get_total(),
652 'priceCurrency' => $order->get_currency(),
653 'valueAddedTaxIncluded' => 'true',
654 );
655 $markup['billingAddress'] = array(
656 '@type' => 'PostalAddress',
657 'name' => $order->get_formatted_billing_full_name(),
658 'streetAddress' => $order->get_billing_address_1(),
659 'postalCode' => $order->get_billing_postcode(),
660 'addressLocality' => $order->get_billing_city(),
661 'addressRegion' => $order->get_billing_state(),
662 'addressCountry' => $order->get_billing_country(),
663 'email' => $order->get_billing_email(),
664 'telephone' => $order->get_billing_phone(),
665 );
666 $markup['customer'] = array(
667 '@type' => 'Person',
668 'name' => $order->get_formatted_billing_full_name(),
669 );
670 $markup['merchant'] = array(
671 '@type' => 'Organization',
672 'name' => $shop_name,
673 'url' => $shop_url,
674 );
675 $markup['potentialAction'] = array(
676 '@type' => 'ViewAction',
677 'name' => 'View Order',
678 'url' => $order_url,
679 'target' => $order_url,
680 );
681
682 $this->set_data( apply_filters( 'woocommerce_structured_data_order', $markup, $sent_to_admin, $order ), true );
683 }
684
685 /**
686 * Check if a GTIN is valid.
687 * A valid GTIN is a string containing 8,12,13 or 14 digits.
688 *
689 * @see https://schema.org/gtin
690 * @param string $gtin The GTIN to check.
691 * @return bool True if valid. False otherwise.
692 */
693 public function is_valid_gtin( $gtin ) {
694 return is_string( $gtin ) && preg_match( '/^(\d{8}|\d{12,14})$/', $gtin );
695 }
696
697 /**
698 * Prepare a GTIN input removing everything except numbers.
699 *
700 * @param string $gtin The GTIN to prepare.
701 * @return string Empty string if no GTIN is provided or the string with the replacements.
702 */
703 public function prepare_gtin( $gtin ) {
704 if ( ! $gtin || ! is_string( $gtin ) ) {
705 return '';
706 }
707
708 return preg_replace( '/[^0-9]/', '', $gtin );
709 }
710 }
711