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§ion=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§ion=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§ion=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§ion=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 |