false, 'protect_all_sites' => false, 'secret' => $secret, 'pending_secret' => '', 'generation' => 1, 'exempt_roles' => array(), 'updated_at' => time(), ); if ( false === $current ) { add_network_option( $network_id, self::OPTION_NAME, $settings ); } else { self::save_settings( $settings, self::normalize_settings( $current ) ); } } /** * Handles a request to the network gate URL. * * @return void */ public static function handle_gate_request() { $candidate = self::gate_candidate_from_request(); if ( null === $candidate ) { return; } $settings = self::get_settings(); if ( ! self::is_network_main_site() && ! $settings['protect_all_sites'] ) { self::reject_request(); } if ( ! MisticGate::is_valid_secret( $candidate ) || ! MisticGate::is_valid_secret( $settings['secret'] ) || ! hash_equals( $settings['secret'], $candidate ) ) { self::reject_request(); } if ( MisticGate::exempts_current_user() ) { self::redirect( self::current_destination_url(), 302 ); } if ( MisticGate::is_emergency_disabled() || ! $settings['enabled'] ) { self::redirect( self::current_login_url(), 302 ); } $method = self::request_method(); if ( 'GET' === $method ) { self::render_post_bridge(); } if ( 'POST' !== $method ) { self::reject_request(); } $ticket = self::issue_ticket( $settings['secret'], $settings['generation'] ); if ( false === $ticket || ! self::set_cookie( self::ticket_cookie_name(), $ticket['value'], $ticket['expires'] ) ) { if ( is_array( $ticket ) ) { delete_network_option( get_current_network_id(), self::ticket_option_name( $ticket['nonce'] ) ); } self::reject_request(); } self::redirect( self::current_login_url(), 303 ); } /** * Checks whether the network gate allows the login request. * * @param string $method Request method. * * @return bool Whether the request is allowed. */ public static function authorize_login( $method ) { if ( MisticGate::is_emergency_disabled() || ! self::protects_current_site() ) { return false; } $settings = self::get_settings(); $generation = self::token_generation( $settings['secret'], $settings['generation'] ); $key = self::signing_key( $settings['secret'], $settings['generation'] ); if ( self::has_valid_pass() ) { return true; } self::clear_cookie( self::pass_cookie_name() ); if ( 'GET' !== $method ) { return false; } $ticket_value = self::read_cookie( self::ticket_cookie_name() ); $ticket_payload = MisticGate_Token::verify( $ticket_value, 'ticket', self::TICKET_TTL, $generation, $key ); self::clear_cookie( self::ticket_cookie_name() ); if ( false === $ticket_payload || ! self::consume_ticket( $ticket_payload ) ) { return false; } try { $login_pass = MisticGate_Token::create( 'pass', self::PASS_TTL, $generation, $key ); } catch ( Exception $exception ) { return false; } self::$login_pass_issued = self::set_cookie( self::pass_cookie_name(), $login_pass['value'], $login_pass['expires'] ); return self::$login_pass_issued; } /** * Checks whether the network gate protects the current site. * * @return bool Whether the site is protected. */ public static function protects_current_site() { return self::protects_site( get_current_blog_id() ); } /** * Checks whether the network gate protects a site. * * @param int $site_id Site ID. * @param array|null $settings Network settings already read in this request, or null to read them. * * @return bool Whether the site is protected. */ public static function protects_site( $site_id, $settings = null ) { if ( ! is_multisite() || MisticGate::is_emergency_disabled() || ! MisticGate::is_active_on_site( $site_id ) ) { return false; } $settings = null === $settings ? self::get_settings() : $settings; return $settings['enabled'] && ( get_main_site_id( get_current_network_id() ) === (int) $site_id || $settings['protect_all_sites'] ); } /** * Checks whether the plugin is network activated. * * @return bool Whether the plugin is network activated. */ public static function is_network_active() { if ( ! is_multisite() ) { return false; } $plugins = get_network_option( get_current_network_id(), 'active_sitewide_plugins', array() ); return is_array( $plugins ) && isset( $plugins[ plugin_basename( MISTICGATE_MAIN_FILE ) ] ); } // Read live configuration in bounded batches; never cache an all-clear across requests. /** * Finds the first site that no gate protects. * * @return int Site ID, or 0 when every site is protected. */ public static function first_unprotected_site_id() { if ( ! is_multisite() ) { return 0; } $settings = self::get_settings(); if ( ! MisticGate::is_emergency_disabled() && self::is_network_active() && $settings['enabled'] && $settings['protect_all_sites'] ) { return 0; } $offset = 0; do { $site_ids = get_sites( array( 'network_id' => get_current_network_id(), 'fields' => 'ids', 'number' => 100, 'offset' => $offset, 'orderby' => 'id', 'order' => 'ASC', 'update_site_meta_cache' => false, ) ); foreach ( $site_ids as $site_id ) { if ( ! wp_is_site_initialized( $site_id ) || ( ! self::protects_site( $site_id, $settings ) && ! MisticGate::protects_site( $site_id ) ) ) { return (int) $site_id; } } $site_count = count( $site_ids ); $offset += $site_count; } while ( 100 === $site_count ); return 0; } // Settings and URLs. /** * Returns the current network settings. * * @return array Normalized settings. */ public static function get_settings() { if ( ! is_multisite() ) { return self::normalize_settings( array() ); } $stored = self::read_stored_settings( get_current_network_id() ); if ( null === $stored ) { return self::normalize_settings( array() ) + array( 'unavailable' => true ); } return self::normalize_settings( $stored ); } /** * Reads the stored network settings from the database. * * @param int $network_id Network ID. * * @return mixed Stored value, false when the settings do not exist, or null when they could not be read. */ private static function read_stored_settings( $network_id ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Authorization must use the committed value; a concurrent cache miss can restore an obsolete value to a shared object cache. $stored = $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM {$wpdb->sitemeta} WHERE site_id = %d AND meta_key = %s ORDER BY meta_id ASC LIMIT 1", $network_id, self::OPTION_NAME ) ); if ( null === $stored ) { return '' === $wpdb->last_error ? false : null; } return maybe_unserialize( $stored ); } /** * Removes unused setup rows that concurrent first-time setup added as duplicates. * * @param int $network_id Network ID. * * @return void */ private static function remove_duplicate_settings( $network_id ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Duplicate rows must be detected in the database itself. $rows = $wpdb->get_results( $wpdb->prepare( "SELECT meta_id, meta_value FROM {$wpdb->sitemeta} WHERE site_id = %d AND meta_key = %s ORDER BY meta_id ASC", $network_id, self::OPTION_NAME ) ); if ( ! is_array( $rows ) || count( $rows ) < 2 ) { return; } $used_ids = array(); $unused_ids = array(); foreach ( $rows as $row ) { $settings = self::normalize_settings( maybe_unserialize( $row->meta_value ) ); if ( ! $settings['enabled'] && ! $settings['protect_all_sites'] && 1 === $settings['generation'] && '' === $settings['pending_secret'] && array() === $settings['exempt_roles'] ) { $unused_ids[] = (int) $row->meta_id; } else { $used_ids[] = (int) $row->meta_id; } } if ( count( $used_ids ) > 1 ) { return; } if ( array() === $used_ids ) { array_shift( $unused_ids ); } foreach ( $unused_ids as $meta_id ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Only unused setup rows are removed; the network option cache is invalidated below. $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->sitemeta} WHERE meta_id = %d AND site_id = %d AND meta_key = %s", $meta_id, $network_id, self::OPTION_NAME ) ); } wp_cache_delete( $network_id . ':' . self::OPTION_NAME, 'site-options' ); } /** * Saves network settings only if the stored settings still match the expected value. * * @param array $settings New settings. * @param array $expected Normalized settings the caller started from. * * @return bool Whether the settings were saved. */ public static function save_settings( $settings, $expected ) { if ( ! is_multisite() ) { return false; } global $wpdb; $network_id = get_current_network_id(); $cache_key = $network_id . ':' . self::OPTION_NAME; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Read an authoritative snapshot; refuse ambiguous duplicate network option rows rather than updating multiple settings. $rows = $wpdb->get_results( $wpdb->prepare( "SELECT meta_id, meta_value FROM {$wpdb->sitemeta} WHERE site_id = %d AND meta_key = %s LIMIT 2", $network_id, self::OPTION_NAME ) ); if ( ! is_array( $rows ) || 1 !== count( $rows ) || self::normalize_settings( maybe_unserialize( $rows[0]->meta_value ) ) !== $expected ) { wp_cache_delete( $cache_key, 'site-options' ); wp_cache_delete( $network_id . ':notoptions', 'site-options' ); return false; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- The complete previous value and row/network identity must still match at write time; invalidate network option caches below. $updated = $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->sitemeta} SET meta_value = %s WHERE meta_id = %d AND site_id = %d AND meta_key = %s AND CAST(meta_value AS BINARY) = CAST(%s AS BINARY)", maybe_serialize( $settings ), (int) $rows[0]->meta_id, $network_id, self::OPTION_NAME, $rows[0]->meta_value ) ); wp_cache_delete( $cache_key, 'site-options' ); wp_cache_delete( $network_id . ':notoptions', 'site-options' ); return 1 === (int) $updated; } /** * Builds the network gate URL. * * @param string $secret Gate secret. * * @return string Gate URL, or an empty string for an invalid secret. */ public static function gate_url( $secret ) { if ( ! is_multisite() || ! MisticGate::is_valid_secret( $secret ) ) { return ''; } $scheme = self::login_scheme(); $base_url = network_site_url( '/', $scheme ); if ( '' === self::main_site_permalink_structure() ) { return add_query_arg( self::GATE_QUERY_KEY, $secret, $base_url ); } return trailingslashit( $base_url ) . self::GATE_SLUG . '/' . rawurlencode( $secret ) . '/'; } /** * Returns the network login URL. * * @return string Login URL. */ public static function login_url() { return network_site_url( 'wp-login.php', self::login_scheme() ); } /** * Outputs the hidden field that marks a network gate login. * * @return void */ public static function render_login_context() { if ( ! self::is_network_main_site() || ! self::protects_current_site() || ( ! self::$login_pass_issued && ! self::has_valid_pass() ) ) { return; } if ( 'POST' === self::request_method() ) { $use_default = self::login_post_value( 'misticgate_network_default' ) === '1' && self::login_post_value( 'redirect_to' ) === admin_url(); } else { $requested = self::query_value( 'redirect_to' ); $use_default = null === $requested || '' === $requested; } if ( $use_default ) { echo ''; } } /** * Sends network gate logins to Network Admin. * * @param string $redirect_to Redirect destination. * @param string $requested_redirect_to Requested destination. * @param WP_User|WP_Error $user Login result. * * @return string Redirect destination. */ public static function filter_login_redirect( $redirect_to, $requested_redirect_to, $user ) { if ( is_wp_error( $user ) || ! $user instanceof WP_User || ! self::is_network_main_site() || ! self::protects_current_site() || ( ! self::has_valid_pass() && self::$authenticated_gate_user_id !== (int) $user->ID ) || ! is_super_admin( $user->ID ) ) { return $redirect_to; } $default_url = admin_url(); $use_default = '' === (string) $requested_redirect_to || ( $default_url === $requested_redirect_to && '1' === self::login_post_value( 'misticgate_network_default' ) ); if ( $use_default && ( '' === (string) $redirect_to || $default_url === $redirect_to ) ) { return network_admin_url(); } return $redirect_to; } /** * Returns a posted login form value. * * @param string $key Field name. * * @return string Sanitized value, or an empty string. */ private static function login_post_value( $key ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Public login routing hint only; never grants access. The signed pass and authenticated super administrator are verified independently. return isset( $_POST[ $key ] ) && is_string( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : ''; } /** * Deletes the network gate cookies. * * @return void */ public static function clear_access_cookies() { if ( ! is_multisite() ) { return; } self::clear_cookie( self::ticket_cookie_name() ); self::clear_cookie( self::pass_cookie_name() ); self::$login_pass_issued = false; self::$authenticated_gate_user_id = 0; } /** * Clears gate cookies after a successful login. * * @param string $user_login Username. * @param WP_User $user Logged-in user. * * @return void */ public static function handle_login_success( $user_login, $user ) { unset( $user_login ); $gate_user_id = $user instanceof WP_User && self::protects_current_site() && self::has_valid_pass() ? (int) $user->ID : 0; self::clear_access_cookies(); // wp_login precedes login_redirect. Retain only this request's verified routing context, not a cookie or future access grant. self::$authenticated_gate_user_id = $gate_user_id; } /** * Deletes all unconsumed network tickets. * * @return void */ public static function invalidate_tickets() { if ( ! is_multisite() ) { return; } global $wpdb; $network_id = get_current_network_id(); $like = $wpdb->esc_like( self::TICKET_OPTION_PREFIX ) . '%'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk invalidation of short-lived network tickets cannot be expressed atomically through the Network Options API. $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->sitemeta} WHERE site_id = %d AND meta_key LIKE %s", $network_id, $like ) ); } /** * Normalizes stored settings. * * @param mixed $value Stored settings. * * @return array Normalized settings. */ private static function normalize_settings( $value ) { $value = is_array( $value ) ? $value : array(); $secret = isset( $value['secret'] ) && is_string( $value['secret'] ) ? $value['secret'] : ''; $pending_secret = isset( $value['pending_secret'] ) && is_string( $value['pending_secret'] ) ? $value['pending_secret'] : ''; return array( 'enabled' => isset( $value['enabled'] ) && true === $value['enabled'] && MisticGate::is_valid_secret( $secret ), 'protect_all_sites' => isset( $value['protect_all_sites'] ) && true === $value['protect_all_sites'], 'secret' => $secret, 'pending_secret' => MisticGate::is_valid_secret( $pending_secret ) && $pending_secret !== $secret ? $pending_secret : '', 'generation' => isset( $value['generation'] ) && (int) $value['generation'] > 0 ? (int) $value['generation'] : 1, 'exempt_roles' => MisticGate::normalize_roles( isset( $value['exempt_roles'] ) ? $value['exempt_roles'] : array() ), 'updated_at' => isset( $value['updated_at'] ) ? (int) $value['updated_at'] : 0, ); } // Tickets and request parsing. /** * Outputs the page that continues to the login page with a POST request, then exits. * * @return void */ private static function render_post_bridge() { self::send_protective_headers(); status_header( 200 ); if ( ! headers_sent() ) { header( 'Content-Type: text/html; charset=UTF-8', true ); header( "Content-Security-Policy: default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'", true ); header( 'X-Content-Type-Options: nosniff', true ); header( 'X-Frame-Options: DENY', true ); } wp_register_style( 'misticgate-bridge', false, array(), MISTICGATE_VERSION ); wp_add_inline_style( 'misticgate-bridge', 'html{background:#f0f0f1}body{display:grid;min-height:100vh;margin:0;place-items:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#1d2327}.box{box-sizing:border-box;width:min(88vw,380px);padding:32px;border:1px solid #dcdcde;border-radius:12px;background:#fff;box-shadow:0 2px 12px rgba(0,0,0,.06);text-align:center}p{margin:0 0 18px;line-height:1.6}button{min-height:44px;padding:0 20px;border:1px solid #2271b1;border-radius:5px;background:#2271b1;color:#fff;font:600 14px inherit;cursor:pointer}button:focus{outline:2px solid #2271b1;outline-offset:3px}' ); wp_register_script( 'misticgate-bridge', false, array(), MISTICGATE_VERSION, true ); wp_add_inline_script( 'misticgate-bridge', 'document.forms[0].submit();' ); ?> > <?php esc_html_e( 'Taking you to the login page', 'misticgate' ); ?>

query( $wpdb->prepare( "DELETE FROM {$wpdb->sitemeta} WHERE site_id = %d AND meta_key = %s AND meta_value = %s", $network_id, $option_name, (string) $payload['e'] ) ); wp_cache_delete( $network_id . ':' . $option_name, 'site-options' ); return 1 === (int) $deleted; } /** * Deletes expired tickets. * * @return void */ private static function cleanup_expired_tickets() { global $wpdb; $network_id = get_current_network_id(); $like = $wpdb->esc_like( self::TICKET_OPTION_PREFIX ) . '%'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk expiry cleanup is bounded to this plugin's current-network ticket prefix. $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->sitemeta} WHERE site_id = %d AND meta_key LIKE %s AND CAST(meta_value AS UNSIGNED) < %d", $network_id, $like, time() ) ); } /** * Returns the option name that stores a ticket. * * @param string $nonce Ticket nonce. * * @return string Option name. */ private static function ticket_option_name( $nonce ) { return self::TICKET_OPTION_PREFIX . hash( 'sha256', $nonce ); } /** * Extracts a gate secret candidate from the request. * * @return string|null Candidate secret, or null when the request is not for the gate. */ private static function gate_candidate_from_request() { $request_uri = self::request_uri(); $path = wp_parse_url( $request_uri, PHP_URL_PATH ); $base_path = wp_parse_url( self::current_site_base_url(), PHP_URL_PATH ); $query_value = self::query_value( self::GATE_QUERY_KEY ); if ( null !== $query_value ) { if ( ! is_string( $path ) || ! is_string( $base_path ) || rtrim( $path, '/' ) !== rtrim( $base_path, '/' ) ) { return null; } return $query_value; } if ( ! is_string( $path ) ) { return null; } $base_path = is_string( $base_path ) ? '/' . trim( $base_path, '/' ) : ''; $prefix = rtrim( $base_path, '/' ) . '/' . self::GATE_SLUG . '/'; if ( 0 !== strpos( $path, $prefix ) ) { return null; } $candidate = rtrim( substr( $path, strlen( $prefix ) ), '/' ); return false === strpos( $candidate, '/' ) ? $candidate : ''; } /** * Checks the network login-pass cookie. * * @return bool Whether the pass is valid. */ private static function has_valid_pass() { $settings = self::get_settings(); if ( ! $settings['enabled'] || ! MisticGate::is_valid_secret( $settings['secret'] ) ) { return false; } $generation = self::token_generation( $settings['secret'], $settings['generation'] ); $key = self::signing_key( $settings['secret'], $settings['generation'] ); $pass = self::read_cookie( self::pass_cookie_name() ); return false !== MisticGate_Token::verify( $pass, 'pass', self::PASS_TTL, $generation, $key ); } /** * Checks whether the current site is the network main site. * * @return bool Whether it is the main site. */ private static function is_network_main_site() { if ( ! is_multisite() ) { return false; } $network_id = get_current_network_id(); return get_current_blog_id() === get_main_site_id( $network_id ); } /** * Returns the base URL of the current site. * * @return string Base URL. */ private static function current_site_base_url() { return set_url_scheme( site_url( '/' ), self::login_scheme() ); } /** * Returns the login URL for the current site. * * @return string Login URL. */ private static function current_login_url() { if ( self::is_network_main_site() ) { return self::login_url(); } return site_url( 'wp-login.php', 'login' ); } /** * Returns the URL to open after the network gate. * * @return string Destination URL. */ private static function current_destination_url() { return self::is_network_main_site() && is_super_admin() ? network_admin_url() : admin_url(); } /** * Returns the permalink structure of the main site. * * @return string Permalink structure. */ private static function main_site_permalink_structure() { $main_site_id = get_main_site_id( get_current_network_id() ); return (string) get_blog_option( $main_site_id, 'permalink_structure', '' ); } /** * Derives the token signing key. * * @param string $secret Gate secret. * @param int $generation Settings generation. * * @return string Binary signing key. */ private static function signing_key( $secret, $generation ) { $context = 'misticgate-network|v2|' . get_current_network_id() . '|' . strtolower( network_site_url( '/', self::login_scheme() ) ) . '|' . (int) $generation . '|' . $secret; return hash_hmac( 'sha256', $context, wp_salt( 'secure_auth' ), true ); } /** * Returns the generation hash bound to tokens. * * @param string $secret Gate secret. * @param int $generation Settings generation. * * @return string Generation hash. */ private static function token_generation( $secret, $generation ) { return hash( 'sha256', 'network|' . get_current_network_id() . '|' . $secret . '|' . (int) $generation ); } /** * Returns the scheme of the network login URL. * * @return string 'http' or 'https'. */ private static function login_scheme() { $scheme = wp_parse_url( network_admin_url(), PHP_URL_SCHEME ); return in_array( $scheme, array( 'http', 'https' ), true ) ? $scheme : ( is_ssl() ? 'https' : 'http' ); } /** * Returns the request method. * * @return string Uppercase request method. */ private static function request_method() { if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || ! is_string( $_SERVER['REQUEST_METHOD'] ) ) { return 'GET'; } return strtoupper( sanitize_key( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ); } /** * Returns the request URI. * * @return string Request URI. */ private static function request_uri() { if ( ! isset( $_SERVER['REQUEST_URI'] ) || ! is_string( $_SERVER['REQUEST_URI'] ) ) { return ''; } return sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ); } /** * Returns a query string value. * * @param string $key Query key. * * @return string|null Sanitized value, or null when absent. */ private static function query_value( $key ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public routing input; no privileged state change is performed from this value. if ( ! isset( $_GET[ $key ] ) ) { return null; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public routing input; no privileged state change is performed from this value. return sanitize_text_field( wp_unslash( $_GET[ $key ] ) ); } // Responses and cookies. /** * Sends no-cache, no-index and no-referrer headers. * * @return void */ private static function send_protective_headers() { if ( headers_sent() ) { return; } nocache_headers(); header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0, private', true ); header( 'Pragma: no-cache', true ); header( 'Expires: Wed, 11 Jan 1984 05:00:00 GMT', true ); header( 'Surrogate-Control: no-store', true ); header( 'X-Robots-Tag: noindex, nofollow, noarchive, nosnippet', true ); header( 'Referrer-Policy: no-referrer', true ); header( 'Vary: Cookie', false ); } /** * Redirects with protective headers, then exits. * * @param string $url Destination URL. * @param int $status HTTP status code. * * @return void */ private static function redirect( $url, $status ) { self::send_protective_headers(); wp_safe_redirect( $url, $status, 'WordPress' ); exit; } /** * Sends an empty 404 response, then exits. * * @return void */ private static function reject_request() { self::send_protective_headers(); status_header( 404 ); exit; } /** * Returns a gate cookie name. * * @param string $type Cookie type: 'ticket' or 'pass'. * * @return string Cookie name. */ private static function cookie_name( $type ) { $suffix = substr( hash( 'sha256', get_current_network_id() . '|' . strtolower( network_site_url( '/', self::login_scheme() ) ) ), 0, 12 ); return 'misticgate_network_' . $type . '_' . $suffix; } /** * Returns the ticket cookie name. * * @return string Cookie name. */ private static function ticket_cookie_name() { return self::cookie_name( 'ticket' ); } /** * Returns the login-pass cookie name. * * @return string Cookie name. */ private static function pass_cookie_name() { return self::cookie_name( 'pass' ); } /** * Reads a cookie value. * * @param string $name Cookie name. * * @return string Sanitized value, or an empty string. */ private static function read_cookie( $name ) { if ( ! isset( $_COOKIE[ $name ] ) ) { return ''; } return sanitize_text_field( wp_unslash( $_COOKIE[ $name ] ) ); } /** * Sets a gate cookie. * * @param string $name Cookie name. * @param string $value Cookie value. * @param int $expires Expiry as a Unix time. * * @return bool Whether the cookie was sent. */ private static function set_cookie( $name, $value, $expires ) { if ( headers_sent() ) { return false; } return setcookie( $name, $value, array( 'expires' => (int) $expires, 'path' => self::cookie_path(), 'secure' => self::use_secure_cookie(), 'httponly' => true, 'samesite' => 'Lax', ) ); } /** * Deletes a gate cookie. * * @param string $name Cookie name. * * @return void */ private static function clear_cookie( $name ) { if ( ! headers_sent() ) { setcookie( $name, '', array( 'expires' => time() - HOUR_IN_SECONDS, 'path' => self::cookie_path(), 'secure' => self::use_secure_cookie(), 'httponly' => true, 'samesite' => 'Lax', ) ); } unset( $_COOKIE[ $name ] ); } /** * Returns the cookie path for the login page. * * @return string Cookie path. */ private static function cookie_path() { $login_path = wp_parse_url( network_site_url( 'wp-login.php', self::login_scheme() ), PHP_URL_PATH ); if ( ! is_string( $login_path ) || '' === $login_path ) { return '/'; } $directory = str_replace( '\\', '/', dirname( $login_path ) ); if ( '.' === $directory || '/' === $directory || '' === trim( $directory, '/' ) ) { return '/'; } return '/' . trim( $directory, '/' ) . '/'; } /** * Checks whether gate cookies must be secure. * * @return bool Whether to set the Secure flag. */ private static function use_secure_cookie() { return 'https' === self::login_scheme(); } }