5, 'timeout' => 10, 'sslverify' => true, 'user-agent' => self::get_user_agent(), ) ); if ( ! is_wp_error( $head_response ) ) { $effective_url = wp_remote_retrieve_header( $head_response, 'location' ); if ( ! empty( $effective_url ) && is_string( $effective_url ) ) { $candidate_url = esc_url_raw( $effective_url ); if ( false !== wp_http_validate_url( $candidate_url ) && preg_match( '/https?:\/\/(?:[a-zA-Z0-9\-]+\.)?amazon\.[a-z\.]{2,6}\/.*/i', $candidate_url ) ) { $clean_url = $candidate_url; } } } } // Extract ASIN (10 alphanumeric characters). $asin = self::extract_amazon_asin( $clean_url ); // Check transient cache if ASIN is available. if ( ! empty( $asin ) ) { $transient_key = 'affistyle_az_' . md5( $asin ); $cached_data = get_transient( $transient_key ); if ( false !== $cached_data && is_array( $cached_data ) ) { return $cached_data; } } // Fetch page contents via WordPress HTTP API. $args = array( 'timeout' => 12, 'redirection' => 5, 'sslverify' => true, 'user-agent' => self::get_user_agent(), 'headers' => array( 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language' => 'en-US,en;q=0.9', 'Cache-Control' => 'no-cache', 'Pragma' => 'no-cache', ), ); $response = wp_safe_remote_get( $clean_url, $args ); if ( is_wp_error( $response ) ) { return new WP_Error( 'affistyle_http_error', sprintf( /* translators: %s: error message */ __( 'Failed to connect to Amazon: %s', 'affistyle' ), $response->get_error_message() ) ); } $response_code = wp_remote_retrieve_response_code( $response ); if ( 200 !== (int) $response_code ) { return new WP_Error( 'affistyle_http_status_' . $response_code, sprintf( /* translators: %d: HTTP status code */ __( 'Amazon returned HTTP status code %d. The product may be unavailable or blocked.', 'affistyle' ), $response_code ) ); } $html = wp_remote_retrieve_body( $response ); if ( empty( $html ) ) { return new WP_Error( 'affistyle_empty_body', __( 'Amazon returned an empty response.', 'affistyle' ) ); } // Detect CAPTCHA or anti-bot challenge pages. if ( false !== stripos( $html, 'validateCaptcha' ) || false !== stripos( $html, 'Robot Check' ) ) { return new WP_Error( 'affistyle_captcha_blocked', __( 'Amazon presented a verification challenge (CAPTCHA). Please enter product details manually.', 'affistyle' ) ); } // Parse the HTML using native DOMDocument and DOMXPath. $data = self::parse_amazon_html( $html, $clean_url, $asin ); if ( is_wp_error( $data ) ) { return $data; } // Cache successful result in transient for 12 hours. if ( ! empty( $asin ) ) { set_transient( 'affistyle_az_' . md5( $asin ), $data, 12 * HOUR_IN_SECONDS ); } return $data; } /** * Extract Amazon ASIN from URL. * * @param string $url Amazon URL. * @return string Extracted ASIN or empty string. */ public static function extract_amazon_asin( $url ) { if ( preg_match( '/\/(?:dp|gp\/product|exec\/obidos\/asin|o\/asin|d)\/([a-z0-9]{10})/i', $url, $matches ) ) { return strtoupper( $matches[1] ); } return ''; } /** * Parse Amazon HTML content using DOMDocument and DOMXPath. * * @param string $html Response HTML. * @param string $clean_url Normalized product URL. * @param string $asin Extracted ASIN. * @return array|WP_Error Parsed product data array or WP_Error. */ private static function parse_amazon_html( $html, $clean_url, $asin ) { // Use libxml error suppression for non-strict HTML5 parsing. $previous_libxml_state = libxml_use_internal_errors( true ); $dom = new DOMDocument(); // Ensure UTF-8 decoding. $dom->loadHTML( '' . $html ); libxml_clear_errors(); libxml_use_internal_errors( $previous_libxml_state ); $xpath = new DOMXPath( $dom ); // 1. Extract Title. $title = self::extract_title( $xpath ); // 2. Extract Image URL. $image_url = self::extract_image( $xpath ); // 3. Extract Price. $price = self::extract_price( $xpath, $html ); // If no title could be found, fail gracefully. if ( empty( $title ) ) { return new WP_Error( 'affistyle_scrape_failed', __( 'Could not extract product details. Please enter them manually or check the URL.', 'affistyle' ) ); } return array( 'title' => $title, 'image_url' => $image_url, 'price' => $price, 'asin' => $asin, 'url' => $clean_url, ); } /** * Extract product title from DOMXPath. * * @param DOMXPath $xpath XPath instance. * @return string Extracted title. */ private static function extract_title( $xpath ) { $title_queries = array( '//*[@id="productTitle"]', '//h1[@id="title"]', '//span[@id="title"]', '//meta[@property="og:title"]/@content', '//meta[@name="title"]/@content', '//title', ); foreach ( $title_queries as $query ) { $nodes = $xpath->query( $query ); if ( $nodes && $nodes->length > 0 ) { $raw_title = trim( $nodes->item( 0 )->nodeValue ); if ( ! empty( $raw_title ) ) { // Clean Amazon marketing suffix (e.g. " : Amazon.com: ..."). $cleaned = preg_replace( '/\s*:\s*Amazon\.[a-z\.]+:.*$/i', '', $raw_title ); $cleaned = preg_replace( '/\s*\|\s*Amazon\.[a-z\.]+/i', '', $cleaned ); $cleaned = preg_replace( '/\s+/', ' ', $cleaned ); return sanitize_text_field( trim( $cleaned ) ); } } } return ''; } /** * Extract product image URL from DOMXPath and raw HTML attributes. * * @param DOMXPath $xpath XPath instance. * @return string Extracted high-res image URL. */ private static function extract_image( $xpath ) { $img_queries = array( '//*[@id="landingImage"]', '//*[@id="imgBlkFront"]', '//img[@id="main-image"]', '//img[contains(@class, "a-dynamic-image")]', ); foreach ( $img_queries as $query ) { $nodes = $xpath->query( $query ); if ( $nodes && $nodes->length > 0 ) { $node = $nodes->item( 0 ); // Check data-old-hires attribute first for full resolution. $old_hires = $node->getAttribute( 'data-old-hires' ); if ( ! empty( $old_hires ) && false !== wp_http_validate_url( $old_hires ) ) { return esc_url_raw( $old_hires ); } // Check data-a-dynamic-image attribute (JSON map of url => [width, height]). $dynamic_image_json = $node->getAttribute( 'data-a-dynamic-image' ); if ( ! empty( $dynamic_image_json ) ) { $img_map = json_decode( $dynamic_image_json, true ); if ( is_array( $img_map ) && ! empty( $img_map ) ) { // Pick highest resolution image based on width. $best_url = ''; $max_width = 0; foreach ( $img_map as $u => $dims ) { $w = ( is_array( $dims ) && isset( $dims[0] ) ) ? (int) $dims[0] : 0; if ( $w >= $max_width ) { $max_width = $w; $best_url = $u; } } if ( ! empty( $best_url ) && false !== wp_http_validate_url( $best_url ) ) { return esc_url_raw( $best_url ); } } } // Check standard src attribute. $src = $node->getAttribute( 'src' ); if ( ! empty( $src ) && false !== wp_http_validate_url( $src ) && false === stripos( $src, 'data:image' ) ) { return esc_url_raw( $src ); } } } // Fallback to Open Graph image tag. $og_nodes = $xpath->query( '//meta[@property="og:image"]/@content' ); if ( $og_nodes && $og_nodes->length > 0 ) { $og_img = trim( $og_nodes->item( 0 )->nodeValue ); if ( ! empty( $og_img ) && false !== wp_http_validate_url( $og_img ) ) { return esc_url_raw( $og_img ); } } return ''; } /** * Extract product price from DOMXPath. * * @param DOMXPath $xpath XPath instance. * @param string $html Raw HTML fallback. * @return string Extracted price string (e.g. "$199.99"). */ private static function extract_price( $xpath, $html ) { $price_queries = array( '//span[contains(@class, "apexPriceToPay")]//span[contains(@class, "a-offscreen")]', '//span[contains(@class, "priceToPay")]//span[contains(@class, "a-offscreen")]', '//*[@id="corePrice_desktop"]//span[contains(@class, "a-offscreen")]', '//*[@id="corePriceDisplay_desktop_feature_div"]//span[contains(@class, "a-offscreen")]', '//*[@id="priceblock_ourprice"]', '//*[@id="priceblock_dealprice"]', '//span[contains(@class, "a-price")]//span[contains(@class, "a-offscreen")]', ); foreach ( $price_queries as $query ) { $nodes = $xpath->query( $query ); if ( $nodes && $nodes->length > 0 ) { for ( $i = 0; $i < $nodes->length; $i++ ) { $val = trim( $nodes->item( $i )->nodeValue ); if ( ! empty( $val ) && preg_match( '/[\$\£\€\¥\₹]?\s*\d+(?:[\.,]\d{2})?/', $val ) ) { return sanitize_text_field( $val ); } } } } // Fallback regex matching in HTML if DOM extraction failed. if ( preg_match( '/"priceAmount"\s*:\s*"?([0-9\.]+)"?/i', $html, $matches ) ) { return '$' . sanitize_text_field( $matches[1] ); } return ''; } /** * Modern realistic desktop User-Agent string. * * @return string */ private static function get_user_agent() { return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'; } }