1<?php 2/* 3 * vim:set softtabstop=4 shiftwidth=4 expandtab: 4 * 5 * LICENSE: GNU Affero General Public License, version 3 (AGPL-3.0-or-later) 6 * Copyright 2001 - 2020 Ampache.org 7 * 8 * This program is free software: you can redistribute it and/or modify 9 * it under the terms of the GNU Affero General Public License as published by 10 * the Free Software Foundation, either version 3 of the License, or 11 * (at your option) any later version. 12 * 13 * This program is distributed in the hope that it will be useful, 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 * GNU Affero General Public License for more details. 17 * 18 * You should have received a copy of the GNU Affero General Public License 19 * along with this program. If not, see <https://www.gnu.org/licenses/>. 20 * 21 */ 22 23declare(strict_types=0); 24 25namespace Ampache\Module\Util; 26 27use Requests; 28 29/** 30 * AmazonSearch Class 31 * 32 * This class accepts 3 tokens: a public_key (Amazon API ID), 33 * a private_key (Amazon API password used for signing requests), 34 * and an associate_tag (Amazon Associate ID tag) and creates a signed 35 * query to request information (images) from the Amazon Web Service API. 36 * Currently it is semi-hardcoded to do music searches and only return 37 * information about the album art. 38 * 39 * This class has been updated to conform to changes made to the AWS on 02/21/2012. 40 * https://affiliate-program.amazon.com/gp/advertising/api/detail/api-changes.html 41 */ 42class AmazonSearch 43{ 44 public $base_url_default = 'webservices.amazon.com'; 45 public $url_suffix = '/onca/xml'; 46 public $base_url; 47 public $search; 48 public $public_key; // AWSAccessKeyId 49 public $private_key; // AWSSecretKey 50 public $associate_tag; // Amazon Affiliate Associate Tag 51 public $results = array(); // Array of results 52 public $_parser; // The XML parser 53 public $_grabtags; // Tags to grab the contents of 54 public $_sourceTag; // source tag don't ask 55 public $_subTag; // Stupid hack to make things come our right 56 public $_currentTag; // Stupid hack to make things come out right 57 public $_currentTagContents; 58 public $_currentPage = 0; 59 public $_maxPage = 1; 60 public $_default_results_pages = 1; 61 public $_proxy_host = ""; // Proxy host 62 public $_proxy_port = ""; // Proxy port 63 public $_proxy_user = ""; // Proxy user 64 public $_proxy_pass = ""; // Proxy pass 65 66 /** 67 * Class Constructor 68 * @param $public_key 69 * @param $private_key 70 * @param $associate_tag 71 * @param string $base_url_param 72 */ 73 public function __construct($public_key, $private_key, $associate_tag, $base_url_param = '') 74 { 75 76 /* If we have a base url then use it */ 77 if ($base_url_param != '') { 78 $this->base_url = str_replace('http://', '', $base_url_param); 79 debug_event(self::class, 'Retrieving from ' . $base_url_param . $this->url_suffix, 5); 80 } else { 81 $this->base_url = $this->base_url_default; 82 debug_event(self::class, 'Retrieving from DEFAULT', 5); 83 } 84 85 // AWS credentials 86 $this->public_key = $public_key; 87 $this->private_key = $private_key; 88 $this->associate_tag = $associate_tag; 89 90 $this->_grabtags = array( 91 'ASIN', 92 'ProductName', 93 'Catalog', 94 'ErrorMsg', 95 'Description', 96 'ReleaseDate', 97 'Manufacturer', 98 'ImageUrlSmall', 99 'ImageUrlMedium', 100 'ImageUrlLarge', 101 'Author', 102 'Artist', 103 'Title', 104 'URL', 105 'SmallImage', 106 'MediumImage', 107 'LargeImage' 108 ); 109 } // AmazonSearch 110 111 /** 112 * setProxy 113 * Set the class up to search through an http proxy. 114 * The parameters are the proxy's hostname or IP address (a string) 115 * port, username, and password. These are passed directly to the 116 * Requests class when the search is done. 117 * @param string $host 118 * @param string $port 119 * @param string $user 120 * @param string $pass 121 */ 122 public function setProxy($host = '', $port = '', $user = '', $pass = '') 123 { 124 if ($host) { 125 $this->_proxy_host = $host; 126 } 127 if ($port) { 128 $this->_proxy_port = $port; 129 } 130 if ($user) { 131 $this->_proxy_user = $user; 132 } 133 if ($pass) { 134 $this->_proxy_pass = $pass; 135 } 136 } // setProxy 137 138 /** 139 * Create the XML parser to process the response. 140 */ 141 public function createParser() 142 { 143 $this->_parser = xml_parser_create(); 144 145 xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, false); 146 147 xml_set_object($this->_parser, $this); 148 149 xml_set_element_handler($this->_parser, 'startElement', 'endElement'); 150 151 xml_set_character_data_handler($this->_parser, 'cdata'); 152 } // createParser 153 154 /** 155 * Run a search. 156 * 157 * @param string $url The URL of the Amazon webservice. 158 */ 159 public function runSearch($url) 160 { 161 162 // create the parser 163 $this->createParser(); 164 165 // get the proxy config 166 $options = $this->getProxyConfig(); 167 168 debug_event(self::class, 'Amazon request: ' . $url, 5); 169 // make the request and retrieve the response 170 $request = Requests::get($url, array(), $options); 171 $contents = $request->body; 172 173 //debug_event(self::class, $contents, 5); 174 if (!xml_parse($this->_parser, $contents)) { 175 debug_event(self::class, 'Error:' . sprintf('XML error: %s at line %d', xml_error_string(xml_get_error_code($this->_parser)), xml_get_current_line_number($this->_parser)), 1); 176 } 177 178 xml_parser_free($this->_parser); 179 } // runSearch 180 181 /** 182 * getProxyConfig 183 * Build the proxy options array. 184 * Returning the array of proxy config options. 185 * @return array 186 */ 187 public function getProxyConfig() 188 { 189 $options = array(); 190 if ($this->_proxy_host) { 191 $proxy = array(); 192 $proxy[] = $this->_proxy_host . ($this->_proxy_port ? ':' . $this->_proxy_port : ''); 193 if ($this->_proxy_user) { 194 $proxy[] = $this->_proxy_user; 195 $proxy[] = $this->_proxy_pass; 196 } 197 $options['proxy'] = $proxy; 198 } 199 200 return $options; 201 } // getProxyConfig 202 203 /** 204 * Create an XML search string. 205 * 206 * @param array $terms The search terms to include within the query. 207 * @param string $type The type of result desired. 208 * @return array 209 */ 210 public function search($terms, $type = 'Music') 211 { 212 $params = array(); 213 214 $params['Service'] = 'AWSECommerceService'; 215 $params['AWSAccessKeyId'] = $this->public_key; 216 $params['AssociateTag'] = $this->associate_tag; 217 $params['Timestamp'] = gmdate("Y-m-d\TH:i:s\Z"); 218 $params['Version'] = '2009-03-31'; 219 $params['Operation'] = 'ItemSearch'; 220 $params['Artist'] = $terms['artist']; 221 $params['Title'] = $terms['album']; 222 $params['Keywords'] = $terms['keywords']; 223 $params['SearchIndex'] = $type; 224 225 // sort by keys 226 ksort($params); 227 228 $canonicalized_query = array(); 229 230 foreach ($params as $param => $value) { 231 $param = str_replace("%7E", "~", rawurlencode($param)); 232 $value = str_replace("%7E", "~", rawurlencode($value)); 233 234 $canonicalized_query[] = $param . "=" . $value; 235 } 236 237 // build the query string 238 $canonicalized_query = implode('&', $canonicalized_query); 239 $string_to_sign = 'GET' . "\n" . $this->base_url . "\n" . $this->url_suffix . "\n" . $canonicalized_query; 240 241 $url = 'http://' . $this->base_url . $this->url_suffix . '?' . $canonicalized_query . '&Signature=' . $this->signString($string_to_sign); 242 243 $this->runSearch($url); 244 245 unset($this->results['ASIN']); 246 247 return $this->results; 248 } // search 249 250 /** 251 * signString 252 * Sign a query string 253 * @param string $string_to_sign The string to sign 254 * @return string 255 */ 256 public function signString($string_to_sign) 257 { 258 259 // hash and encode the query string 260 $signature = base64_encode(hash_hmac("sha256", $string_to_sign, $this->private_key, true)); 261 262 // urlencode the signed string, replace illegal char 263 $signature = str_replace("%7E", "~", rawurlencode($signature)); 264 265 return $signature; 266 } // signString 267 268 /** 269 * Lookup the selected item by the 'Amazon Standard Identification Number' 270 * 271 * @param string $asin The 'Amazon standard Identification Number' 272 * @param string $type The category of results desired from the web service. 273 * @return array 274 */ 275 public function lookup($asin, $type = 'Music') 276 { 277 if (is_array($asin)) { 278 foreach ($asin as $key => $value) { 279 $this->runSearchAsin($key); 280 } 281 } else { 282 // if array of asin's 283 $this->runSearchAsin($asin); 284 } // else 285 286 unset($this->results['ASIN']); 287 288 return $this->results; 289 } // lookup 290 291 /** 292 * Query the AWS for information about the selected item by ASIN and parse the results. 293 * 294 * @param string $asin The 'Amazon standard Identification Number' 295 */ 296 public function runSearchAsin($asin) 297 { 298 299 // get the proxy config 300 $options = $this->getProxyConfig(); 301 302 // create the xml parser 303 $this->createParser(); 304 305 $params = array(); 306 $params['Service'] = 'AWSECommerceService'; 307 $params['AWSAccessKeyId'] = $this->public_key; 308 $params['AssociateTag'] = $this->associate_tag; 309 $params['Timestamp'] = gmdate("Y-m-d\TH:i:s\Z"); 310 $params['Version'] = '2009-03-31'; 311 $params['Operation'] = 'ItemLookup'; 312 $params['ItemId'] = $asin; 313 $params['ResponseGroup'] = 'Images'; 314 315 ksort($params); 316 317 // assemble the query terms 318 $canonicalized_query = array(); 319 foreach ($params as $param => $value) { 320 $param = str_replace("%7E", "~", rawurlencode($param)); 321 $value = str_replace("%7E", "~", rawurlencode($value)); 322 323 $canonicalized_query[] = $param . "=" . $value; 324 } 325 326 // build the url query string 327 $canonicalized_query = implode('&', $canonicalized_query); 328 $string_to_sign = 'GET' . "\n" . $this->base_url . "\n" . $this->url_suffix . "\n" . $canonicalized_query; 329 330 $url = 'http://' . $this->base_url . $this->url_suffix . '?' . $canonicalized_query . '&Signature=' . $this->signString($string_to_sign); 331 332 // make the request 333 $request = Requests::get($url, array(), $options); 334 $contents = $request->body; 335 336 if (!xml_parse($this->_parser, $contents)) { 337 debug_event(self::class, 'Error:' . sprintf('XML error: %s at line %d', xml_error_string(xml_get_error_code($this->_parser)), xml_get_current_line_number($this->_parser)), 1); 338 } 339 340 xml_parser_free($this->_parser); 341 } // runSearchAsin 342 343 /** 344 * Start XML Element. 345 * @param $parser 346 * @param $tag 347 * @param $attributes 348 */ 349 public function startElement($parser, $tag, $attributes) 350 { 351 if ($tag == "ASIN") { 352 $this->_sourceTag = $tag; 353 } 354 if ($tag == "SmallImage" || $tag == "MediumImage" || $tag == "LargeImage") { 355 $this->_subTag = $tag; 356 } 357 358 // If it's in the tag list, don't grab our search results 359 if (strlen($this->_sourceTag)) { 360 $this->_currentTag = $tag; 361 } else { 362 if ($tag != "TotalPages") { 363 $this->_currentTag = ''; 364 } else { 365 $this->_currentTag = $tag; 366 } 367 } 368 } // startElement 369 370 /** 371 * CDATA handler. 372 * @param $parser 373 * @param $cdata 374 */ 375 public function cdata($parser, $cdata) 376 { 377 $tag = $this->_currentTag; 378 $subtag = $this->_subTag; 379 $source = $this->_sourceTag; 380 381 switch ($tag) { 382 case 'URL': 383 $this->results[$source][$subtag] = trim($cdata); 384 break; 385 case 'ASIN': 386 $this->_sourceTag = trim($cdata); 387 break; 388 case 'TotalPages': 389 debug_event(self::class, "TotalPages= " . trim($cdata), 5); 390 $this->_maxPage = trim($cdata); 391 break; 392 default: 393 if (strlen($tag)) { 394 $this->results[$source][$tag] = trim($cdata); 395 } 396 break; 397 } // end switch 398 } // cdata 399 400 /** 401 * End XML Element 402 * @param $parser 403 * @param $tag 404 */ 405 public function endElement($parser, $tag) 406 { 407 408 // zero the tag 409 $this->_currentTag = ''; 410 } // endElement 411} 412