*/ public static function defaults() { return array( 'mode' => 'auto', 'mail_mode' => 'catch', 'mail_redirect_to' => '', 'mail_keep_days' => self::MAIL_KEEP_DAYS_DEFAULT, 'pause_cron' => true, 'allow_wporg' => true, 'allowed_hosts' => '', 'live_url' => '', 'noindex' => true, 'lock_visitors' => false, 'access_ips' => '', 'access_token' => '', ); } /** * Gespeicherte Einstellungen, immer vollstaendig und typsicher. * * @return array */ public static function get() { $saved = get_option( self::OPTION_KEY, array() ); return self::sanitize( is_array( $saved ) ? $saved : array() ); } /** * Nimmt beliebig geformte Eingaben entgegen und liefert garantiert * vollstaendige Einstellungen zurueck. Ein fehlender Schluessel (Daten * einer aelteren Version) behaelt die Voreinstellung, statt still auf * "aus" zu fallen. * * @param array $input Rohdaten. * @return array */ public static function sanitize( array $input ) { $defaults = self::defaults(); $out = $defaults; $mode = isset( $input['mode'] ) && is_string( $input['mode'] ) ? $input['mode'] : $defaults['mode']; if ( in_array( $mode, self::MODES, true ) ) { $out['mode'] = $mode; } $mail_mode = isset( $input['mail_mode'] ) && is_string( $input['mail_mode'] ) ? $input['mail_mode'] : $defaults['mail_mode']; if ( in_array( $mail_mode, self::MAIL_MODES, true ) ) { $out['mail_mode'] = $mail_mode; } $out['mail_redirect_to'] = self::sanitize_email_address( $input['mail_redirect_to'] ?? '' ); $days = $input['mail_keep_days'] ?? $defaults['mail_keep_days']; $days = is_numeric( $days ) ? (int) $days : $defaults['mail_keep_days']; $out['mail_keep_days'] = max( self::MAIL_KEEP_DAYS_MIN, min( self::MAIL_KEEP_DAYS_MAX, $days ) ); $out['pause_cron'] = self::bool_or_default( $input, 'pause_cron', $defaults['pause_cron'] ); $out['allow_wporg'] = self::bool_or_default( $input, 'allow_wporg', $defaults['allow_wporg'] ); $out['noindex'] = self::bool_or_default( $input, 'noindex', $defaults['noindex'] ); $out['lock_visitors'] = self::bool_or_default( $input, 'lock_visitors', $defaults['lock_visitors'] ); $out['access_ips'] = Wp360_Staging_Guard_Access_Gate::sanitize_ip_list( $input['access_ips'] ?? '' ); $out['access_token'] = isset( $input['access_token'] ) && Wp360_Staging_Guard_Access_Gate::is_valid_token( $input['access_token'] ) ? $input['access_token'] : ''; $out['allowed_hosts'] = self::sanitize_host_list( $input['allowed_hosts'] ?? '' ); $out['live_url'] = self::normalize_live_url( $input['live_url'] ?? '' ); return $out; } /** * Speichert bereinigte Einstellungen und liefert sie zurueck. * * @param array $input Rohdaten. * @return array */ public static function save( array $input ) { $settings = self::sanitize( $input ); // Gespeicherte Schluessel, die diese Variante nicht kennt (die // PRO-Einstellungen, wenn gerade FREE laeuft), bleiben beim Speichern // erhalten -- sonst gingen sie bei einem Wechsel von PRO auf FREE mit // dem ersten Speichern verloren. $stored = get_option( self::OPTION_KEY, array() ); $unknown = is_array( $stored ) ? array_diff_key( $stored, $settings ) : array(); update_option( self::OPTION_KEY, array_merge( $unknown, $settings ) ); // Der Zustand des Requests wurde mit den alten Einstellungen berechnet. if ( class_exists( 'Wp360_Staging_Guard_Environment' ) ) { Wp360_Staging_Guard_Environment::reset(); } return $settings; } /** * Legt die Option beim ersten Aktivieren an und merkt sich dabei die * Adresse der Website als Live-Adresse. Eine vorhandene Option bleibt * unveraendert -- die Live-Adresse wird nie ueberschrieben. */ public static function ensure_defaults() { if ( false !== get_option( self::OPTION_KEY, false ) ) { return; } $settings = self::defaults(); $settings['live_url'] = self::normalize_live_url( home_url() ); add_option( self::OPTION_KEY, $settings, '', true ); } /** * Wahrheitswert aus Formular- oder JSON-Daten; fehlt der Schluessel, * gilt die Voreinstellung. * * @param array $input Rohdaten. * @param string $key Schluessel. * @param bool $default Voreinstellung. * @return bool */ private static function bool_or_default( array $input, $key, $default ) { if ( ! array_key_exists( $key, $input ) ) { return (bool) $default; } return filter_var( $input[ $key ], FILTER_VALIDATE_BOOLEAN ); } /** * Eine einzelne, gueltige E-Mail-Adresse oder ein leerer String. * * @param mixed $value Rohwert. * @return string */ private static function sanitize_email_address( $value ) { if ( ! is_string( $value ) ) { return ''; } $email = sanitize_email( trim( $value ) ); return is_email( $email ) ? $email : ''; } /** * Reduziert eine Adresse auf "Schema://Host[:Port]", ohne Pfad, Query und * Zugangsdaten. Ungueltiges ergibt einen leeren String. * * @param mixed $value Rohwert. * @return string */ public static function normalize_live_url( $value ) { if ( ! is_string( $value ) || '' === trim( $value ) ) { return ''; } $parts = wp_parse_url( trim( $value ) ); if ( ! is_array( $parts ) || empty( $parts['host'] ) || empty( $parts['scheme'] ) ) { return ''; } $scheme = strtolower( $parts['scheme'] ); if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) { return ''; } $host = self::normalize_host_pattern( rawurldecode( $parts['host'] ) ); if ( '' === $host || 0 === strpos( $host, '*.' ) ) { return ''; } $port = isset( $parts['port'] ) ? (int) $parts['port'] : 0; return $scheme . '://' . $host . ( $port > 0 ? ':' . $port : '' ); } /** * Zeilenweise Liste von Hosts oder Platzhaltern (`*.beispiel.de`). * Ungueltiges und Doppeltes faellt weg, die Zahl der Zeilen ist begrenzt. * * @param mixed $value Text mit einem Eintrag je Zeile (oder Array). * @return string Bereinigte Zeilen, durch "\n" getrennt. */ public static function sanitize_host_list( $value ) { if ( is_array( $value ) ) { $lines = $value; } else { $lines = preg_split( '/[\r\n,;]+/', (string) $value ); } $out = array(); foreach ( (array) $lines as $line ) { if ( ! is_string( $line ) ) { continue; } $host = self::normalize_host_pattern( $line ); if ( '' !== $host && ! in_array( $host, $out, true ) ) { $out[] = $host; } if ( count( $out ) >= self::MAX_HOSTS ) { break; } } return implode( "\n", $out ); } /** * Normalisiert einen Host oder Platzhalter: Kleinschreibung, ohne Schema, * Pfad und Port. Ein Platzhalter (`*.`) braucht mindestens zwei weitere * Namensteile, damit `*.de` oder `*` nie eine ganze Endung freigeben. * * @param string $input Eingabe, z. B. "https://API.Beispiel.de/v1". * @return string Host, Platzhalter oder leer, wenn ungueltig. */ public static function normalize_host_pattern( $input ) { $input = trim( (string) $input ); if ( '' === $input || strlen( $input ) > 300 || false !== strpos( $input, '\\' ) ) { return ''; } if ( false !== strpos( $input, '://' ) ) { $host = wp_parse_url( $input, PHP_URL_HOST ); $input = is_string( $host ) ? $host : ''; } else { $input = (string) preg_replace( '/[\/?#].*$/', '', $input ); $input = (string) preg_replace( '/:\d+$/', '', $input ); } $input = rtrim( strtolower( $input ), '.' ); if ( '' === $input || strlen( $input ) > 253 ) { return ''; } $wildcard = false; if ( 0 === strpos( $input, '*.' ) ) { $wildcard = true; $input = substr( $input, 2 ); } // Umlaut-Domains in Punycode wandeln, damit sie wie der HTTP-Client // verglichen werden (nur wenn die Hilfsklasse geladen ist). if ( class_exists( 'Wp360_Staging_Guard_Host_Matcher' ) ) { $input = Wp360_Staging_Guard_Host_Matcher::to_ascii( $input ); } $label = '[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?'; $valid = $wildcard ? preg_match( '/^' . $label . '(?:\.' . $label . ')+$/', $input ) : preg_match( '/^' . $label . '(?:\.' . $label . ')*$/', $input ); if ( ! $valid ) { return ''; } return ( $wildcard ? '*.' : '' ) . $input; } }