= self::MAX_IPS ) { break; } } return implode( "\n", $out ); } /** * @param string $entry Adresse oder CIDR. * @return bool */ public static function is_valid_ip_entry( $entry ) { $entry = (string) $entry; if ( false === strpos( $entry, '/' ) ) { return false !== filter_var( $entry, FILTER_VALIDATE_IP ); } list( $address, $bits ) = explode( '/', $entry, 2 ); if ( false === filter_var( $address, FILTER_VALIDATE_IP ) || ! ctype_digit( $bits ) ) { return false; } $max = false !== strpos( $address, ':' ) ? 128 : 32; return (int) $bits >= 1 && (int) $bits <= $max; } /** * @param string $ip Besucher-Adresse. * @param string $list Bereinigte Zeilen aus sanitize_ip_list(). * @return bool */ public static function ip_matches( $ip, $list ) { $ip_bin = false !== filter_var( $ip, FILTER_VALIDATE_IP ) ? inet_pton( (string) $ip ) : false; if ( false === $ip_bin || '' === trim( (string) $list ) ) { return false; } foreach ( explode( "\n", (string) $list ) as $entry ) { if ( ! self::is_valid_ip_entry( $entry ) ) { continue; } $bits = false === strpos( $entry, '/' ) ? null : (int) substr( $entry, (int) strpos( $entry, '/' ) + 1 ); $net_bin = inet_pton( null === $bits ? $entry : (string) substr( $entry, 0, (int) strpos( $entry, '/' ) ) ); if ( false === $net_bin || strlen( $ip_bin ) !== strlen( $net_bin ) ) { continue; } if ( null === $bits ) { if ( $ip_bin === $net_bin ) { return true; } continue; } $full = intdiv( $bits, 8 ); $rest = $bits % 8; if ( 0 !== strncmp( $ip_bin, $net_bin, $full ) ) { continue; } if ( 0 === $rest ) { return true; } $mask = ( 0xFF << ( 8 - $rest ) ) & 0xFF; if ( ( ord( $ip_bin[ $full ] ) & $mask ) === ( ord( $net_bin[ $full ] ) & $mask ) ) { return true; } } return false; } /** * Cookie-Wert "."; die Signatur bindet Schluessel und * Ablaufzeit, ein neuer Schluessel macht alte Cookies ungueltig. * * @param string $token Gespeicherter Schluessel. * @param int $expires Ablauf als Zeitstempel. * @param string $salt Geheimnis der Installation. * @return string */ public static function cookie_value( $token, $expires, $salt ) { $expires = (int) $expires; return $expires . '.' . hash_hmac( 'sha256', 'sg|' . $token . '|' . $expires, (string) $salt ); } /** * @param string $cookie Wert aus dem Cookie. * @param string $token Gespeicherter Schluessel. * @param string $salt Geheimnis der Installation. * @param int $now Aktuelle Zeit. * @return bool */ public static function cookie_is_valid( $cookie, $token, $salt, $now ) { if ( ! self::is_valid_token( $token ) || 1 !== preg_match( '/^(\d{1,12})\.([a-f0-9]{64})$/', (string) $cookie, $m ) ) { return false; } if ( (int) $m[1] <= (int) $now ) { return false; } return hash_equals( self::cookie_value( $token, (int) $m[1], $salt ), (string) $cookie ); } /** * Darf die aktuelle Anfrage die gesperrte Kopie sehen? * * @param array $settings Bereinigte Einstellungen. * @return bool */ public static function allows( array $settings ) { // Fail-open: Wer die Einstellungen verwalten darf, wird nie ausgesperrt. if ( is_user_logged_in() && current_user_can( 'manage_options' ) ) { return true; } if ( ! empty( $settings['access_ips'] ) && self::ip_matches( self::client_ip(), (string) $settings['access_ips'] ) ) { return true; } if ( isset( $_COOKIE[ self::COOKIE_NAME ] ) && is_string( $_COOKIE[ self::COOKIE_NAME ] ) ) { return self::cookie_is_valid( sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE_NAME ] ) ), (string) $settings['access_token'], wp_salt( 'auth' ), time() ); } return false; } /** * Verbindungsadresse des Besuchers. Proxy-Header werden bewusst nicht * gelesen, weil der Besucher sie selbst mitschicken koennte; hinter einem * Proxy oder CDN hilft der Zugangslink. * * @return string Gueltige IP oder leerer String. */ private static function client_ip() { $remote = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( (string) $_SERVER['REMOTE_ADDR'] ) ) : ''; return false !== filter_var( $remote, FILTER_VALIDATE_IP ) ? $remote : ''; } /** * Kontexte, in denen nie gesperrt wird. * * @return bool */ private static function is_exempt_context() { if ( is_admin() || wp_doing_ajax() || wp_doing_cron() || wp_installing() ) { return true; } if ( defined( 'WP_CLI' ) && WP_CLI ) { return true; } return function_exists( 'is_customize_preview' ) && is_customize_preview(); } /** * Frontend, Feeds und Sitemaps: Absage statt Inhalt. */ public static function maybe_block_request() { if ( self::is_exempt_context() || ! self::is_enabled() ) { return; } // robots.txt (verbietet ohnehin alles) und Favicon bleiben erreichbar. if ( ( function_exists( 'is_robots' ) && is_robots() ) || ( function_exists( 'is_favicon' ) && is_favicon() ) ) { return; } if ( self::allows( Wp360_Staging_Guard_Settings::get() ) ) { self::keep_out_of_page_caches(); return; } if ( ! headers_sent() ) { header( 'X-Robots-Tag: noindex, nofollow' ); } wp_die( esc_html__( 'This website is a protected copy and is not open to the public.', '360-orbit-staging-guard' ), esc_html__( 'Access restricted', '360-orbit-staging-guard' ), array( 'response' => 403 ) ); } /** * Eine durchgelassene Seite darf nicht in einen Seiten-Cache gelangen und * von dort an gesperrte Besucher ausgeliefert werden. */ private static function keep_out_of_page_caches() { if ( ! defined( 'DONOTCACHEPAGE' ) ) { define( 'DONOTCACHEPAGE', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Konstante ist Konvention der Cache-Plugins. } if ( ! headers_sent() ) { nocache_headers(); } } /** * REST: anonyme Anfragen erhalten 403. * * @param mixed $result Bisheriges Ergebnis der Authentifizierung. * @return mixed */ public static function maybe_block_rest( $result ) { if ( is_wp_error( $result ) || self::is_exempt_context() || ! self::is_enabled() ) { return $result; } if ( self::allows( Wp360_Staging_Guard_Settings::get() ) ) { return $result; } return new WP_Error( 'wp360_sg_locked', __( 'This website is a protected copy and is not open to the public.', '360-orbit-staging-guard' ), array( 'status' => 403 ) ); } /** * Zugangslink: ?wp360_sg_key=... setzt das Cookie und leitet auf die * Adresse ohne Parameter weiter. Ein falscher Schluessel tut nichts. */ public static function handle_access_key() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Zugangslink ohne Sitzung, der Schluessel selbst ist das Geheimnis. if ( ! isset( $_GET[ self::QUERY_ARG ] ) || is_admin() || wp_doing_cron() || ! self::is_enabled() ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $key = is_string( $_GET[ self::QUERY_ARG ] ) ? sanitize_text_field( wp_unslash( $_GET[ self::QUERY_ARG ] ) ) : ''; $token = (string) Wp360_Staging_Guard_Settings::get()['access_token']; if ( ! self::is_valid_token( $token ) || ! hash_equals( $token, $key ) ) { return; } $expires = time() + self::COOKIE_DAYS * DAY_IN_SECONDS; $value = self::cookie_value( $token, $expires, wp_salt( 'auth' ) ); if ( ! headers_sent() ) { setcookie( self::COOKIE_NAME, $value, array( 'expires' => $expires, 'path' => COOKIEPATH ? COOKIEPATH : '/', 'domain' => COOKIE_DOMAIN, 'secure' => is_ssl(), 'httponly' => true, 'samesite' => 'Lax', ) ); } nocache_headers(); wp_safe_redirect( remove_query_arg( self::QUERY_ARG ), 302 ); exit; } }