JezK
Edit File: AccelerateWpCdnCheck.php
<?php /** * Copyright (с) Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2023 All Rights Reserved */ namespace WP_Rocket\Cli; use Exception; /** * AccelerateWp Check CDN */ class AccelerateWpCdnCheck { /** * Url for checking. * * @var null|string */ protected $url = null; /** * Http response code. * * @var null|int|string */ protected $url_code = null; /** * Static css for checking. * * @var null|string */ protected $static_url = null; /** * Http response code. * * @var null|int|string */ protected $static_code = null; /** * Advice ID. * * @var null|int */ protected $advice_id = null; /** * Error reason. * * @var string */ protected $reason = null; /** * Whether the checked URL could not be fetched (invalid, blocked or unreachable). * * @var bool */ protected $url_refused = false; /** * AWP Account ID. * * @var int */ protected $account_id; /** * AWP CDN url. * * @var string */ protected $cdn_url; /** * Constructor. * * @param int $account_id Account id. * @param string $cdn_url Cdn url. */ public function __construct( $account_id, $cdn_url ) { $this->account_id = $account_id; $this->cdn_url = $cdn_url; } /** * Get account id. * * @return int */ public function account_id() { return $this->account_id; } /** * Get cdn url. * * @return string */ public function cdn_url() { return $this->cdn_url; } /** * Find advice. * * @return array|null */ protected function advice() { $smart_advice_data = get_option( 'cl_smart_advice' ); if ( is_array( $smart_advice_data ) && array_key_exists( 'advices', $smart_advice_data ) ) { foreach ( $smart_advice_data['advices'] as $advice ) { if ( 'CDN' === $advice['type'] ) { return $advice; } } } return null; } /** * Normalize a possibly scheme-less URL to an absolute http(s) URL. * * A scheme-less value is completed against $context_url (scheme defaults to https) so the * fetch always targets an absolute URL. SSRF / private-IP / redirect / TLS enforcement is * delegated to wp_safe_remote_get(), which validates the resolved host and every redirect * hop; this method only fixes the URL shape: * - absolute ("https://host/path") -> left unchanged. * - protocol-relative ("//host/path") -> context scheme prepended (host kept). * - root-relative ("/path") -> rebuilt as scheme://{context host}/path so the * path resolves against the context origin, not the first path segment; '' when the * context has no host (fail closed rather than misparse the path as a host). * - bare ("host/path") -> scheme prepended (first segment is the host). * * @param string $url Url to normalize. * @param string $context_url Url whose scheme/host are borrowed when $url is scheme-less. * * @return string Absolute URL, or '' when one cannot be built. */ protected function normalize_url( $url, $context_url ) { if ( ! is_string( $url ) || '' === $url ) { return ''; } $url_parts = wp_parse_url( $url ); // A genuine scheme means the URL is already absolute; leave it untouched. if ( is_array( $url_parts ) && ! empty( $url_parts['scheme'] ) ) { return $url; } $context = wp_parse_url( (string) $context_url ); $scheme = ( is_array( $context ) && ! empty( $context['scheme'] ) ) ? $context['scheme'] : 'https'; if ( 0 === strpos( $url, '//' ) ) { return $scheme . ':' . $url; } if ( '/' === $url[0] ) { $context_host = ( is_array( $context ) && ! empty( $context['host'] ) ) ? $context['host'] : ''; return '' === $context_host ? '' : $scheme . '://' . $context_host . $url; } return $scheme . '://' . $url; } /** * Request to url. * * @param string $url Webpage. * @param string $context_url Url whose scheme is borrowed when $url has none. * * @return string */ protected function url_request( $url, $context_url ) { $safe_url = $this->normalize_url( $url, $context_url ); if ( '' === $safe_url ) { $this->url_refused = true; $this->url_code = 'Refused to fetch invalid CDN check URL'; $this->reason = 'Refused to fetch invalid CDN check URL ' . $url; return ''; } // wp_safe_remote_get blocks private/reserved hosts and unsafe redirects and enforces TLS. $response = wp_safe_remote_get( $safe_url, [ 'timeout' => 10, 'sslverify' => true, 'redirection' => 3, ] ); // A WP_Error means the target was blocked or unreachable; surface it instead of the // misleading "CDN url not found" the empty-body substring test would otherwise report. if ( is_wp_error( $response ) ) { $this->url_refused = true; $this->url_code = $this->url_code( $response ); $this->reason = 'CDN check URL ' . $safe_url . ' was blocked or unreachable: ' . $this->url_code; return ''; } $this->url_code = $this->url_code( $response ); return $this->url_body( $response ); } /** * Url response code. * * @param array|\WP_Error $response The response or WP_Error on failure. * * @return int|string */ protected function url_code( $response ) { if ( is_wp_error( $response ) ) { return $response->get_error_message(); } else { return wp_remote_retrieve_response_code( $response ); } } /** * Url response body. * * @param array $response request. * * @return string */ protected function url_body( $response ) { return wp_remote_retrieve_body( $response ); } /** * Request to static. * * @param string $url Css file. * * @return void */ protected function static_request( $url ) { $safe_url = $this->normalize_url( $url, $this->url ); if ( '' === $safe_url ) { $this->static_code = 'Refused to fetch invalid CDN resource URL'; return; } $response = wp_safe_remote_get( $safe_url, [ 'timeout' => 2, 'sslverify' => true, 'redirection' => 3, ] ); $this->static_code = $this->static_code( $response ); } /** * Static response code. * * @param array $response request. * * @return int */ protected function static_code( $response ) { return $this->url_code( $response ); } /** * Fine AWP Debug. * * @param string $body For parse. * * @return string|null */ protected function find_reason( $body ) { preg_match_all( '/<!--\sAccelerateWP Debug:\s(.*)\s-->/mi', $body, $matches ); if ( array_key_exists( 1, $matches ) && array_key_exists( 0, $matches[1] ) ) { return (string) $matches[1][0]; } return null; } /** * Find first css file. * * @param string $body For parse. * * @return string|null */ public function find_static( $body ) { preg_match_all( '/\/\/' . preg_quote( $this->cdn_url(), '/' ) . '\/\S+\.css[\'"?]/mi', $body, $matches ); if ( array_key_exists( 0, $matches ) ) { foreach ( $matches[0] as $url ) { $url = rtrim( $url, '\'"?' ); if ( ! empty( $url ) ) { return $url; } } } return null; } /** * Cdn post-check. * * @return bool * @throws Exception Something went wrong. */ public function check() { $home_url = defined( 'WP_HOME' ) ? WP_HOME : get_home_url(); $this->url = $home_url; $advice = $this->advice(); if ( ! empty( $advice ) ) { if ( array_key_exists( 'requests', $advice ) && is_array( $advice['requests'] ) && ! empty( $advice['requests'] ) ) { $this->advice_id = $advice['id']; $this->url = array_shift( $advice['requests'] ); } } // Resolve the (possibly scheme-less) advice target to an absolute URL up front so the page // fetch and the protocol-relative CDN-CSS probe share one scheme. Otherwise static_request() // would normalize against a scheme-less $this->url and fall back to https while the page was // fetched over the home_url scheme (e.g. http), causing false post-check failures. $normalized = $this->normalize_url( $this->url, $home_url ); if ( '' !== $normalized ) { $this->url = $normalized; } $body = $this->url_request( $this->url, $home_url ); // A blocked/unreachable target already set the reason; surface it instead of the misleading not-found message. if ( $this->url_refused ) { return false; } $this->reason = $this->find_reason( $body ); $this->static_url = $this->find_static( $body ); if ( ! empty( $this->static_url ) ) { $this->static_request( $this->static_url ); } if ( false === strpos( $body, $this->cdn_url() ) ) { $this->reason = 'CDN url not found on page ' . $this->url; return false; } elseif ( 200 > $this->url_code || 300 <= $this->url_code ) { $this->reason = 'URL ' . $this->url . ' returned unexpected response status (' . $this->url_code . ')'; return false; } elseif ( 200 > $this->static_code || 300 <= $this->static_code ) { $this->reason = 'CDN url returned unexpected response status (' . $this->static_code . ')'; return false; } return true; } /** * Reason. * * @return string|null */ public function reason() { return $this->reason; } /** * Sent report. * * @return void * * @phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound */ public function report() { do_action( 'accelerate_wp_set_error', E_WARNING, 'AccelerateWP CDN pos-check failed', __FILE__, __LINE__, [ 'url' => $this->url, 'url_code' => $this->url_code, 'static_url' => $this->static_url, 'static_code' => $this->static_code, 'advice_id' => $this->advice_id, 'reason' => $this->reason, 'account_id' => $this->account_id(), 'cdn_url' => $this->cdn_url(), ] ); } }