isset( $a['access'] ) ? $a['access'] : '', 'secret' => isset( $a['secret'] ) ? AICitare_Crypto::decrypt( $a['secret'] ) : '', 'partner' => isset( $a['partner'] ) ? $a['partner'] : '', ); } /** Is the Amazon module switched on? */ public static function enabled() { $s = AICitare_Data::get_settings(); return ! empty( $s['amazon_enabled'] ); } public static function configured() { if ( ! self::enabled() ) { return false; // Module off → behave as if Amazon support does not exist. } $c = self::creds(); return '' !== $c['access'] && '' !== $c['secret'] && '' !== $c['partner']; } public static function is_amazon_host( $host ) { $host = strtolower( (string) $host ); return (bool) preg_match( '#(^|\.)amazon\.[a-z.]+$#', $host ); } /** Extract the 10-char ASIN from an Amazon product URL, or '' if not present. */ public static function asin( $url ) { if ( preg_match( '#/(?:dp|gp/product|gp/aw/d|product|ASIN)/([A-Z0-9]{10})(?:[/?]|$)#i', $url, $m ) ) { return strtoupper( $m[1] ); } // Bare /XXXXXXXXXX/ segment that looks like an ASIN (has a digit). if ( preg_match( '#/([A-Z0-9]{10})(?:[/?]|$)#', $url, $m ) && preg_match( '#[0-9]#', $m[1] ) ) { return strtoupper( $m[1] ); } return ''; } /** * Verify a set of ASINs for one marketplace host. * Returns map: ASIN => [ 'ok' => bool, 'reason' => string ]. */ public static function check_asins( $asins, $host ) { $mk = self::marketplace( $host ); $out = array(); foreach ( array_chunk( array_values( array_unique( $asins ) ), 10 ) as $chunk ) { $res = self::get_items( $chunk, $mk ); if ( is_wp_error( $res ) ) { foreach ( $chunk as $a ) { $out[ $a ] = array( 'ok' => true, 'reason' => 'API error (unverified)' ); } continue; } $code = (int) wp_remote_retrieve_response_code( $res ); $body = json_decode( wp_remote_retrieve_body( $res ), true ); $found = array(); if ( isset( $body['ItemsResult']['Items'] ) && is_array( $body['ItemsResult']['Items'] ) ) { foreach ( $body['ItemsResult']['Items'] as $it ) { if ( ! empty( $it['ASIN'] ) ) { $found[ strtoupper( $it['ASIN'] ) ] = true; } } } // Classify per-ASIN errors. Only InvalidParameterValue (bad ItemId) means // the product truly does not exist. Everything else (ItemNotAccessible, // throttling, eligibility) is "cannot verify" — never a false "not found". $invalid = array(); // asin => true (genuine: product does not exist) $unverifiable = array(); // asin => code (exists but API won't return it) $global_err = ''; if ( isset( $body['Errors'] ) && is_array( $body['Errors'] ) ) { foreach ( $body['Errors'] as $er ) { $ecode = isset( $er['Code'] ) ? $er['Code'] : ''; $msg = isset( $er['Message'] ) ? $er['Message'] : ''; $matched = false; foreach ( $chunk as $a ) { if ( '' !== $msg && false !== strpos( $msg, $a ) ) { $matched = true; if ( 'InvalidParameterValue' === $ecode || false !== stripos( $msg, 'not a valid' ) || false !== stripos( $msg, 'is invalid' ) ) { $invalid[ $a ] = true; } else { $unverifiable[ $a ] = $ecode ? $ecode : 'unverified'; } } } if ( ! $matched ) { $global_err = $ecode ? $ecode : ( $msg ? $msg : 'API error' ); } } } foreach ( $chunk as $a ) { if ( isset( $found[ $a ] ) ) { $out[ $a ] = array( 'ok' => true, 'reason' => '' ); } elseif ( isset( $invalid[ $a ] ) ) { $out[ $a ] = array( 'ok' => false, 'reason' => 'invalid ASIN — product not found' ); } elseif ( isset( $unverifiable[ $a ] ) ) { $out[ $a ] = array( 'ok' => true, 'reason' => $unverifiable[ $a ] . ' (unverified)' ); } elseif ( '' !== $global_err ) { $out[ $a ] = array( 'ok' => true, 'reason' => $global_err . ' (unverified)' ); } else { // API responded but returned neither the item nor an error for it. $out[ $a ] = array( 'ok' => true, 'reason' => 'no data (unverified)' ); } } } return $out; } /** Signed GetItems request (AWS SigV4). */ private static function get_items( $asins, $mk ) { $creds = self::creds(); $payload = wp_json_encode( array( 'ItemIds' => array_values( $asins ), 'Resources' => array( 'ItemInfo.Title' ), 'PartnerTag' => $creds['partner'], 'PartnerType' => 'Associates', 'Marketplace' => $mk['marketplace'], ) ); return self::call_api( '/paapi5/getitems', 'com.amazon.paapi5.v1.ProductAdvertisingAPIv1.GetItems', $payload, $mk ); } /** * Search for live, available alternative products by keywords. * Returns [ 'ok'=>true, 'items'=>[ {asin,title,url,price}, ... ] ] or [ 'ok'=>false, 'msg'=> ]. */ public static function search( $keywords, $host = 'amazon.com', $n = 5 ) { if ( ! self::configured() ) { return array( 'ok' => false, 'msg' => 'Amazon PA-API is not configured.' ); } $keywords = trim( (string) $keywords ); if ( '' === $keywords ) { return array( 'ok' => false, 'msg' => 'Enter keywords to search for alternatives.' ); } $n = max( 1, min( 10, (int) $n ) ); $mk = self::marketplace( $host ); $creds = self::creds(); $payload = wp_json_encode( array( 'Keywords' => $keywords, 'SearchIndex' => 'All', 'ItemCount' => $n, 'PartnerTag' => $creds['partner'], 'PartnerType' => 'Associates', 'Marketplace' => $mk['marketplace'], 'Resources' => array( 'ItemInfo.Title', 'Offers.Listings.Price', 'Images.Primary.Small' ), ) ); $res = self::call_api( '/paapi5/searchitems', 'com.amazon.paapi5.v1.ProductAdvertisingAPIv1.SearchItems', $payload, $mk ); if ( is_wp_error( $res ) ) { return array( 'ok' => false, 'msg' => $res->get_error_message() ); } $body = json_decode( wp_remote_retrieve_body( $res ), true ); if ( ! isset( $body['SearchResult']['Items'] ) ) { $err = isset( $body['Errors'][0]['Message'] ) ? $body['Errors'][0]['Message'] : ( 'HTTP ' . (int) wp_remote_retrieve_response_code( $res ) ); return array( 'ok' => false, 'msg' => $err ); } $items = array(); foreach ( $body['SearchResult']['Items'] as $it ) { $items[] = array( 'asin' => isset( $it['ASIN'] ) ? $it['ASIN'] : '', 'title' => isset( $it['ItemInfo']['Title']['DisplayValue'] ) ? $it['ItemInfo']['Title']['DisplayValue'] : '(no title)', 'url' => isset( $it['DetailPageURL'] ) ? $it['DetailPageURL'] : '', 'price' => isset( $it['Offers']['Listings'][0]['Price']['DisplayAmount'] ) ? $it['Offers']['Listings'][0]['Price']['DisplayAmount'] : '', 'image' => isset( $it['Images']['Primary']['Small']['URL'] ) ? $it['Images']['Primary']['Small']['URL'] : '', ); } return array( 'ok' => true, 'items' => $items ); } /** Sign (AWS SigV4) and POST a PA-API request. */ private static function call_api( $uri, $target, $payload, $mk ) { $creds = self::creds(); $service = 'ProductAdvertisingAPI'; $host = $mk['host']; $region = $mk['region']; $amzdate = gmdate( 'Ymd\THis\Z' ); $datestamp = gmdate( 'Ymd' ); $canonical_headers = "content-encoding:amz-1.0\nhost:{$host}\nx-amz-date:{$amzdate}\nx-amz-target:{$target}\n"; $signed_headers = 'content-encoding;host;x-amz-date;x-amz-target'; $payload_hash = hash( 'sha256', $payload ); $canonical_request = "POST\n{$uri}\n\n{$canonical_headers}\n{$signed_headers}\n{$payload_hash}"; $algo = 'AWS4-HMAC-SHA256'; $cred_scope = "{$datestamp}/{$region}/{$service}/aws4_request"; $to_sign = "{$algo}\n{$amzdate}\n{$cred_scope}\n" . hash( 'sha256', $canonical_request ); $k_date = hash_hmac( 'sha256', $datestamp, 'AWS4' . $creds['secret'], true ); $k_region = hash_hmac( 'sha256', $region, $k_date, true ); $k_service = hash_hmac( 'sha256', $service, $k_region, true ); $k_signing = hash_hmac( 'sha256', 'aws4_request', $k_service, true ); $signature = hash_hmac( 'sha256', $to_sign, $k_signing ); $authorization = "{$algo} Credential={$creds['access']}/{$cred_scope}, SignedHeaders={$signed_headers}, Signature={$signature}"; return wp_remote_post( "https://{$host}{$uri}", array( 'timeout' => 15, 'headers' => array( 'content-encoding' => 'amz-1.0', 'content-type' => 'application/json; charset=utf-8', 'host' => $host, 'x-amz-date' => $amzdate, 'x-amz-target' => $target, 'Authorization' => $authorization, ), 'body' => $payload, ) ); } /** Lightweight credential test: query a known-good ASIN. */ public static function test() { if ( ! self::configured() ) { return array( 'ok' => false, 'msg' => 'Enter Access Key, Secret Key and Partner Tag first.' ); } $res = self::get_items( array( 'B00KY8UKUI' ), self::marketplace( 'amazon.com' ) ); if ( is_wp_error( $res ) ) { return array( 'ok' => false, 'msg' => $res->get_error_message() ); } $code = (int) wp_remote_retrieve_response_code( $res ); $body = json_decode( wp_remote_retrieve_body( $res ), true ); if ( isset( $body['ItemsResult'] ) ) { return array( 'ok' => true, 'msg' => 'Connected — PA-API responding.' ); } $err = ''; if ( isset( $body['Errors'][0]['Message'] ) ) { $err = $body['Errors'][0]['Message']; } elseif ( isset( $body['__type'] ) ) { $err = $body['__type']; } return array( 'ok' => false, 'msg' => 'HTTP ' . $code . ( $err ? ': ' . $err : '' ) ); } /** Marketplace → PA-API host + region. */ private static function marketplace( $host ) { $host = strtolower( (string) $host ); $map = array( 'amazon.com' => array( 'host' => 'webservices.amazon.com', 'region' => 'us-east-1', 'marketplace' => 'www.amazon.com' ), 'amazon.co.uk' => array( 'host' => 'webservices.amazon.co.uk', 'region' => 'eu-west-1', 'marketplace' => 'www.amazon.co.uk' ), 'amazon.de' => array( 'host' => 'webservices.amazon.de', 'region' => 'eu-west-1', 'marketplace' => 'www.amazon.de' ), 'amazon.fr' => array( 'host' => 'webservices.amazon.fr', 'region' => 'eu-west-1', 'marketplace' => 'www.amazon.fr' ), 'amazon.es' => array( 'host' => 'webservices.amazon.es', 'region' => 'eu-west-1', 'marketplace' => 'www.amazon.es' ), 'amazon.it' => array( 'host' => 'webservices.amazon.it', 'region' => 'eu-west-1', 'marketplace' => 'www.amazon.it' ), 'amazon.ca' => array( 'host' => 'webservices.amazon.ca', 'region' => 'us-east-1', 'marketplace' => 'www.amazon.ca' ), 'amazon.co.jp' => array( 'host' => 'webservices.amazon.co.jp', 'region' => 'us-west-2', 'marketplace' => 'www.amazon.co.jp' ), 'amazon.in' => array( 'host' => 'webservices.amazon.in', 'region' => 'eu-west-1', 'marketplace' => 'www.amazon.in' ), 'amazon.com.au' => array( 'host' => 'webservices.amazon.com.au', 'region' => 'us-west-2', 'marketplace' => 'www.amazon.com.au' ), 'amazon.com.mx' => array( 'host' => 'webservices.amazon.com.mx', 'region' => 'us-east-1', 'marketplace' => 'www.amazon.com.mx' ), ); foreach ( $map as $dom => $mk ) { if ( substr( $host, -strlen( $dom ) ) === $dom ) { return $mk; } } return $map['amazon.com']; } }