__('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);
}
}