__('Permission denied', '1001web-maintenance-launch-pages')]); } $check_id = isset($_POST['check_id']) ? sanitize_key( wp_unslash( $_POST['check_id'] ) ) : ''; if (in_array($check_id, ['email', 'http503'], true)) { set_transient('mgenius_diag_banner_dismissed_' . $check_id, 1, DAY_IN_SECONDS); } wp_send_json_success(); } // PERSISTANCE /** * Dernier résultat persisté (ou null si jamais scanné). * @return array|null {timestamp:int, checks:array, fails:int, warns:int} */ public static function get_results() { $results = get_option(self::RESULTS_OPTION, null); return is_array($results) ? $results : null; } /** * v2.9.181 — Le résultat enregistré a-t-il été rédigé dans une autre langue * que celle de l'utilisateur ? Avant la 2.9.181, la langue n'était pas * enregistrée : un résultat sans langue est présumé anglais. * * @param array|null $results * @return bool */ public static function results_in_other_language($results) { if (!is_array($results)) { return false; } $current = (string) get_user_locale(); if (!empty($results['locale'])) { return (string) $results['locale'] !== $current; } return 0 !== strpos($current, 'en'); } /** * Statut d'un check individuel depuis le dernier scan. * @param string $check_id * @return string 'ok' | 'warn' | 'fail' | 'skip' | 'never' */ public static function get_check_status($check_id) { $results = self::get_results(); if (!$results || empty($results['checks'][$check_id]['status'])) { return 'never'; } return $results['checks'][$check_id]['status']; } /** * Compteurs pour la pastille d'onglet. {fails:int, warns:int, never:bool} * @return array */ public static function get_badge_counts() { $results = self::get_results(); if (!$results) { return ['fails' => 0, 'warns' => 0, 'never' => true]; } return [ 'fails' => (int) ($results['fails'] ?? 0), 'warns' => (int) ($results['warns'] ?? 0), 'never' => false, ]; } /** * Le bandeau critique doit-il s'afficher ? Retourne le message ou null. * Règle : uniquement les échecs qui trahissent la promesse du produit, * max 1 bandeau (le plus grave), pages MG seulement (géré par l'appelant). * @return array|null {message:string, check_id:string} */ public static function get_critical_banner() { $results = self::get_results(); if (!$results) { return null; } $settings = MGENIUS_Settings::load(); $checks = $results['checks'] ?? []; // Critique #1 : collecte d'emails active + envoi courriel cassé. if ( !empty($settings['notification_enabled']) && isset($checks['email']['status']) && 'fail' === $checks['email']['status'] ) { return [ 'check_id' => 'email', 'message' => __('Email collection is enabled but your site cannot send emails — subscribers will never be notified.', '1001web-maintenance-launch-pages'), ]; } // Critique #2 : maintenance ON + 503 pas réellement servi. if ( !empty($settings['maintenance_enabled']) && isset($checks['http503']['status']) && 'fail' === $checks['http503']['status'] ) { return [ 'check_id' => 'http503', 'message' => __('Maintenance mode is ON but your server is not actually serving the maintenance page — a cache layer is probably bypassing it.', '1001web-maintenance-launch-pages'), ]; } return null; } // AJAX /** * AJAX : lance l'analyse complète et persiste le résultat. */ public static function ajax_run_diagnostic() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } // Rafraîchissement lancé à l'ouverture de la fenêtre d'assistance : aucun // courriel de test n'est envoyé, le dernier résultat connu est repris. $skip_email = isset($_POST['skip_email']) && '1' === sanitize_text_field(wp_unslash($_POST['skip_email'])); $checks = [ 'email' => $skip_email ? self::check_email_without_sending() : self::check_email(), 'db_write' => self::check_db_write(), 'http503' => self::check_http503(), 'cache_guard' => self::check_cache_guard(), 'cache' => self::check_cache_conflicts(), 'cron' => self::check_cron(), 'turnstile' => self::check_turnstile(), 'env' => self::check_environment(), ]; $fails = 0; $warns = 0; foreach ($checks as $check) { if ('fail' === $check['status']) { $fails++; } if ('warn' === $check['status']) { $warns++; } } $results = [ 'timestamp' => time(), // v2.9.181 — langue des textes enregistrés : un résultat lu dans // une autre langue est signalé au lieu d'être affiché tel quel. 'locale' => get_user_locale(), 'checks' => $checks, 'fails' => $fails, 'warns' => $warns, ]; update_option(self::RESULTS_OPTION, $results, false); // Un nouveau scan ré-arme les bandeaux : si le problème persiste, il réapparaît. delete_transient('mgenius_diag_banner_dismissed_email'); delete_transient('mgenius_diag_banner_dismissed_http503'); wp_send_json_success([ 'results' => $results, 'date_human'=> wp_date(get_option('date_format') . ' ' . get_option('time_format'), $results['timestamp']), ]); } /** * AJAX : envoi d'un courriel test (throttlé 1/minute). */ public static function ajax_send_test_email() { header('Content-Type: application/json; charset=utf-8'); check_ajax_referer('mgenius_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission denied', '1001web-maintenance-launch-pages')]); } if (get_transient(self::EMAIL_THROTTLE_TRANSIENT)) { wp_send_json_error(['message' => __('Please wait one minute between test emails.', '1001web-maintenance-launch-pages')]); } set_transient(self::EMAIL_THROTTLE_TRANSIENT, 1, MINUTE_IN_SECONDS); $to = isset($_POST['email']) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : ''; if (empty($to) || !is_email($to)) { $to = get_option('admin_email'); } $sent = self::do_send_test_email($to); if ($sent) { wp_send_json_success([ /* translators: %s: email address */ 'message' => sprintf(__('Test email sent to %s — check your inbox (and spam folder).', '1001web-maintenance-launch-pages'), $to), ]); } wp_send_json_error([ 'message' => __('Sending failed — your site cannot send emails. Configure an SMTP service or contact your host.', '1001web-maintenance-launch-pages'), ]); } // CHECKS — chacun retourne le même contrat : // ['status','label','detail','fix','tooltip'] /** * #1 — Envoi de courriel via wp_mail (test réel vers l'admin). * Throttlé 1/minute : un re-scan rapproché réutilise le dernier résultat * (évite de spammer l'admin si l'utilisateur relance l'analyse en boucle). */ private static function check_email() { $to = get_option('admin_email'); if (get_transient(self::EMAIL_THROTTLE_TRANSIENT)) { $previous = self::get_results(); if ($previous && isset($previous['checks']['email'])) { $cached = $previous['checks']['email']; $cached['detail'] = __('(Result from the previous scan — email tests are limited to one per minute.)', '1001web-maintenance-launch-pages') . ' ' . $cached['detail']; return $cached; } return self::result('warn', __('Email test skipped (rate limit)', '1001web-maintenance-launch-pages'), __('An email test was already sent less than a minute ago. Wait a minute, then run the analysis again.', '1001web-maintenance-launch-pages'), '', __('Test emails are limited to one per minute to protect your mail relay reputation.', '1001web-maintenance-launch-pages') ); } set_transient(self::EMAIL_THROTTLE_TRANSIENT, 1, MINUTE_IN_SECONDS); $sent = self::do_send_test_email($to); if ($sent) { return self::result('ok', __('Your site can send emails', '1001web-maintenance-launch-pages'), /* translators: %s: email address */ sprintf(__('A test email was sent to %s. Check the inbox (and spam folder) to confirm delivery.', '1001web-maintenance-launch-pages'), $to), '', __('wp_mail() returned true. Delivery to the inbox still depends on your SPF/DKIM alignment.', '1001web-maintenance-launch-pages') ); } return self::result('fail', __('Your site cannot send emails', '1001web-maintenance-launch-pages'), __('The test email could not be sent. Subscriber notifications and admin alerts will silently fail.', '1001web-maintenance-launch-pages'), __('Install an SMTP plugin (e.g. WP Mail SMTP) or ask your host to enable outgoing mail.', '1001web-maintenance-launch-pages'), __('wp_mail() returned false — PHP mail()/SMTP is not configured on this server.', '1001web-maintenance-launch-pages') ); } /** * Contrôle du courriel SANS envoi : le résultat de la dernière analyse est * repris (ou « non testé »). Sert au rafraîchissement lancé à l'ouverture * de la fenêtre d'assistance, qui ne doit envoyer aucun courriel. * * @return array */ private static function check_email_without_sending() { $previous = self::get_results(); if ($previous && isset($previous['checks']['email']['status'])) { $email = $previous['checks']['email']; $prefix = __('(Result of the last analysis: no test email is sent when the assistance window opens.)', '1001web-maintenance-launch-pages'); $detail = isset($email['detail']) ? (string) $email['detail'] : ''; if (0 !== strpos($detail, $prefix)) { $email['detail'] = $prefix . ' ' . $detail; } return $email; } return self::result('skip', __('Email sending not tested', '1001web-maintenance-launch-pages'), __('No test email is sent when the assistance window opens. Run the analysis in the Diagnostic tab to test email sending.', '1001web-maintenance-launch-pages'), '', __('The email test runs only when you start the analysis yourself.', '1001web-maintenance-launch-pages') ); } /** * #2 — Écriture DB vérifiée (roundtrip write/read, cache purgé). * Détecte le cas Redis/object-cache où update_option échoue en silence. */ private static function check_db_write() { global $wpdb; $probe_key = 'mgenius_diag_probe'; $probe_value = 'probe_' . time() . '_' . wp_rand(1000, 9999); // Écriture SQL directe (comme le stockage emails S2). // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Diagnostic probe $exists = $wpdb->get_var( $wpdb->prepare( "SELECT option_id FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", $probe_key ) ); if (null === $exists) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Diagnostic probe $write = $wpdb->insert($wpdb->options, ['option_name' => $probe_key, 'option_value' => $probe_value, 'autoload' => 'no'], ['%s', '%s', '%s']); } else { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Diagnostic probe $write = $wpdb->update($wpdb->options, ['option_value' => $probe_value], ['option_name' => $probe_key], ['%s'], ['%s']); } wp_cache_delete($probe_key, 'options'); wp_cache_delete('alloptions', 'options'); wp_cache_delete('notoptions', 'options'); // Relecture via l'API standard : c'est CE chemin que le plugin utilise partout. $read_api = get_option($probe_key, null); // Relecture SQL directe pour distinguer « écriture perdue » de « cache stale ». // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Diagnostic probe $read_sql = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", $probe_key ) ); // Nettoyage. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Diagnostic probe cleanup $wpdb->delete($wpdb->options, ['option_name' => $probe_key], ['%s']); wp_cache_delete($probe_key, 'options'); if (false === $write || $read_sql !== $probe_value) { return self::result('fail', __('Database writes are failing', '1001web-maintenance-launch-pages'), __('A test value could not be written to the database. Settings and subscriber emails may be silently lost.', '1001web-maintenance-launch-pages'), __('Contact your host — the database user may lack write permissions, or the disk may be full.', '1001web-maintenance-launch-pages'), __('Direct SQL write to wp_options failed or the value did not persist.', '1001web-maintenance-launch-pages') ); } if ($read_api !== $probe_value) { return self::result('warn', __('Object cache is serving stale data', '1001web-maintenance-launch-pages'), __('The database write succeeded but WordPress read back an outdated value. A persistent object cache (Redis/Memcached) is not being invalidated correctly.', '1001web-maintenance-launch-pages'), __('Flush the object cache (e.g. "wp cache flush") or check your Redis drop-in configuration.', '1001web-maintenance-launch-pages'), __('SQL read returned the fresh value but get_option() served a stale one despite wp_cache_delete().', '1001web-maintenance-launch-pages') ); } return self::result('ok', __('Database writes work correctly', '1001web-maintenance-launch-pages'), __('A test value was written, read back and deleted successfully — including through the object cache.', '1001web-maintenance-launch-pages'), '', __('Write/read roundtrip verified via both direct SQL and get_option() after cache purge.', '1001web-maintenance-launch-pages') ); } /** * #3 — La page maintenance est-elle RÉELLEMENT servie (503/200) ? * Self-request avec timeout court. « Non concluant » plutôt que faux échec : * loopback bloqué = courant sur mutualisé, ce n'est PAS une erreur du site. */ private static function check_http503() { $settings = MGENIUS_Settings::load(); if (empty($settings['maintenance_enabled'])) { return self::result('skip', __('Maintenance page delivery', '1001web-maintenance-launch-pages'), __('Maintenance mode is currently OFF — this check runs only while it is ON.', '1001web-maintenance-launch-pages'), '', __('Enable maintenance mode, then run the diagnostic again to verify the HTTP 503 response.', '1001web-maintenance-launch-pages') ); } // Même vérification que celle lancée à l'activation : // page servie, HTTP 503, Retry-After et interdiction de mise en cache. $visitor = MGENIUS_Healthcheck::check_visitor_view(); // Le message de la vérification visiteur dit ce qui a été reçu (défi de // CDN, absence de réponse…) : c'est lui qui est affiché. if ('unknown' === $visitor['status']) { return self::result('warn', __('Maintenance page delivery could not be verified', '1001web-maintenance-launch-pages'), $visitor['message'], __('Open your site in a private/incognito window to confirm visitors see the maintenance page.', '1001web-maintenance-launch-pages'), ($visitor['code'] > 0) /* translators: %d: HTTP status code */ ? sprintf(__('Self-request returned HTTP %d.', '1001web-maintenance-launch-pages'), $visitor['code']) : __('Your server blocked the self-request (loopback) — common on shared hosting. This does not mean the page is broken.', '1001web-maintenance-launch-pages') ); } // Page d'erreur de WordPress au lieu de la page de maintenance. if (!empty($visitor['error_page'])) { return self::result('fail', __('Visitors are NOT seeing the maintenance page', '1001web-maintenance-launch-pages'), $visitor['message'], __('WordPress usually emails the error details to the site administrator. You can also check the PHP error log, or deactivate the plugin or theme you updated last.', '1001web-maintenance-launch-pages'), /* translators: %d: HTTP status code */ sprintf(__('Self-request returned HTTP %d.', '1001web-maintenance-launch-pages'), $visitor['code']) ); } $failed = []; $passed = []; foreach ($visitor['checks'] as $check) { if ($check['ok']) { $passed[] = $check['label']; } else { $failed[] = $check['label']; } } if ('ok' === $visitor['status']) { return self::result('ok', __('The maintenance page is being served correctly', '1001web-maintenance-launch-pages'), /* translators: %s: list of passed checks */ sprintf(__('Checked from your server as an anonymous visitor: %s.', '1001web-maintenance-launch-pages'), implode(' · ', $passed)), '', /* translators: %d: HTTP status code */ sprintf(__('Self-request returned HTTP %d and the response body contains the maintenance markup.', '1001web-maintenance-launch-pages'), $visitor['code']) ); } if ('warn' === $visitor['status']) { return self::result('warn', __('Maintenance page served, but some headers are not as expected', '1001web-maintenance-launch-pages'), /* translators: %s: list of failed checks */ sprintf(__('Not as expected: %s.', '1001web-maintenance-launch-pages'), implode(' · ', $failed)), __('A cache or proxy layer (Cloudflare, server cache) is rewriting the response — exclude your site URL from full-page caching during maintenance.', '1001web-maintenance-launch-pages'), /* translators: %d: HTTP status code */ sprintf(__('Self-request returned HTTP %d.', '1001web-maintenance-launch-pages'), $visitor['code']) ); } return self::result('fail', __('Visitors are NOT seeing the maintenance page', '1001web-maintenance-launch-pages'), /* translators: %d: HTTP status code */ sprintf(__('The self-request received your normal site (HTTP %d) instead of the maintenance page. A cache layer is bypassing the plugin.', '1001web-maintenance-launch-pages'), $visitor['code']), __('Purge all caches (plugin, server, Cloudflare) and exclude your homepage from caching while maintenance is active.', '1001web-maintenance-launch-pages'), __('Response body does not contain the maintenance page markup.', '1001web-maintenance-launch-pages') ); } /** * #4 — Cache Guard (v2.9.179 : integre au plugin, aucun fichier). * Verifie qu'il s'execute avant les plugins de cache connus : WordPress * charge les plugins dans l'ordre de active_plugins. */ private static function check_cache_guard() { $settings = MGENIUS_Settings::load(); $about = __('Built into the plugin. While maintenance is on, it marks every page as not cacheable, sends no-cache headers and empties the cache folders it knows. It runs when WordPress loads this plugin, before cache plugins.', '1001web-maintenance-launch-pages'); if (empty($settings['maintenance_enabled'])) { return self::result('skip', __('Cache Guard', '1001web-maintenance-launch-pages'), __('Maintenance mode is OFF — the Cache Guard only acts while it is ON.', '1001web-maintenance-launch-pages'), '', $about ); } $known = array( 'wp-rocket/wp-rocket.php', 'litespeed-cache/litespeed-cache.php', 'wp-fastest-cache/wpFastestCache.php', 'wp-fastest-cache-premium/wpFastestCachePremium.php', 'w3-total-cache/w3-total-cache.php', 'wp-super-cache/wp-cache.php', 'seraphinite-accelerator/seraphinite-accelerator.php', 'autoptimize/autoptimize.php', 'nitropack/main.php', 'cache-enabler/cache-enabler.php', 'sg-cachepress/sg-cachepress.php', 'breeze/breeze.php', ); $active = array_values((array) get_option('active_plugins', array())); $self = array_search(plugin_basename(MGENIUS_PLUGIN_FILE), $active, true); $before = array(); foreach ($active as $i => $file) { if (false !== $self && $i < $self && in_array($file, $known, true)) { $before[] = $file; } } if (false !== $self && empty($before)) { return self::result('ok', __('Cache Guard is active', '1001web-maintenance-launch-pages'), __('The plugin loads before your cache plugins: while maintenance is on, pages are not cached and the known cache folders are emptied.', '1001web-maintenance-launch-pages'), '', $about ); } return self::result('warn', __('Cache Guard loads after a cache plugin', '1001web-maintenance-launch-pages'), __('A cache plugin is loaded before this plugin, so it may still serve a cached page to some visitors during maintenance.', '1001web-maintenance-launch-pages'), __('Deactivate this plugin, then activate it again: WordPress then loads it first.', '1001web-maintenance-launch-pages'), /* translators: %s: list of plugin files */ sprintf(__('Loaded before: %s', '1001web-maintenance-launch-pages'), implode(', ', $before)) ); } /** * #5 — Plugins de cache actifs (information, pas un échec). */ private static function check_cache_conflicts() { // Seuls les plugins réellement actifs comptent. Les options // qu'un plugin de cache laisse en base après sa désactivation (WP // Fastest Cache, LiteSpeed, SiteGround) le faisaient signaler à tort. $active = (array) get_option('active_plugins', []); if (is_multisite()) { $active = array_merge($active, array_keys((array) get_site_option('active_sitewide_plugins', []))); } $known = [ 'wp-rocket/wp-rocket.php' => 'WP Rocket', 'w3-total-cache/w3-total-cache.php' => 'W3 Total Cache', 'litespeed-cache/litespeed-cache.php' => 'LiteSpeed Cache', 'wp-super-cache/wp-cache.php' => 'WP Super Cache', 'wp-fastest-cache/wpFastestCache.php' => 'WP Fastest Cache', 'sg-cachepress/sg-cachepress.php' => 'SiteGround Optimizer', 'wp-optimize/wp-optimize.php' => 'WP-Optimize', 'cache-enabler/cache-enabler.php' => 'Cache Enabler', 'hummingbird-performance/wp-hummingbird.php' => 'Hummingbird', 'breeze/breeze.php' => 'Breeze', ]; $detected = []; foreach ($known as $file => $name) { if (in_array($file, $active, true)) { $detected[] = $name; } } // Dossier renommé : la constante n'existe que si le plugin est chargé. $runtime = [ 'WP_ROCKET_VERSION' => 'WP Rocket', 'W3TC' => 'W3 Total Cache', 'LSCWP_V' => 'LiteSpeed Cache', 'WPO_VERSION' => 'WP-Optimize', ]; foreach ($runtime as $const => $name) { if (defined($const) && !in_array($name, $detected, true)) { $detected[] = $name; } } if (empty($detected)) { return self::result('ok', __('No cache plugin conflicts', '1001web-maintenance-launch-pages'), __('No known full-page cache plugin was detected.', '1001web-maintenance-launch-pages'), '', __('Checked: WP Rocket, W3TC, LiteSpeed, WP Super Cache, WP Fastest Cache, SiteGround Optimizer, WP-Optimize, Cache Enabler, Hummingbird, Breeze.', '1001web-maintenance-launch-pages') ); } // Un plugin de cache actif n'est pas un problème : c'est le cas que // Cache Guard prend en charge. Le contrôle « Maintenance page // delivery » vérifie, lui, le résultat réel. return self::result('ok', __('Cache plugins handled', '1001web-maintenance-launch-pages'), /* translators: %s: comma-separated plugin names */ sprintf(__('Detected: %s. While maintenance is on, their page cache is emptied and they are asked not to store pages; the settings switched off for maintenance are restored when the site goes back online.', '1001web-maintenance-launch-pages'), implode(', ', $detected)), '', __('The "Maintenance page delivery" check confirms what visitors actually receive.', '1001web-maintenance-launch-pages') ); } /** * #6 — WP-Cron fonctionnel (requis pour la comparaison en tâche de fond après * une fin planifiée ou de compte à rebours ; la planification s'applique à la visite). */ private static function check_cron() { if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) { // v2.9.181 — preuve par la file d'attente : si aucune tâche n'est // en retard de plus de 30 minutes, une tâche cron du serveur appelle // wp-cron.php. Sinon, avertissement (rien ne prouve qu'elle existe). $mgenius_queue = _get_cron_array(); if (is_array($mgenius_queue) && !empty($mgenius_queue)) { $mgenius_oldest = (int) min(array_keys($mgenius_queue)); if ($mgenius_oldest >= time() - 30 * MINUTE_IN_SECONDS) { return self::result('ok', __('Scheduled tasks run through a server cron job', '1001web-maintenance-launch-pages'), __('WP-Cron is disabled in wp-config.php, and no scheduled task is more than 30 minutes late: a cron job on your server runs them.', '1001web-maintenance-launch-pages'), '', __('DISABLE_WP_CRON is true and the task queue is up to date.', '1001web-maintenance-launch-pages') ); } } return self::result('warn', __('WP-Cron is disabled in wp-config.php', '1001web-maintenance-launch-pages'), __('Some scheduled tasks are more than 30 minutes late, so no server cron job seems to call wp-cron.php. Without one, the background page comparison after a scheduled or countdown end will not run.', '1001web-maintenance-launch-pages'), __('Confirm a real cron job calls wp-cron.php every few minutes, or remove DISABLE_WP_CRON.', '1001web-maintenance-launch-pages'), __('DISABLE_WP_CRON is true — WordPress will not trigger cron on page loads.', '1001web-maintenance-launch-pages') ); } $crons = _get_cron_array(); if (empty($crons)) { return self::result('warn', __('No scheduled tasks found', '1001web-maintenance-launch-pages'), __('The WordPress cron queue is empty, which is unusual and may indicate cron is not running.', '1001web-maintenance-launch-pages'), __('Install a cron-checking plugin (WP Crontrol) to inspect, or contact your host.', '1001web-maintenance-launch-pages'), __('_get_cron_array() returned empty.', '1001web-maintenance-launch-pages') ); } return self::result('ok', __('Scheduled tasks are operational', '1001web-maintenance-launch-pages'), __('WP-Cron is enabled and the task queue is populated — scheduling features will run.', '1001web-maintenance-launch-pages'), '', __('Note: the countdown auto-disable triggers on visitor requests, independent of cron.', '1001web-maintenance-launch-pages') ); } /** * #7 — Turnstile : clés présentes + endpoint joignable (si activé). */ private static function check_turnstile() { $settings = MGENIUS_Settings::load(); if (empty($settings['turnstile_enabled'])) { return self::result('skip', __('Cloudflare Turnstile', '1001web-maintenance-launch-pages'), __('Turnstile is disabled — nothing to verify.', '1001web-maintenance-launch-pages'), '', __('Enable Turnstile in the Access tab to protect your forms against bots.', '1001web-maintenance-launch-pages') ); } $site_key = $settings['turnstile_site_key'] ?? ''; $secret_key = $settings['turnstile_secret_key'] ?? ''; if (empty($site_key) || empty($secret_key)) { return self::result('fail', __('Turnstile keys are missing', '1001web-maintenance-launch-pages'), __('Turnstile is enabled but the site key and/or secret key is empty — protected forms will reject every submission.', '1001web-maintenance-launch-pages'), __('Paste both keys from your Cloudflare dashboard (Turnstile section), or disable Turnstile.', '1001web-maintenance-launch-pages'), __('Both turnstile_site_key and turnstile_secret_key must be set for verification to work.', '1001web-maintenance-launch-pages') ); } // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- Turnstile server-side verification: the token can only be validated by Cloudflare. Declared under External Services in readme.txt. $response = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [ 'timeout' => 5, 'body' => ['secret' => $secret_key, 'response' => 'mg-diagnostic-probe'], ]); if (is_wp_error($response)) { return self::result('warn', __('Turnstile endpoint unreachable', '1001web-maintenance-launch-pages'), __('Your server could not reach the Cloudflare verification endpoint. Form submissions may fail while this persists.', '1001web-maintenance-launch-pages'), // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- Plain help text, not a loaded resource __('Check that your host allows outgoing HTTPS requests to challenges.cloudflare.com.', '1001web-maintenance-launch-pages'), /* translators: %s: technical error message */ sprintf(__('wp_remote_post() failed: %s', '1001web-maintenance-launch-pages'), $response->get_error_message()) ); } $body = json_decode(wp_remote_retrieve_body($response), true); $error_codes = isset($body['error-codes']) && is_array($body['error-codes']) ? $body['error-codes'] : []; // Une clé secrète invalide est signalée explicitement par l'API. if (in_array('invalid-input-secret', $error_codes, true)) { return self::result('fail', __('Turnstile secret key is invalid', '1001web-maintenance-launch-pages'), __('Cloudflare rejected your secret key — every protected form submission will fail verification.', '1001web-maintenance-launch-pages'), __('Re-copy the secret key from the Cloudflare dashboard (it differs from the site key).', '1001web-maintenance-launch-pages'), __('siteverify returned invalid-input-secret.', '1001web-maintenance-launch-pages') ); } // invalid-input-response attendu (probe factice) → la clé secrète est acceptée. return self::result('ok', __('Turnstile is configured correctly', '1001web-maintenance-launch-pages'), __('Keys are present and the Cloudflare endpoint accepted your secret key.', '1001web-maintenance-launch-pages'), '', __('The probe intentionally sends a dummy token; only the secret-key validity is asserted.', '1001web-maintenance-launch-pages') ); } /** * #8 — Environnement : PHP/WP, HTTPS, salts. */ private static function check_environment() { global $wp_version; $issues = []; if (version_compare(PHP_VERSION, '7.4', '<')) { /* translators: %s: PHP version */ $issues[] = sprintf(__('PHP %s is end-of-life — upgrade to PHP 8.x for security and speed.', '1001web-maintenance-launch-pages'), PHP_VERSION); } if (version_compare($wp_version, '6.0', '<')) { /* translators: %s: WordPress version */ $issues[] = sprintf(__('WordPress %s is outdated — update to the latest version.', '1001web-maintenance-launch-pages'), $wp_version); } if (!is_ssl()) { $issues[] = __('The site is not served over HTTPS — visitor passwords and access links travel unencrypted.', '1001web-maintenance-launch-pages'); } if (defined('AUTH_KEY') && ('put your unique phrase here' === AUTH_KEY || strlen(AUTH_KEY) < 32)) { $issues[] = __('Security salts in wp-config.php are default or too short — regenerate them.', '1001web-maintenance-launch-pages'); } if (empty($issues)) { return self::result('ok', __('Server environment is healthy', '1001web-maintenance-launch-pages'), /* translators: 1: PHP version, 2: WordPress version */ sprintf(__('PHP %1$s · WordPress %2$s · HTTPS active · security salts set.', '1001web-maintenance-launch-pages'), PHP_VERSION, $wp_version), '', __('Checked: PHP version, WordPress version, SSL, wp-config.php salts.', '1001web-maintenance-launch-pages') ); } return self::result('warn', __('Server environment needs attention', '1001web-maintenance-launch-pages'), implode(' ', $issues), __('Address the points above via your hosting panel or wp-config.php.', '1001web-maintenance-launch-pages'), /* translators: 1: PHP version, 2: WordPress version */ sprintf(__('PHP %1$s · WordPress %2$s', '1001web-maintenance-launch-pages'), PHP_VERSION, $wp_version) ); } /** * Badge contextuel de section (échappé, prêt à imprimer). * Lit UNIQUEMENT le dernier résultat persisté — zéro check live. * @param string $check_id * @return string HTML sûr ('' si le check est ok ou skip) */ public static function section_badge($check_id) { $status = self::get_check_status($check_id); if ('never' === $status) { // v2.9.174 — libellé lisible au lieu d'une icône pâle sans explication. return '🩺 ' . esc_html__('Not checked yet', '1001web-maintenance-launch-pages') . ''; } if ('fail' === $status) { $results = self::get_results(); $fix = $results['checks'][$check_id]['fix'] ?? ''; return '⚠️'; } if ('warn' === $status) { return '⚠️'; } return ''; } // HELPERS /** * Contrat de résultat uniforme. * @param string $status ok|warn|fail|skip * @param string $label Titre néophyte * @param string $detail Explication humaine * @param string $fix Correctif en une phrase (vide si ok/skip) * @param string $tooltip Détail technique (tooltip H2) * @return array */ private static function result($status, $label, $detail, $fix, $tooltip) { return [ 'status' => $status, 'label' => $label, 'detail' => $detail, 'fix' => $fix, 'tooltip' => $tooltip, ]; } /** * Envoi réel du courriel test. * @param string $to * @return bool */ private static function do_send_test_email($to) { $site_name = get_bloginfo('name'); /* translators: %s: site name */ $subject = sprintf(__('[%s] 1001Web Maintenance & Launch Pages — test email', '1001web-maintenance-launch-pages'), $site_name); $message = __("This is a test email sent by the 1001Web Maintenance & Launch Pages diagnostic.\n\nIf you received it, your site can deliver subscriber notifications.", '1001web-maintenance-launch-pages'); $headers = ['Content-Type: text/plain; charset=UTF-8']; return (bool) wp_mail($to, $subject, $message, $headers); } }