_accessId = $accessId; $this->_secretKey = $secretKey; $this->_errors = array( 1 => 'DatafeedrBadRequestError', 2 => 'DatafeedrAuthenticationError', 3 => 'DatafeedrLimitExceededError', 4 => 'DatafeedrQueryError', 7 => 'DatafeedrExternalError', 9 => 'DatafeedrServerError', ); $this->_parseOptions( $options ); } /** * Returns API status information. * * @since 1.0.0 * * @return array An array of API Status information. */ public function getStatus() { $this->apiCall( 'status' ); return $this->_status; } /** * Return status information from the last request. * * If no API request has been made, return NULL * * @since 1.0.0 * * @return array|null Status information or NULL. */ public function lastStatus() { return $this->_status; } /** * Return the list of networks. * * @since 1.0.0 * * @param boolean $includeEmpty Optional. If FALSE, omit networks with 0 products. * @param array $fields Optional. An array of fields to retrieve. * * @param integer|array $networkId Optional. Network ID or an array of network IDs. * * @return array An array of Networks. */ public function getNetworks( $networkId = null, $includeEmpty = false, $fields = null ) { $request = array(); if ( $networkId ) { $request['_ids'] = $this->_intarray( $networkId ); } $request['skip_empty'] = intval( ! $includeEmpty ); if ( $fields ) { $request['fields'] = $fields; } $response = $this->apiCall( 'networks', $request ); return $this->_get( $response, 'networks' ); } /** * Return the list of merchants. * * @since 1.0.0 * * @param bool $includeEmpty Optional. If FALSE, omit merchants with 0 products. * @param array $fields Optional. An array of fields to retrieve. * * @param integer|array $networkId Optional. Network ID or array of Network IDs. * * @return array An array of merchants. */ public function getMerchants( $networkId = null, $includeEmpty = false, $fields = null ) { $request = array(); if ( $networkId ) { $request['source_ids'] = $this->_intarray( $networkId ); } $request['skip_empty'] = intval( ! $includeEmpty ); if ( $fields ) { $request['fields'] = $fields; } $response = $this->apiCall( 'merchants', $request ); return $this->_get( $response, 'merchants' ); } /** * Return a list of merchants by their IDs. * * @since 1.0.0 * * @param boolean $includeEmpty Optional. If FALSE, omit merchants with 0 products. * @param array $fields Optional. An array of fields to retrieve. * * @param integer|array $merchantId Merchant ID or array of Merchant IDs. * * @return array An array of merchants. */ public function getMerchantsById( $merchantId, $includeEmpty = false, $fields = null ) { $request = array(); $request['_ids'] = $this->_intarray( $merchantId ); $request['skip_empty'] = intval( ! $includeEmpty ); if ( $fields ) { $request['fields'] = $fields; } $response = $this->apiCall( 'merchants', $request ); return $this->_get( $response, 'merchants' ); } /** * Return a list of searchable fields. * * @since 1.0.0 * * @param integer|array $networkId Optional. Network ID or array of network IDs. * * @return array An array of searchable fields. * @todo - Does this return a list of all fields for a specific network or only indexed/searchable fields? Update docs accordingly. * */ public function getFields( $networkId = null ) { $request = array(); if ( $networkId ) { $request['source_ids'] = $this->_intarray( $networkId ); } $response = $this->apiCall( 'fields', $request ); return $this->_get( $response, 'fields' ); } /** * Return a list of products by their IDs. * * @since 1.0.0 * * @param array $fields Optional. An array of fields to retrieve. * * @param integer|array $productId Product ID or an array of products IDs. * * @return array An array of Products. */ public function getProducts( $productId, $fields = null ) { $request = array(); $request['_ids'] = $this->_intarray( $productId ); $request['string_ids'] = 1; if ( $fields ) { $request['fields'] = $fields; } $response = $this->apiCall( 'get', $request ); return $this->_get( $response, 'products' ); } /** * Return a list of Zanox merchant IDs ("zmids"). * * @since 1.0.0 * * @param integer $adspaceId Zanox Adspace ID. * @param string $connectId Zanox Connection ID. * * @param integer|array $merchantId Merchant ID or an array of merchant IDs. * * @return array An array of arrays (adspace_id, merchant_id, program_id, zmid). */ public function getZanoxMerchantIds( $merchantId, $adspaceId, $connectId ) { $request = array(); $request['merchant_ids'] = $this->_intarray( $merchantId ); $request['adspace_id'] = $adspaceId; $request['connect_id'] = $connectId; $response = $this->apiCall( 'zanox_merchant_ids', $request ); return $this->_get( $response, 'zanox_merchant_ids' ); } /** * Return a list of PerformanceHorizon campaign references ("camrefs"). * * @since 2.0.0 * * @param string $applicationKey PerformanceHorizon application_key. * @param string $userApiKey PerformanceHorizon user_api_key. * @param string $publisherId PerformanceHorizon publisher_id. * * @param integer|array $merchantId Merchant ID or an array of merchant IDs. * * @return array An array of arrays (campaign_id, camref, merchant_id). */ public function getPerformanceHorizonCamrefs( $merchantId, $applicationKey, $userApiKey, $publisherId ) { $request = array(); $request['merchant_ids'] = $this->_intarray( $merchantId ); $request['application_key'] = $applicationKey; $request['user_api_key'] = $userApiKey; $request['publisher_id'] = $publisherId; $response = $this->apiCall( 'performancehorizon_camrefs', $request ); return $this->_get( $response, 'performancehorizon_camrefs' ); } /** * Return a list of Effiliation affiliate ids. * * @since 2.0.2 * * @param string $apiKey Effiliation api_key. * * @param integer|array $merchantId Merchant ID or an array of merchant IDs. * * @return array An array of arrays (affiliate_id, merchant_id). */ public function getEffiliationAffiliateIds( $merchantId, $apiKey ) { $request = array(); $request['merchant_ids'] = $this->_intarray( $merchantId ); $request['api_key'] = $apiKey; $response = $this->apiCall( 'effiliation_affiliate_ids', $request ); return $this->_get( $response, 'effiliation_affiliate_ids' ); } /** * Create a new DatafeedrSearchRequest object. * * @since 1.0.0 * * @return DatafeedrSearchRequest */ public function searchRequest() { return new DatafeedrSearchRequest( $this ); } /** * Create a new DatafeedrMerchantSearchRequest object. * * @since 1.0.0 * * @return DatafeedrMerchantSearchRequest */ public function merchantSearchRequest() { return new DatafeedrMerchantSearchRequest( $this ); } /** * Create a new DatafeedrAmazonSearchRequest object. * * @since 1.0.0 * * @param string $awsSecretKey Amazon Secret Key. * @param string $awsAssociateTag Amazon Associates tag. * @param string $locale The country locale code. * * @param string $awsAccessKeyId Amazon Access Key. * * @return DatafeedrAmazonSearchRequest */ public function amazonSearchRequest( $awsAccessKeyId, $awsSecretKey, $awsAssociateTag, $locale = 'US' ) { return new DatafeedrAmazonSearchRequest( $this, $awsAccessKeyId, $awsSecretKey, $awsAssociateTag, $locale ); } /** * Create a new DatafeedrAmazonLookupRequest object. * * @since 1.0.0 * * @param string $awsSecretKey Amazon Secret Key. * @param string $awsAssociateTag Amazon Associates tag. * @param string $locale The country locale code. * * @param string $awsAccessKeyId Amazon Access Key. * * @return DatafeedrAmazonLookupRequest */ public function amazonLookupRequest( $awsAccessKeyId, $awsSecretKey, $awsAssociateTag, $locale = 'US' ) { return new DatafeedrAmazonLookupRequest( $this, $awsAccessKeyId, $awsSecretKey, $awsAssociateTag, $locale ); } /** CreatorAPI */ public function amazonCreatorApiSearchRequest( $capiMarketplace = 'US' ) { return new DatafeedrAmazonCreatorApiSearchRequest( $this, $capiMarketplace = 'US' ); } /** CreatorAPI */ // public function amazonCreatorApiLookupRequest( $awsAccessKeyId, $awsSecretKey, $awsAssociateTag, $locale = 'US' ) { // return new DatafeedrAmazonCreatorApiLookupRequest( $this, $awsAccessKeyId, $awsSecretKey, $awsAssociateTag, $locale ); // } /** * Perform the raw API call. * * @since 1.0.0 * * @param array $request Optional. Request data. * * @param string $action API Action. (Examples: status, merchants, networks, search, etc...) * * @return array Returns $response array. * @throws DatafeedrHTTPError Throws error if request status is not 200. * */ public function apiCall( $action, $request = null ) { if ( ! $request ) { $request = array(); } $request['aid'] = $this->_accessId; $request['timestamp'] = gmdate( 'Y-m-d H:i:s' ); $request['domain'] = $this->_domain; if ( $this->_https ) { $request['akey'] = $this->_secretKey; } else { $message = $request['aid'] . $action . $request['timestamp']; $request['signature'] = hash_hmac( 'sha256', $message, $this->_secretKey, false ); } $postdata = json_encode( $request ); $url = $this->_url . '/' . $action; $headers = array( 'Host: ' . $this->_host, 'Content-Type: application/json', 'Accept: application/json', 'Connection: close', 'User-Agent: ' . $this->_userAgent ); if ( $this->_hasZlib && strlen( $postdata ) >= self::REQUEST_COMPRESSION_THRESHOLD ) { $postdata = gzcompress( $postdata ); $headers [] = 'Content-Encoding: deflate'; } $headers [] = 'Content-Length: ' . strlen( $postdata ); list( $status, $response ) = $this->_performRequest( $url, $headers, $postdata ); if ( strlen( $response ) ) { $response = json_decode( $response, ! $this->_returnObjects ); } $error = $this->_get( $response, 'error' ); if ( $error ) { $type = $this->_get( $response, 'type' ); $cls = isset( $this->_errors[ $type ] ) ? $this->_errors[ $type ] : 'DatafeedrError'; throw new $cls( $this->_get( $response, 'message' ), $error ); } if ( 200 != $status ) { throw new DatafeedrHTTPError( "Status $status" ); } $this->_status = $this->_get( $response, 'status' ); return $response; } /** * Returns the default set of options. * * @since 2.0.0 * * @returns array $options. * */ protected function _defaultOptions() { return array( 'host' => self::DEFAULT_HOST, 'https' => false, 'transport' => 'wordpress_or_curl', 'timeout' => 30, 'returnObjects' => false, 'retry' => 0, 'retryTimeout' => 5, 'domain' => '', ); } /** * Parse constructor options. * * @since 2.0.0 * * @param array $options . * * @throws DatafeedrError Throws error if any option is invalid. */ protected function _parseOptions( $options ) { $opts = $this->_defaultOptions(); if ( ! is_null( $options ) ) { if ( ! is_array( $options ) ) { throw new DatafeedrError( "Options must be an array" ); } foreach ( $options as $key => $value ) { if ( isset( $opts[ $key ] ) ) { $opts[ $key ] = $value; } } } $pt = $opts['https'] ? 'https' : 'http'; $tr = $opts['transport']; $this->_url = $pt . '://' . $opts['host']; $this->_https = $opts['https']; $this->_host = $opts['host']; $this->_timeout = (int) $opts['timeout']; $this->_returnObjects = (int) $opts['returnObjects']; $this->_retry = (int) $opts['retry']; $this->_retryTimeout = (int) $opts['retryTimeout']; $this->_domain = (string) $opts['domain']; switch ( $tr ) { case 'curl': $this->_transport = array( $this, '_transportCurl' ); break; case 'file': $this->_transport = array( $this, '_transportFile' ); break; case 'wordpress': if ( ! function_exists( 'wp_remote_post' ) ) { throw new DatafeedrError( "Wordpress transport requires wp_remote_post" ); } $this->_transport = array( $this, '_transportWordpress' ); break; case 'wordpress_or_curl': if ( ! function_exists( 'wp_remote_post' ) ) { $this->_transport = array( $this, '_transportCurl' ); $tr = 'curl'; } else { $this->_transport = array( $this, '_transportWordpress' ); $tr = 'wordpress'; } break; default: if ( ! is_callable( $tr ) ) { throw new DatafeedrError( "Transport must be a function" ); } $this->_transport = $tr; $tr = 'custom'; } $this->_hasZlib = function_exists( 'gzcompress' ); $this->_userAgent = sprintf( 'datafeedr.php.%s/%s/zlib=%s', self::VERSION, $tr, $this->_hasZlib ? 'yes' : 'no' ); } /** * Perform an HTTP request. * * @since 2.0.0 * * @param array $headers * @param string $postdata * * @param string $url * * @return array An array of (status, responseBody) * @throws DatafeedrConnectionError * */ protected function _performRequest( $url, $headers, $postdata ) { $retry = $this->_retry; while ( true ) { try { return call_user_func( $this->_transport, $url, $headers, $postdata ); } catch ( DatafeedrConnectionError $err ) { if ( $retry <= 0 ) { throw $err; } sleep( $this->_retryTimeout ); $retry --; } } return array(); } /** * Convert an ID or an array of IDs to a simple array of IDs. * * @since 1.0.0 * * @param integer|string|array $id_or_ids An ID or an array of IDs. * * @return array An array of IDs. */ protected function _intarray( $id_or_ids ) { if ( is_numeric( $id_or_ids ) ) { return array( $id_or_ids ); } if ( is_array( $id_or_ids ) ) { return array_values( $id_or_ids ); } return array(); } /** * Returns a specific value from an array or object for a given key or property. If key or * property does not exist, returns $default. * * @since 1.0.0 * * @param string $prop The array key or object property to get the value for. * @param null $default Optional. The value to return if $obj or $prop does not exist. * * @param array|object $obj An array or object to extract value from. * * @return mixed|null The returned value. */ protected function _get( $obj, $prop, $default = null ) { if ( is_array( $obj ) && isset( $obj[ $prop ] ) ) { return $obj[ $prop ]; } if ( is_object( $obj ) && isset( $obj->$prop ) ) { return $obj->$prop; } return $default; } /** * Perform an HTTP POST request using the CURL library. * * @since 1.0.0 * * @param array $headers Array of headers. * @param string $postdata Post data. * * @param string $url Request url. * * @return array (int http status, string response body) * @throws DatafeedrConnectionError Throws error if curl_errno() returns an error. * */ protected function _transportCurl( $url, $headers, $postdata ) { $ch = curl_init(); curl_setopt( $ch, CURLOPT_URL, $url ); curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, $this->_timeout ); curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ); curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers ); curl_setopt( $ch, CURLOPT_POST, 1 ); curl_setopt( $ch, CURLOPT_POSTFIELDS, $postdata ); curl_setopt( $ch, CURLOPT_ENCODING, '' ); $response = curl_exec( $ch ); $status = intval( curl_getinfo( $ch, CURLINFO_HTTP_CODE ) ); $errno = curl_errno( $ch ); $errmsg = curl_error( $ch ); curl_close( $ch ); if ( $errno ) { throw new DatafeedrConnectionError( $errmsg, $errno ); } return array( $status, $response ); } /** * Perform a HTTP post request using file functions. * * @since 1.0.0 * * @param array $headers Array of headers. * @param string $postdata Post data. * * @param string $url Request url. * * @return array (int http status, string response body) * @throws DatafeedrConnectionError Throws error if $response is false. * */ protected function _transportFile( $url, $headers, $postdata ) { $options = array( 'http' => array( 'method' => 'POST', 'content' => $postdata, 'header' => implode( "\r\n", $headers ), 'ignore_errors' => true, 'timeout' => $this->_timeout, ) ); $context = stream_context_create( $options ); $response = file_get_contents( $url, false, $context ); $status = 200; if ( isset( $http_response_header ) && isset( $http_response_header[0] ) ) { if ( preg_match( '/HTTP.+?(\d\d\d)/', $http_response_header[0], $match ) ) { $status = intval( $match[1] ); } } else if ( $response === false ) { throw new DatafeedrConnectionError( "Invalid response" ); } return array( $status, $response ); } /** * Perform a HTTP post request using Wordpress functions. * * @since 1.0.0 * * @param array $headers Array of headers. * @param string $postdata Post data. * * @param string $url Request url. * * @return array (int http status, string response body) * @throws DatafeedrConnectionError Throws error if $response is WP_Error and the error code is 'http_request_failed'. * @throws DatafeedrHTTPError Throws error if $response is WP_Error. * */ protected function _transportWordpress( $url, $headers, $postdata ) { $ha = array(); foreach ( $headers as $h ) { $h = explode( ':', $h, 2 ); $ha[ strtolower( $h[0] ) ] = $h[1]; } $args = array( 'method' => 'POST', 'headers' => $ha, 'body' => $postdata, 'httpversion' => '1.1', 'timeout' => $this->_timeout, 'blocking' => true, 'compress' => false, 'decompress' => true, 'user-agent' => $ha['user-agent'] ); $res = wp_remote_post( $url, $args ); if ( is_wp_error( $res ) ) { $code = $res->get_error_code(); $message = $res->get_error_message(); if ( $code === 'http_request_failed' ) { throw new DatafeedrConnectionError( $message ); } else { throw new DatafeedrHTTPError( $message ); } } return array( $res['response']['code'], $res['body'] ); } } /** * Generic Datafeedr API search request. */ class DatafeedrSearchRequestBase { /** * The DatafeedrAPI object. * * @since 1.0.0 * @var DatafeedrAPI $_api */ protected $_api; /** * An array containing the full response of the last API request. * * @since 1.0.0 * @var array $_lastResponse Response array. */ protected $_lastResponse; /** * DatafeedrSearchRequestBase constructor. * * @since 1.0.0 * * @param DatafeedrApi $api * */ public function __construct( $api ) { $this->_api = $api; } /** * Get the number of found products. * * @since 1.0.0 * * @return integer */ public function getFoundCount() { return $this->_responseItem( 'found_count', 0 ); } /** * Get the number of products that can be retrieved from the server. * * @since 1.0.0 * * @return integer */ public function getResultCount() { return $this->_responseItem( 'result_count', 0 ); } /** * Get the complexity score of the current query. * * @since 1.2.12 * @return int * @throws DatafeedrError */ public function getQueryScore() { return abs( (int) $this->_responseItem( 'score', 0 ) ); } /** * Returns the full response from the last search. * * Possible items in array include the following: * * Array ( * 'found_count' => integer, * 'length' => integer, * 'merchants' => array, * 'networks' => array, * 'price_groups' => array, * 'products' => array, * 'result_count' => integer, * 'status' => array, * 'time' => integer, * 'version' => string, * ) * * @since 1.0.0 * * @return array An array of the full response. */ public function getResponse() { return $this->_lastResponse; } /** * Returns a specific item or property from the response data. * * @since 1.0.0 * * @param mixed $default Return if $prop is not found in the array or object. * * @param string $prop The item or property to get from the response array or object. * * @return mixed Specific item or property from response data. * @throws DatafeedrError Throws error if $this->_lastResponse is NULL. * */ protected function _responseItem( $prop, $default ) { if ( is_null( $this->_lastResponse ) ) { throw new DatafeedrError( "Reading from an empty request" ); } if ( is_object( $this->_lastResponse ) && isset( $this->_lastResponse->$prop ) ) { return $this->_lastResponse->$prop; } if ( is_array( $this->_lastResponse ) && isset( $this->_lastResponse[ $prop ] ) ) { return $this->_lastResponse[ $prop ]; } return $default; } /** * Sets the $_lastResponse property to the the full response from the last API request. * * @since 1.0.0 * * @param array $request The current API request. * * @param string $action The request action (ex. status, search, merchants, networks, etc...) */ function _apiCall( $action, $request = null ) { $this->_lastResponse = $this->_api->apiCall( $action, $request ); } } /** * Class DatafeedrSearchRequest * * Search request for Datafeedr API. */ class DatafeedrSearchRequest extends DatafeedrSearchRequestBase { protected $_query; protected $_sort; protected $_fields; protected $_limit; protected $_offset; protected $_priceGroups; protected $_excludeDuplicates; protected $_merchantLimit; /** * DatafeedrSearchRequest constructor. * * @since 1.0.0 * * @param DatafeedrApi $api * */ public function __construct( $api ) { parent::__construct( $api ); $this->_query = array(); $this->_sort = array(); $this->_fields = array(); $this->_limit = 0; $this->_offset = 0; $this->_priceGroups = 0; $this->_excludeDuplicates = ''; $this->_merchantLimit = 0; } /** * Add a query filter. * * @since 1.0.0 * * @param string $filter Query filter. * * @return DatafeedrSearchRequest Returns $this. */ public function addFilter( $filter ) { $this->_query[] = $filter; return $this; } /** * Adds a sort field. * * @since 1.0.0 * * @param integer $order One of DatafeedrApi::SORT_ASCENDING or DatafeedrApi::SORT_DESCENDING * * @param string $field Field name. * * @return DatafeedrSearchRequest Returns $this. * @throws DatafeedrError Throws error if sort order is invalid. * */ public function addSort( $field, $order = DatafeedrApi::SORT_ASCENDING ) { if ( strlen( $field ) && ( $field[0] == '+' || $field[0] == '-' ) ) { $this->_sort [] = $field; } else if ( $order == DatafeedrApi::SORT_ASCENDING ) { $this->_sort [] = '+' . $field; } else if ( $order == DatafeedrApi::SORT_DESCENDING ) { $this->_sort [] = '-' . $field; } else { throw new DatafeedrError( "Invalid sort order" ); } return $this; } /** * Set which fields to retrieve. * * @since 1.0.0 * * @param array $fields An array of fields to return for each requested item. * * Example: * * Array ( * 'name', * 'price', * 'description', * 'url', * ) * * @return DatafeedrSearchRequest Returns $this. */ public function setFields( $fields ) { $this->_fields = $fields; return $this; } /** * Exclude duplicate filter. * * @since 1.0.0 * * @param string $filter Equality filter in form "field1 field2 | field3". * * @return DatafeedrSearchRequest Returns $this. */ public function excludeDuplicates( $filter ) { if ( is_array( $filter ) ) { $filter = implode( ' ', $filter ); } $this->_excludeDuplicates = $filter; return $this; } /** * Set a limit of number of records to return. * * @since 1.0.0 * * @param integer $limit The maximum number of records to return. * * @return DatafeedrSearchRequest Returns $this. */ public function setLimit( $limit ) { $this->_limit = $limit; return $this; } /** * Set an offset for the search. * * @since 1.0.0 * * @param integer $offset The offset. * * @return DatafeedrSearchRequest Returns $this. */ public function setOffset( $offset ) { $this->_offset = $offset; return $this; } /** * Set a limit of results by merchant. * * @since 2.0.3 * * @param integer $limit The limit. * * @return DatafeedrSearchRequest Returns $this. */ public function setMerchantLimit( $limit ) { $this->_merchantLimit = $limit; return $this; } /** * Set a price group count. * * This should be used in conjunction with the $this->getPriceGroups() method. * * Setting $groups to 3 will organize the products into 3 price groups like this: * * Array ( * [0] => Array ( * [product_count] => 321 * [max] => 9633332 * [min] => 0 * ) * [1] => Array ( * [product_count] => 116 * [max] => 19266665 * [min] => 9633333 * ) * [2] => Array ( * [product_count] => 43 * [max] => 28900000 * [min] => 19266666 * ) * ) * * @since 1.0.0 * * @param integer $groups Number of price groups to create. * * @return DatafeedrSearchRequest Returns $this. */ public function setPriceGroups( $groups ) { $this->_priceGroups = $groups; return $this; } /** * Get networks found in this request. * * @since 1.0.0 * * @return array */ public function getNetworks() { return $this->_responseItem( 'networks', array() ); } /** * Get merchants found in this request. * * @since 1.0.0 * * @return array */ public function getMerchants() { return $this->_responseItem( 'merchants', array() ); } /** * Get price groups found in this request. * * This must be used in conjunction with $this->setPriceGroups(); * * Returns an array like this: * * Array ( * [0] => Array ( * [product_count] => 321 * [max] => 9633332 * [min] => 0 * ) * [1] => Array ( * [product_count] => 116 * [max] => 19266665 * [min] => 9633333 * ) * [2] => Array ( * [product_count] => 43 * [max] => 28900000 * [min] => 19266666 * ) * ) * * @since 1.0.0 * * @return array An array of price groups and the product count, min and max prices in each group. */ public function getPriceGroups() { return $this->_responseItem( 'price_groups', array() ); } /** * Create a request array to use for querying the API. * * @since 1.0.0 * * @return array The $request array(). */ public function getParams() { $request = array(); if ( $this->_query ) { $request['query'] = $this->_query; } if ( $this->_sort ) { $request['sort'] = $this->_sort; } if ( $this->_fields ) { $request['fields'] = $this->_fields; } if ( $this->_limit ) { $request['limit'] = $this->_limit; } if ( $this->_offset ) { $request['offset'] = $this->_offset; } if ( $this->_priceGroups ) { $request['price_groups'] = $this->_priceGroups; } if ( $this->_merchantLimit ) { $request['merchant_limit'] = $this->_merchantLimit; } if ( $this->_excludeDuplicates ) { $request['exclude_duplicates'] = $this->_excludeDuplicates; } $request['string_ids'] = 1; return $request; } /** * Run search and return a list of products. * * @since 1.0.0 * * @return array An array of products. * @throws DatafeedrError Throw error if query is empty. * */ public function execute() { $params = $this->getParams(); if ( ! isset( $params['query'] ) ) { throw new DatafeedrError( "Query can't be empty" ); } $this->_apiCall( 'search', $params ); return $this->_responseItem( 'products', array() ); } } /** * Search request for Datafeedr Merchants. */ class DatafeedrMerchantSearchRequest extends DatafeedrSearchRequestBase { protected $_query; protected $_sort; protected $_fields; protected $_limit; protected $_offset; /** * DatafeedrMerchantSearchRequest constructor. * * @since 1.0.0 * * @param DatafeedrApi $api * */ public function __construct( $api ) { parent::__construct( $api ); $this->_query = array(); $this->_sort = array(); $this->_fields = array(); $this->_limit = 0; $this->_offset = 0; } /** * Add a query filter. * * @since 1.0.0 * * @param string $filter Query filter. * * @return DatafeedrMerchantSearchRequest Returns $this. */ public function addFilter( $filter ) { $this->_query [] = $filter; return $this; } /** * Add a sort field. * * @since 1.0.0 * * @param integer $order One of DatafeedrApi::SORT_ASCENDING or DatafeedrApi::SORT_DESCENDING * * @param string $field Field name. * * @return DatafeedrMerchantSearchRequest Returns $this. * @throws DatafeedrError Throw error if invalid sort order. * */ public function addSort( $field, $order = DatafeedrApi::SORT_ASCENDING ) { if ( strlen( $field ) && ( $field[0] == '+' || $field[0] == '-' ) ) { $this->_sort [] = $field; } else if ( $order == DatafeedrApi::SORT_ASCENDING ) { $this->_sort [] = '+' . $field; } else if ( $order == DatafeedrApi::SORT_DESCENDING ) { $this->_sort [] = '-' . $field; } else { throw new DatafeedrError( "Invalid sort order" ); } return $this; } /** * Set which fields to retrieve. * * @since 1.0.0 * * @param array $fields An array of field names. * * @return DatafeedrMerchantSearchRequest Returns $this. */ public function setFields( $fields ) { $this->_fields = $fields; return $this; } /** * Set a limit. * * @since 1.0.0 * * @param integer $limit Number of items to return. * * @return DatafeedrMerchantSearchRequest Returns $this. */ public function setLimit( $limit ) { $this->_limit = $limit; return $this; } /** * Set an offset. * * @since 1.0.0 * * @param integer $offset The offset. * * @return DatafeedrMerchantSearchRequest Returns $this. */ public function setOffset( $offset ) { $this->_offset = $offset; return $this; } /** * Get networks found in this request. * * @since 1.0.0 * * @return array */ public function getNetworks() { return $this->_responseItem( 'networks', array() ); } /** * Get merchants found in this request. * * @since 1.0.0 * * @return array */ public function getMerchants() { return $this->_responseItem( 'merchants', array() ); } /** * Run search and return an array of merchants. * * @since 1.0.0 * * @return array Array of merchants * @throws DatafeedrError Throws error if query is empty. * */ public function execute() { $params = $this->getParams(); if ( ! isset( $params['query'] ) ) { throw new DatafeedrError( "Query can't be empty" ); } $this->_apiCall( 'merchant_search', $params ); return $this->_responseItem( 'merchants', array() ); } /** * Create a request array to use for querying the API. * * @since 1.0.0 * * @return array The $request array(). */ public function getParams() { $request = array(); if ( $this->_query ) { $request['query'] = $this->_query; } if ( $this->_sort ) { $request['sort'] = $this->_sort; } if ( $this->_fields ) { $request['fields'] = $this->_fields; } if ( $this->_limit ) { $request['limit'] = $this->_limit; } if ( $this->_offset ) { $request['offset'] = $this->_offset; } return $request; } } /** * Generic Amazon request. */ class DatafeedrAmazonRequest extends DatafeedrSearchRequestBase { protected $_found = - 1; protected $_hosts; protected $_params; protected $_locale; protected $_awsAccessKeyId; protected $_awsSecretKey; protected $_awsAssociateTag; const AWS_VERSION = "2011-08-01"; /** * DatafeedrAmazonRequest constructor * * @since 1.0.0 * * @param string $awsAccessKeyId Amazon Access Key. * @param string $awsSecretKey Amazon Secret Key. * @param string $awsAssociateTag Amazon Associate tag. * @param string $locale Optional. Amazon locale (two-letter code). * * @param DatafeedrApi $api * * @throws DatafeedrError Throws error if Amazon Locale is invalid. */ public function __construct( $api, $awsAccessKeyId, $awsSecretKey, $awsAssociateTag, $locale = 'US' ) { parent::__construct( $api ); $this->_hosts = array( 'AU' => 'webservices.amazon.com.au', 'BR' => 'webservices.amazon.com.br', 'CA' => 'webservices.amazon.ca', 'FR' => 'webservices.amazon.fr', 'DE' => 'webservices.amazon.de', 'IN' => 'webservices.amazon.in', 'IT' => 'webservices.amazon.it', 'JP' => 'webservices.amazon.co.jp', 'MX' => 'webservices.amazon.com.mx', 'NL' => 'webservices.amazon.nl', 'SG' => 'webservices.amazon.sg', 'SA' => 'webservices.amazon.sa', 'ES' => 'webservices.amazon.es', 'SE' => 'webservices.amazon.se', 'TR' => 'webservices.amazon.com.tr', 'AE' => 'webservices.amazon.ae', 'UK' => 'webservices.amazon.co.uk', 'US' => 'webservices.amazon.com', ); $this->_params = array(); $this->_locale = strtoupper( $locale ); if ( ! isset( $this->_hosts[ $this->_locale ] ) ) { throw new DatafeedrError( 'Invalid Amazon locale' ); } $this->_awsAccessKeyId = $awsAccessKeyId; $this->_awsSecretKey = $awsSecretKey; $this->_awsAssociateTag = $awsAssociateTag; } /** * Returns all parameters. * * @since 1.0.0 * * @return array */ public function getParams() { return $this->_params; } /** * Prepare am API request for Amazon. * * @since 3.0.0 * * @param array $params * * @param string $operation * * @return array */ protected function _amazonRequest( $operation, $params ) { return array( 'amz_access' => $this->_awsAccessKeyId, 'amz_key' => $this->_awsSecretKey, 'amz_tag' => $this->_awsAssociateTag, 'locale' => $this->_locale, 'operation' => $operation, 'params' => $params, ); } } /** * Class DatafeedrAmazonSearchRequest * * Amazon search request class. */ class DatafeedrAmazonSearchRequest extends DatafeedrAmazonRequest { /** * Add a parameter. * * @since 1.0.0 * * @param string $value Parameter value. * * @param string $name Parameter name. * * @return DatafeedrAmazonSearchRequest Returns $this. * * @see https://webservices.amazon.com/paapi5/documentation/search-items.html#ItemLookup-rp */ public function addParam( $name, $value ) { $this->_params[ $name ] = $value; return $this; } /** * Run search and return an array of products. * * IMPORTANT - The Amazon API returns a MAXIMUM of 10 products per API request and a maximum of * 50 products per search query. * * @since 1.0.0 * * @return array An array of products. * @throws DatafeedrError */ public function execute() { $params = array_filter( $this->_params ); $req = $this->_amazonRequest( 'SearchItems', $params ); $this->_apiCall( 'amazon_find', $req ); return $this->_responseItem( 'products', array() ); } } /** * Class DatafeedrAmazonLookupRequest * * Amazon lookup request. */ class DatafeedrAmazonLookupRequest extends DatafeedrAmazonRequest { /** * Add a parameter. * * @since 1.0.0 * * @param string|array $value Parameter value or an array of values (up to 10). * * @param string $name Parameter name - one of 'ASIN', 'SKU', 'UPC', 'EAN', 'ISBN' * * @return DatafeedrAmazonLookupRequest Returns $this. * * @see https://webservices.amazon.com/paapi5/documentation/get-items.html#ItemLookup-rp */ public function addParam( $name, $value ) { $this->_params[ $name ] = $value; return $this; } /** * Run search and return an array of products. * * @since 1.0.0 * * @return array An array of products. */ public function execute() { $params = array_filter( $this->_params ); $types = array( 'ASIN', 'SKU', 'UPC', 'EAN', 'ISBN', 'asin', 'sku', 'upc', 'ean', 'isbn' ); foreach ( $types as $type ) { if ( isset( $params[ $type ] ) ) { $params['ItemIdType'] = strtoupper( $type ); $params['ItemIds'] = $params[ $type ]; if ( is_string( $params['ItemIds'] ) ) { $params['ItemIds'] = explode( ',', $params['ItemIds'] ); } unset( $params[ $type ] ); } } $req = $this->_amazonRequest( 'GetItems', $params ); $this->_apiCall( 'amazon_find', $req ); return $this->_responseItem( 'products', array() ); } } /** CreatorAPI */ class DatafeedrAmazonCreatorApiRequest extends DatafeedrSearchRequestBase { protected $_found = - 1; protected $_hosts; protected $_params; // protected $_capiCredentialId; // protected $_capiCredentialSecret; // protected $_capiVersion; protected $_capiPartnerTag; protected $_capiMarketplace; public const AWS_VERSION = "2026-01-05"; public function __construct( $api, $capiMarketplace = 'US' ) { parent::__construct( $api ); $capiMarketplace = strtoupper( trim( $capiMarketplace ) ); foreach ( dfrapi_get_capi_marketplaces() as $code => $marketplace ) { $this->_hosts[ $code ] = $marketplace['domain']; } if ( ! isset( $this->_hosts[ $capiMarketplace ] ) ) { throw new DatafeedrError( 'Invalid Amazon CAPI locale' ); } $this->_params = array(); // $capiVersion = dfrapi_get_capi_marketplace( $capiMarketplace )['region']['version']; // $this->_capiCredentialId = $capiCredentialId; // $this->_capiCredentialSecret = $capiCredentialSecret; // $this->_capiVersion = $capiVersion; // $this->_capiPartnerTag = $capiPartnerTag; $this->_capiMarketplace = $capiMarketplace; } /** * Returns all parameters. * * @since 1.0.0 * * @return array */ public function getParams() { return $this->_params; } /** * Prepare am API request for Amazon. * * @since 3.0.0 * * @param array $params * * @param string $operation * * @return array */ // protected function _amazonRequest( $operation, $params ) { // return array( // 'capi_credential_id' => $this->_capiCredentialId, // 'capi_credential_secret' => $this->_capiCredentialSecret, // 'capi_version' => $this->_capiVersion, // 'capi_partner_tag' => $this->_capiPartnerTag, // 'capi_marketplace' => $this->_capiMarketplace, // 'operation' => $operation, // 'params' => $params, // ); // } } /** CreatorAPI */ class DatafeedrAmazonCreatorApiSearchRequest extends DatafeedrAmazonCreatorApiRequest { /** * Add a parameter. * * @since 1.0.0 * * @param string $value Parameter value. * * @param string $name Parameter name. * * @return DatafeedrAmazonCreatorApiSearchRequest Returns $this. * * @see https://webservices.amazon.com/paapi5/documentation/search-items.html#ItemLookup-rp */ public function addParam( $name, $value ) { $this->_params[ $name ] = $value; return $this; } /** * Run search and return an array of products. * * IMPORTANT - The Amazon API returns a MAXIMUM of 10 products per API request and a maximum of * 50 products per search query. * * @since 1.0.0 * * @return array An array of products. * @throws DatafeedrError */ public function execute() { $access_token = dfrapi_get_capi_access_token(); if ( is_wp_error( $access_token ) ) { throw new DatafeedrError( $access_token->get_error_message() ); } $body = array_merge( array_filter( $this->_params ), [] ); $credentials = dfrapi_get_capi_credentials(); $marketplace = dfrapi_get_capi_marketplace( $this->_capiMarketplace ); $domain = $marketplace['domain']; $credential_version = $marketplace['region']['version']; $body['partnerTag'] = $credentials['partner_tag']; // https://affiliate-program.amazon.com/creatorsapi/docs/en-us/api-reference/operations/search-items $resources = [ // 'browseNodeInfo.browseNodes', // 'browseNodeInfo.browseNodes.ancestor', // 'browseNodeInfo.browseNodes.salesRank', // 'browseNodeInfo.websiteSalesRank', 'customerReviews.count', 'customerReviews.starRating', 'images.primary.highRes', 'images.primary.large', 'images.primary.medium', 'images.primary.small', // 'images.variants.highRes', // 'images.variants.large', // 'images.variants.medium', // 'images.variants.small', 'itemInfo.byLineInfo', // 'itemInfo.classifications', // 'itemInfo.contentInfo', // 'itemInfo.contentRating', 'itemInfo.externalIds', // 'itemInfo.features', // 'itemInfo.manufactureInfo', 'itemInfo.productInfo', // 'itemInfo.technicalInfo', 'itemInfo.title', 'itemInfo.tradeInInfo', 'offersV2.listings.availability', 'offersV2.listings.condition', // 'offersV2.listings.dealDetails', // 'offersV2.listings.isBuyBoxWinner', // 'offersV2.listings.loyaltyPoints', // 'offersV2.listings.merchantInfo', 'offersV2.listings.price', 'offersV2.listings.type', 'parentASIN', // 'searchRefinements', ]; $body['resources'] = $resources; $response = wp_remote_post( 'https://creatorsapi.amazon/catalog/v1/searchItems', [ 'headers' => [ 'Authorization' => sprintf( 'Bearer %s, Version %s', $access_token, $credential_version ), 'Content-Type' => 'application/json', 'x-marketplace' => $domain, ], 'body' => wp_json_encode( $body ), 'timeout' => 20, 'httpversion' => '1.1', // important for Amazon APIs ] ); if ( is_wp_error( $response ) ) { error_log( $response->get_error_message() ); throw new DatafeedrConnectionError( $response->get_error_message() ); } $status = wp_remote_retrieve_response_code( $response ); if ( 404 === $status ) { $body = wp_remote_retrieve_body( $response ); $data = json_decode( $body, true ); if ( isset( $data['type'] ) && 'ResourceNotFoundException' === $data['type'] ) { return []; } } if ( 200 !== $status ) { $error_body = wp_remote_retrieve_body( $response ); $error_data = json_decode( $error_body, true ); $status_message = wp_remote_retrieve_response_message( $response ); if ( ! empty( $error_data['message'] ) ) { $message = $error_data['message']; } elseif ( ! empty( $error_data['reason'] ) ) { $message = $error_data['reason']; } else { $message = $status_message; } error_log( sprintf( '[Datafeedr CAPI] SearchItems error (%d): %s | Response: %s', $status, $message, $error_body ) ); throw new DatafeedrHTTPError( $message, $status ); } $body = wp_remote_retrieve_body( $response ); /* [ "errors" => null, "searchResult" => [ "items" => [ [ "asin" => "0545162076", "browseNodeInfo" => null, "customerReviews" => null, "detailPageURL" => "https://www.amazon.com/dp/0545162076?tag=xyz-20&linkCode=osi&th=1&psc=1", "images" => null, "itemInfo" => [ "byLineInfo" => null, "classifications" => null, "contentInfo" => null, "contentRating" => null, "externalIds" => null, "features" => null, "manufactureInfo" => null, "productInfo" => null, "technicalInfo" => null, "title" => [ "displayValue" => "Harry Potter Paperback Box Set (Books 1-7)", "label" => "Title", "locale" => "en_US", ], "tradeInInfo" => null, ], "offersV2" => null, "parentASIN" => null, "score" => null, "variationAttributes" => null, ], [ ... ], [ ... ], [ ... ], ], "searchRefinements" => null, "searchURL" => "https://www.amazon.com/s?k=Harry+Potter&rh=p_n_availability%3A2661600011&tag=xyz-20&linkCode=osi", "totalResultCount" => 306, ], ] */ /** * [ * "errors" => null, * "searchResult" => [ * "items" => [ * [ * "asin" => "0545162076", * "browseNodeInfo" => null, * "customerReviews" => null, * "detailPageURL" => "https://www.amazon.com/dp/0545162076?tag=xyz-20&linkCode=osi&th=1&psc=1", * "images" => null, * "itemInfo" => [ * "byLineInfo" => null, * "classifications" => null, * "contentInfo" => null, * "contentRating" => null, * "externalIds" => null, * "features" => null, * "manufactureInfo" => null, * "productInfo" => null, * "technicalInfo" => null, * "title" => [ * "displayValue" => "Harry Potter Paperback Box Set (Books 1-7)", * "label" => "Title", * "locale" => "en_US", * ], * "tradeInInfo" => null, * ], * "offersV2" => null, * "parentASIN" => null, * "score" => null, * "variationAttributes" => null, * ], * [ ... ], * [ ... ], * [ ... ], * ], * "searchRefinements" => null, * "searchURL" => "https://www.amazon.com/s?k=Harry+Potter&rh=p_n_availability%3A2661600011&tag=xyz-20&linkCode=osi", * "totalResultCount" => 306, * ], * ] */ $data = json_decode( $body, true ); if ( ! empty( $data['errors'] ) ) { $error = $data['errors'][0]; error_log( $error['message'] ); throw new DatafeedrExternalError( $error['message'] ); } $items = $data['searchResult']['items'] ?? []; $products = []; foreach ( $items as $item ) { $products[] = dfrapi_transform_capi_item_into_datafeedr_product_array( $item ); } return $products; // $data now contains the API response } } /** CreatorAPI */ //class DatafeedrAmazonCreatorApiLookupRequest extends DatafeedrAmazonCreatorApiRequest { // // /** // * Add a parameter. // * // * @since 1.0.0 // * // * @param string|array $value Parameter value or an array of values (up to 10). // * // * @param string $name Parameter name - one of 'ASIN', 'SKU', 'UPC', 'EAN', 'ISBN' // * // * @return DatafeedrAmazonCreatorApiLookupRequest Returns $this. // * // * @see https://webservices.amazon.com/paapi5/documentation/get-items.html#ItemLookup-rp // */ // public function addParam( $name, $value ) { // $this->_params[ $name ] = $value; // // return $this; // } // // /** // * Run search and return an array of products. // * // * @since 1.0.0 // * // * @return array An array of products. // */ // public function execute() { // // $params = array_filter( $this->_params ); // $types = array( 'ASIN', 'SKU', 'UPC', 'EAN', 'ISBN', 'asin', 'sku', 'upc', 'ean', 'isbn' ); // // foreach ( $types as $type ) { // if ( isset( $params[ $type ] ) ) { // $params['itemIdType'] = strtoupper( $type ); // $params['itemIds'] = $params[ $type ]; // if ( is_string( $params['itemIds'] ) ) { // $params['itemIds'] = explode( ',', $params['itemIds'] ); // } // unset( $params[ $type ] ); // } // } // // $req = $this->_amazonRequest( 'GetItems', $params ); // $this->_apiCall( 'amazon_find', $req ); // // return $this->_responseItem( 'products', array() ); // } //} /** * Class DatafeedrError. * * Generic Api error. */ class DatafeedrError extends Exception { } /** * Class DatafeedrBadRequestError. * * API error: Invalid Request. */ class DatafeedrBadRequestError extends DatafeedrError { } /** * Class DatafeedrAuthenticationError. * * API error: Authentication failed. */ class DatafeedrAuthenticationError extends DatafeedrError { } /** * Class DatafeedrLimitExceededError. * * API error: Query limit exceeded. */ class DatafeedrLimitExceededError extends DatafeedrError { } /** * Class DatafeedrHTTPError. * * API error: Unspecified HTTP error. */ class DatafeedrHTTPError extends DatafeedrError { } /** * Class DatafeedrConnectionError. * * API error: Connection error. */ class DatafeedrConnectionError extends DatafeedrError { } /** * Class DatafeedrQueryError. * * API error: Error in the search query. */ class DatafeedrQueryError extends DatafeedrError { } /** * Class DatafeedrExternalError. * * API error: External service error. */ class DatafeedrExternalError extends DatafeedrError { } /** * Class DatafeedrServerError. * * API error: Internal server error. */ class DatafeedrServerError extends DatafeedrError { }