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