settings(); update_option(Settings::OPTION, $settings->toArray(), false); // Anderes Token, andere Organisation oder andere API: Die Kennzahlen gehören dann zu einem anderen Konto. // Weg damit, sonst zeigt das Dashboard bis zum nächsten Abruf (bis zu einer Stunde) fremde Domains. if ($before->token !== $settings->token || $before->organization !== $settings->organization || $before->apiBaseUrl !== $settings->apiBaseUrl) { delete_option(self::STATISTICS_OPTION); } } public function wordpressVersion(): ?string { $version = get_bloginfo('version'); return is_string($version) && $version !== '' ? $version : null; } public function phpVersion(): ?string { return PHP_VERSION; } /** * @return array{php?: array, database?: array, server?: array, settings?: array} */ public function environment(): array { global $wpdb; $serverInfo = is_object($wpdb) && method_exists($wpdb, 'db_server_info') ? $wpdb->db_server_info() : null; return [ 'php' => Environment::php(), 'database' => Environment::database(is_string($serverInfo) ? $serverInfo : null), // im Cron-Aufruf über die Website gesetzt, auf der Kommandozeile nicht – dann fehlt der Webserver eben 'server' => Environment::server($_SERVER['SERVER_SOFTWARE'] ?? null), 'settings' => self::settingsFacts(), ]; } /** * Schalter von WordPress, angelehnt an die Tests der Seite „Website-Zustand“: nur Wahrheitswerte und Zahlen * * @return array */ private static function settingsFacts(): array { $debug = defined('WP_DEBUG') && WP_DEBUG; $crons = function_exists('_get_cron_array') ? _get_cron_array() : []; $next = is_array($crons) && $crons !== [] ? min(array_map('intval', array_keys($crons))) : null; return [ 'debug' => $debug, // WordPress zeigt Fehler nur mit WP_DEBUG an; WP_DEBUG_DISPLAY ist dann von Haus aus an 'debug_display' => $debug && (! defined('WP_DEBUG_DISPLAY') || WP_DEBUG_DISPLAY !== false), 'file_edit' => ! ((defined('DISALLOW_FILE_EDIT') && DISALLOW_FILE_EDIT) || (defined('DISALLOW_FILE_MODS') && DISALLOW_FILE_MODS)), 'core_auto_updates' => self::coreAutoUpdates(), 'plugin_auto_updates' => count((array) get_site_option('auto_update_plugins', [])), 'xmlrpc' => (bool) apply_filters('xmlrpc_enabled', true), 'https' => str_starts_with((string) home_url(), 'https://') && str_starts_with((string) site_url(), 'https://'), 'cron_disabled' => defined('DISABLE_WP_CRON') && DISABLE_WP_CRON, // die älteste fällige Aufgabe wartet länger als eine Stunde: WP-Cron läuft nicht (mehr) 'cron_overdue' => $next !== null && $next < time() - 3600, 'object_cache' => (bool) wp_using_ext_object_cache(), ]; } /** Automatische Updates des Kerns: off, minor (Wartungs- und Sicherheitsversionen) oder all */ private static function coreAutoUpdates(): string { if ((defined('AUTOMATIC_UPDATER_DISABLED') && AUTOMATIC_UPDATER_DISABLED) || apply_filters('automatic_updater_disabled', false)) { return 'off'; } if (defined('WP_AUTO_UPDATE_CORE')) { return match (WP_AUTO_UPDATE_CORE) { false => 'off', 'minor' => 'minor', default => 'all', }; } return get_site_option('auto_update_core_major') === 'enabled' ? 'all' : 'minor'; } /** * @return list */ public function siteUrls(): array { $urls = []; foreach ($this->sites() as $site) { foreach ($site['domains'] as $domain) { $urls[] = $domain; } } return $urls; } /** * Ob diese Website für das ganze Netzwerk meldet: nur die Hauptseite eines Netzwerks. Eine Unterseite meldet nur * sich selbst – ihr Administrator ist nicht der des Netzwerks und soll weder Namen und Adressen der übrigen * Websites noch die Plugin-Inventur des Netzwerks an sein Konto melden (und Adressen aufnehmen) können. Die * Einstellungsseite der Hauptseite verlangt dafür manage_network_options (SettingsPage::capability). */ public static function networkScope(bool $multisite, bool $mainSite): bool { return $multisite && $mainSite; } /** * Was eine Unterseite von den installierten Plugins und Themes meldet: nur, was auf ihr läuft. Installiert ist * im Netzwerk alles für alle; welche Plugins und Versionen es dort gibt, sieht ein Administrator einer Unterseite * in WordPress nicht, und er soll es auch nicht über DomainWarn erfahren. * * @param list $extensions * @return list */ public static function scopedExtensions(array $extensions, bool $networkScope, bool $multisite): array { if ($networkScope || ! $multisite) { return $extensions; } return array_values(array_filter($extensions, static fn (array $extension) => ($extension['active'] ?? false) === true)); } /** * Auf der Hauptseite eines Netzwerks jede Seite, sonst die eine Website. Die Adresse des Backends kommt dazu, falls sie auf einem * anderen Host liegt (WordPress erlaubt das über WP_SITEURL) – sonst bliebe genau der Login unüberwacht. * * Ohne Obergrenze (number 0): Die Vorgabe von get_sites wären 100 Websites, und ab der 101. hätte niemand mehr * gemeldet oder synchronisiert. Gekürzt wird erst die Kanalliste der Innensicht, auf das, was die API annimmt * (InsightPayload::MAX_CHANNELS) – die Hostnamen aller Seiten (siteUrls) bleiben vollständig. * * @return list}> */ public function sites(): array { if (self::networkScope(is_multisite(), is_main_site())) { $out = []; foreach (get_sites(['number' => 0, 'archived' => 0, 'deleted' => 0, 'spam' => 0]) as $site) { $details = get_blog_details($site->blog_id); $home = get_home_url((int) $site->blog_id); $out[] = [ 'name' => is_object($details) && isset($details->blogname) ? (string) $details->blogname : (string) $site->domain, 'domains' => array_values(array_unique(array_filter([is_string($home) ? $home : null]))), ]; } return $out; } $name = get_bloginfo('name'); $domains = array_values(array_unique(array_filter([home_url(), site_url()]))); return [['name' => is_string($name) && $name !== '' ? $name : (string) home_url(), 'domains' => $domains]]; } /** * Plugins und Themes in einer Liste. Themes tragen das Präfix „theme:“, damit ein Theme und ein Plugin * gleichen Namens nicht zusammenfallen; der Klartextname steht daneben im Label. * * @return list */ public function extensions(): array { if (! function_exists('get_plugins')) { require_once ABSPATH.'wp-admin/includes/plugin.php'; } $out = []; $pluginUpdates = $this->updates('update_plugins'); foreach (get_plugins() as $file => $plugin) { $out[] = [ 'name' => (string) $file, 'label' => isset($plugin['Name']) ? (string) $plugin['Name'] : null, 'version' => isset($plugin['Version']) ? (string) $plugin['Version'] : null, 'latest_version' => $pluginUpdates[(string) $file] ?? null, 'active' => is_plugin_active((string) $file), ]; } $themeUpdates = $this->updates('update_themes'); $active = wp_get_theme(); foreach (wp_get_themes() as $stylesheet => $theme) { $out[] = [ 'name' => 'theme:'.$stylesheet, 'label' => (string) $theme->get('Name'), 'version' => (string) $theme->get('Version'), 'latest_version' => $themeUpdates[(string) $stylesheet] ?? null, 'active' => $active->get_stylesheet() === $stylesheet, ]; } return self::scopedExtensions($out, self::networkScope(is_multisite(), is_main_site()), is_multisite()); } /** * Verfügbare Versionen aus den Update-Transients von WordPress. Sie stehen dort, weil WordPress ohnehin * regelmäßig nachfragt – die Erweiterung fragt deshalb nirgends selbst nach. * * @return array */ private function updates(string $transient): array { $data = get_site_transient($transient); $out = []; foreach ((array) ($data->response ?? []) as $key => $update) { $version = is_object($update) ? ($update->new_version ?? null) : ($update['new_version'] ?? null); if (is_string($version) && $version !== '') { $out[(string) $key] = $version; } } return $out; } /** * Absender und Mailserver. Der Absender ist der, den WordPress tatsächlich setzt – inklusive dessen, was * Plugins über die Filter daraus machen. Genau dieser Wert entscheidet, ob SPF und DKIM greifen. * * @return array{transport?: ?string, host?: ?string, sender?: ?string} */ public function mailer(): array { $admin = get_option('admin_email'); $host = wp_parse_url((string) home_url(), PHP_URL_HOST); $default = 'wordpress@'.(is_string($host) ? preg_replace('/^www\./i', '', $host) : 'localhost'); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Filter von WordPress; wir fragen nur, was er liefert. $sender = apply_filters('wp_mail_from', $default); $smtpHost = defined('SMTP_HOST') ? (string) constant('SMTP_HOST') : null; return array_filter([ 'transport' => $smtpHost !== null ? 'smtp' : 'mail', 'host' => $smtpHost, 'sender' => is_string($sender) && $sender !== '' ? $sender : (is_string($admin) ? $admin : null), ], static fn ($value) => $value !== null); } /** Der Wartungsmodus von WordPress: die Datei .maintenance im Wurzelverzeichnis. */ public function maintenance(): bool { return file_exists(ABSPATH.'.maintenance'); } /** * Eine eigene Option statt eines Transients: Ein Transient kann jederzeit verschwinden (Objekt-Cache voll, * Cache geleert), und dann müsste der nächste Abruf der security.txt erst bei DomainWarn fragen. Ob der Stand * frisch ist, sagt sein Zeitstempel. Nicht automatisch geladen: gebraucht wird er nur auf der Einstellungsseite, * im geplanten Lauf und beim Abruf von /.well-known/security.txt. * * @return ?array */ public function hostingSnapshot(): ?array { $raw = get_option(self::HOSTING_OPTION, null); return is_array($raw) ? $raw : null; } public function saveHostingSnapshot(array $snapshot): void { update_option(self::HOSTING_OPTION, $snapshot, false); } /** * Eigene Option, aus demselben Grund wie beim Stand der gehosteten Funktionen: Ein Transient kann jederzeit * verschwinden, und die Übersicht stünde dann leer, bis der nächste Lauf ihn wieder füllt. Nicht automatisch * geladen – gebraucht wird er nur auf dem Dashboard und im geplanten Lauf, nicht bei jedem Seitenaufruf. * * @return ?array */ public function statisticsSnapshot(): ?array { $raw = get_option(self::STATISTICS_OPTION, null); return is_array($raw) ? $raw : null; } public function saveStatisticsSnapshot(array $snapshot): void { update_option(self::STATISTICS_OPTION, $snapshot, false); } }