false, 'profiled' => false, 'skipped' => true, 'status' => 0, 'ms' => 0.0, /* translators: %s: scheme, host and port of the site's admin, e.g. https://example.com:443 */ 'error' => sprintf( __( 'Skipped: login cookies are only sent to this site\'s own admin address (%s).', 'bloatwright' ), UrlSet::admin_origin() ), ); } $cookies = $this->admin_cookies( $url ); if ( ! $cookies ) { return array( 'ok' => false, 'profiled' => false, 'skipped' => true, 'status' => 0, 'ms' => 0.0, 'error' => __( 'Skipped: admin URLs are only profiled as a logged-in user. In WP-CLI add --user=.', 'bloatwright' ), ); } } $token = Signature::sign( array( 'session' => $session_id, 'url' => $url, 'pass' => $pass, 'exclude' => $exclude, 'mode' => 'auto', 'exp' => time() + 120, ) ); $target = add_query_arg( 'bloatwright_nocache', (string) wp_rand( 1000, 999999 ), $url ); $args = array( 'timeout' => (int) Settings::get( 'timeout', 60 ), 'redirection' => 0, // Never follow a redirect with auth cookies attached; 3xx is reported below. 'reject_unsafe_urls' => true, 'sslverify' => (bool) apply_filters( 'bloatwright_loopback_sslverify', ! self::is_local( $url ) ), 'headers' => array( Signature::HEADER => $token, 'X-BLOATWRIGHT-Bypass' => '1', 'Cache-Control' => 'no-cache', 'Pragma' => 'no-cache', ), 'cookies' => $cookies, 'user-agent' => 'Bloatwright/' . BLOATWRIGHT_VERSION, ); $start = hrtime( true ); $r = wp_safe_remote_get( $target, $args ); $ms = round( ( hrtime( true ) - $start ) / 1e6, 2 ); if ( is_wp_error( $r ) ) { return array( 'ok' => false, 'profiled' => false, 'skipped' => false, 'status' => 0, 'ms' => $ms, 'error' => $r->get_error_message(), ); } $status = (int) wp_remote_retrieve_response_code( $r ); if ( $status >= 300 && $status < 400 ) { return array( 'ok' => false, 'profiled' => false, 'skipped' => false, 'status' => $status, 'ms' => $ms, /* translators: 1: HTTP status code, 2: redirect target URL */ 'error' => sprintf( __( 'HTTP %1$d redirect to %2$s: add the final URL instead.', 'bloatwright' ), $status, (string) wp_remote_retrieve_header( $r, 'location' ) ), ); } $profiled = '1' === (string) wp_remote_retrieve_header( $r, 'x-bloatwright-profiled' ) || false !== strpos( (string) wp_remote_retrieve_body( $r ), 'bloatwright-profiled' ); return array( 'ok' => $status >= 200 && $status < 300, 'profiled' => $profiled, 'skipped' => false, 'status' => $status, 'ms' => $ms, 'error' => $status >= 400 ? sprintf( 'HTTP %d', $status ) : ( $profiled ? '' : __( 'Response was not profiled (served from a cache or the loader is missing).', 'bloatwright' ) ), ); } /** * Is this a wp-admin URL (requested as the acting user)? * * @param string $url URL. */ public static function is_admin_url( string $url ): bool { return false !== strpos( (string) wp_parse_url( $url, PHP_URL_PATH ), '/wp-admin/' ); } /** * Auth + logged-in cookies for the acting user, minted for a session token of our own and scoped * to the URL's host. Empty when nobody is logged in, or when the URL's scheme, host and port are * not those of the site's admin, site address or home URL (never an http URL when the admin is * https). * * The auth cookie flavour follows the scheme of the URL being requested: WordPress checks the * secure_auth cookie on https admin requests and the auth cookie on http ones. For admin URLs * built in a web request that is the same as is_ssl(); under WP-CLI is_ssl() is always false, so * the URL is the only reliable signal. * * @param string $url URL. * @return \WP_Http_Cookie[] */ private function admin_cookies( string $url ): array { $user_id = get_current_user_id(); if ( $user_id <= 0 ) { return array(); } // Login cookies only ever go to this site's own origin, whatever URL list reached us. if ( ! in_array( UrlSet::origin( $url ), UrlSet::login_origins(), true ) ) { return array(); } if ( null === $this->auth || $this->auth['user'] !== $user_id || $this->auth['expiration'] - MINUTE_IN_SECONDS < time() ) { $this->release(); $expiration = time() + self::AUTH_TTL; $this->auth = array( 'user' => $user_id, 'token' => \WP_Session_Tokens::get_instance( $user_id )->create( $expiration ), 'expiration' => $expiration, ); if ( ! $this->release_on_shutdown ) { register_shutdown_function( array( $this, 'release' ) ); // Idempotent: a no-op once released. $this->release_on_shutdown = true; } } $host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) ); $secure = 'https' === strtolower( (string) wp_parse_url( $url, PHP_URL_SCHEME ) ); return array( new \WP_Http_Cookie( array( 'name' => self::cookie_name( $secure ? 'SECURE_AUTH_COOKIE' : 'AUTH_COOKIE' ), 'value' => wp_generate_auth_cookie( $user_id, $this->auth['expiration'], $secure ? 'secure_auth' : 'auth', $this->auth['token'] ), 'domain' => $host, ) ), new \WP_Http_Cookie( array( 'name' => self::cookie_name( 'LOGGED_IN_COOKIE' ), 'value' => wp_generate_auth_cookie( $user_id, $this->auth['expiration'], 'logged_in', $this->auth['token'] ), 'domain' => $host, ) ), ); } /** * Is release() registered to run when the PHP request ends? (tests) */ public function releases_on_shutdown(): bool { return $this->release_on_shutdown; } /** * Destroy the session token minted for admin URLs (no-op when none was minted or it is already * destroyed). */ public function release(): void { if ( null === $this->auth ) { return; } \WP_Session_Tokens::get_instance( $this->auth['user'] )->destroy( $this->auth['token'] ); $this->auth = null; } /** * Cookie constants are defined at runtime by wp_cookie_constants(). * * @param string $name Constant name. */ private static function cookie_name( string $name ): string { return defined( $name ) ? (string) constant( $name ) : strtolower( $name ); } /** * Local/dev host? Used only to relax certificate verification: local sites almost always run on * self-signed certificates, and the loopback never targets anything but this site's own URLs. * Override with the bloatwright_loopback_sslverify filter. * * @param string $url URL. */ public static function is_local( string $url ): bool { $host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) ); return in_array( $host, array( 'localhost', '127.0.0.1', '::1' ), true ) || preg_match( '/\.(test|local|localhost|ddev\.site)$/', $host ) || preg_match( '/^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/', $host ); } /** * Can this site reach itself? Returns ['ok'=>bool,'error'=>string]. */ public function detect(): array { $r = wp_safe_remote_get( add_query_arg( 'bloatwright_ping', '1', home_url( '/' ) ), array( 'timeout' => 10, 'redirection' => 0, 'reject_unsafe_urls' => true, 'sslverify' => (bool) apply_filters( 'bloatwright_loopback_sslverify', ! self::is_local( home_url() ) ), // See is_local(). 'user-agent' => 'Bloatwright/' . BLOATWRIGHT_VERSION, ) ); if ( is_wp_error( $r ) ) { return array( 'ok' => false, 'error' => $r->get_error_message(), ); } $code = (int) wp_remote_retrieve_response_code( $r ); return array( 'ok' => $code >= 200 && $code < 400, 'error' => $code >= 400 ? sprintf( 'HTTP %d', $code ) : '', ); } }