2048 ) {
throw new \Exception( esc_html__( 'The URL is too long.', 'ai-support-assistant' ) );
}
$response = wp_safe_remote_get(
$url,
array(
'timeout' => 30,
'redirection' => 0,
'limit_response_size' => 2 * 1024 * 1024,
'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 WordPress-ai-support-assistant/' . ( defined( 'AI_SUPPORT_ASSISTANT_VERSION' ) ? AI_SUPPORT_ASSISTANT_VERSION : '1.0.2' ),
)
);
if ( is_wp_error( $response ) ) {
$err = $response->get_error_message();
Logger::error( "URL Crawl failed for $url: " . $err );
/* translators: %s: Crawl error details. */
throw new \Exception( esc_html( sprintf( __( 'Failed to fetch URL: %s', 'ai-support-assistant' ), $err ) ) );
}
$code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $code ) {
Logger::error( "URL Crawl failed with HTTP $code for $url" );
/* translators: %d: HTTP status code. */
throw new \Exception( esc_html( sprintf( __( 'Failed to fetch URL, HTTP error code: %d', 'ai-support-assistant' ), $code ) ) );
}
$html = wp_remote_retrieve_body( $response );
$content_type = strtolower( (string) wp_remote_retrieve_header( $response, 'content-type' ) );
if ( $content_type && false === strpos( $content_type, 'text/html' ) && false === strpos( $content_type, 'text/plain' ) ) {
throw new \Exception( esc_html__( 'The URL did not return an HTML or text document.', 'ai-support-assistant' ) );
}
if ( empty( $html ) ) {
throw new \Exception( esc_html__( 'Webpage content is empty.', 'ai-support-assistant' ) );
}
// Parse Title.
$title = '';
if ( preg_match( '/
(.*?)<\/title>/is', $html, $matches ) ) {
$title = trim( html_entity_decode( $matches[1], ENT_QUOTES | ENT_HTML5, 'UTF-8' ) );
}
if ( empty( $title ) ) {
$title = $url;
}
// Clean HTML content to obtain readable text.
$text = self::clean_html( $html );
return array(
'title' => $title,
'text' => $text,
);
}
/** Reject loopback, private, link-local, and metadata hosts before the request. */
private static function is_public_host( string $host ): bool {
$host = trim( strtolower( $host ), '[]' );
if ( 'localhost' === $host || substr( $host, -6 ) === '.local' || substr( $host, -6 ) === '.internal' ) {
return false;
}
$ips = filter_var( $host, FILTER_VALIDATE_IP ) ? array( $host ) : ( gethostbynamel( $host ) ?: array() );
if ( empty( $ips ) ) { return false; }
foreach ( $ips as $ip ) {
if ( false === filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
return false;
}
}
return true;
}
/**
* Strip out style tags, scripts, layout/headers, navigation menus and keep readable content.
*
* @param string $html Raw HTML content.
* @return string Plain text representation.
*/
private static function clean_html( string $html ): string {
// Strip comments.
$html = preg_replace( '//ms', '', $html );
// Strip style, script and template blocks.
$html = preg_replace( '/<(script|style|template|svg|noscript|iframe)[^>]*>.*?<\/\1>/is', '', $html );
// Strip headers, footers, and sidebars to target the main article/body text as much as possible.
$html = preg_replace( '/<(header|footer|nav|aside)[^>]*>.*?<\/\1>/is', '', $html );
// Replace structural blocks with newlines to preserve spacing/paragraphs.
$html = preg_replace( '/<\/(p|div|h1|h2|h3|h4|h5|h6|li|tr)>/i', "\n", $html );
// Strip remaining HTML tags.
$text = wp_strip_all_tags( $html );
// Decode entities.
$text = html_entity_decode( $text, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
// Clean up extra spaces/newlines.
$text = preg_replace( "/[ \t]+/u", ' ', $text );
$text = preg_replace( "/\n\s*\n+/u", "\n\n", $text );
return trim( $text );
}
}