This repository has been archived by the owner on Aug 1, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
amazon_simple_ads_class.php
369 lines (335 loc) · 14.8 KB
/
amazon_simple_ads_class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
<?php
define('CACHE_DIR','/web/cache/amazon_api');
require_once("amazon_api_class.php"); // located in /web/include now
require_once(__DIR__."/class.simplecache.php");
/** Retrieving advertizements via the Amazon product API
* @package Amazon
* @class AmazonAds
* @author Andreas Itzchak Rehberg (Izzy)
* @brief This class makes use of the Amazon API by CodeDiesel, (see http://www.codediesel.com/php/accessing-amazon-product-advertising-api-in-php/), including additions done to it by Izzy.
*/
class AmazonAds {
/** Are we in SSL mode?
* If so, we want to have Amazon image URLs adjusted accordingly.
* @class AmazonAds
* @attribute private bool ssl
*/
protected $ssl = false;
/** URL of the directory we serve images from
* This is for more privacy of your visitors. Default is NULL, i.e. this is disabled.
* @class AmazonAds
* @attribute private string imgBaseURL
* @see self::setImgBase
*/
protected $imgBaseURL = NULL;
/** Name of the directory to save image files to
* @class AmazonAds
* @attribute private string imgBaseURL
* @see self::setImgBase
*/
protected $imgCacheName = NULL;
/** Activate privacy mode: set the ImgBase
* Note: Setting this to NULL will disable local images. Otherwise, $urlbase must match
* CACHE_DIR.'/'.$cachename (which must exist and be writeable by the web server; use e.g. symlinks).
* @class AmazonAds
* @method setImgBase
* @param string urlbase Base name of the URL images can be retrieved from (e.g. '/shared/images/asap')
* @param opt string cachename Name to use for the images to store. This will be appended to the CACHE_DIR constant. Default: 'asap'
* @return bool success
*/
public function setImgBase($urlbase,$cachename='asap') {
if ( empty($urlbase) || $urlbase == NULL ) {
$this->imgBaseURL = NULL;
return true;
} elseif ( empty($cachename) || !is_dir(CACHE_DIR.'/'.$cachename) ) {
trigger_error('The specified cache directory does not exist! Switching off local image mode.', E_USER_NOTICE);
$this->imgBaseURL = NULL;
return false;
} else {
$this->imgBaseURL = $urlbase;
$this->imgCacheName = CACHE_DIR.'/'.$cachename;
return true;
}
}
/** Initialize the class with your credentials
* @constructor AmazonAds
* @param string public Public key to be used
* @param string private Private string to be used
* @param string associate_tag Amazon Partner ID
* @param optional string local_site Amazon site to query (e.g. 'com','de', …), defaults to 'de'
*/
public function __construct($public, $private, $associate_tag, $local_site='de') {
$this->api = new AmazonProductAPI($public, $private, $associate_tag, $local_site);
$this->cache = new SimpleCache();
if ( isset($_SERVER['HTTPS']) ) $this->ssl = TRUE;
else $this->ssl = FALSE;
}
/** Override auto-SSL setting
* By default, the constructor checks the current connection and matches SSL
* mode (to update image URLs accordingly). Here you can override this.
* @class AmazonAds
* @method setSSL
* @param boolean useSSL
*/
public function setSSL($ssl) {
$this->ssl = (bool) $ssl;
}
/** Get an image file from Amazon and save it locally
* @class AmazonAds
* @method downloadImage
* @param str url URL to download
* @param str asin ASIN of the product (used as filename, extension added)
* @return bool success
*/
protected function downloadImage($url,$asin) {
if ( empty($url) || empty($asin) ) return false;
$ext = preg_replace('!.*\.([^\.]+)$!','\1',$url);
$localname = $this->imgCacheName."/${asin}.${ext}";
if ( file_exists($localname) ) return true;
if ( file_put_contents($localname, file_get_contents($url)) !== FALSE ) return true;
return false;
}
/** Load from cache if available
* @class AmazonAds
* @method getCache
* @param string cachename Name of the cache object
* @param ref string content Where to place the content in
* @return int cacheAge (UnixTimeStamp; 0 if no cache)
*/
protected function getCache($cachename,&$content) {
$tmp = '';
$age = $this->cache->getCacheUnixTime($cachename);
if ( $age>0 && time() - $age < 86400 ) { // valid to use
$this->cache->getCache($tmp,$cachename);
if ( !empty($tmp) ) $content = simplexml_load_string($tmp);
}
return $age;
}
/** Return details of a product searched by ASIN
* @class AmazonAds
* @method getItemByAsin
* @param int $asin_code ASIN code of the product to search (comma-separated list)
* @param optional string $response_group Response-Group. Defaults to 'Medium'.
* @return array SearchResults (or empty array if none): int cachedate (UnixTime), array[0..n] of array of strings title,url,img,price
*/
public function getItemByAsin($asin, $response_group='Medium') {
$xml = '';
$age = $this->getCache($asin,$xml);
if ( empty($xml) ) { // cache was either invalid or empty
$tmp = '';
try {
$xml = $this->api->getItemByAsin($asin, $response_group);
} catch(Exception $e) {
trigger_error( $e->getMessage(), E_USER_NOTICE );
$xml = '';
}
if ( !empty($xml) ) {
$tmp = $xml->asXML();
$this->cache->setCache($tmp,$asin);
}
$age = time();
}
if ( empty($xml) ) return array();
$cachedate = date('Y-m-d H:i',$age);
$resItems = count($xml->Items->Item);
$items = array();
for ($i=0; $i<$resItems; ++$i) {
if (isset($xml->Items->Item[$i]->ItemAttributes->Title)) $title = (string) $xml->Items->Item[$i]->ItemAttributes->Title; else continue;
if (isset($xml->Items->Item[$i]->DetailPageURL)) $url = (string) $xml->Items->Item[$i]->DetailPageURL; else continue;
isset($xml->Items->Item[$i]->SmallImage->URL) ? $img = (string) $xml->Items->Item[$i]->SmallImage->URL : $img = '';
if ( $this->ssl ) $img = str_replace('http://ecx.images-amazon.com/','https://images-na.ssl-images-amazon.com/',$img);
if ( $this->imgBaseURL !== NULL ) { // share images from our server, so cache them
$iasin = (string) $xml->Items->Item[$i]->ASIN;
if ( $this->downloadImage($img,$iasin) ) $img = $this->imgBaseURL . "/${iasin}" . preg_replace('!.*(\.[^\.]+)$!','\1',$img);
}
isset($xml->Items->Item[$i]->OfferSummary->LowestNewPrice->FormattedPrice) ? $price = (string) $xml->Items->Item[$i]->OfferSummary->LowestNewPrice->FormattedPrice : $price = '';
$items[] = array(
'title' => preg_replace('!([A-z],)([A-z])!','\1​\2',$title), // insert zero-width space if comma not followed by space, for auto line breaks
'url' => preg_replace('/&(?!amp;)/','&',$url),
'img' => $img,
'price' => $price
);
}
return array(
'cachedate' => $cachedate,
'items' => $items
);
}
/** Remove items which are too similar.
* We certainly don't want just a list of harddisks from the same manufacturer in all sizes, for example
* @class AmazonAds
* @method removeSimilar
* @param ref array items [0..n] of array title,url,img,price
* @param optional float similarity percentage of maximum allowed similarity of the titles. Defaults to 90
*/
function removeSimilar(&$items,$similarity=90) {
$ic = count($items); $sim = array(); $pct = 0;
for ($i=0; $i<$ic; ++$i) {
for ($k=$i+1; $k<$ic; ++$k) {
$foo = similar_text($items[$i]['title'], $items[$k]['title'], $pct );
if ( $pct > $similarity ) { // too similar
if ( !in_array($i,$sim) ) $sim[] = $k;
}
}
}
if ( !empty($sim) ) { // we've got some too close matches
$sim = array_unique($sim);
foreach( $sim as $s ) {
unset ($items[$s]);
}
$items = array_merge($items);
}
}
/** Return details of a product searched by keyword.
* Wrapper to getItemByKeyword() which takes care for multiple SearchIndexes (product_types)
* @class AmazonAds
* @method getItemsByKeyword
* @param string $keyword space separated list of keywords to search
* @param string $product_type type of the product (comma separated list of SearchIndexes)
* @param optional int limit How many results do we want? Defaults to 3; set to "0" for all-we-can-get
* @param optional float similarity percentage of maximum allowed similarity of the titles. Defaults to 90; set to 0 to keep all
* @return array SearchResults (or empty array if none): int cachedate (UnixTime), array[0..n] of strings title,url,img,price
*/
public function getItemsByKeyword($keyword, $product_type, $limit=3, $similarity=90) {
$searchIndexes = explode(',',$product_type);
$sic = count($searchIndexes);
// verify input
if ( empty($keyword) ) {
trigger_error('getItemsByKeyword: $keyword must not be empty, or we cannot perform a search', E_USER_ERROR);
return array();
}
if ( $sic == 0 ) {
trigger_error('getItemsByKeyword: $product_type must not be empty, or we cannot perform a search', E_USER_ERROR);
return array();
}
// prepare for keyword-multi-search if requested
$keywords = array(); $plus = ''; $many = array();
$tmp = explode(' ',$keyword);
foreach ( $tmp as $key ) {
if ( substr($key,0,1)=='+' ) $plus .= substr($key,1).' ';
else $many[] = $key;
}
unset ($tmp); // discard
if ( empty($plus) ) { // normal searchstring like "word1 word2 word3"
$keywords = explode(' ',$keyword);
} elseif ( count($many)==1 ) { // just a single word without prefix, like "+word1 +word2 word3"
$keywords[] = $plus . $many[0];
} elseif ( empty($many) ) { // all words with prefix, like "+word1 +word2 +word3"
$keywords = explode(' ',trim($plus));
} else { // we have at least one prefixed and multiple non-prefixed words, e.g. "+word1 word2 word3"
foreach ( $many as $m ) {
$keywords[] = $plus . $m;
}
}
// "ordinary" searches need no further processing
$kc = count($keywords);
if ( $sic==1 && $kc==1 ) { // "ordinary search": one index, list of keywords
return $this->getItemByKeyword($keywords[0], $product_type, $limit);
}
// Still here? OK, enter Multi-Search mode:
$items = array();
$cachedate = '9999-12-31 23:59';
// Get ads for each SearchIndex and keyword group separately, then merge results
for ($i=0; $i<$sic; ++$i) { // walk search indexes
for ( $k=0; $k<$kc; ++$k ) { // walk keyword combinations
$tmp = $this->getItemByKeyword($keywords[$k], $searchIndexes[$i], 0); // unlimited results
if ( array_key_exists('items',$tmp) && is_array($tmp['items']) ) { // no results, nothing to do
$items = array_merge( $items, $tmp['items'] );
if ( !empty($tmp['cachedate']) && $tmp['cachedate'] < $cachedate ) $cachedate = $tmp['cachedate'];
}
}
}
if ($similarity > 0) $this->removeSimilar($items, $similarity);
if ($limit < 1) { // getAll
return array(
'cachedate' => $cachedate,
'items' => $items
);
}
// Pick $limit random elements if we have more than requested
$litems = array();
if ( $limit < count($items) ) {
$ids = array_rand($items,$limit);
if ( is_array($ids) ) {
foreach ($ids as $id) $litems[] = $items[$id];
} else {
$litems[] = $items[$ids];
}
}
return array(
'cachedate' => $cachedate,
'items' => $litems
);
}
/** Return details of a product searched by keyword.
* Search is done in titles and descriptions, max 10 results are returned
* @class AmazonAds
* @method getItemByKeyword
* @param string $keyword space separated list of keywords to search
* @param string $product_type type of the product (only one SearchIndex! If more shall be queried, use getItemsByKeyword() instead)
* @param optional int limit How many results do we want? Defaults to 3; set to "0" for all-we-can-get
* @param optional float similarity percentage of maximum allowed similarity of the titles. Defaults to 90; set to 0 to keep all
* @return array SearchResults (or empty array if none): int cachedate (UnixTime), array[0..n] of strings title,url,img,price
*/
public function getItemByKeyword($keyword, $product_type, $limit=3, $similarity=90) {
$keywords = urlencode($keyword);
$cachename = "${product_type}--${keywords}";
$xml = ''; $tmp = '';
$age = $this->getCache($cachename,$xml);
if ( empty($xml) ) { // cache was either invalid or empty
try {
$xml = $this->api->getItemByKeyword($keyword, $product_type, 'Medium');
$tmp = $xml->asXML();
$this->cache->setCache($tmp,$cachename); // create or replace cache
$age = time();
}
catch(Exception $e) { // just in case something bad happens, it should not "show up"
// echo $e->getMessage();
trigger_error( $e->getMessage(), E_USER_NOTICE );
$xml = '';
}
}
if ( empty($xml) ) return array();
$cachedate = date('Y-m-d H:i',$age);
$resItems = count($xml->Items->Item);
$items = array();
for ($i=0; $i<$resItems; ++$i) {
$img = (string) $xml->Items->Item[$i]->SmallImage->URL;
if ( $this->ssl ) $img = str_replace('http://ecx.images-amazon.com/','https://images-na.ssl-images-amazon.com/',$img);
if ( $this->imgBaseURL !== NULL ) { // share images from our server, so cache them
$asin = (string) $xml->Items->Item[$i]->ASIN;
if ( $this->downloadImage($img,$asin) ) $img = $this->imgBaseURL . "/${asin}" . preg_replace('!.*(\.[^\.]+)$!','\1',$img);
}
$title = (string) $xml->Items->Item[$i]->ItemAttributes->Title;
$url = (string) $xml->Items->Item[$i]->DetailPageURL;
if ( property_exists($xml->Items->Item[$i]->OfferSummary,'LowestNewPrice') && property_exists($xml->Items->Item[$i]->OfferSummary->LowestNewPrice,'FormattedPrice') ) $price = (string) $xml->Items->Item[$i]->OfferSummary->LowestNewPrice->FormattedPrice; // some items have no price
else $price = '';
$items[] = array(
'title' => preg_replace('!([A-z],)([A-z])!','\1​\2',$title), // insert zero-width space if comma not followed by space, for auto line breaks
'url' => preg_replace('/&(?!amp;)/','&',$url),
'img' => $img,
'price' => $price
);
}
// remove items which are too similar
if ($similarity > 0) $this->removeSimilar($items, $similarity);
if ( $limit < 1 || count($items) <= $limit ) { // getAll
return array(
'cachedate' => $cachedate,
'items' => $items
);
}
$ids = array_rand($items,$limit);
$litems = array();
if ( is_array($ids) ) {
foreach ($ids as $id) $litems[] = $items[$id];
} else {
$litems[] = $items[$ids];
}
return array(
'cachedate' => $cachedate,
'items' => $litems
);
}
}
?>