'GET', 'timeout' => (int) Settings::get( 'request_timeout' ), 'max_bytes' => (int) Settings::get( 'max_response_kb' ) * 1024, 'max_redirects' => 5, ) ); $result = array( 'status' => 0, 'headers' => array(), 'body' => '', 'final_url' => $url, 'chain' => array(), 'loop' => false, 'error' => '', 'error_code' => '', 'time_ms' => 0, ); $start = microtime( true ); $current = $url; $seen = array(); for ( $hop = 0; $hop <= $args['max_redirects']; $hop++ ) { $valid = Security::validate_url( $current ); if ( is_wp_error( $valid ) ) { $result['error'] = $valid->get_error_message(); $result['error_code'] = $valid->get_error_code(); break; } $key = strtolower( $current ); if ( isset( $seen[ $key ] ) ) { $result['loop'] = true; $result['error'] = __( 'Redirect loop detected.', 'siteaudithub' ); $result['error_code'] = 'siteaudithub_redirect_loop'; break; } $seen[ $key ] = true; $internal = Security::is_internal( $current ); $request = array( 'method' => $args['method'], 'timeout' => max( 1, (int) $args['timeout'] ), 'redirection' => 0, 'limit_response_size' => max( 1024, (int) $args['max_bytes'] ), 'user-agent' => 'SiteAuditHub/' . SITEAUDITHUB_VERSION . ' (WordPress audit; +' . home_url( '/' ) . ')', 'headers' => array( 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' ), // External hosts additionally go through WordPress' own unsafe-URL guard // which re-resolves the host at request time (DNS rebinding mitigation). 'reject_unsafe_urls' => ! $internal, 'sslverify' => $internal ? (bool) apply_filters( 'https_local_ssl_verify', true ) : true, // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Core WordPress filter, used the same way as core loopback requests. ); $response = wp_remote_request( $current, $request ); if ( is_wp_error( $response ) ) { $result['error'] = $response->get_error_message(); $result['error_code'] = $response->get_error_code(); $result['final_url'] = $current; break; } $status = (int) wp_remote_retrieve_response_code( $response ); $headers = array(); $raw_headers = wp_remote_retrieve_headers( $response ); if ( is_object( $raw_headers ) && method_exists( $raw_headers, 'getAll' ) ) { $raw_headers = $raw_headers->getAll(); } foreach ( is_array( $raw_headers ) ? $raw_headers : array() as $name => $value ) { $headers[ strtolower( $name ) ] = is_array( $value ) ? implode( ', ', $value ) : (string) $value; } $result['status'] = $status; $result['headers'] = $headers; $result['final_url'] = $current; if ( $status >= 300 && $status < 400 && ! empty( $headers['location'] ) ) { $result['chain'][] = array( $current, $status ); $next = self::absolutize( $headers['location'], $current ); if ( ! $next ) { $result['error'] = __( 'Invalid redirect target.', 'siteaudithub' ); $result['error_code'] = 'siteaudithub_redirect_invalid'; break; } if ( $hop === $args['max_redirects'] ) { $result['error'] = __( 'Too many redirects.', 'siteaudithub' ); $result['error_code'] = 'siteaudithub_redirect_limit'; break; } $current = $next; continue; } $result['body'] = (string) wp_remote_retrieve_body( $response ); break; } $result['time_ms'] = (int) round( ( microtime( true ) - $start ) * 1000 ); return $result; } /** * Resolve a possibly relative URL against a base. * * @param string $href Href. * @param string $base Base URL. * @return string|false */ public static function absolutize( $href, $base ) { $href = trim( html_entity_decode( (string) $href, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ); if ( '' === $href ) { return false; } if ( preg_match( '#^[a-z][a-z0-9+\-.]*:#i', $href ) ) { return preg_match( '#^https?://#i', $href ) ? $href : false; } $b = wp_parse_url( $base ); if ( empty( $b['scheme'] ) || empty( $b['host'] ) ) { return false; } $origin = $b['scheme'] . '://' . $b['host'] . ( isset( $b['port'] ) ? ':' . $b['port'] : '' ); if ( 0 === strpos( $href, '//' ) ) { return $b['scheme'] . ':' . $href; } if ( '#' === $href[0] ) { return false; } if ( '?' === $href[0] ) { return $origin . ( isset( $b['path'] ) ? $b['path'] : '/' ) . $href; } if ( '/' === $href[0] ) { return $origin . self::remove_dot_segments( $href ); } $path = isset( $b['path'] ) ? $b['path'] : '/'; $dir = substr( $path, 0, strrpos( $path, '/' ) + 1 ); return $origin . self::remove_dot_segments( $dir . $href ); } /** * RFC 3986 dot-segment removal (keeps query string intact). * * @param string $path Path (may include ?query). * @return string */ private static function remove_dot_segments( $path ) { $query = ''; $qpos = strpos( $path, '?' ); if ( false !== $qpos ) { $query = substr( $path, $qpos ); $path = substr( $path, 0, $qpos ); } $out = array(); foreach ( explode( '/', $path ) as $seg ) { if ( '..' === $seg ) { if ( count( $out ) > 1 ) { array_pop( $out ); } } elseif ( '.' !== $seg ) { $out[] = $seg; } } $result = implode( '/', $out ); if ( '' === $result || '/' !== $result[0] ) { $result = '/' . ltrim( $result, '/' ); } if ( preg_match( '#/\.{1,2}$#', $path ) ) { $result = rtrim( $result, '/' ) . '/'; } return $result . $query; } }