> */ public static function detect(): array { return array_values(array_filter([ self::wordPressDatabase(), self::elementor( 'elementor_db_upgrade', 'ELEMENTOR_VERSION', 'elementor_version', 'Elementor', 'elementor' ), self::elementor( 'elementor_pro_db_upgrade', 'ELEMENTOR_PRO_VERSION', 'elementor_pro_version', 'Elementor Pro', 'elementor-pro' ), self::wooCommerce(), ])); } /** * Le seul geste de cette liste que l'agent exécute réellement. * * `wp_upgrade()` est la fonction que `/wp-admin/upgrade.php` appelle lui-même : pas * de tâche de fond, pas d'écran intermédiaire, et le cœur de WordPress la veut * idempotente. D'où `runnable: true` ici et nulle part ailleurs. * * @return array|null */ private static function wordPressDatabase(): ?array { global $wp_db_version; $expected = (int) $wp_db_version; $stored = (int) get_option('db_version', 0); if ($expected <= 0 || $stored <= 0 || $stored >= $expected) { return null; } return [ 'id' => 'wp_db_upgrade', 'type' => 'wp_db_upgrade', 'label' => 'Mise à niveau de la base WordPress', 'source' => 'wordpress', // Le cœur détourne l'administration vers upgrade.php tant que ce n'est pas // fait : ce n'est pas un rappel, c'est un site à moitié arrêté. 'severity' => 'critical', 'runnable' => true, 'reason' => null, 'observed' => ['installed' => (string) $expected, 'stored' => (string) $stored], 'admin_url' => self::adminUrl('upgrade.php'), ]; } /** * Elementor et Elementor Pro tiennent chacun leur version en base et la comparent à * celle du code : c'est le critère de `should_upgrade()`, et il ne dépend d'aucune * plomberie interne — donc il ne cassera pas au prochain remaniement. * * **`runnable` est `false`, et ce n'est pas une lacune.** La mise à niveau des * données Elementor est une tâche de FOND, découpée en lots par Elementor lui-même * parce qu'elle réécrit le contenu des pages. La déclencher hors de son ordonnanceur * reviendrait à parier qu'une requête HTTP de notre côté ira au bout — sur les * hébergements mutualisés qu'on supervise, elle est coupée à 360 s, et une réécriture * de pages interrompue au milieu ne se répare pas avec la sauvegarde de la veille. * On signale, l'humain clique. * * @return array|null */ private static function elementor( string $id, string $versionConstant, string $versionOption, string $label, string $slug ): ?array { if (!defined($versionConstant)) { return null; } $installed = (string) constant($versionConstant); $stored = get_option($versionOption, ''); $stored = is_string($stored) ? $stored : ''; // Une option absente décrit une installation qui n'a pas encore tourné une seule // fois, pas un retard : Elementor l'écrit à sa première initialisation. if ('' === $installed || '' === $stored || version_compare($stored, $installed, '>=')) { return null; } return [ 'id' => $id, 'type' => $id, 'label' => sprintf('Mise à niveau des données %s', $label), 'source' => $slug, 'severity' => 'warning', 'runnable' => false, 'reason' => 'À lancer depuis le tableau de bord du site : Elementor réécrit le ' . 'contenu des pages par lots, et une exécution interrompue à distance ne ' . 'se rattrape pas.', 'observed' => ['installed' => $installed, 'stored' => $stored], 'admin_url' => self::adminUrl(), ]; } /** * WooCommerce publie `needs_db_update()`, donc on le lui demande plutôt que de * refaire son raisonnement. Le repli par comparaison de versions ne sert que si cette * méthode disparaît — et il applique alors le même critère. * * @return array|null */ private static function wooCommerce(): ?array { if (!defined('WC_VERSION')) { return null; } $installed = (string) constant('WC_VERSION'); $stored = get_option('woocommerce_db_version', ''); $stored = is_string($stored) ? $stored : ''; $needs = is_callable(['WC_Install', 'needs_db_update']) ? (bool) \WC_Install::needs_db_update() : ('' !== $stored && '' !== $installed && version_compare($stored, $installed, '<')); if (!$needs) { return null; } return [ 'id' => 'woocommerce_db_upgrade', 'type' => 'woocommerce_db_upgrade', 'label' => 'Mise à niveau de la base WooCommerce', 'source' => 'woocommerce', 'severity' => 'warning', // Même raison qu'Elementor : WooCommerce migre par lots planifiés. 'runnable' => false, 'reason' => 'À lancer depuis le tableau de bord du site : WooCommerce migre ses ' . 'tables par lots planifiés.', 'observed' => ['installed' => $installed, 'stored' => $stored], 'admin_url' => self::adminUrl(), ]; } /** * Le tableau de bord, jamais une page d'extension devinée. * * L'avis qui réclame le geste s'affiche sur TOUTE page d'administration ; viser une * page précise supposerait de connaître le slug de menu d'une extension tierce, qui * n'est ni documenté ni stable. Une URL exacte qui rend 404 coûte plus cher qu'une * URL générique qui marche. */ private static function adminUrl(string $path = ''): ?string { if (!function_exists('admin_url')) { return null; } $url = admin_url($path); return is_string($url) && '' !== $url ? $url : null; } }