$tests Existing WordPress Site Health tests. * @return array */ public function add_site_health_test(array $tests): array { if (!isset($tests['direct']) || !is_array($tests['direct'])) { $tests['direct'] = array(); } $tests['direct']['commentmate_checkpress_compatibility'] = array( 'label' => __('CommentMate compatibility', 'commentmate'), 'test' => array($this, 'site_health_test'), ); return $tests; } /** * Produces the WordPress Site Health result from the same safe local checks * used by the owner dashboard. No environment values, file paths, addresses, * secrets, or transport diagnostics are included in the result. * * @return array */ public function site_health_test(): array { $summary = self::summary(); $description = '

' . esc_html((string) $summary['headline']) . '

'; $attention = array(); foreach (array_merge($summary['required'], $summary['optional']) as $check) { if ('good' !== $check['status']) { $attention[] = $check; } } if (empty($attention)) { $description .= '

' . esc_html__('All locally detectable CommentMate requirements are available.', 'commentmate') . '

'; } else { $description .= ''; } $badge_color = match ($summary['status']) { 'critical' => 'red', 'recommended' => 'orange', default => 'blue', }; return array( 'label' => (string) $summary['title'], 'status' => (string) $summary['status'], 'badge' => array( 'label' => __('CommentMate', 'commentmate'), 'color' => $badge_color, ), 'description' => $description, 'actions' => '', 'test' => 'commentmate_checkpress_compatibility', ); } /** * Returns only locally-derived, non-sensitive status text for use in the * CommentMate admin screen or Site Health. Required checks describe conditions * needed to run CommentMate safely; optional checks identify reduced features * or operational follow-up without preventing text feedback. * * @return array{ * title: string, * status: 'good'|'recommended'|'critical', * headline: string, * required: list, * optional: list, * required_failures: list * } */ public static function summary(): array { $review_protection_signals = self::review_protection_signals(); $required = array( self::wordpress_check(), self::php_check(), self::multisite_check(), self::https_check(), self::sodium_check(), self::innodb_check(), self::private_storage_check(), self::review_protection_check($review_protection_signals), ); $optional = array( self::gd_webp_check(), self::fileinfo_check(), self::zip_check(), self::cron_loopback_check(), self::mail_check(), self::cache_risk_check($review_protection_signals), self::update_readiness_check(), ); $required_failures = array_values( array_filter( $required, static fn (array $check): bool => 'good' !== $check['status'] ) ); $status = self::overall_status($required, $optional); $headline = match ($status) { 'critical' => __('CommentMate has a required compatibility or safety issue that needs attention.', 'commentmate'), 'recommended' => __('CommentMate meets its core requirements, but an optional feature or operational check needs attention.', 'commentmate'), default => __('CommentMate meets the locally detectable requirements.', 'commentmate'), }; return array( 'title' => __('CommentMate compatibility', 'commentmate'), 'status' => $status, 'headline' => $headline, 'required' => $required, 'optional' => $optional, 'required_failures' => $required_failures, ); } /** * @return array{id: string, category: 'required', label: string, status: 'good'|'recommended'|'critical', message: string, remediation: string} */ private static function wordpress_check(): array { $version = function_exists('get_bloginfo') ? (string) get_bloginfo('version') : ''; if ('' === $version || version_compare($version, self::MINIMUM_WORDPRESS, '<')) { return self::check( 'wordpress', 'required', __('WordPress', 'commentmate'), 'critical', __('CommentMate requires WordPress 6.5 or newer.', 'commentmate'), __('Update WordPress before activating CommentMate.', 'commentmate') ); } return self::check('wordpress', 'required', __('WordPress', 'commentmate'), 'good', __('WordPress meets the CommentMate minimum.', 'commentmate')); } /** * @return array{id: string, category: 'required', label: string, status: 'good'|'recommended'|'critical', message: string, remediation: string} */ private static function php_check(): array { if (version_compare(PHP_VERSION, self::MINIMUM_PHP, '<')) { return self::check( 'php', 'required', __('PHP', 'commentmate'), 'critical', __('CommentMate requires PHP 8.1 or newer.', 'commentmate'), __('Ask your host to update the site’s PHP version before using CommentMate.', 'commentmate') ); } if (version_compare(PHP_VERSION, '8.2', '<')) { return self::check('php', 'required', __('PHP', 'commentmate'), 'recommended', __('This site meets the PHP 8.1 compatibility minimum, but PHP 8.1 no longer receives security updates.', 'commentmate'), __('Ask your host to upgrade to supported PHP 8.3 or newer. CommentMate does not block this compatible installation.', 'commentmate')); } if (version_compare(PHP_VERSION, '8.3', '<')) { return self::check('php', 'required', __('PHP', 'commentmate'), 'recommended', __('This PHP version meets the compatibility minimum, but PHP 8.3 or newer is recommended for supported hosting.', 'commentmate'), __('Ask your host to move this site to a supported PHP version. PHP 8.1 compatibility is retained.', 'commentmate')); } return self::check('php', 'required', __('PHP', 'commentmate'), 'good', __('PHP meets the CommentMate minimum.', 'commentmate')); } /** * @return array{id: string, category: 'required', label: string, status: 'good'|'recommended'|'critical', message: string, remediation: string} */ private static function multisite_check(): array { if (function_exists('is_multisite') && is_multisite()) { return self::check( 'multisite', 'required', __('Single-site installation', 'commentmate'), 'critical', __('CommentMate currently supports single-site WordPress installations only.', 'commentmate'), __('Do not activate CommentMate network-wide or on a multisite installation.', 'commentmate') ); } return self::check('multisite', 'required', __('Single-site installation', 'commentmate'), 'good', __('A single-site WordPress installation was detected.', 'commentmate')); } /** * @return array{id: string, category: 'required', label: string, status: 'good'|'recommended'|'critical', message: string, remediation: string} */ private static function https_check(): array { $home = function_exists('home_url') ? (string) home_url('/') : ''; $site = function_exists('site_url') ? (string) site_url('/') : ''; if (!self::url_uses_https($home) || !self::url_uses_https($site)) { return self::check( 'https', 'required', __('HTTPS', 'commentmate'), 'critical', __('CommentMate requires HTTPS for private review links and Secure cookies.', 'commentmate'), __('Set both WordPress URLs to HTTPS and confirm any reverse proxy passes the HTTPS state correctly.', 'commentmate') ); } return self::check('https', 'required', __('HTTPS', 'commentmate'), 'good', __('The configured WordPress URLs use HTTPS.', 'commentmate')); } /** * @return array{id: string, category: 'required', label: string, status: 'good'|'recommended'|'critical', message: string, remediation: string} */ private static function sodium_check(): array { $available = function_exists('sodium_crypto_secretstream_xchacha20poly1305_init_push') && function_exists('sodium_crypto_aead_xchacha20poly1305_ietf_encrypt') && defined('SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES'); if (!$available) { return self::check( 'sodium', 'required', __('PHP Sodium', 'commentmate'), 'critical', __('CommentMate requires the PHP Sodium extension to protect private review data.', 'commentmate'), __('Enable PHP Sodium, then reactivate CommentMate.', 'commentmate') ); } return self::check('sodium', 'required', __('PHP Sodium', 'commentmate'), 'good', __('PHP Sodium is available.', 'commentmate')); } /** * @return array{id: string, category: 'required', label: string, status: 'good'|'recommended'|'critical', message: string, remediation: string} */ private static function innodb_check(): array { global $wpdb; if (!isset($wpdb) || !is_object($wpdb) || !method_exists($wpdb, 'get_results')) { return self::check( 'innodb', 'required', __('InnoDB database support', 'commentmate'), 'critical', __('CommentMate could not verify InnoDB database support.', 'commentmate'), __('Ask your database host to confirm InnoDB support and database permissions.', 'commentmate') ); } $engine_available = self::database_has_innodb_engine($wpdb); $table_engine = self::existing_review_table_engine($wpdb); if ('innodb' === $table_engine) { return self::check('innodb', 'required', __('InnoDB database support', 'commentmate'), 'good', __('The existing CommentMate review table uses InnoDB.', 'commentmate')); } if ('' !== $table_engine) { return self::check( 'innodb', 'required', __('InnoDB database support', 'commentmate'), 'critical', __('An existing CommentMate review table is not using InnoDB.', 'commentmate'), __('Ask your database host to enable InnoDB, then repair or reactivate CommentMate.', 'commentmate') ); } if (!$engine_available) { return self::check( 'innodb', 'required', __('InnoDB database support', 'commentmate'), 'critical', __('CommentMate could not confirm that InnoDB is available.', 'commentmate'), __('Ask your database host to enable InnoDB and allow the required database checks.', 'commentmate') ); } return self::check('innodb', 'required', __('InnoDB database support', 'commentmate'), 'good', __('InnoDB database support is available.', 'commentmate')); } /** * @param object $wpdb WordPress database object. */ private static function database_has_innodb_engine(object $wpdb): bool { try { $rows = $wpdb->get_results('SHOW ENGINES', ARRAY_A); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Read-only database capability probe must inspect the current server engine state; object-cache data cannot establish compatibility. } catch (\Throwable) { return false; } if (!is_array($rows)) { return false; } foreach ($rows as $row) { $engine = strtolower(self::database_row_value($row, 'Engine')); $support = strtoupper(self::database_row_value($row, 'Support')); if ('innodb' === $engine && in_array($support, array('YES', 'DEFAULT'), true)) { return true; } } return false; } /** * @param object $wpdb WordPress database object. */ private static function existing_review_table_engine(object $wpdb): string { if (!isset($wpdb->prefix) || !is_string($wpdb->prefix)) { return ''; } $table = $wpdb->prefix . 'commentmate_reviews'; if (!method_exists($wpdb, 'prepare')) { return ''; } $query = $wpdb->prepare('SHOW TABLE STATUS LIKE %s', $wpdb->esc_like($table)); if (!is_string($query) || '' === $query) { return ''; } try { $rows = $wpdb->get_results($query, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The SHOW statement above is prepared with the escaped table name as its only LIKE value; this is a read-only engine probe. Read-only database capability probe must inspect the current server engine state; object-cache data cannot establish compatibility. } catch (\Throwable) { return ''; } if (!is_array($rows) || empty($rows)) { return ''; } return strtolower(self::database_row_value($rows[0], 'Engine')); } /** * @param array|object|mixed $row Row returned by $wpdb. */ private static function database_row_value(mixed $row, string $expected_key): string { if (is_object($row)) { $row = get_object_vars($row); } if (!is_array($row)) { return ''; } foreach ($row as $key => $value) { if (strtolower((string) $key) === strtolower($expected_key) && is_scalar($value)) { return (string) $value; } } return ''; } /** * @return array{id: string, category: 'required', label: string, status: 'good'|'recommended'|'critical', message: string, remediation: string} */ private static function private_storage_check(): array { $install_id = function_exists('get_option') ? get_option('commentmate_private_storage_install_id', '') : ''; if (!is_string($install_id) || !preg_match('/\A[a-f0-9]{24}\z/', $install_id)) { return self::check( 'private_storage', 'required', __('Private storage', 'commentmate'), 'critical', __('CommentMate could not verify its private review-storage directory.', 'commentmate'), __('Reactivate CommentMate after configuring a writable private storage location outside the web root.', 'commentmate') ); } foreach (self::private_storage_candidates($install_id) as $candidate) { if (self::verified_private_storage($candidate, $install_id)) { $temporary = self::trim_trailing_separators(sys_get_temp_dir()) . DIRECTORY_SEPARATOR; if (str_starts_with($candidate, $temporary)) { return self::check('private_storage', 'required', __('Private storage', 'commentmate'), 'recommended', __('Existing private review files are in temporary system storage.', 'commentmate'), __('Back up the database and private files together. Finish and delete retained reviews, then configure durable private storage outside all web roots before starting a new review.', 'commentmate')); } return self::check('private_storage', 'required', __('Private storage', 'commentmate'), 'good', __('Private review storage is writable and verified outside the web root.', 'commentmate')); } } return self::check( 'private_storage', 'required', __('Private storage', 'commentmate'), 'critical', __('CommentMate could not verify writable private storage outside the web root.', 'commentmate'), __('Configure a private storage parent outside every public web root, check permissions, then reactivate CommentMate.', 'commentmate') ); } /** * Builds only the location that Private_Storage would currently prefer. It * deliberately never calls Private_Storage::path(), because that method can * initialise state during a first activation. * * @return list */ private static function private_storage_candidates(string $install_id): array { $directory = self::STORAGE_DIRECTORY_PREFIX . $install_id; $configured = defined('COMMENTMATE_PRIVATE_STORAGE_DIR') ? constant('COMMENTMATE_PRIVATE_STORAGE_DIR') : ''; if (is_string($configured) && '' !== trim($configured)) { return array(self::trim_trailing_separators(trim($configured)) . DIRECTORY_SEPARATOR . $directory); } $candidates = array(); $document_root = self::document_root(); if (null !== $document_root) { $automatic = dirname(self::trim_trailing_separators(Private_Storage::wordpress_root())) . DIRECTORY_SEPARATOR . $directory; if (self::path_is_outside_web_roots($automatic)) { $candidates[] = $automatic; } } $tmp = function_exists('sys_get_temp_dir') ? sys_get_temp_dir() : ''; if (is_string($tmp) && '' !== trim($tmp)) { $candidate = self::trim_trailing_separators(trim($tmp)) . DIRECTORY_SEPARATOR . $directory; if (self::path_is_outside_web_roots($candidate)) { $candidates[] = $candidate; } } return array_values(array_unique($candidates)); } private static function verified_private_storage(string $candidate, string $install_id): bool { if ('' === $candidate || is_link($candidate) || !is_dir($candidate) || !is_readable($candidate) || !is_writable($candidate)) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Read-only local permission preflight; remote WP_Filesystem writability cannot establish private local storage safety. return false; } $real = realpath($candidate); if (false === $real || !self::path_is_outside_web_roots($real)) { return false; } $permissions = @fileperms($real); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- a missing mode means private storage cannot be verified. if (false === $permissions || 0 !== ($permissions & 0077)) { return false; } $marker = $real . DIRECTORY_SEPARATOR . self::STORAGE_MARKER; if (is_link($marker) || !is_file($marker) || !is_readable($marker)) { return false; } $expected = "CommentMate private storage\ninstall=" . $install_id . "\n"; $marker_content = @file_get_contents($marker, false, null, 0, strlen($expected) + 1); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.PHP.NoSilencedErrors.Discouraged -- bounded read of a plugin-controlled marker only. return is_string($marker_content) && hash_equals($expected, $marker_content); } /** * @return array{id: string, category: 'required', label: string, status: 'good'|'recommended'|'critical', message: string, remediation: string} */ private static function review_protection_check(array $signals): array { if (!$signals['no_cache'] || !$signals['noindex']) { return self::check( 'review_privacy_headers', 'required', __('Review cache and indexing safeguards', 'commentmate'), 'critical', __('CommentMate could not confirm all review-page no-cache and noindex safeguards.', 'commentmate'), __('Reactivate CommentMate and verify that review pages send no-cache and noindex protections.', 'commentmate') ); } return self::check('review_privacy_headers', 'required', __('Review cache and indexing safeguards', 'commentmate'), 'good', __('Review-page no-cache and noindex safeguards are registered.', 'commentmate')); } /** * @return array{no_cache: bool, noindex: bool, cache_detected: bool} */ private static function review_protection_signals(): array { $signals = array( 'no_cache' => false, 'noindex' => false, 'cache_detected' => self::cache_layer_detected(), ); if (class_exists(Plugin::class, false) && function_exists('has_action') && function_exists('has_filter')) { $plugin = Plugin::instance(); $signals['no_cache'] = false !== has_action('init', array($plugin, 'protect_review_request')) && false !== has_action('template_redirect', array($plugin, 'send_review_headers')); $signals['noindex'] = false !== has_filter('wp_robots', array($plugin, 'noindex_review_request')); } $filtered = self::apply_filter('commentmate_compatibility_cache_signals', $signals); if (is_array($filtered)) { foreach (array_keys($signals) as $key) { if (array_key_exists($key, $filtered)) { $signals[$key] = self::is_truthy($filtered[$key]); } } } return $signals; } /** * @return array{id: string, category: 'optional', label: string, status: 'good'|'recommended'|'critical', message: string, remediation: string} */ private static function gd_webp_check(): array { $gd_available = extension_loaded('gd') && function_exists('gd_info'); if (!$gd_available) { return self::check( 'gd_webp', 'optional', __('Image attachments and screenshots', 'commentmate'), 'recommended', __('PHP GD is unavailable, so image attachments and contextual screenshots are unavailable.', 'commentmate'), __('Enable PHP GD to use image attachments and screenshots; text feedback remains available.', 'commentmate') ); } $gd_info = gd_info(); $webp = function_exists('imagecreatefromwebp') && function_exists('imagewebp') && is_array($gd_info) && !empty($gd_info['WebP Support']); if (!$webp) { return self::check( 'gd_webp', 'optional', __('WebP attachments', 'commentmate'), 'recommended', __('PHP GD is available, but WebP support is unavailable.', 'commentmate'), __('Enable WebP support in PHP GD to accept WebP attachments. JPEG and PNG workflows remain available.', 'commentmate') ); } return self::check('gd_webp', 'optional', __('Image attachments and screenshots', 'commentmate'), 'good', __('PHP GD with WebP support is available.', 'commentmate')); } /** * @return array{id: string, category: 'optional', label: string, status: 'good'|'recommended'|'critical', message: string, remediation: string} */ private static function zip_check(): array { if (!class_exists('ZipArchive')) { return self::check('zip', 'optional', __('Review downloads', 'commentmate'), 'recommended', __('ZIP support is unavailable, so Download review cannot create an archive.', 'commentmate'), __('Ask your host to enable PHP ZIP support. Text feedback and complete website backups remain available.', 'commentmate')); } return self::check('zip', 'optional', __('Review downloads', 'commentmate'), 'good', __('PHP ZIP support is available for private review downloads.', 'commentmate')); } private static function fileinfo_check(): array { if (!extension_loaded('fileinfo') && !function_exists('finfo_open')) { return self::check( 'fileinfo', 'optional', __('PDF validation', 'commentmate'), 'recommended', __('PHP Fileinfo is unavailable, so PDF attachments cannot be safely validated.', 'commentmate'), __('Enable PHP Fileinfo to allow PDF attachments. Text and supported image feedback remain available.', 'commentmate') ); } return self::check('fileinfo', 'optional', __('PDF validation', 'commentmate'), 'good', __('PHP Fileinfo is available for PDF validation.', 'commentmate')); } /** * @return array{id: string, category: 'optional', label: string, status: 'good'|'recommended'|'critical', message: string, remediation: string} */ private static function cron_loopback_check(): array { if (!self::daily_email_enabled()) { return self::check('cron_loopback', 'optional', __('Daily email scheduling', 'commentmate'), 'good', __('Daily client-feedback summaries are off, so scheduled delivery is not required.', 'commentmate')); } if (defined('DISABLE_WP_CRON') && self::is_truthy(constant('DISABLE_WP_CRON'))) { return self::check( 'cron_loopback', 'optional', __('Daily email scheduling', 'commentmate'), 'recommended', __('Daily client-feedback summaries are enabled, but WordPress Cron is disabled.', 'commentmate'), __('Configure a real server cron for WordPress and confirm the core Site Health loopback check passes.', 'commentmate') ); } if (!function_exists('wp_schedule_event') || !function_exists('wp_next_scheduled')) { return self::check( 'cron_loopback', 'optional', __('Daily email scheduling', 'commentmate'), 'recommended', __('WordPress Cron scheduling functions are unavailable.', 'commentmate'), __('Restore WordPress Cron support before relying on daily client-feedback summaries.', 'commentmate') ); } $loopback = self::loopback_status(); if ('available' === $loopback) { return self::check('cron_loopback', 'optional', __('Daily email scheduling', 'commentmate'), 'good', __('WordPress Cron is enabled and a local loopback check is available.', 'commentmate')); } if ('unavailable' === $loopback) { return self::check( 'cron_loopback', 'optional', __('Daily email scheduling', 'commentmate'), 'recommended', __('A local loopback check reports that WordPress cannot reach itself.', 'commentmate'), __('Resolve the core Site Health loopback issue before relying on daily client-feedback summaries.', 'commentmate') ); } return self::check( 'cron_loopback', 'optional', __('Daily email scheduling', 'commentmate'), 'recommended', __('WordPress Cron is enabled, but this read-only preflight has not run a loopback request.', 'commentmate'), __('Open Tools → Site Health and confirm that the core loopback request test passes before relying on daily client-feedback summaries.', 'commentmate') ); } /** * @return 'available'|'unavailable'|'unknown' */ private static function loopback_status(): string { $value = self::apply_filter('commentmate_compatibility_loopback_status', 'unknown'); if (is_array($value)) { $value = $value['status'] ?? 'unknown'; } $value = is_string($value) ? strtolower($value) : 'unknown'; return in_array($value, array('available', 'unavailable'), true) ? $value : 'unknown'; } /** * @return array{id: string, category: 'optional', label: string, status: 'good'|'recommended'|'critical', message: string, remediation: string} */ private static function mail_check(): array { $settings = self::notification_settings(); if (!self::is_truthy($settings['enabled'] ?? false)) { return self::check('mail', 'optional', __('WordPress mail', 'commentmate'), 'good', __('CommentMate email notifications are disabled. No CommentMate email will be sent.', 'commentmate')); } if (!self::has_notification_recipients($settings)) { return self::check( 'mail', 'optional', __('WordPress mail', 'commentmate'), 'recommended', __('CommentMate email notifications are enabled, but no valid recipient is configured.', 'commentmate'), __('Add at least one notification recipient or turn email notifications off.', 'commentmate') ); } // The enabled setting always allows explicit reviewer-completion mail; // daily summaries and immediate alerts are additional delivery choices. if (!function_exists('wp_mail')) { return self::check( 'mail', 'optional', __('WordPress mail', 'commentmate'), 'recommended', __('WordPress’s local mail function is unavailable.', 'commentmate'), __('Restore WordPress mail support before enabling CommentMate email notifications.', 'commentmate') ); } return self::check('mail', 'optional', __('WordPress mail', 'commentmate'), 'good', __('WordPress’s local mail function is available. This check sends no email and cannot confirm delivery.', 'commentmate')); } /** * @return array{id: string, category: 'optional', label: string, status: 'good'|'recommended'|'critical', message: string, remediation: string} */ private static function cache_risk_check(array $signals): array { if ($signals['cache_detected']) { return self::check( 'cache_cdn_risk', 'optional', __('Cache or CDN review-page risk', 'commentmate'), 'recommended', __('A local cache or proxy signal was detected.', 'commentmate'), __('Confirm that every cache and CDN bypasses review URLs, review cookies, and CommentMate REST responses.', 'commentmate') ); } return self::check('cache_cdn_risk', 'optional', __('Cache or CDN review-page risk', 'commentmate'), 'good', __('No local cache or proxy signal was detected. External cache and CDN behaviour still needs deployment testing.', 'commentmate')); } /** * @return array{id: string, category: 'optional', label: string, status: 'good'|'recommended'|'critical', message: string, remediation: string} */ private static function update_readiness_check(): array { return self::check('update_readiness', 'optional', __('WordPress updates', 'commentmate'), 'good', __('Free uses WordPress.org updates after directory publication. Until then, replace the plugin in place using a verified ZIP; never delete it to update.', 'commentmate')); } /** * @return array */ private static function notification_settings(): array { $settings = function_exists('get_option') ? get_option('commentmate_notification_settings', array()) : array(); return is_array($settings) ? $settings : array(); } private static function daily_email_enabled(): bool { $settings = self::notification_settings(); $enabled = self::is_truthy($settings['enabled'] ?? false) && self::is_truthy($settings['daily_summary'] ?? false) && self::has_notification_recipients($settings); if (class_exists(Notification_Service::class) && method_exists(Notification_Service::class, 'daily_summary_enabled')) { try { $enabled = (bool) Notification_Service::daily_summary_enabled(); } catch (\Throwable) { // The fallback option check above deliberately remains sufficient. } } return self::is_truthy(self::apply_filter('commentmate_compatibility_daily_email_enabled', $enabled)); } /** * @param array $settings */ private static function has_notification_recipients(array $settings): bool { if (!isset($settings['recipients']) || !is_array($settings['recipients'])) { return false; } foreach ($settings['recipients'] as $recipient) { if (!is_string($recipient) || '' === trim($recipient)) { continue; } if (!function_exists('is_email') || false !== is_email($recipient)) { return true; } } return false; } private static function cache_layer_detected(): bool { $constants = array('WP_CACHE', 'WPCACHEHOME', 'WP_ROCKET_VERSION', 'LSCWP_V', 'W3TC', 'SWIFT_PERFORMANCE_VERSION'); foreach ($constants as $constant) { if (defined($constant) && self::is_truthy(constant($constant))) { return true; } } $headers = array('HTTP_CF_RAY', 'HTTP_X_CACHE', 'HTTP_X_SUCURI_ID', 'HTTP_X_VARNISH', 'HTTP_FASTLY_ORIG_HOST'); foreach ($headers as $header) { if (isset($_SERVER[$header]) && is_string($_SERVER[$header]) && '' !== trim(wp_unslash($_SERVER[$header]))) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Only presence of a string header is tested after unslashing; its contents are never output or used as configuration. return true; } } return false; } private static function url_uses_https(string $url): bool { $scheme = wp_parse_url($url, PHP_URL_SCHEME); return is_string($scheme) && 'https' === strtolower($scheme); } private static function path_is_outside_web_roots(string $path): bool { $real = realpath($path); if (false === $real || !is_dir($real)) { return false; } $path = self::normalise_path($real); foreach (self::web_roots() as $root) { $root = self::normalise_path($root); if ('/' === $root || $path === $root || str_starts_with($path, $root . '/')) { return false; } } return true; } /** * @return list */ private static function web_roots(): array { return Private_Storage::public_roots(); } private static function document_root(): ?string { if (!isset($_SERVER['DOCUMENT_ROOT']) || !is_string($_SERVER['DOCUMENT_ROOT']) || '' === trim(wp_unslash($_SERVER['DOCUMENT_ROOT']))) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Literal filesystem path is string-checked, unslashed and resolved with realpath before containment checks; text sanitization can change the protected path. return null; } $real = realpath(wp_unslash($_SERVER['DOCUMENT_ROOT'])); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Literal filesystem path is string-checked, unslashed and resolved with realpath before containment checks; text sanitization can change the protected path. return false !== $real && is_dir($real) ? $real : null; } private static function normalise_path(string $path): string { $path = str_replace('\\', '/', $path); $path = rtrim($path, '/'); if (preg_match('/\A[A-Z]:/', $path)) { $path = strtolower(substr($path, 0, 1)) . substr($path, 1); } return '' === $path ? '/' : $path; } private static function trim_trailing_separators(string $path): string { $trimmed = rtrim($path, "\\/"); return '' === $trimmed ? DIRECTORY_SEPARATOR : $trimmed; } /** * @param list $required * @param list $optional * @return 'good'|'recommended'|'critical' */ private static function overall_status(array $required, array $optional): string { foreach (array_merge($required, $optional) as $check) { if ('critical' === $check['status']) { return 'critical'; } } foreach (array_merge($required, $optional) as $check) { if ('recommended' === $check['status']) { return 'recommended'; } } return 'good'; } /** * @return array{id: string, category: 'required'|'optional', label: string, status: 'good'|'recommended'|'critical', message: string, remediation: string} */ private static function check(string $id, string $category, string $label, string $status, string $message, string $remediation = ''): array { return array( 'id' => $id, 'category' => $category, 'label' => $label, 'status' => $status, 'message' => $message, 'remediation' => $remediation, ); } private static function is_truthy(mixed $value): bool { if (is_bool($value)) { return $value; } if (is_int($value) || is_float($value)) { return 0 !== (int) $value; } if (is_string($value)) { return in_array(strtolower(trim($value)), array('1', 'true', 'yes', 'on', 'available', 'ready'), true); } return false; } private static function apply_filter(string $hook, mixed $value): mixed { return function_exists('apply_filters') ? apply_filters($hook, $value) : $value; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Private wrapper callers use literal commentmate_ hook names only. } }