Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <?php
- /**
- * Plugin Name: Sentinel Plugins Essentiels for WordPress (MU)
- * Description: MU-plugin résilient qui surveille, réactive et alerte sur l’état des plugins essentiels. Maintenance-aware, logs détaillés, rotation, rate limiting, cron.
- * Version: 1.0.0
- * Author: @loran750 CoeurDuWeb.com
- *
- * --------------------------------------------------------------------------------
- *
- * -- RÔLE DU PLUGIN --
- *
- * "Sentinel" est un gardien indésactivable pour vos plugins WordPress critiques.
- * Il surveille en permanence leur état, les réactive automatiquement s'ils sont désactivés
- * et vous alerte par email en cas de problème, tout en tenant un journal d'événements détaillé.
- *
- * -- FONCTIONNALITÉS PRINCIPALES --
- *
- * - Surveillance Multi-Vecteurs : Le check s'exécute au chargement (muplugins_loaded),
- * périodiquement (cron), en fin de script (shutdown) et lors de modifications.
- * - Réactivation Automatique : Tente de réactiver immédiatement tout plugin surveillé trouvé inactif.
- * - Logs Détaillés & Séparés : Crée des logs pour les actions (sentinel-plugins.log) et les
- * emails (sentinel-mails.log) dans /wp-content/logs/, avec rotation automatique.
- * - Emails d'Alerte avec Anti-Spam (Rate Limiting) : Prévient l'inondation de votre boîte
- * mail en espaçant les alertes de même type (15 min par défaut).
- * - Ignore le Mode Maintenance : Se met en pause durant les mises à jour de WordPress pour
- * éviter tout conflit.
- * - Endpoint de Diagnostic JSON : Fournit une URL pour un bilan de santé instantané.
- *
- * -- CONFIGURATION RAPIDE --
- *
- * 1. Placer ce fichier dans le dossier /wp-content/mu-plugins/ (créez-le si besoin).
- * 2. Adapter la section // ===== CONFIG ===== ci-dessous (liste des plugins, email...).
- *
- * -- UTILISATION & TESTS --
- *
- * - DIAGNOSTIC RAPIDE (ENDPOINT) :
- * Connectez-vous en tant qu'administrateur et visitez l'URL suivante :
- * https://votresite.com/?sentinel_status=1
- * Le statut (existence et activation) de chaque plugin surveillé s'affichera en JSON.
- *
- * - DÉSACTIVATION VOLONTAIRE D'UN PLUGIN :
- * 1. Éditez ce fichier (via FTP/SFTP).
- * 2. Commentez la ligne du plugin à désactiver dans la section CONFIG (ajoutez // au début).
- * 3. Désactivez le plugin depuis l'administration WordPress.
- * 4. IMPORTANT : Après votre intervention, n'oubliez pas de réactiver le plugin et de
- * retirer le commentaire (//) pour reprendre la surveillance.
- *
- * - TEST DE RÉACTIVITÉ :
- * Pour vérifier que tout fonctionne, désactivez un plugin surveillé depuis l'admin.
- * Il doit se réactiver seul quasi-instantanément. Vérifiez ensuite les logs générés
- * dans le dossier /wp-content/logs/.
- *
- * --------------------------------------------------------------------------------
- */
- if (!defined('ABSPATH')) { exit; }
- if (!class_exists('Sentinel_Plugins_Essentiels')) {
- final class Sentinel_Plugins_Essentiels {
- // ===== CONFIG =====
- private $config = [
- // Slugs exacts "dossier/fichier.php" => "Nom"
- 'essential_plugins' => [
- 'elementor/elementor.php' => 'Elementor',
- 'elementor-pro/elementor-pro.php' => 'Elementor Pro',
- 'jet-engine/jet-engine.php' => 'JetEngine',
- 'astra-addon/astra-addon.php' => 'Astra Pro',
- ],
- // Emails
- 'email_to' => null, // null = admin_email
- 'email_prefix' => '[Alerte Sentinelle : '.parse_url(home_url(), PHP_URL_HOST).']',
- 'enable_email' => true,
- 'mail_rate_limit_seconds' => 900, // 15 min par type d’alerte
- // Logging
- 'log_dir_primary' => WP_CONTENT_DIR . '/logs',
- 'log_dir_fallback' => null, // Défini vers uploads/logs dynamiquement
- 'log_file' => 'sentinel-plugins.log',
- 'mail_log_file' => 'sentinel-mails.log',
- 'log_max_size_bytes' => 5 * 1024 * 1024, // 5 Mo
- // Réaction
- 'enable_auto_reactivate' => true,
- // Cron healthcheck
- 'enable_cron' => true,
- 'cron_recurrence' => 'hourly', // hourly, twicedaily, daily
- // Multisite
- 'multisite_scope_network' => true, // surveiller activations réseau
- ];
- // ===== FIN CONFIG =====
- private $cid;
- private $log_dir;
- private $cron_hook = 'sentinel_plugins_healthcheck';
- private $last_mail_option = 'sentinel_last_mail_times';
- public function __construct() {
- $this->cid = $this->correlation_id();
- $this->prepare_log_dir();
- // Scans principaux
- add_action('muplugins_loaded', [$this, 'early_scan'], 1);
- add_action('shutdown', [$this, 'shutdown_scan'], 999);
- add_action('init', [$this, 'check_paused_plugins'], 20);
- // Surveiller les changements d’active_plugins
- add_action('update_option_active_plugins', [$this, 'on_active_plugins_change'], 10, 3);
- // Multisite réseau
- if (is_multisite() && $this->config['multisite_scope_network']) {
- add_action('update_site_option_active_sitewide_plugins', [$this, 'on_network_plugins_change'], 10, 3);
- }
- // Cron healthcheck périodique
- if ($this->config['enable_cron']) {
- add_action($this->cron_hook, [$this, 'periodic_healthcheck']);
- $this->ensure_cron();
- }
- // Endpoint de statut simple (admin uniquement)
- add_action('init', [$this, 'status_endpoint'], 1);
- }
- // ===== Utils =====
- private function correlation_id() {
- try {
- $r = function_exists('random_bytes') ? bin2hex(random_bytes(4)) : substr(md5(mt_rand()), 0, 8);
- } catch (Throwable $e) {
- $r = substr(md5(mt_rand()), 0, 8);
- }
- return strtoupper($r);
- }
- private function prepare_log_dir() {
- if (!$this->config['log_dir_fallback'] && function_exists('wp_upload_dir')) {
- $up = @wp_upload_dir();
- if (is_array($up) && !empty($up['basedir'])) {
- $this->config['log_dir_fallback'] = trailingslashit($up['basedir']) . 'logs';
- }
- }
- $candidates = [$this->config['log_dir_primary'], $this->config['log_dir_fallback']];
- foreach ($candidates as $dir) {
- if (!$dir) continue;
- if (!is_dir($dir)) { @wp_mkdir_p($dir); }
- if (is_dir($dir) && is_writable($dir)) { $this->log_dir = $dir; break; }
- }
- if (!$this->log_dir && is_writable(WP_CONTENT_DIR)) {
- $this->log_dir = WP_CONTENT_DIR;
- }
- }
- private function log_path_main() { return trailingslashit($this->log_dir) . $this->config['log_file']; }
- private function log_path_mail() { return trailingslashit($this->log_dir) . $this->config['mail_log_file']; }
- private function rotate_if_needed($file) {
- if (!$file || !@file_exists($file)) return;
- clearstatcache(true, $file);
- $size = @filesize($file);
- if ($size !== false && $size >= $this->config['log_max_size_bytes']) {
- @rename($file, $file . '.' . date('Ymd-His'));
- }
- }
- private function log($message, $context = [], $is_mail_log = false) {
- $time = function_exists('wp_date') ? wp_date('Y-m-d H:i:s') : date('Y-m-d H:i:s');
- $url = $_SERVER['REQUEST_URI'] ?? '-';
- $ip = $_SERVER['REMOTE_ADDR'] ?? (php_sapi_name() ?: 'CLI');
- $ua = $_SERVER['HTTP_USER_AGENT'] ?? '-';
- $user = 'anon';
- if (function_exists('wp_get_current_user')) {
- $u = @wp_get_current_user();
- if ($u && $u->ID) $user = $u->user_login . " (ID {$u->ID})";
- }
- $ctx = '';
- if (is_array($context) && !empty($context)) {
- $safe = [];
- foreach ($context as $k => $v) { $safe[$k] = is_scalar($v) ? $v : @json_encode($v, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); }
- $ctx = ' | ' . json_encode($safe, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
- }
- $line = "[$time] [CID:{$this->cid}] user=$user ip=$ip url=$url ua=\"$ua\" :: $message$ctx" . PHP_EOL;
- $file = $is_mail_log ? $this->log_path_mail() : $this->log_path_main();
- if ($this->log_dir) {
- $this->rotate_if_needed($file);
- if (@file_put_contents($file, $line, FILE_APPEND | LOCK_EX) === false) { @error_log('[sentinel] ' . $line); }
- } else {
- @error_log('[sentinel] ' . $line);
- }
- }
- private function mail_rate_allowed($key) {
- $limit = (int)$this->config['mail_rate_limit_seconds'];
- if ($limit <= 0) return true;
- $map = get_option($this->last_mail_option, []);
- $last = isset($map[$key]) ? (int)$map[$key] : 0;
- if ((time() - $last) < $limit) return false;
- $map[$key] = time();
- update_option($this->last_mail_option, $map, false);
- return true;
- }
- private function send_mail($subject, $body, $context = [], $rate_key = null) {
- $to = $this->config['email_to'] ?? get_option('admin_email');
- $site = function_exists('home_url') ? home_url() : ($_SERVER['HTTP_HOST'] ?? 'site');
- $subject_full = trim($this->config['email_prefix'] . ' ' . $subject);
- $body_full = "Site: $site\nCID: {$this->cid}\nTime: " . (function_exists('wp_date') ? wp_date('c') : date('c')) . "\n\n" . $body;
- $sent = false; $err = null;
- if ($this->config['enable_email'] && function_exists('wp_mail') && !empty($to)) {
- $rk = $rate_key ?: md5($subject_full);
- if ($this->mail_rate_allowed($rk)) {
- try {
- $sent = @wp_mail($to, $subject_full, $body_full);
- } catch (Throwable $e) { $err = 'Throwable: ' . $e->getMessage(); }
- } else { $err = 'Rate-limited'; }
- } else { $err = 'Email disabled or unavailable'; }
- $this->log('Mail attempt: ' . ($sent ? 'SENT' : 'FAILED') . " | subj=\"$subject_full\" to=$to", ['error' => $err, 'context' => $context], true);
- return $sent;
- }
- private function is_plugin_active_any($plugin_file) {
- if (!function_exists('is_plugin_active')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; }
- if (is_multisite() && function_exists('is_plugin_active_for_network') && @is_plugin_active_for_network($plugin_file)) { return true; }
- return @is_plugin_active($plugin_file);
- }
- private function activate_safely($plugin_file) {
- require_once ABSPATH . 'wp-admin/includes/plugin.php';
- try {
- $res = @activate_plugin($plugin_file, '', false, true);
- if (is_wp_error($res)) {
- $this->log("Activation error for $plugin_file", ['wp_error' => $res->get_error_message()]);
- return $res;
- }
- return true;
- } catch (Throwable $e) {
- $this->log("Activation throwable for $plugin_file", ['ex' => $e->getMessage()]);
- return new WP_Error('sentinel_activation_throwable', $e->getMessage());
- }
- }
- // ===== Scans =====
- public function early_scan() {
- if (file_exists(ABSPATH . '.maintenance')) { return; }
- $ess = $this->config['essential_plugins'];
- if (empty($ess)) return;
- $missing = []; $inactive = [];
- foreach ($ess as $file => $name) {
- if (!file_exists(WP_PLUGIN_DIR . '/' . $file)) { $missing[$file] = $name; continue; }
- if (!$this->is_plugin_active_any($file)) { $inactive[$file] = $name; }
- }
- if ($missing) {
- $lines = []; foreach ($missing as $f => $n) $lines[] = "$n ($f)";
- $msg = 'Plugins manquants: ' . implode(', ', $lines);
- $this->log($msg);
- $this->send_mail('Plugins essentiels manquants', $msg . "\nAction requise: restaurer les fichiers.", [], 'missing');
- }
- if ($inactive) {
- $lines = []; foreach ($inactive as $f => $n) $lines[] = "$n ($f)";
- $msg = 'Plugins inactifs détectés: ' . implode(', ', $lines);
- $this->log($msg);
- if ($this->config['enable_auto_reactivate']) {
- foreach ($inactive as $f => $n) {
- $res = $this->activate_safely($f);
- if ($res === true) { $this->log("Réactivation automatique OK: $n ($f)"); }
- else { $err = is_wp_error($res) ? $res->get_error_message() : 'unknown'; $this->log("Réactivation automatique ÉCHEC: $n ($f)", ['error' => $err]); }
- }
- $this->send_mail('Réactivation automatique effectuée', $msg . "\nVérifiez le fonctionnement du site.", [], 'reactivated');
- } else {
- $this->send_mail('Plugins essentiels inactifs', $msg . "\nAuto-réactivation désactivée.", [], 'inactive');
- }
- }
- }
- public function shutdown_scan() {
- $ess = $this->config['essential_plugins'];
- $missing = [];
- foreach ($ess as $file => $name) { if (!file_exists(WP_PLUGIN_DIR . '/' . $file)) $missing[$file] = $name; }
- if ($missing) {
- $lines = []; foreach ($missing as $f => $n) $lines[] = "$n ($f)";
- $msg = 'Plugins manquants détectés en shutdown: ' . implode(', ', $lines);
- $this->log($msg);
- $this->send_mail('Plugins essentiels manquants (shutdown)', $msg, [], 'missing_shutdown');
- }
- }
- public function on_active_plugins_change($old, $new, $option) {
- $removed = array_values(array_diff((array)$old, (array)$new));
- $added = array_values(array_diff((array)$new, (array)$old));
- if (!$removed && !$added) return;
- $stack = function_exists('debug_backtrace') ? @debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10) : [];
- $frames = []; foreach ((array)$stack as $f) { $frames[] = (isset($f['class']) ? $f['class'].'::' : '') . ($f['function'] ?? ''); }
- $user = 'anon'; if (function_exists('wp_get_current_user')) { $u = @wp_get_current_user(); if ($u && $u->ID) $user = $u->user_login . " (ID {$u->ID})"; }
- $msg = "Modification active_plugins; +[" . implode(', ', $added) . "] -[" . implode(', ', $removed) . "]; by=$user; trace=" . implode(' > ', array_filter($frames));
- $this->log($msg);
- $this->send_mail('Modification de la liste des plugins actifs', $msg, [], 'active_plugins_change');
- }
- public function check_paused_plugins() {
- try {
- $paused = get_option('paused_plugins');
- if (is_array($paused) && !empty($paused)) {
- $ess = array_keys($this->config['essential_plugins']);
- $paused_ess = array_values(array_intersect($ess, array_keys($paused)));
- if (!empty($paused_ess)) {
- $this->log('Plugins en pause (Recovery Mode)', ['paused' => $paused_ess]);
- $this->send_mail('Plugins essentiels en pause (Recovery Mode)', "Plugins: " . implode(', ', $paused_ess) . "\nVérifiez debug.log.", [], 'paused');
- }
- }
- } catch (Throwable $e) { /* silence */ }
- }
- public function on_network_plugins_change($old, $new, $option) {
- $old_keys = array_keys((array)$old); $new_keys = array_keys((array)$new);
- $removed = array_values(array_diff($old_keys, $new_keys));
- $added = array_values(array_diff($new_keys, $old_keys));
- if (!$removed && !$added) return;
- $msg = "Modification plugins réseau; +[" . implode(', ', $added) . "] -[" . implode(', ', $removed) . "]";
- $this->log($msg);
- $this->send_mail('Modification des plugins réseau', $msg, [], 'network_change');
- }
- public function periodic_healthcheck() {
- if (file_exists(ABSPATH . '.maintenance')) { return; }
- $this->log('Cron healthcheck démarré');
- $this->early_scan();
- $this->log('Cron healthcheck terminé');
- }
- private function ensure_cron() {
- if (!function_exists('wp_next_scheduled') || !function_exists('wp_schedule_event')) return;
- if (!wp_next_scheduled($this->cron_hook)) {
- @wp_schedule_event(time(), $this->config['cron_recurrence'], $this->cron_hook);
- }
- }
- public function status_endpoint() {
- if (isset($_GET['sentinel_status']) && current_user_can('manage_options')) {
- $ess = $this->config['essential_plugins'];
- $status = [];
- foreach ($ess as $file => $name) {
- $exists = file_exists(WP_PLUGIN_DIR . '/' . $file);
- $active = $exists ? $this->is_plugin_active_any($file) : false;
- $status[] = [ 'plugin' => $name, 'file' => $file, 'exists' => (int)$exists, 'active' => (int)$active ];
- }
- header('Content-Type: application/json; charset=utf-8');
- echo json_encode(['cid' => $this->cid, 'time' => (function_exists('wp_date') ? wp_date('c') : date('c')), 'log_dir' => $this->log_dir, 'status' => $status], JSON_PRETTY_PRINT);
- exit;
- }
- }
- } // fin classe
- // Instanciation
- new Sentinel_Plugins_Essentiels();
- } // fin if class_exists guard
Advertisement
Add Comment
Please, Sign In to add comment