PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 11.0.0-beta.1
WooCommerce v11.0.0-beta.1
11.0.0-beta.1 10.9.4 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 / src / Internal / Admin / SiteHealth.php
woocommerce / src / Internal / Admin Last commit date
Agentic 4 months ago EmailImprovements 1 year ago EmailPreview 4 days ago Emails 1 year ago ImportExport 1 year ago Logging 4 days ago Marketing 2 years ago Notes 1 month ago Onboarding 5 months ago Orders 4 days ago ProductForm 2 years ago ProductReviews 1 month ago RemoteFreeExtensions 1 month ago Schedulers 4 days ago Settings 4 days ago Suggestions 1 month ago WCPayPromotion 11 months ago ActivityPanels.php 3 years ago Analytics.php 1 month ago CategoryLookup.php 4 years ago Coupons.php 1 year ago CouponsMovedTrait.php 5 months ago CustomerEffortScoreTracks.php 7 months ago Events.php 2 months ago FeaturePlugin.php 4 months ago Homescreen.php 2 months ago Loader.php 4 days ago Marketing.php 1 year ago Marketplace.php 5 months ago MobileAppBanner.php 4 years ago OrderMilestoneEasterEgg.php 1 month ago RemoteInboxNotifications.php 3 years ago Settings.php 4 days ago ShippingLabelBanner.php 1 year ago ShippingLabelBannerDisplayRules.php 1 year ago SiteHealth.php 4 days ago Survey.php 4 years ago SystemStatusReport.php 1 year ago TaxSettingsRecommendations.php 4 days ago Translations.php 1 year ago WCAdminAssets.php 4 days ago WCAdminSharedSettings.php 1 year ago WCAdminUser.php 9 months ago WcPayWelcomePage.php 1 year ago
SiteHealth.php
721 lines
1 <?php
2 /**
3 * Customize Site Health recommendations for WooCommerce.
4 */
5
6 declare( strict_types = 1 );
7
8 namespace Automattic\WooCommerce\Internal\Admin;
9
10 use Automattic\WooCommerce\Enums\DefaultCustomerAddress;
11 use Automattic\WooCommerce\Enums\ProductStatus;
12 use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
13 use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
14 use Automattic\WooCommerce\Utilities\OrderUtil;
15 use WC_Admin_Notices;
16 use WC_Admin_Status;
17 use WC_Helper_Updater;
18 use WC_Install;
19 use WC_Order_Query;
20 use WC_Product_Query;
21
22 defined( 'ABSPATH' ) || exit;
23
24 /**
25 * SiteHealth class.
26 */
27 class SiteHealth {
28 /**
29 * Class instance.
30 *
31 * @var SiteHealth instance
32 */
33 protected static $instance = null;
34
35 /**
36 * Get class instance.
37 */
38 public static function get_instance() {
39 if ( ! self::$instance ) {
40 self::$instance = new self();
41 }
42 return self::$instance;
43 }
44
45 /**
46 * Hook into WooCommerce.
47 */
48 public function __construct() {
49 add_filter( 'site_status_should_suggest_persistent_object_cache', array( $this, 'should_suggest_persistent_object_cache' ) );
50 add_filter( 'site_status_tests', array( $this, 'handle_site_status_tests' ) );
51 }
52
53 /**
54 * Register WooCommerce Site Health status tests.
55 *
56 * @internal
57 *
58 * @param array $tests Site Health tests.
59 * @return array
60 */
61 public function handle_site_status_tests( $tests ) {
62 $tests['direct'] = $tests['direct'] ?? array();
63
64 foreach ( $this->get_woocommerce_site_health_tests() as $test_id => $test ) {
65 $tests['direct'][ $test_id ] = array(
66 'label' => $test['label'],
67 'test' => function () use ( $test_id ) {
68 return $this->run_test( $test_id );
69 },
70 'skip_cron' => true,
71 );
72 }
73
74 return $tests;
75 }
76
77 /**
78 * Run a registered WooCommerce Site Health test by ID.
79 *
80 * @param string $test_id Site Health test ID.
81 * @return array
82 */
83 public function run_test( string $test_id ): array {
84 $tests = $this->get_woocommerce_site_health_tests();
85
86 if ( ! isset( $tests[ $test_id ] ) ) {
87 return array();
88 }
89
90 $test = $tests[ $test_id ];
91 $check_result = ( $test['check'] )();
92
93 if ( is_array( $check_result ) ) {
94 $context = $check_result;
95 $is_good = empty( $check_result );
96 } else {
97 $context = null;
98 $is_good = (bool) $check_result;
99 }
100
101 $data = $is_good ? $test['good'] : $test['fail'];
102 $label = is_callable( $data['label'] )
103 ? ( $data['label'] )( $context )
104 : $data['label'];
105 $status = $is_good ? 'good' : ( $data['status'] ?? 'recommended' );
106 $status = is_callable( $status )
107 ? $status( $context )
108 : $status;
109 $description = is_callable( $data['description'] )
110 ? ( $data['description'] )( $context )
111 : $data['description'];
112
113 $actions_html = '';
114 if ( ! $is_good && ! empty( $data['actions'] ) ) {
115 foreach ( $data['actions'] as $action ) {
116 $url = is_callable( $action['url'] ) ? ( $action['url'] )( $context ) : $action['url'];
117 $actions_html .= $this->get_site_health_action( $url, $action['label'], ! empty( $action['new_tab'] ) );
118 }
119 }
120
121 return array(
122 'label' => $label,
123 'status' => $status,
124 'badge' => array(
125 'label' => 'security' === $test['badge'] ? __( 'Security', 'woocommerce' ) : __( 'Performance', 'woocommerce' ),
126 'color' => 'blue',
127 ),
128 'description' => '<p>' . esc_html( $description ) . '</p>',
129 'actions' => $actions_html,
130 'test' => $test_id,
131 );
132 }
133
134 /**
135 * Get the registry of WooCommerce Site Health tests.
136 *
137 * Each entry describes one test:
138 * - label: visible test name in Site Health.
139 * - badge: 'security' or 'performance'.
140 * - check: callable returning bool (true = good) or array (empty = good, otherwise context for description/actions).
141 * - good: result data when the check passes (label, description).
142 * - fail: result data when the check fails (status defaults to 'recommended', label, description, optional actions).
143 * Label, status, and description may be callables that receive the check context.
144 *
145 * @return array<string, array>
146 */
147 protected function get_woocommerce_site_health_tests(): array {
148 return array(
149 'woocommerce_secure_connection' => array(
150 'label' => __( 'WooCommerce secure connection', 'woocommerce' ),
151 'badge' => 'security',
152 'check' => array( $this, 'is_store_using_secure_connection' ),
153 'good' => array(
154 'label' => __( 'WooCommerce store uses a secure connection', 'woocommerce' ),
155 'description' => __( 'Customer data is protected by HTTPS on your store pages.', 'woocommerce' ),
156 ),
157 'fail' => array(
158 'status' => 'critical',
159 'label' => __( 'WooCommerce store is not using a secure connection', 'woocommerce' ),
160 'description' => __( 'WooCommerce strongly recommends serving your entire store over HTTPS to help keep customer data secure.', 'woocommerce' ),
161 'actions' => array(
162 array(
163 'url' => 'https://woocommerce.com/document/ssl-and-https/',
164 'label' => __( 'Learn more about HTTPS', 'woocommerce' ),
165 'new_tab' => true,
166 ),
167 ),
168 ),
169 ),
170 'woocommerce_uploads_directory_protection' => array(
171 'label' => __( 'WooCommerce uploads directory protection', 'woocommerce' ),
172 'badge' => 'security',
173 'check' => array( $this, 'is_uploads_directory_protected' ),
174 'good' => array(
175 'label' => __( 'WooCommerce uploads directory is protected', 'woocommerce' ),
176 'description' => __( 'The directory used for downloadable product files is not browsable from the web.', 'woocommerce' ),
177 ),
178 'fail' => array(
179 'status' => function ( ?array $context ) {
180 return ! empty( $context['unverified'] ) ? 'recommended' : 'critical';
181 },
182 'label' => function ( ?array $context ) {
183 return ! empty( $context['unverified'] )
184 ? __( 'WooCommerce could not verify uploads directory protection', 'woocommerce' )
185 : __( 'WooCommerce uploads directory is browsable from the web', 'woocommerce' );
186 },
187 'description' => function ( ?array $context ) {
188 return ! empty( $context['unverified'] )
189 ? __( 'A loopback request to the WooCommerce uploads directory failed, so WooCommerce could not confirm whether directory browsing is disabled. Check your server configuration or try again later.', 'woocommerce' )
190 : __( 'Directory browsing can expose downloadable product files. Configure your web server to prevent directory indexing for the WooCommerce uploads directory.', 'woocommerce' );
191 },
192 'actions' => array(
193 array(
194 'url' => 'https://woocommerce.com/document/digital-downloadable-product-handling/#protecting-your-uploads-directory',
195 'label' => __( 'Learn how to protect downloads', 'woocommerce' ),
196 'new_tab' => true,
197 ),
198 ),
199 ),
200 ),
201 'woocommerce_template_overrides' => array(
202 'label' => __( 'WooCommerce template overrides', 'woocommerce' ),
203 'badge' => 'performance',
204 'check' => fn() => ! $this->has_outdated_template_overrides(),
205 'good' => array(
206 'label' => __( 'WooCommerce template overrides are up to date', 'woocommerce' ),
207 'description' => __( 'The active theme does not contain outdated WooCommerce template overrides.', 'woocommerce' ),
208 ),
209 'fail' => array(
210 'label' => __( 'Your theme contains outdated WooCommerce template overrides', 'woocommerce' ),
211 'description' => __( 'Outdated template overrides may not be compatible with the current version of WooCommerce.', 'woocommerce' ),
212 'actions' => array(
213 array(
214 'url' => admin_url( 'admin.php?page=wc-status#status-table-templates' ),
215 'label' => __( 'View affected templates', 'woocommerce' ),
216 ),
217 array(
218 'url' => 'https://woocommerce.com/document/template-structure/',
219 'label' => __( 'Learn more about templates', 'woocommerce' ),
220 'new_tab' => true,
221 ),
222 ),
223 ),
224 ),
225 'woocommerce_maxmind_geolocation' => array(
226 'label' => __( 'WooCommerce MaxMind geolocation', 'woocommerce' ),
227 'badge' => 'performance',
228 'check' => fn() => ! $this->needs_maxmind_license_key(),
229 'good' => array(
230 'label' => __( 'WooCommerce geolocation is configured', 'woocommerce' ),
231 'description' => __( 'Your store does not require further MaxMind geolocation configuration.', 'woocommerce' ),
232 ),
233 'fail' => array(
234 'label' => __( 'WooCommerce geolocation needs a MaxMind license key', 'woocommerce' ),
235 'description' => __( 'A MaxMind license key is required when the default customer location uses geolocation.', 'woocommerce' ),
236 'actions' => array(
237 array(
238 'url' => admin_url( 'admin.php?page=wc-settings&tab=integration&section=maxmind_geolocation' ),
239 'label' => __( 'Configure MaxMind geolocation', 'woocommerce' ),
240 ),
241 array(
242 'url' => admin_url( 'admin.php?page=wc-settings&tab=general' ),
243 'label' => __( 'Change default customer location', 'woocommerce' ),
244 ),
245 ),
246 ),
247 ),
248 'woocommerce_download_method' => array(
249 'label' => __( 'WooCommerce download method', 'woocommerce' ),
250 'badge' => 'security',
251 'check' => fn() => 'redirect' !== get_option( 'woocommerce_file_download_method' ),
252 'good' => array(
253 'label' => __( 'WooCommerce uses a supported download method', 'woocommerce' ),
254 'description' => __( 'Your store is not configured to use the deprecated Redirect only download method.', 'woocommerce' ),
255 ),
256 'fail' => array(
257 'label' => __( 'WooCommerce is using the deprecated Redirect only download method', 'woocommerce' ),
258 'description' => __( 'Redirect only is deprecated for downloadable products. Choose a different download method, or allow redirects only as a fallback.', 'woocommerce' ),
259 'actions' => array(
260 array(
261 'url' => admin_url( 'admin.php?page=wc-settings&tab=products&section=downloadable' ),
262 'label' => __( 'Review download settings', 'woocommerce' ),
263 ),
264 ),
265 ),
266 ),
267 'woocommerce_database_tables' => array(
268 'label' => __( 'WooCommerce database tables', 'woocommerce' ),
269 'badge' => 'performance',
270 'check' => fn() => WC_Install::get_missing_base_tables(),
271 'good' => array(
272 'label' => __( 'WooCommerce database tables are present', 'woocommerce' ),
273 'description' => __( 'All required WooCommerce database tables exist.', 'woocommerce' ),
274 ),
275 'fail' => array(
276 'status' => 'critical',
277 'label' => __( 'WooCommerce database tables are missing', 'woocommerce' ),
278 'description' => fn( array $missing_tables ) => sprintf(
279 /* translators: %s: Comma-separated list of missing database tables. */
280 __( 'One or more tables required for WooCommerce to function are missing: %s.', 'woocommerce' ),
281 implode( ', ', $missing_tables )
282 ),
283 'actions' => array(
284 array(
285 'url' => wp_nonce_url( admin_url( 'admin.php?page=wc-status&tab=tools&action=verify_db_tables' ), 'debug_action' ),
286 'label' => __( 'Check database tables again', 'woocommerce' ),
287 ),
288 ),
289 ),
290 ),
291 'woocommerce_hpos_sync_on_read' => array(
292 'label' => __( 'WooCommerce HPOS sync on read', 'woocommerce' ),
293 'badge' => 'performance',
294 'check' => fn() => ! $this->should_show_hpos_sync_on_read_status(),
295 'good' => array(
296 'label' => __( 'WooCommerce HPOS sync on read does not require attention', 'woocommerce' ),
297 'description' => __( 'Your current order storage configuration is not affected by the HPOS sync-on-read change.', 'woocommerce' ),
298 ),
299 'fail' => array(
300 'label' => __( 'WooCommerce HPOS sync on read is disabled', 'woocommerce' ),
301 'description' => __( 'Compatibility mode for HPOS no longer pulls order changes made to the posts database back into orders automatically. Review this if custom code modifies orders outside WooCommerce.', 'woocommerce' ),
302 'actions' => array(
303 array(
304 'url' => admin_url( 'admin.php?page=wc-settings&tab=advanced&section=features' ),
305 'label' => __( 'Review order storage settings', 'woocommerce' ),
306 ),
307 array(
308 'url' => 'https://developer.woocommerce.com/2026/02/16/hpos-sync-on-read-to-be-disabled-by-default-in-woocommerce-10-7/',
309 'label' => __( 'Learn more about this change', 'woocommerce' ),
310 'new_tab' => true,
311 ),
312 ),
313 ),
314 ),
315 'woocommerce_legacy_shipping_methods' => array(
316 'label' => __( 'WooCommerce legacy shipping methods', 'woocommerce' ),
317 'badge' => 'performance',
318 'check' => fn() => ! $this->has_legacy_shipping_methods_enabled(),
319 'good' => array(
320 'label' => __( 'WooCommerce legacy shipping methods are not enabled', 'woocommerce' ),
321 'description' => __( 'Your store is not using deprecated legacy shipping methods.', 'woocommerce' ),
322 ),
323 'fail' => array(
324 'label' => __( 'WooCommerce legacy shipping methods are enabled', 'woocommerce' ),
325 'description' => __( 'Legacy shipping methods are deprecated. Set up rates with shipping zones instead.', 'woocommerce' ),
326 'actions' => array(
327 array(
328 'url' => admin_url( 'admin.php?page=wc-settings&tab=shipping' ),
329 'label' => __( 'Review shipping zones', 'woocommerce' ),
330 ),
331 array(
332 'url' => 'https://woocommerce.com/document/setting-up-shipping-zones/',
333 'label' => __( 'Learn more about shipping zones', 'woocommerce' ),
334 'new_tab' => true,
335 ),
336 ),
337 ),
338 ),
339 'woocommerce_shipping_methods' => array(
340 'label' => __( 'WooCommerce shipping methods', 'woocommerce' ),
341 'badge' => 'performance',
342 'check' => fn() => ! $this->needs_shipping_methods(),
343 'good' => array(
344 'label' => __( 'WooCommerce shipping methods are configured', 'woocommerce' ),
345 'description' => __( 'Your store does not need additional shipping method setup.', 'woocommerce' ),
346 ),
347 'fail' => array(
348 'status' => 'critical',
349 'label' => __( 'WooCommerce shipping is enabled but no shipping methods are configured', 'woocommerce' ),
350 'description' => __( 'Customers cannot purchase physical goods until at least one shipping method is available.', 'woocommerce' ),
351 'actions' => array(
352 array(
353 'url' => admin_url( 'admin.php?page=wc-settings&tab=shipping' ),
354 'label' => __( 'Set up shipping methods', 'woocommerce' ),
355 ),
356 array(
357 'url' => 'https://woocommerce.com/document/setting-up-shipping-zones/',
358 'label' => __( 'Learn more about shipping zones', 'woocommerce' ),
359 'new_tab' => true,
360 ),
361 ),
362 ),
363 ),
364 'woocommerce_approved_download_directories_sync' => array(
365 'label' => __( 'WooCommerce approved download directories', 'woocommerce' ),
366 'badge' => 'security',
367 'check' => fn() => ! WC_Admin_Notices::has_notice( 'download_directories_sync_complete' ),
368 'good' => array(
369 'label' => __( 'WooCommerce approved download directories do not require review', 'woocommerce' ),
370 'description' => __( 'There is no completed approved download directories sync waiting for review.', 'woocommerce' ),
371 ),
372 'fail' => array(
373 'label' => __( 'WooCommerce approved download directories need review', 'woocommerce' ),
374 'description' => __( 'The approved product download directories list was updated. Review it to confirm downloadable product files remain protected.', 'woocommerce' ),
375 'actions' => array(
376 array(
377 'url' => admin_url( 'admin.php?page=wc-settings&tab=products&section=download_urls' ),
378 'label' => __( 'Review approved directories', 'woocommerce' ),
379 ),
380 array(
381 'url' => wp_nonce_url(
382 add_query_arg( 'wc-hide-notice', 'download_directories_sync_complete', admin_url( 'site-health.php' ) ),
383 'woocommerce_hide_notices_nonce',
384 '_wc_notice_nonce'
385 ),
386 'label' => __( 'Mark as reviewed', 'woocommerce' ),
387 ),
388 array(
389 'url' => 'https://woocommerce.com/document/approved-download-directories',
390 'label' => __( 'Learn more about approved directories', 'woocommerce' ),
391 'new_tab' => true,
392 ),
393 ),
394 ),
395 ),
396 'woocommerce_com_extension_updates' => array(
397 'label' => __( 'WooCommerce.com plugin updates', 'woocommerce' ),
398 'badge' => 'security',
399 'check' => fn() => ! $this->has_outdated_woocommerce_com_plugins(),
400 'good' => array(
401 'label' => __( 'WooCommerce.com plugin updates do not require attention', 'woocommerce' ),
402 'description' => __( 'Your store can receive WooCommerce.com plugin updates, or no outdated WooCommerce.com plugins were found.', 'woocommerce' ),
403 ),
404 'fail' => array(
405 'label' => __( 'WooCommerce.com plugin updates require attention', 'woocommerce' ),
406 'description' => __( 'Your store might be at risk because it is running old versions of WooCommerce plugins. Connect your store to WooCommerce.com to get updates and streamlined support for your subscriptions.', 'woocommerce' ),
407 'actions' => array(
408 array(
409 'url' => $this->get_woocommerce_com_extensions_url(),
410 'label' => __( 'Connect your store', 'woocommerce' ),
411 ),
412 ),
413 ),
414 ),
415 );
416 }
417
418 /**
419 * Counts specific types of WooCommerce entities to determine if a persistent object cache would be beneficial.
420 *
421 * Note that if all measured WooCommerce entities are below their thresholds, this will return null so that the
422 * other normal WordPress checks will still be run.
423 *
424 * @param true|null $check A non-null value will short-circuit WP's normal tests for this.
425 *
426 * @return true|null True if the store would benefit from a persistent object cache. Otherwise null.
427 */
428 public function should_suggest_persistent_object_cache( $check ) {
429 // Skip this if some other filter has already determined yes.
430 if ( true === $check ) {
431 return $check;
432 }
433
434 $thresholds = array(
435 'orders' => 100,
436 'products' => 100,
437 );
438
439 foreach ( $thresholds as $key => $threshold ) {
440 try {
441 switch ( $key ) {
442 case 'orders':
443 $orders_query = new WC_Order_Query(
444 array(
445 'status' => 'any',
446 'limit' => 1,
447 'paginate' => true,
448 'return' => 'ids',
449 )
450 );
451 $orders_results = $orders_query->get_orders();
452 if ( $orders_results->total >= $threshold ) {
453 $check = true;
454 }
455 break;
456
457 case 'products':
458 $products_query = new WC_Product_Query(
459 array(
460 'status' => 'any',
461 'limit' => 1,
462 'paginate' => true,
463 'return' => 'ids',
464 )
465 );
466 $products_results = $products_query->get_products();
467 if ( $products_results->total >= $threshold ) {
468 $check = true;
469 }
470 break;
471 }
472 } catch ( \Exception $exception ) {
473 break;
474 }
475
476 if ( ! is_null( $check ) ) {
477 break;
478 }
479 }
480
481 return $check;
482 }
483
484 /**
485 * Create a Site Health action link.
486 *
487 * @param string $url URL.
488 * @param string $label Link label.
489 * @param bool $opens_in_new_tab Whether the link opens in a new tab.
490 * @return string
491 */
492 private function get_site_health_action( $url, $label, $opens_in_new_tab = false ) {
493 if ( $opens_in_new_tab ) {
494 return sprintf(
495 '<p><a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s' .
496 '<span class="screen-reader-text"> %3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
497 esc_url( $url ),
498 esc_html( $label ),
499 esc_html__( '(opens in a new tab)', 'woocommerce' )
500 );
501 }
502
503 return sprintf(
504 '<p><a href="%1$s">%2$s</a></p>',
505 esc_url( $url ),
506 esc_html( $label )
507 );
508 }
509
510 /**
511 * Determine whether the store is using HTTPS for WooCommerce pages.
512 *
513 * @return bool
514 */
515 private function is_store_using_secure_connection() {
516 $shop_page = wc_get_page_permalink( 'shop' );
517
518 return is_ssl() && 'https' === substr( $shop_page, 0, 5 );
519 }
520
521 /**
522 * Check if the WooCommerce uploads directory is protected from directory browsing.
523 *
524 * @return bool|array{unverified: true}
525 */
526 private function is_uploads_directory_protected() {
527 $cache_key = '_woocommerce_upload_directory_status';
528 $status = get_transient( $cache_key );
529
530 if ( false !== $status ) {
531 if ( 'unverified' === $status ) {
532 return array( 'unverified' => true );
533 }
534
535 return 'protected' === $status;
536 }
537
538 $uploads = wp_get_upload_dir();
539 $response = wp_safe_remote_get(
540 esc_url_raw( $uploads['baseurl'] . '/woocommerce_uploads/' ),
541 array(
542 'redirection' => 0,
543 )
544 );
545
546 if ( is_wp_error( $response ) ) {
547 set_transient( $cache_key, 'unverified', HOUR_IN_SECONDS );
548 return array( 'unverified' => true );
549 }
550
551 $response_code = intval( wp_remote_retrieve_response_code( $response ) );
552
553 if ( 0 === $response_code ) {
554 set_transient( $cache_key, 'unverified', HOUR_IN_SECONDS );
555 return array( 'unverified' => true );
556 }
557
558 $response_content = wp_remote_retrieve_body( $response );
559 $is_protected = ( 200 === $response_code && empty( $response_content ) ) || ( 200 !== $response_code );
560
561 set_transient( $cache_key, $is_protected ? 'protected' : 'unprotected', DAY_IN_SECONDS );
562
563 return $is_protected;
564 }
565
566 /**
567 * Determine whether the active theme has outdated WooCommerce template overrides.
568 *
569 * @return bool
570 */
571 private function has_outdated_template_overrides() {
572 $core_templates = WC_Admin_Status::scan_template_files( WC()->plugin_path() . '/templates' );
573
574 foreach ( $core_templates as $file ) {
575 $theme_file = $this->get_theme_template_override_path( $file );
576
577 if ( ! $theme_file ) {
578 continue;
579 }
580
581 $core_version = WC_Admin_Status::get_file_version( WC()->plugin_path() . '/templates/' . $file );
582 $theme_version = WC_Admin_Status::get_file_version( $theme_file );
583
584 if ( $core_version && $theme_version && version_compare( $theme_version, $core_version, '<' ) ) {
585 return true;
586 }
587 }
588
589 return false;
590 }
591
592 /**
593 * Get the path to a theme template override, if one exists.
594 *
595 * @param string $file Template file.
596 * @return string|false
597 */
598 private function get_theme_template_override_path( $file ) {
599 $paths = array(
600 get_stylesheet_directory() . '/' . $file,
601 get_stylesheet_directory() . '/' . WC()->template_path() . $file,
602 get_template_directory() . '/' . $file,
603 get_template_directory() . '/' . WC()->template_path() . $file,
604 );
605
606 foreach ( $paths as $path ) {
607 if ( file_exists( $path ) ) {
608 return $path;
609 }
610 }
611
612 return false;
613 }
614
615 /**
616 * Determine whether MaxMind geolocation requires a license key.
617 *
618 * @return bool
619 */
620 private function needs_maxmind_license_key() {
621 /**
622 * Filter whether MaxMind geolocation notices should be displayed.
623 *
624 * Previously used to suppress the MaxMind license key admin notice. Honoured
625 * here so the equivalent Site Health warning can be suppressed the same way.
626 *
627 * @since 3.9.0
628 *
629 * @param bool $display Whether to display MaxMind geolocation notices.
630 */
631 if ( ! apply_filters( 'woocommerce_maxmind_geolocation_display_notices', true ) ) {
632 return false;
633 }
634
635 $default_address = get_option( 'woocommerce_default_customer_address' );
636
637 if ( ! in_array( $default_address, array( DefaultCustomerAddress::GEOLOCATION, DefaultCustomerAddress::GEOLOCATION_AJAX ), true ) ) {
638 return false;
639 }
640
641 $integration_options = get_option( 'woocommerce_maxmind_geolocation_settings' );
642
643 return empty( $integration_options['license_key'] );
644 }
645
646 /**
647 * Determine whether the HPOS sync-on-read status should be shown.
648 *
649 * @return bool
650 */
651 private function should_show_hpos_sync_on_read_status() {
652 return OrderUtil::custom_orders_table_usage_is_enabled()
653 && wc_get_container()->get( DataSynchronizer::class )->data_sync_is_enabled();
654 }
655
656 /**
657 * Determine whether any legacy shipping methods are enabled.
658 *
659 * @return bool
660 */
661 private function has_legacy_shipping_methods_enabled() {
662 $legacy_methods = array( 'flat_rate', 'free_shipping', 'international_delivery', 'local_delivery', 'local_pickup' );
663
664 foreach ( $legacy_methods as $method ) {
665 $options = get_option( 'woocommerce_' . $method . '_settings' );
666
667 if ( $options && isset( $options['enabled'] ) && 'yes' === $options['enabled'] ) {
668 return true;
669 }
670 }
671
672 return false;
673 }
674
675 /**
676 * Determine whether shipping is enabled but no methods are configured.
677 *
678 * @return bool
679 */
680 private function needs_shipping_methods() {
681 if ( ! wc_shipping_enabled() ) {
682 return false;
683 }
684
685 $product_count = wc_get_container()->get( ProductUtil::class )->get_counts_for_type( 'product' );
686
687 return ( $product_count[ ProductStatus::PUBLISH ] ?? 0 ) > 0 && 0 === wc_get_shipping_method_count();
688 }
689
690 /**
691 * Determine whether the store has outdated WooCommerce.com plugins that need connection to update.
692 *
693 * @return bool
694 */
695 private function has_outdated_woocommerce_com_plugins() {
696 if ( ! class_exists( WC_Helper_Updater::class ) ) {
697 return false;
698 }
699
700 return 'long' === WC_Helper_Updater::get_woo_connect_notice_type();
701 }
702
703 /**
704 * Get the WooCommerce.com extensions admin URL.
705 *
706 * @return string
707 */
708 private function get_woocommerce_com_extensions_url() {
709 return add_query_arg(
710 array(
711 'page' => 'wc-admin',
712 'tab' => 'my-subscriptions',
713 'path' => rawurlencode( '/extensions' ),
714 'utm_source' => 'site-health',
715 'utm_campaign' => 'woo_extension_updates',
716 ),
717 admin_url( 'admin.php' )
718 );
719 }
720 }
721