PluginProbe
Lasso Lite – Affiliate Link Manager & Product Displays / 151
Lasso Lite – Affiliate Link Manager & Product Displays v151
157 155 156 154 153 152 151 150 149 148 trunk 0.9.9 104 105 106 107 108 109 110 111 112 113 114 115 116 All 56 releases
simple-urls / classes / class-amazon-api.php

class-amazon-api.php in Lasso Lite – Affiliate Link Manager & Product Displays 151, at classes/class-amazon-api.php

1,823 lines 58.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Declare class Config
4 *
5 * @package Config
6 */
7
8 namespace LassoLite\Classes;
9
10 use LassoLite\Admin\Constant;
11 use LassoLite\Classes\Affiliate_Link;
12 use LassoLite\Classes\Cache_Per_Process;
13 use LassoLite\Classes\Helper;
14 use LassoLite\Classes\Setting;
15
16 use LassoLite\Libs\Amazon_Api_V5\AwsV5;
17
18 use LassoLite\Models\Amazon_Products;
19
20 /**
21 * Config
22 */
23 class Amazon_Api {
24
25 const OBJECT_KEY = 'lasso_amazon_api';
26 const FUNCTION_NAME_GET_LASSO_ID_BY_PRODUCT_ID_AND_TYPE = 'get_lasso_id_by_product_id_and_type';
27 const PRODUCT_TYPE = 'amazon';
28 const SHORT_LINK_DOMAINS = array( 'amzn.com', 'amzn.to' );
29 const FILTER_AMAZON_PRODUCT = 'filter_amazon_product';
30 const TRACKING_ID_REGEX = '^[a-zA-Z0-9-]+-\d{2,3}$';
31 const CURRENCY_ISO = array( 'USD', 'AUD', 'CAD', 'EUR', 'MXN', 'CNY', 'JPY', 'INR', 'SEK', 'BRL', 'TRY', 'GBP', 'PLN', 'EGP', 'SGD', 'AED' );
32 const VARIATION_PAGE_LIMIT = 2;
33
34 /**
35 * Get amazon API countries
36 */
37 public static function get_amazon_api_countries() {
38 return array(
39 'us' => array(
40 'name' => 'United States',
41 'amazon_domain' => 'www.amazon.com',
42 'pa_endpoint' => 'webservices.amazon.com',
43 'region' => 'us-east-1',
44 ),
45 'usa' => array(
46 'name' => 'United States',
47 'amazon_domain' => 'www.amazon.com',
48 'pa_endpoint' => 'webservices.amazon.com',
49 'region' => 'us-east-1',
50 ),
51 'au' => array(
52 'name' => 'Australia',
53 'amazon_domain' => 'www.amazon.com.au',
54 'pa_endpoint' => 'webservices.amazon.com.au',
55 'region' => 'us-west-2',
56 ),
57 'aus' => array(
58 'name' => 'Australia',
59 'amazon_domain' => 'www.amazon.com.au',
60 'pa_endpoint' => 'webservices.amazon.com.au',
61 'region' => 'us-west-2',
62 ),
63 'br' => array(
64 'name' => 'Brazil',
65 'amazon_domain' => 'www.amazon.com.br',
66 'pa_endpoint' => 'webservices.amazon.com.br',
67 'region' => 'us-east-1',
68 ),
69 'bra' => array(
70 'name' => 'Brazil',
71 'amazon_domain' => 'www.amazon.com.br',
72 'pa_endpoint' => 'webservices.amazon.com.br',
73 'region' => 'us-east-1',
74 ),
75 'ca' => array(
76 'name' => 'Canada',
77 'amazon_domain' => 'www.amazon.ca',
78 'pa_endpoint' => 'webservices.amazon.ca',
79 'region' => 'us-east-1',
80 ),
81 'can' => array(
82 'name' => 'Canada',
83 'amazon_domain' => 'www.amazon.ca',
84 'pa_endpoint' => 'webservices.amazon.ca',
85 'region' => 'us-east-1',
86 ),
87 'cn' => array(
88 'name' => 'China',
89 'amazon_domain' => 'www.amazon.cn',
90 'pa_endpoint' => 'webservices.amazon.cn',
91 'region' => 'us-east-1',
92 ),
93 'chn' => array(
94 'name' => 'China',
95 'amazon_domain' => 'www.amazon.cn',
96 'pa_endpoint' => 'webservices.amazon.cn',
97 'region' => 'us-east-1',
98 ),
99 'fr' => array(
100 'name' => 'France',
101 'amazon_domain' => 'www.amazon.fr',
102 'pa_endpoint' => 'webservices.amazon.fr',
103 'region' => 'eu-west-1',
104 ),
105 'fra' => array(
106 'name' => 'France',
107 'amazon_domain' => 'www.amazon.fr',
108 'pa_endpoint' => 'webservices.amazon.fr',
109 'region' => 'eu-west-1',
110 ),
111 'de' => array(
112 'name' => 'Germany',
113 'amazon_domain' => 'www.amazon.de',
114 'pa_endpoint' => 'webservices.amazon.de',
115 'region' => 'eu-west-1',
116 ),
117 'deu' => array(
118 'name' => 'Germany',
119 'amazon_domain' => 'www.amazon.de',
120 'pa_endpoint' => 'webservices.amazon.de',
121 'region' => 'eu-west-1',
122 ),
123 'in' => array(
124 'name' => 'India',
125 'amazon_domain' => 'www.amazon.in',
126 'pa_endpoint' => 'webservices.amazon.in',
127 'region' => 'eu-west-1',
128 ),
129 'ind' => array(
130 'name' => 'India',
131 'amazon_domain' => 'www.amazon.in',
132 'pa_endpoint' => 'webservices.amazon.in',
133 'region' => 'eu-west-1',
134 ),
135 'it' => array(
136 'name' => 'Italy',
137 'amazon_domain' => 'www.amazon.it',
138 'pa_endpoint' => 'webservices.amazon.it',
139 'region' => 'eu-west-1',
140 ),
141 'ita' => array(
142 'name' => 'Italy',
143 'amazon_domain' => 'www.amazon.it',
144 'pa_endpoint' => 'webservices.amazon.it',
145 'region' => 'eu-west-1',
146 ),
147 'jp' => array(
148 'name' => 'Japan',
149 'amazon_domain' => 'www.amazon.co.jp',
150 'pa_endpoint' => 'webservices.amazon.co.jp',
151 'region' => 'us-west-2',
152 ),
153 'jpn' => array(
154 'name' => 'Japan',
155 'amazon_domain' => 'www.amazon.co.jp',
156 'pa_endpoint' => 'webservices.amazon.co.jp',
157 'region' => 'us-west-2',
158 ),
159 'mx' => array(
160 'name' => 'Mexico',
161 'amazon_domain' => 'www.amazon.com.mx',
162 'pa_endpoint' => 'webservices.amazon.com.mx',
163 'region' => 'us-east-1',
164 ),
165 'mex' => array(
166 'name' => 'Mexico',
167 'amazon_domain' => 'www.amazon.com.mx',
168 'pa_endpoint' => 'webservices.amazon.com.mx',
169 'region' => 'us-east-1',
170 ),
171 'nl' => array(
172 'name' => 'Netherlands',
173 'amazon_domain' => 'www.amazon.nl',
174 'pa_endpoint' => 'webservices.amazon.nl',
175 'region' => 'eu-west-1',
176 ),
177 'nld' => array(
178 'name' => 'Netherlands',
179 'amazon_domain' => 'www.amazon.nl',
180 'pa_endpoint' => 'webservices.amazon.nl',
181 'region' => 'eu-west-1',
182 ),
183 'se' => array(
184 'name' => 'Sweden',
185 'amazon_domain' => 'www.amazon.se',
186 'pa_endpoint' => 'webservices.amazon.se',
187 'region' => 'us-west-1',
188 ),
189 'sek' => array(
190 'name' => 'Sweden',
191 'amazon_domain' => 'www.amazon.se',
192 'pa_endpoint' => 'webservices.amazon.se',
193 'region' => 'us-west-1',
194 ),
195 'sg' => array(
196 'name' => 'Singapore',
197 'amazon_domain' => 'www.amazon.sg',
198 'pa_endpoint' => 'webservices.amazon.sg',
199 'region' => 'us-west-2',
200 ),
201 'sgp' => array(
202 'name' => 'Singapore',
203 'amazon_domain' => 'www.amazon.sg',
204 'pa_endpoint' => 'webservices.amazon.sg',
205 'region' => 'us-west-2',
206 ),
207 'es' => array(
208 'name' => 'Spain',
209 'amazon_domain' => 'www.amazon.es',
210 'pa_endpoint' => 'webservices.amazon.es',
211 'region' => 'eu-west-1',
212 ),
213 'esp' => array(
214 'name' => 'Spain',
215 'amazon_domain' => 'www.amazon.es',
216 'pa_endpoint' => 'webservices.amazon.es',
217 'region' => 'eu-west-1',
218 ),
219 'tr' => array(
220 'name' => 'Turkey',
221 'amazon_domain' => 'www.amazon.com.tr',
222 'pa_endpoint' => 'webservices.amazon.com.tr',
223 'region' => 'eu-west-1',
224 ),
225 'tur' => array(
226 'name' => 'Turkey',
227 'amazon_domain' => 'www.amazon.com.tr',
228 'pa_endpoint' => 'webservices.amazon.com.tr',
229 'region' => 'eu-west-1',
230 ),
231 'ae' => array(
232 'name' => 'United Arab Emirates',
233 'amazon_domain' => 'www.amazon.ae',
234 'pa_endpoint' => 'webservices.amazon.ae',
235 'region' => 'eu-west-1',
236 ),
237 'are' => array(
238 'name' => 'United Arab Emirates',
239 'amazon_domain' => 'www.amazon.ae',
240 'pa_endpoint' => 'webservices.amazon.ae',
241 'region' => 'eu-west-1',
242 ),
243 'gb' => array(
244 'name' => 'United Kingdom',
245 'amazon_domain' => 'www.amazon.co.uk',
246 'pa_endpoint' => 'webservices.amazon.co.uk',
247 'region' => 'eu-west-1',
248 ),
249 'gbr' => array(
250 'name' => 'United Kingdom',
251 'amazon_domain' => 'www.amazon.co.uk',
252 'pa_endpoint' => 'webservices.amazon.co.uk',
253 'region' => 'eu-west-1',
254 ),
255 );
256 }
257
258 /**
259 * Get ignore errors list
260 */
261 public static function get_ignore_error_codes() {
262 return array(
263 'ItemNotAccessible',
264 'InvalidParameterValue',
265 'AccessDeniedException',
266 'AccessDenied',
267 'TooManyRequestsException',
268 'TooManyRequests',
269 'ThrottlingException',
270 'RequestThrottled',
271 'AWS.ThrottlingException',
272 'AWS.RequestThrottled',
273 'AWS.AccessDeniedException',
274 'UnrecognizedClient',
275 );
276 }
277
278 /**
279 * Get amazon domains
280 */
281 public static function get_domains() {
282 return array(
283 'amazon.com', // ? US
284 'amazon.ca', // ? Canada
285 'amazon.co.uk', // ? UK
286 'amazon.com.au', // ? Australia
287 'amazon.com.br', // ? Brazil
288 'amazon.com.mx', // ? Mexico
289 'amazon.fr', // ? France
290 'amazon.de', // ? Germany
291 'amazon.it', // ? Italy
292 'amazon.in', // ? India
293 'amazon.es', // ? Spain
294 'amazon.cn', // ? China
295 'amazon.co.jp', // ? Japan
296 'amazon.nl', // ? Netherlands
297 'amazon.se', // ? Sweden
298 'amazon.sg', // ? Singapore
299 'amazon.com.tr', // ? Turkey
300 'amazon.ae', // ? United Arab Emirates
301 'amzn.com', // ? Short URL
302 'amzn.to', // ? Short URL
303 'amazon-adsystem.com', // ? Amazon Embed
304 'smile.amazon.com', // ? Amazon Smile
305 );
306 }
307
308 /**
309 * Get amazon link and flag
310 */
311 public static function get_aff_link_and_flag() {
312 return array(
313 'www.amazon.com' => array(
314 'flag' => '🇺🇸',
315 'aff_link' => 'https://affiliate-program.amazon.com/',
316 ),
317 'www.amazon.ca' => array(
318 'flag' => '🇨🇦',
319 'aff_link' => 'https://associates.amazon.ca/',
320 ),
321 'www.amazon.com.br' => array(
322 'flag' => '🇧🇷',
323 'aff_link' => 'https://associados.amazon.com.br/',
324 ),
325 'www.amazon.com.mx' => array(
326 'flag' => '🇲🇽',
327 'aff_link' => 'https://afiliados.amazon.com.mx/',
328 ),
329 'www.amazon.fr' => array(
330 'flag' => '🇫🇷',
331 'aff_link' => 'https://partenaires.amazon.fr/',
332 ),
333 'www.amazon.de' => array(
334 'flag' => '🇩🇪',
335 'aff_link' => 'https://partnernet.amazon.de/',
336 ),
337 'www.amazon.it' => array(
338 'flag' => '🇮🇹',
339 'aff_link' => 'https://programma-affiliazione.amazon.it/',
340 ),
341 'www.amazon.es' => array(
342 'flag' => '🇪🇸',
343 'aff_link' => 'https://afiliados.amazon.es/',
344 ),
345 'www.amazon.co.uk' => array(
346 'flag' => '🇬🇧',
347 'aff_link' => 'https://affiliate-program.amazon.co.uk/',
348 ),
349 'www.amazon.cn' => array(
350 'flag' => '🇨🇳',
351 'aff_link' => 'https://associates.amazon.cn/',
352 ),
353 'www.amazon.co.jp' => array(
354 'flag' => '🇯🇵',
355 'aff_link' => 'https://affiliate.amazon.co.jp/',
356 ),
357 'www.amazon.in' => array(
358 'flag' => '🇮🇳',
359 'aff_link' => 'https://affiliate-program.amazon.in/',
360 ),
361 'www.amazon.com.au' => array(
362 'flag' => '🇦🇺',
363 'aff_link' => 'https://affiliate-program.amazon.com.au/',
364 ),
365 );
366 }
367
368 /**
369 * Check whether a url is amazon link or not
370 *
371 * @param string $url Url.
372 */
373 public static function is_amazon_url( $url ) {
374 if ( empty( $url ) ) {
375 return false;
376 }
377
378 $domains = self::get_domains();
379 $url = Helper::add_https( $url );
380
381 if ( ! Helper::validate_url( $url ) ) {
382 return false;
383 }
384
385 $parse_url = wp_parse_url( $url );
386 if ( ! isset( $parse_url['host'] ) ) {
387 return false;
388 }
389
390 $domain = ltrim( $parse_url['host'], 'www.' );
391
392 if ( in_array( $domain, $domains, true ) ) {
393 return true;
394 }
395
396 return false;
397 }
398
399 /**
400 * Check whether a url is amazon link or not
401 *
402 * @param string $url Url.
403 */
404 public static function is_amazon_shortened_url( $url ) {
405 $is_amazon_url = self::is_amazon_url( $url );
406 $is_shortented_url = strpos( $url, 'amzn.to' ) || strpos( $url, 'amzn.com' );
407
408 return $is_amazon_url && $is_shortented_url;
409 }
410
411 /**
412 * Search amazon product
413 *
414 * @param string $keyword Keyword: product name,...
415 */
416 public function search_product( $keyword ) {
417 $result = $this->get_product_by_keyword_v5( $keyword, 'All' );
418 if ( isset( $result->SearchResult->Items ) ) { // phpcs:ignore
419 $items = $result->SearchResult->Items; // phpcs:ignore
420 $products = array();
421
422 foreach ( $items as $item ) {
423 $product = $this->extract_search_result_v5( $item );
424 array_push( $products, $product );
425 }
426
427 return $products;
428
429 } elseif ( isset( $result->Errors ) ) { // phpcs:ignore
430 return array(
431 'error' => $result->Errors, // phpcs:ignore
432 );
433 }
434 }
435
436 /**
437 * Check whether product url has the same domain with Amazon settings
438 *
439 * @param string $product_url Amazon link.
440 */
441 public function is_same_domain( $product_url ) {
442 if ( '' === $product_url ) {
443 return false;
444 }
445
446 $amazon_default_tracking_country = Setting::get_setting( 'amazon_default_tracking_country', 'usa' );
447 $all_countries = self::get_amazon_api_countries();
448 $domain = $all_countries[ $amazon_default_tracking_country ]['amazon_domain'] ?? 'www.amazon.com';
449 $domain = str_replace( 'www.', '', $domain );
450
451 return strpos( $product_url, $domain ) !== false;
452 }
453
454 /**
455 * Fetch amazon product from Amazon API v5
456 *
457 * @param string $product_id Amazon product id.
458 * @param bool $store_product Store product into DB or not. Default to false.
459 * @param bool|string $updated_at Set date time or not. Default to false.
460 * @param string $amz_link Amazon link. Default to empty.
461 * @param bool $url_version_param Is add version param to request api url to ignore cache. Default to false.
462 * @param bool $force_bls Force fetch product from BLS or not. Default to false.
463 * @param bool $refresh_image Bypass cache and fetch fresh product image/metadata. Default to false.
464 * @return mixed
465 */
466 public function fetch_product_info( $product_id, $store_product = false, $updated_at = false, $amz_link = '', $url_version_param = false, $force_bls = false, $refresh_image = false ) {
467 $lasso_db = new Lasso_DB();
468
469 $lasso_settings = Setting::get_settings();
470 $is_amazon_configured = $lasso_settings['amazon_access_key_id'] && $lasso_settings['amazon_secret_key'] && $lasso_settings['amazon_tracking_id'];
471
472 if (
473 self::is_amazon_creators_verified( $lasso_settings )
474 && $this->is_same_domain( $amz_link )
475 ) {
476 if ( $refresh_image && ! self::is_lite_account_connected() ) {
477 return array(
478 'product' => array(),
479 'api' => 'no',
480 'full_item' => array(),
481 'status' => 'failed',
482 'error_code' => 'LiteAccountNotConnected',
483 );
484 }
485
486 $creators_result = $this->fetch_product_info_from_creators_api(
487 $product_id,
488 $store_product,
489 $updated_at,
490 $amz_link,
491 $lasso_settings,
492 $refresh_image
493 );
494 if ( null !== $creators_result ) {
495 return $creators_result;
496 }
497 }
498
499 $result = ! $force_bls && $is_amazon_configured && $this->is_same_domain( $amz_link ) ? $this->get_product_by_id_v5( $product_id ) : false;
500 // phpcs:ignore
501 if ( is_object( $result ) && isset( $result->Errors[0] ) && (
502 "The ItemId $product_id provided in the request is invalid." === $result->Errors[0]->Message // phpcs:ignore
503 || "The value [$product_id] provided in the request for ItemIds is invalid." === $result->Errors[0]->Message // phpcs:ignore
504 )
505 ) {
506 return array(
507 'product' => array(),
508 'api' => 'yes',
509 'full_item' => array(),
510 'status' => 'failed',
511 'error_code' => 'NotFound',
512 );
513 } elseif ( is_object( $result ) && isset( $result->ItemsResult->Items[0] ) ) { // phpcs:ignore
514 $item = $result->ItemsResult->Items[0]; // phpcs:ignore
515
516 $product = $this->extract_search_result_v5( $item, true );
517 $product_url = $amz_link ? $amz_link : $product['url'];
518 $product['status_code'] = 200;
519 $product['url'] = self::get_amazon_product_url( $product_url );
520
521 // ? If $item->Offers is missing, we try to get the quantity from variation data
522 if ( ! isset( $item->Offers ) ) { // phpcs:ignore
523 sleep( 1 ); // ? Delay for a while before call the next request
524 $variation_product = $this->get_product_variation( $product_id, $product_url );
525 if ( $variation_product && isset( $variation_product['quantity'] ) && $variation_product['quantity'] ) {
526 $product = $variation_product;
527 }
528 }
529
530 $product = $this->enrich_fetched_amazon_product( $product, $store_product, $updated_at, $amz_link );
531
532 return array(
533 'product' => $product,
534 'api' => 'yes',
535 'full_item' => $item,
536 'status' => 'success',
537 'error_code' => '',
538 );
539 } elseif ( '' !== $lasso_settings['license_serial'] || $force_bls ) {
540 list( $product, $status ) = $this->fetch_product_from_bls( $product_id, $store_product, $updated_at, $amz_link, $url_version_param, $force_bls, $refresh_image );
541
542 return array(
543 'product' => $product,
544 'api' => 'no',
545 'full_item' => array(),
546 'status' => 200 === $status ? 'success' : 'fail',
547 'error_code' => 404 === $status ? 'NotFound' : '',
548 );
549 }
550
551 return array(
552 'product' => array(),
553 'api' => 'no',
554 'full_item' => array(),
555 'status' => 'failed',
556 'error_code' => 'NotFound',
557 );
558 }
559
560 /**
561 * Fetch amazon product from BLS (Lambda)
562 *
563 * @param string $product_id Amazon product id.
564 * @param bool $store_product Store product into DB or not. Default to false.
565 * @param bool|string $updated_at Set date time or not. Default to false.
566 * @param string $amz_link Amazon link. Default to empty.
567 * @param bool $url_version_param Is add version param to request api url to ignore cache. Default to false.
568 * @param bool $force_bls Force fetch product from BLS or not. Default to false.
569 * @param bool $refresh_image Bypass cache and fetch fresh product image/metadata. Default to false.
570 */
571 public function fetch_product_from_bls( $product_id, $store_product = false, $updated_at = false, $amz_link = '', $url_version_param = false, $force_bls = false, $refresh_image = false ) {
572 $url = strpos( $amz_link, 'amazon.' ) !== false ? $amz_link : $this->get_amazon_link_by_product_id( $product_id, $amz_link );
573 $m_link = self::get_amazon_product_url( $url );
574 $url = self::get_amazon_product_url( $url, false );
575
576 $amazon_product = array(
577 'title' => '',
578 'image' => '',
579 'url' => $m_link,
580 'price' => '',
581 'currency' => '',
582 'savings_amount' => 0,
583 'savings_percent' => 0,
584 'savings_basis' => 0,
585 );
586
587 try {
588 $res = Helper::get_url_status_code_by_broken_link_service( $url, true, false, $url_version_param, $force_bls, $refresh_image );
589 } catch ( \Throwable $e ) {
590 $res = array(
591 'status_code' => 500,
592 'response' => null,
593 );
594 }
595
596 $bls_response = ( isset( $res['response'] ) && is_object( $res['response'] ) ) ? $res['response'] : null;
597 $bls_status = ( null !== $bls_response && isset( $bls_response->status ) ) ? intval( $bls_response->status ) : 0;
598
599 if ( 200 === $res['status_code'] && 200 === $bls_status ) {
600 $img_url = $bls_response->imgUrl ?? '';
601 $img_url = '' !== $img_url ? $img_url : '';
602 $product_name = $bls_response->productName ?? '';
603 $product_name = '' === $product_name ? ( $bls_response->pageTitle ?? '' ) : $product_name;
604 $quantity = $bls_response->quantity ?? 200;
605 $price = $bls_response->price ?? '';
606 $product_id = self::get_product_id_by_url( $url );
607
608 $temp_url = $bls_response->finalUrl ?? $url;
609 $url = '' !== $temp_url ? $temp_url : $url;
610 $m_link = self::get_amazon_product_url( $amz_link ? $amz_link : $url );
611
612 if ( strpos( $product_name, 'Amazon.com:' ) === 0 ) {
613 $product_name = str_replace( 'Amazon.com:', '', $product_name );
614 $product_name = trim( $product_name );
615 }
616
617 if ( $product_id && $store_product ) {
618 $store_data = array(
619 'product_id' => $product_id,
620 'title' => $product_name,
621 'price' => $price,
622 'default_url' => $url,
623 'url' => $m_link,
624 'image' => trim( $img_url ),
625 'quantity' => intval( $quantity ), // Manual checks won't show out of stock for now. TODO: Add BLS to out of stock checks.
626 'is_manual' => 1,
627 );
628
629 // ? Set additional data for amazon product
630 if ( isset( $bls_response->additionalData ) && ! empty( $bls_response->additionalData ) ) {
631 $basis_price = $bls_response->additionalData->basis_price ?? '';
632 $basis_price = Helper::get_price_value_from_price_text( $basis_price );
633 $savings_amount = $bls_response->additionalData->saving_amount ?? '';
634 $savings_amount = Helper::get_price_value_from_price_text( $savings_amount );
635
636 $store_data['currency'] = $bls_response->additionalData->currency_name ?? '';
637 $store_data['savings_basis'] = $basis_price;
638 $store_data['savings_amount'] = $savings_amount;
639 $store_data['savings_percent'] = $bls_response->additionalData->saving_amount_percent ?? '';
640
641 $amazon_product['currency'] = $store_data['currency'];
642 $amazon_product['savings_basis'] = $store_data['savings_basis'];
643 $amazon_product['savings_amount'] = $store_data['savings_amount'];
644 $amazon_product['savings_percent'] = $store_data['savings_percent'];
645 }
646
647 $this->update_amazon_product_in_db( $store_data, $updated_at, true );
648 }
649
650 $amazon_product['title'] = $product_name;
651 $amazon_product['image'] = $img_url;
652 $amazon_product['url'] = $m_link;
653 $amazon_product['price'] = $price;
654 $amazon_product['quantity'] = $quantity;
655 $amazon_product['status_code'] = $bls_status;
656 }
657 if ( 404 === $res['status_code'] || ( 200 === $res['status_code'] && 404 === $bls_status ) ) {
658 $amz_model = new Amazon_Products();
659 $last_updated = gmdate( 'Y-m-d H:i:s', time() );
660 $amz_model->update_amazon_field( $product_id, 'last_updated', $last_updated );
661 $amz_model->update_amazon_field( $product_id, 'out_of_stock', 0 );
662 }
663
664 if ( $bls_status ) {
665 $status = $bls_status;
666 } elseif ( 200 === intval( $res['status_code'] ?? 0 ) ) {
667 $status = 500;
668 } else {
669 $status = intval( $res['status_code'] ?? 500 );
670 }
671
672 return array( $amazon_product, intval( $status ) );
673 }
674
675 /**
676 * Insert or Update Amazon Product Data
677 *
678 * @param array $product Amazon product.
679 * @param bool|string $updated_at Set update date time. Default to false.
680 */
681 public function update_amazon_product_in_db( $product, $updated_at = false ) {
682 global $wpdb;
683
684 $lasso_db = new Lasso_DB();
685
686 $amazon_id = $product['product_id'] ?? '';
687 $default_product_name = $product['title'] ?? '';
688 $latest_price = $product['price'] ?? '';
689 $latest_price = '0' === $latest_price || ( is_int( $latest_price ) && 0 === $latest_price ) ? '' : $latest_price;
690 $base_url = self::get_amazon_product_url( $product['default_url'] ?? '', false, false );
691 $monetized_url = self::get_amazon_product_url( $product['url'] ?? '', true, false );
692 $default_image = trim( $product['image'] ?? '' );
693 $last_updated = gmdate( 'Y-m-d H:i:s', time() );
694 $last_updated = $updated_at ? $updated_at : $last_updated;
695 $is_prime = $product['is_prime'] ?? '';
696 $currency = $product['currency'] ?? '';
697 $features = wp_json_encode( $product['features'] ?? array() );
698 $savings_amount = $product['savings_amount'] ?? '';
699 $savings_percent = $product['savings_percent'] ?? '';
700 $savings_basis = $product['savings_basis'] ?? '';
701 $is_manual = $product['is_manual'] ?? 0;
702 $quantity = intval( $product['quantity'] ?? 200 );
703 $out_of_stock = 0 === $quantity ? 1 : 0;
704
705 if ( '' === $amazon_id || '' === $default_product_name || '' === $default_image
706 || ( '' !== $default_image && Helper::validate_url( $default_image ) === false && strpos( $default_image, 'data:image' ) !== 0 )
707 ) {
708 return false;
709 }
710
711 $base_url = trim( $base_url );
712 $monetized_url = trim( $monetized_url );
713
714 $query = '
715 INSERT INTO ' . $lasso_db->amazon_products . "
716 (
717 amazon_id, default_product_name, latest_price, base_url,
718 monetized_url, default_image, last_updated, is_prime,
719 currency, features, savings_amount, savings_percent,
720 savings_basis, is_manual, out_of_stock
721 )
722 VALUES
723 (
724 %s, %s, %s, %s,
725 %s, %s, %s, %d,
726 %s, %s, %s, %d,
727 %s, %d, %d
728 )
729 ON DUPLICATE KEY UPDATE
730 amazon_id = %s,
731 default_product_name = %s,
732 latest_price = %s,
733 base_url = %s,
734 monetized_url = %s,
735 default_image = (CASE WHEN %s='' or %s IS NULL THEN `default_image` ELSE %s END),
736 last_updated = %s,
737 is_prime = %d,
738 currency = %s,
739 features = %s,
740 savings_amount = %s,
741 savings_percent = %d,
742 savings_basis = %s,
743 is_manual = %d,
744 out_of_stock = %d
745 ;
746 ";
747 $prepare = $wpdb->prepare(
748 // phpcs:ignore
749 $query,
750 // ? First for insert
751 $amazon_id,
752 $default_product_name,
753 $latest_price,
754 $base_url,
755 $monetized_url,
756 $default_image,
757 $last_updated,
758 $is_prime,
759 $currency,
760 $features,
761 $savings_amount,
762 $savings_percent,
763 $savings_basis,
764 $is_manual,
765 $out_of_stock,
766 // ? Second for update
767 $amazon_id,
768 $default_product_name,
769 $latest_price,
770 $base_url,
771 $monetized_url,
772 $default_image,
773 $default_image,
774 $default_image,
775 $last_updated,
776 $is_prime,
777 $currency,
778 $features,
779 $savings_amount,
780 $savings_percent,
781 $savings_basis,
782 $is_manual,
783 $out_of_stock
784 );
785
786 $lasso_db->query( $prepare );
787
788 return true;
789 }
790
791 /**
792 * Get amazon product from DB
793 *
794 * @param string $product_id Amazon Product id.
795 */
796 public function get_amazon_product_from_db( $product_id ) {
797 if ( empty( $product_id ) ) {
798 return false;
799 }
800
801 global $wpdb;
802
803 $lasso_db = new Lasso_DB();
804
805 $sql = '
806 SELECT *
807 FROM ' . $lasso_db->amazon_products . '
808 WHERE amazon_id = %s
809 ';
810
811 $prepare = $wpdb->prepare( $sql, $product_id ); // phpcs:ignore
812 $result = $lasso_db->get_row( $prepare, ARRAY_A );
813
814 if ( $result ) {
815 $result = apply_filters( self::FILTER_AMAZON_PRODUCT, $result );
816 $result['monetized_url'] = self::get_amazon_product_url( $result['monetized_url'] );
817 $result['base_url'] = self::get_amazon_product_url( $result['base_url'], false );
818 $result['features'] = json_decode( $result['features'] );
819 }
820
821 return $result;
822 }
823
824 /**
825 * Extract result to an array
826 *
827 * @param object $response Data from Amazon.
828 * @param bool $large_image Get large image size. Default to false.
829 * @param string $product_id Product Id. Default to empty.
830 * @param string $product_url Product url. Default to empty.
831 *
832 * @return array
833 */
834 private function extract_search_result_v5( $response, $large_image = false, $product_id = '', $product_url = '' ) {
835 $image = '';
836 if ( isset( $response->Images->Primary ) ) { // phpcs:ignore
837 $image = $large_image ? $response->Images->Primary->Large->URL : $response->Images->Primary->Small->URL; // phpcs:ignore
838 }
839
840 // @codingStandardsIgnoreStart
841 $result = array(
842 'product_id' => $product_id ? $product_id : ( $response->ASIN ?? 0 ),
843 'title' => $response->ItemInfo->Title->DisplayValue ?? '',
844 'url' => $product_url ? $product_url : ( $response->DetailPageURL ?? '' ),
845 'default_url' => $response->DetailPageURL ?? '',
846 'image' => $image,
847 'quantity' => $response->Offers->Summaries[0]->OfferCount ?? 0,
848 'is_prime' => $response->Offers->Listings[0]->DeliveryInfo->IsPrimeEligible ?? false,
849 'price' => $response->Offers->Listings[0]->Price->DisplayAmount ?? 0,
850 'amount' => $response->Offers->Listings[0]->Price->Amount ?? 0,
851 'currency' => $response->Offers->Listings[0]->Price->Currency ?? '',
852 'features' => $response->ItemInfo->Features->DisplayValues ?? array(),
853 'savings_amount' => $response->Offers->Listings[0]->Price->Savings->Amount ?? 0.0,
854 'savings_percent' => $response->Offers->Listings[0]->Price->Savings->Percentage ?? 0,
855 'savings_basis' => $response->Offers->Listings[0]->SavingBasis->Amount ?? 0.0,
856 );
857 // @codingStandardsIgnoreEnd
858
859 return $result;
860 }
861
862 /**
863 * Query amazon v5
864 *
865 * @param array $parameters Amazon API params.
866 * @param boolean $lasso_settings Lasso settings. Default to false.
867 *
868 * @return array
869 */
870 public function query_amazon_v5( $parameters, $lasso_settings = false ) {
871 try {
872 if ( ! $lasso_settings ) {
873 $lasso_settings = Setting::get_settings();
874 }
875
876 $this->amazon_access_key_id = $lasso_settings['amazon_access_key_id'] ?? '';
877 $this->amazon_secret_key = $lasso_settings['amazon_secret_key'] ?? '';
878 $this->amazon_tracking_id = $lasso_settings['amazon_tracking_id'] ?? '';
879
880 $result = $this->aws_signed_request_v5( $parameters, $this->amazon_access_key_id, $this->amazon_secret_key, $this->amazon_tracking_id );
881
882 if ( isset( $result->Errors ) ) { // phpcs:ignore
883 $error = $result->Errors[0]; // phpcs:ignore
884 }
885
886 return $result;
887 } catch ( \Exception $e ) {
888 return array();
889 }
890 }
891
892 /**
893 * Get Amazon product by product id
894 *
895 * @param string $product_id Amazon product id.
896 *
897 * @return object
898 */
899 public function get_product_by_id_v5( $product_id ) {
900 $parameters = array(
901 'Operation' => 'GetItems',
902 'ItemIds' => array( $product_id ),
903 'Resources' => array(
904 'Images.Primary.Small',
905 'Images.Primary.Large',
906 'ItemInfo.Title',
907 'ItemInfo.ContentRating',
908 'ItemInfo.Features',
909 'ItemInfo.ProductInfo',
910 'ItemInfo.TechnicalInfo',
911 'Offers.Listings.Price',
912 'Offers.Listings.SavingBasis',
913 'Offers.Summaries.OfferCount',
914 'Offers.Listings.DeliveryInfo.IsPrimeEligible',
915 ),
916 );
917
918 $json_response = $this->query_amazon_v5( $parameters );
919
920 return $json_response;
921 }
922
923 /**
924 * Get product from Amazon by product name
925 *
926 * @param string $keyword Keyword.
927 * @param string $product_type Product type.
928 *
929 * @return object
930 */
931 public function get_product_by_keyword_v5( $keyword, $product_type ) {
932 $parameters = array(
933 'Operation' => 'SearchItems',
934 'Keywords' => $keyword,
935 'SearchIndex' => $product_type,
936 'Resources' => array(
937 'Images.Primary.Small',
938 'Images.Primary.Large',
939 'ItemInfo.Title',
940 'ItemInfo.ContentRating',
941 'ItemInfo.Features',
942 'ItemInfo.ProductInfo',
943 'ItemInfo.TechnicalInfo',
944 'Offers.Listings.Price',
945 'Offers.Listings.SavingBasis',
946 'Offers.Summaries.OfferCount',
947 'Offers.Listings.DeliveryInfo.IsPrimeEligible',
948 ),
949 );
950
951 $json_response = $this->query_amazon_v5( $parameters );
952
953 return $json_response;
954 }
955
956 /**
957 * Sign request v5
958 *
959 * @param array $params Amazon params.
960 * @param string $amazon_access_key_id Amazon access key.
961 * @param string $amazon_secret_key Amazon secret key.
962 * @param string $amazon_tracking_id Amazon tracking id.
963 *
964 * @return object|bool
965 */
966 private function aws_signed_request_v5( $params, $amazon_access_key_id, $amazon_secret_key, $amazon_tracking_id ) {
967 // phpcs:ignore
968 // $amazon_domain = 'www.amazon.com';
969 // $pa_endpoint = 'webservices.amazon.com';
970
971 $country = Setting::get_setting( 'amazon_default_tracking_country', 'usa' );
972 $countries = self::get_amazon_api_countries();
973 $amazon_domain = $countries[ $country ]['amazon_domain'];
974 $pa_endpoint = $countries[ $country ]['pa_endpoint'];
975 $amazon_region = $countries[ $country ]['region'];
976
977 $params['Marketplace'] = $amazon_domain;
978 $params['PartnerType'] = 'Associates';
979 $params['PartnerTag'] = $amazon_tracking_id;
980 $post_fields = wp_json_encode( $params );
981
982 $aws_v5 = new AwsV5( $amazon_access_key_id, $amazon_secret_key );
983 $aws_v5->setHost( $pa_endpoint );
984 $aws_v5->setRegionName( $amazon_region );
985 $aws_v5->setPayload( $post_fields );
986 $aws_v5->addHeader( 'x-amz-target', 'com.amazon.paapi5.v1.ProductAdvertisingAPIv1.' . $params['Operation'] );
987 $headers = $aws_v5->getHeaders( true );
988 $url = "https://$pa_endpoint/paapi5/searchitems";
989
990 // @codingStandardsIgnoreStart
991 $ch = curl_init();
992 curl_setopt( $ch, CURLOPT_URL, $url );
993 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
994 curl_setopt( $ch, CURLOPT_POSTFIELDS, $post_fields );
995 curl_setopt( $ch, CURLOPT_POST, 1 );
996
997 curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
998
999 $result = curl_exec( $ch );
1000 if ( curl_errno( $ch ) ) {
1001 // phpcs:ignore
1002 // $error = curl_error( $ch );
1003 return false;
1004 }
1005 curl_close( $ch );
1006 // @codingStandardsIgnoreEnd
1007
1008 return json_decode( $result );
1009 }
1010
1011 /**
1012 * Get Amazon product id by url
1013 *
1014 * @param string $url Amazon link.
1015 * @return string|bool
1016 */
1017 public static function get_product_id_by_url( $url ) {
1018 $url = Helper::add_https( $url );
1019
1020 if ( ! self::is_amazon_url( $url ) || strpos( $url, '.' ) === false ) {
1021 return false;
1022 }
1023
1024 $parse_url = wp_parse_url( $url );
1025 $amazon_domain = trim( $parse_url['host'] ?? '', 'www.' );
1026 $amazon_domain_regex = str_replace( '.', '\.', str_replace( 'www.', '', $amazon_domain ) );
1027
1028 $reg = '#(?:https?://(?:www\.){0,1}' . $amazon_domain_regex . '(?:/.*){0,1}(?:/dp/|/gp/product/|/ASIN/|/gp/video/detail/))([a-zA-Z0-9]*)(.*?)(?:/.*|$)#';
1029 $matches = array();
1030 preg_match( $reg, $url, $matches );
1031
1032 return isset( $matches[1] ) && ! empty( $matches[1] ) ? $matches[1] : false;
1033 }
1034
1035 /**
1036 * Keep URL arguments for Amazon URL
1037 *
1038 * @param string $product_url Amazon product url.
1039 *
1040 * @return array $results
1041 */
1042 private static function keep_args( $product_url ) {
1043 // ? amazon link but it is not product url
1044 if ( ! self::is_amazon_url( $product_url ) || self::is_amazon_shortened_url( $product_url ) ) {
1045 return array();
1046 }
1047
1048 $results = array();
1049 $url_params = array(
1050 'maas',
1051 'ref_',
1052 's',
1053 'aa_campaignid',
1054 'aa_creativeid',
1055 'aa_adgroupid',
1056 'campaignId',
1057 'linkCode',
1058 'linkId',
1059 );
1060
1061 foreach ( $url_params as $param_name ) {
1062 $param_value = Helper::get_argument_from_url( $product_url, $param_name );
1063 if ( $param_value ) {
1064 $results[] = $param_name . '=' . $param_value;
1065 }
1066 }
1067
1068 return $results;
1069 }
1070
1071 /**
1072 * Get amazon tracking id by url
1073 * This function will be deprecated in the future. Please use Helper::get_argument_from_url() instead
1074 *
1075 * @param string $link Amazon link.
1076 * @return string
1077 */
1078 public static function get_amazon_tracking_id_by_url( $link ) {
1079 $search = '/([?|&|&amp;|\/]{1})tag=([a-zA-Z0-9\-\_]*)/i';
1080 $matches = array();
1081 preg_match( $search, $link, $matches );
1082
1083 return $matches[2] ?? '';
1084 }
1085
1086 /**
1087 * Clean Amazon URL that contains maas parameter
1088 *
1089 * @param string $product_url Amazon product URL.
1090 * @return string
1091 */
1092 public static function clean_maas_url( $product_url ) {
1093 return $product_url;
1094 }
1095
1096 /**
1097 * Make product url to shorten url if this setting is enabled
1098 *
1099 * @param string $product_url Amazon product url.
1100 * @param bool $monetize Monetize link or not. Default to true.
1101 * @param bool $check_lasso_post Check Lasso post or ignore. Default to true.
1102 * @param string $custom_tracking_id Allow to use custom tracking id.
1103 */
1104 public static function get_amazon_product_url( $product_url, $monetize = true, $check_lasso_post = true, $custom_tracking_id = '' ) {
1105 // ? amazon link but it is not product url
1106 if ( ! self::is_amazon_url( $product_url ) || self::is_amazon_shortened_url( $product_url ) ) {
1107 return $product_url;
1108 }
1109
1110 // If MAAS param exists and the original tag is exactly "maas", leave the URL untouched
1111 $__maas_arg = Helper::get_argument_from_url( $product_url, 'maas' );
1112 $__orig_tag = Helper::get_argument_from_url( $product_url, 'tag' );
1113 if ( $__maas_arg && strtolower( $__orig_tag ) === 'maas' ) {
1114 return $product_url;
1115 }
1116
1117 $product_id = self::get_product_id_by_url( $product_url );
1118
1119 // ? remove all url queries, just keep needed args
1120 $url_without_params = explode( '?', $product_url )[0];
1121 if ( $product_id && ! $monetize ) {
1122 $keep_args = self::keep_args( $product_url );
1123 $args = Helper::build_url_parameter_string( $keep_args );
1124
1125 $product_url = $args ? $url_without_params . '?' . $args : $url_without_params;
1126 $product_url = self::clean_maas_url( $product_url );
1127
1128 return $product_url;
1129 }
1130
1131 $lasso_settings = Setting::get_settings();
1132 $amazon_tracking_id = trim( $lasso_settings['amazon_tracking_id'] ?? '' );
1133 $amazon_add_tracking_id_to_attribution = $lasso_settings['amazon_add_tracking_id_to_attribution'] ?? true;
1134
1135 $lasso_db = new Lasso_DB();
1136 $amz_cache_key = self::OBJECT_KEY . '_' . self::FUNCTION_NAME_GET_LASSO_ID_BY_PRODUCT_ID_AND_TYPE . '_' . $product_id . '_' . self::PRODUCT_TYPE;
1137 $lasso_id = Cache_Per_Process::get_instance()->get_cache( $amz_cache_key, null );
1138 if ( null === $lasso_id ) {
1139 $lasso_id = $lasso_db->get_lasso_id_by_product_id_and_type( $product_id, self::PRODUCT_TYPE, $product_url );
1140 Cache_Per_Process::get_instance()->set_cache( $amz_cache_key, $lasso_id );
1141 }
1142
1143 if ( $check_lasso_post && ! $lasso_id ) {
1144 $product_url = self::clean_maas_url( $product_url );
1145 return $product_url;
1146 }
1147
1148 $tag = Helper::get_argument_from_url( $product_url, 'tag' );
1149 $tag = $custom_tracking_id ? $custom_tracking_id : $tag;
1150 $tag = $tag ? $tag : '';
1151 $tag = ! empty( $tag ) ? $tag : $amazon_tracking_id;
1152
1153 // ? Return the remove all url queries for product url
1154 if ( $product_id ) {
1155 $tag_args = $tag ? 'tag=' . $tag : '';
1156 $keep_args = self::keep_args( $product_url );
1157 $maas = Helper::get_argument_from_url( $product_url, 'maas' );
1158 $add_tag_to_url = ( $maas && $amazon_add_tracking_id_to_attribution ) || ! $maas;
1159
1160 if ( $add_tag_to_url ) {
1161 array_unshift( $keep_args, $tag_args );
1162 }
1163
1164 $args = Helper::build_url_parameter_string( $keep_args );
1165 $product_url = $args ? $url_without_params . '?' . $args : $url_without_params;
1166 } else {
1167 $product_url = str_replace( '&amp;', '&', $product_url );
1168 $parse = wp_parse_url( $product_url );
1169 parse_str( $parse['query'] ?? '', $query );
1170
1171 // ? set tag id (tracking id) at the end of the url
1172 if ( $tag ) {
1173 $query['tag'] = $tag;
1174 } elseif ( ! empty( $amazon_tracking_id ) ) {
1175 $query['tag'] = $amazon_tracking_id;
1176 }
1177
1178 if ( ! $monetize ) {
1179 unset( $query['tag'] );
1180 }
1181
1182 $parse['query'] = Helper::get_query_from_array( $query );
1183 $product_url = Helper::get_url_from_parse( $parse );
1184 $product_url = trim( $product_url );
1185 $product_url = trim( $product_url, '?' );
1186 }
1187
1188 $product_url = self::clean_maas_url( $product_url );
1189
1190 return $product_url;
1191 }
1192
1193 /**
1194 * Get Amazon product info from url accept url or post_id
1195 *
1196 * @param string|int $url_or_post_id URL or post id.
1197 */
1198 public function get_amazon_product( $url_or_post_id ) {
1199
1200 if ( is_numeric( $url_or_post_id ) ) {
1201 // ? get amazon product using post_id
1202 $post_id = $url_or_post_id;
1203 $amazon_id = Affiliate_Link::get_amazon_id( $post_id );
1204 $product = $this->get_amazon_product_from_db( $amazon_id );
1205 return $product;
1206 } else {
1207 $url = $url_or_post_id;
1208 }
1209
1210 // ? get amazon prodcut using url
1211 if ( empty( $url ) ) {
1212 return '';
1213 }
1214
1215 $url = trim( $url, '/' );
1216 $product_id = self::get_product_id_by_url( $url );
1217 $product = $this->fetch_product_info( $product_id, true ); // ? Let's save all Amazon details as well
1218 $amazon_product = '';
1219
1220 if ( 'success' === $product['status'] ) {
1221 $product = $product['product'];
1222 $shorten_url = self::get_amazon_product_url( $product['url'] );
1223 $amazon_product = array(
1224 'id' => $product['product_id'],
1225 'name' => $product['title'],
1226 'price' => $product['price'],
1227 'url' => $shorten_url,
1228 'image' => $product['image'],
1229 'description' => '',
1230 );
1231 }
1232
1233 return $amazon_product;
1234 }
1235
1236 /**
1237 * Check whether a URL is amazon search page
1238 *
1239 * @param string $url URL.
1240 *
1241 * @return bool|string
1242 */
1243 public static function is_amazon_search_page( $url ) {
1244 $amazon_id = self::get_product_id_by_url( $url );
1245 $parse = wp_parse_url( $url );
1246 $path = $parse['path'] ?? '';
1247 $path = rtrim( $path, '/' );
1248 $keywords = Helper::get_argument_from_url( $url, 'keywords' );
1249 $field_keywords = Helper::get_argument_from_url( $url, 'field-keywords' );
1250 $k = Helper::get_argument_from_url( $url, 'k' );
1251 $k = $k ? $k : $field_keywords;
1252 $k = $k ? $k : $keywords;
1253
1254 if ( ! $amazon_id && ( '/s' === substr( $path, -2 ) || strpos( $path, '/s/' ) !== false ) && $k ) {
1255 return $k;
1256 }
1257
1258 return false;
1259 }
1260
1261 /**
1262 * Check whether a URL is amazon search page
1263 *
1264 * @param string $url URL.
1265 *
1266 * @return bool|string
1267 */
1268 public static function get_search_page_title( $url ) {
1269 $new_title = 'Amazon';
1270 $k = self::is_amazon_search_page( $url );
1271 if ( $k ) {
1272 $base_domain = Helper::get_base_domain( $url );
1273 $title_prefix = ucfirst( $base_domain );
1274 $new_title = $title_prefix . ' : ' . $k;
1275 }
1276
1277 return $new_title;
1278 }
1279
1280 /**
1281 * Validate Amazon tracking id
1282 *
1283 * @param string $tracking_id Amazon tracking id.
1284 * @return boolean
1285 */
1286 public static function validate_tracking_id( $tracking_id ) {
1287 return (bool) preg_match( '/' . self::TRACKING_ID_REGEX . '/i', $tracking_id );
1288 }
1289
1290 /**
1291 * Get Amazon link by product id
1292 *
1293 * @param string $product_id Amazon product id.
1294 * @param string $amz_link Amazon link. Default to empty.
1295 */
1296 public function get_amazon_link_by_product_id( $product_id, $amz_link = '' ) {
1297 if ( ! $product_id ) {
1298 return $amz_link;
1299 }
1300
1301 if ( '' !== $amz_link ) {
1302 $parse = wp_parse_url( $amz_link );
1303 $host = $parse['host'] ?? '';
1304 if ( '' !== $host ) {
1305 return 'https://' . $host . '/dp/' . $product_id;
1306 }
1307 }
1308
1309 $country = Setting::get_setting( 'amazon_default_tracking_country', 'usa' );
1310 $countries = self::get_amazon_api_countries();
1311 $amazon_domain = $countries[ $country ]['amazon_domain'];
1312
1313 return 'https://' . $amazon_domain . '/dp/' . $product_id;
1314 }
1315
1316 /**
1317 * Check whether a URL is Amazon redirect page
1318 *
1319 * @param string $url Amazon URL.
1320 */
1321 public static function is_amazon_redirect_page( $url ) {
1322 if ( ! self::is_amazon_url( $url ) || strpos( $url, '/gp/slredirect/' ) === false ) {
1323 return false;
1324 }
1325
1326 return true;
1327 }
1328
1329 /**
1330 * Get redirect url
1331 *
1332 * @param string $url Amazon URL.
1333 */
1334 public static function get_redirect_url( $url ) {
1335 if ( self::is_amazon_redirect_page( $url ) ) {
1336 $url_param = Helper::get_argument_from_url( $url, 'url' );
1337 $amazon_domain = Helper::get_base_domain( $url );
1338 $amazon_domain = Helper::add_https( $amazon_domain );
1339 $new_url = $amazon_domain . $url_param;
1340 $product_id = self::get_product_id_by_url( $new_url );
1341
1342 if ( $product_id ) {
1343 $url = $new_url;
1344 }
1345 }
1346
1347 return $url;
1348 }
1349
1350 /**
1351 * Format price
1352 * Ex: convert 19.89USD to $19.89
1353 *
1354 * @param string $price Price.
1355 * @param string $currency_iso Currency ISO.
1356 * @return string
1357 */
1358 public static function format_price( $price, $currency_iso = null ) {
1359 $currency_iso = $currency_iso ? $currency_iso : self::get_currency_iso_from_price_text( $price );
1360
1361 if ( $price && $currency_iso ) {
1362 return self::build_price_with_currency_iso( $price, $currency_iso );
1363 }
1364
1365 return $price;
1366 }
1367
1368 /**
1369 * Get Currency ISO from price text
1370 *
1371 * @param string $price Price text.
1372 * @return mixed|string
1373 */
1374 public static function get_currency_iso_from_price_text( $price ) {
1375 $result = '';
1376
1377 foreach ( self::CURRENCY_ISO as $currency_iso ) {
1378 if ( strpos( $price, $currency_iso ) !== false ) {
1379 return $currency_iso;
1380 }
1381 }
1382
1383 return $result;
1384 }
1385
1386 /**
1387 * Build price final format base on the currency ISO
1388 *
1389 * @param string $price_value Price value.
1390 * @param string $currency_iso Currency ISO.
1391 * @return string
1392 */
1393 public static function build_price_with_currency_iso( $price_value, $currency_iso ) {
1394 $currency_symbol = Helper::get_currency_symbol_from_iso_code( $currency_iso );
1395 $price_without_iso = str_replace( $currency_iso, '', $price_value );
1396 $price_value = Helper::get_price_value_from_price_text( $price_without_iso, $currency_symbol );
1397 $currency_position = preg_match( '/[]|R\$|TL|kr|zł/', $currency_symbol ) ? 'end' : 'begin';
1398 $price_format = preg_match( '/[]|R\$|TL|kr|zł/', $currency_symbol ) ? number_format( $price_value, 2, ',', '.' ) : number_format( $price_value, 2, '.', ',' );
1399
1400 return 'begin' === $currency_position ? $currency_symbol . $price_format : $price_format . $currency_symbol;
1401 }
1402
1403 /**
1404 * Get the variation item in stock
1405 *
1406 * @param string $product_id Product id.
1407 * @param string $product_url Product url.
1408 * @param int $variation_page Variation page.
1409 * @return array
1410 */
1411 public function get_product_variation( $product_id, $product_url, $variation_page = 1 ) {
1412 $result = $this->get_product_variations_by_id_v5( $product_id, $variation_page );
1413 $items = $result->VariationsResult->Items ?? array(); // phpcs:ignore
1414
1415 if ( ! empty( $items ) ) {
1416 $items = $result->VariationsResult->Items; // phpcs:ignore
1417 $product_variations = array();
1418
1419 // ? Get product variation list
1420 foreach ( $items as $item ) {
1421 $product_variations[] = $this->extract_search_result_v5( $item, true, $product_id, $product_url );
1422 }
1423
1424 // ? Sort price from lowest to highest
1425 usort(
1426 $product_variations,
1427 function( $a, $b ) {
1428 return strcmp( $a['amount'], $b['amount'] );
1429 }
1430 );
1431
1432 // ? Get the in-stock product
1433 foreach ( $product_variations as $product_variation ) {
1434 if ( $product_variation['quantity'] && $product_variation['price'] ) {
1435 return $product_variation;
1436 }
1437 }
1438
1439 // ? If all variation products in this page unavailable, we request to the next page
1440 $page_count = $result->VariationsResult->VariationSummary->PageCount ?? 1; // phpcs:ignore
1441 if ( $variation_page < self::VARIATION_PAGE_LIMIT && $variation_page < $page_count ) {
1442 sleep( 1 ); // ? Delay for a while before call the next request
1443 return $this->get_product_variation( $product_id, $product_url, $variation_page + 1 );
1444 }
1445 }
1446
1447 return array();
1448 }
1449
1450 /**
1451 * Get Amazon product variations by product id
1452 *
1453 * @param string $product_id Amazon product id.
1454 * @param int $variation_page Variation page.
1455 *
1456 * @return object
1457 */
1458 public function get_product_variations_by_id_v5( $product_id, $variation_page = 1 ) {
1459 $parameters = array(
1460 'Operation' => 'GetVariations',
1461 'ASIN' => $product_id,
1462 'Condition' => 'New',
1463 'VariationPage' => $variation_page,
1464 'Resources' => array(
1465 'Images.Primary.Small',
1466 'Images.Primary.Large',
1467 'ItemInfo.Title',
1468 'ItemInfo.ContentRating',
1469 'ItemInfo.Features',
1470 'ItemInfo.ProductInfo',
1471 'ItemInfo.TechnicalInfo',
1472 'Offers.Listings.Price',
1473 'Offers.Listings.SavingBasis',
1474 'Offers.Summaries.OfferCount',
1475 'Offers.Listings.DeliveryInfo.IsPrimeEligible',
1476 ),
1477 );
1478
1479 $json_response = $this->query_amazon_v5( $parameters );
1480
1481 return $json_response;
1482 }
1483
1484 /**
1485 * Build discount pricing html.
1486 *
1487 * @param string $latest_price Latest price.
1488 * @param mixed $basis_price Basis price value.
1489 * @param string $currency Currency ISO.
1490 */
1491 public static function build_discount_pricing_html( $latest_price, $basis_price, $currency ) {
1492 $result = '';
1493
1494 try {
1495 $latest_price_value = Helper::get_price_value_from_price_text( $latest_price );
1496 $basis_price_value = Helper::get_price_value_from_price_text( $basis_price );
1497
1498 if ( $basis_price_value && ( round( $latest_price_value, 2 ) < round( $basis_price_value, 2 ) ) ) {
1499 $currency_symbol = Helper::get_currency_symbol_from_iso_code( $currency );
1500 $currency_position = preg_match( '/[]|R\$|TL|kr|zł/', $latest_price ) ? 'end' : 'begin';
1501 $basis_price_format = preg_match( '/[]|R\$|TL|kr|zł/', $latest_price ) ? number_format( $basis_price_value, 2, ',', '.' ) : number_format( $basis_price_value, 2, '.', ',' );
1502 $format_price = 'begin' === $currency_position ? $currency_symbol . $basis_price_format : $basis_price_format . ' ' . $currency_symbol;
1503
1504 $result = "<strike>$format_price</strike>";
1505 }
1506 } catch ( \Exception $e ) {
1507 $result = '';
1508 }
1509
1510 return $result;
1511 }
1512
1513 /**
1514 * Check amazon setting is configured or not
1515 *
1516 * @return boolean
1517 */
1518 public static function is_amazon_setting_configured() {
1519 $lasso_settings = Setting::get_settings();
1520 $is_amazon_configured = $lasso_settings['amazon_access_key_id'] && $lasso_settings['amazon_secret_key'] && $lasso_settings['amazon_tracking_id'];
1521
1522 return $is_amazon_configured || self::is_amazon_creators_verified( $lasso_settings );
1523 }
1524
1525 /**
1526 * Whether Amazon Creators API credentials are complete in settings.
1527 *
1528 * @param array|false $lasso_settings Settings array. Default false loads current settings.
1529 * @return bool
1530 */
1531 public static function is_amazon_creators_configured( $lasso_settings = false ) {
1532 if ( ! is_array( $lasso_settings ) ) {
1533 $lasso_settings = Setting::get_settings();
1534 }
1535
1536 $fields = array(
1537 'amazon_creators_credential_id',
1538 'amazon_creators_secret',
1539 'amazon_creators_version',
1540 'amazon_creators_partner_tag',
1541 );
1542
1543 foreach ( $fields as $field ) {
1544 if ( '' === trim( (string) ( $lasso_settings[ $field ] ?? '' ) ) ) {
1545 return false;
1546 }
1547 }
1548
1549 return true;
1550 }
1551
1552 /**
1553 * Whether Creators credentials were verified successfully (signature matches stored settings).
1554 *
1555 * @param array|false $lasso_settings Settings array. Default false loads current settings.
1556 * @return bool
1557 */
1558 public static function is_amazon_creators_verified( $lasso_settings = false ) {
1559 if ( ! self::is_amazon_creators_configured( $lasso_settings ) ) {
1560 return false;
1561 }
1562
1563 if ( ! is_array( $lasso_settings ) ) {
1564 $lasso_settings = Setting::get_settings();
1565 }
1566
1567 $current_signature = self::get_amazon_creators_signature( $lasso_settings );
1568 $saved_signature = (string) Helper::get_option( Constant::LASSO_OPTION_AMAZON_CREATORS_VERIFIED_SIGNATURE, '' );
1569
1570 return '' !== $current_signature && '' !== $saved_signature && hash_equals( $saved_signature, $current_signature );
1571 }
1572
1573 /**
1574 * Build a stable signature for the Creators payload.
1575 *
1576 * @param array $settings Settings values.
1577 * @return string Empty when Creators credentials are incomplete.
1578 */
1579 public static function get_amazon_creators_signature( $settings ) {
1580 if ( ! self::is_amazon_creators_configured( $settings ) ) {
1581 return '';
1582 }
1583
1584 return hash(
1585 'sha256',
1586 wp_json_encode(
1587 array(
1588 'credential_id' => (string) ( $settings['amazon_creators_credential_id'] ?? '' ),
1589 'secret' => (string) ( $settings['amazon_creators_secret'] ?? '' ),
1590 'credential_version' => (string) ( $settings['amazon_creators_version'] ?? '' ),
1591 'partner_tag' => (string) ( $settings['amazon_creators_partner_tag'] ?? '' ),
1592 'country' => (string) ( $settings['amazon_default_tracking_country'] ?? '' ),
1593 )
1594 )
1595 );
1596 }
1597
1598 /**
1599 * Whether the Lite install is linked to a Lasso account email (required for Creators fetch).
1600 *
1601 * @return bool
1602 */
1603 public static function is_lite_account_connected() {
1604 $email = Helper::get_option( Constant::LASSO_ACCOUNT_EMAIL, '' );
1605
1606 return is_string( $email ) && '' !== trim( $email );
1607 }
1608
1609 /**
1610 * Lite account token for lasso.link (md5 of linked account email).
1611 *
1612 * @return string Empty when Lite account email is not linked.
1613 */
1614 private static function get_lite_account_token() {
1615 $email = Helper::get_option( Constant::LASSO_ACCOUNT_EMAIL, '' );
1616 if ( ! is_string( $email ) || '' === trim( $email ) ) {
1617 return '';
1618 }
1619
1620 return md5( strtolower( trim( $email ) ) );
1621 }
1622
1623 /**
1624 * Fetch product data via lasso.link Creators API proxy (replaces deprecated PA-API GetItems).
1625 *
1626 * @param string $product_id Amazon ASIN.
1627 * @param bool $store_product Store in local DB.
1628 * @param bool|string $updated_at Optional updated timestamp.
1629 * @param string $amz_link Amazon URL.
1630 * @param array $lasso_settings Plugin settings.
1631 * @param bool $refresh_image Bypass cache and fetch fresh product image/metadata.
1632 * @return array|null Same shape as fetch_product_info success/failure, or null to fall back.
1633 */
1634 private function fetch_product_info_from_creators_api( $product_id, $store_product, $updated_at, $amz_link, $lasso_settings, $refresh_image = false ) {
1635 $token = self::get_lite_account_token();
1636 if ( '' === $token ) {
1637 return null;
1638 }
1639
1640 $headers = Helper::get_headers();
1641 $headers['token'] = $token;
1642
1643 $payload = array(
1644 'country' => $lasso_settings['amazon_default_tracking_country'] ?? '',
1645 'asin' => $product_id,
1646 'url' => $amz_link,
1647 );
1648
1649 if ( $refresh_image ) {
1650 $payload['refresh_image'] = 1;
1651 }
1652
1653 $response = Helper::send_request(
1654 'post',
1655 rtrim( Constant::LASSO_LINK, '/' ) . '/amazon/creators/product',
1656 $payload,
1657 $headers
1658 );
1659
1660 $status_code = intval( $response['status_code'] ?? 500 );
1661 $response_body = $response['response'] ?? null;
1662
1663 if ( empty( $response_body ) ) {
1664 return null;
1665 }
1666
1667 if ( ! empty( $response_body->error_code ) && 'CredentialsNotFound' === $response_body->error_code ) {
1668 return null;
1669 }
1670
1671 $fetch_status = isset( $response_body->status ) ? (string) $response_body->status : '';
1672 $error_code = isset( $response_body->error_code ) ? (string) $response_body->error_code : '';
1673 $api_flag = isset( $response_body->api ) ? (string) $response_body->api : 'yes';
1674 $product_data = isset( $response_body->product ) ? json_decode( wp_json_encode( $response_body->product ), true ) : array();
1675 $product_data = is_array( $product_data ) ? $product_data : array();
1676 $full_item = isset( $response_body->full_item ) ? $response_body->full_item : array();
1677
1678 if ( 'failed' === $fetch_status || 'NotFound' === $error_code ) {
1679 return $this->build_fetch_product_info_response( array(), $api_flag, $full_item, 'failed', $error_code );
1680 }
1681
1682 if ( $status_code >= 400 || empty( $response_body->result ) || empty( $product_data ) ) {
1683 return null;
1684 }
1685
1686 $product = $this->finalize_fetch_product_info_product(
1687 $product_data,
1688 $product_id,
1689 $store_product,
1690 $updated_at,
1691 $amz_link
1692 );
1693
1694 return $this->build_fetch_product_info_response( $product, $api_flag, $full_item, 'success', '' );
1695 }
1696
1697 /**
1698 * Build fetch_product_info() return array (same contract as get_product_by_id_v5 path).
1699 *
1700 * @param array $product Product fields.
1701 * @param string $api API source flag.
1702 * @param array|object $full_item Raw item payload.
1703 * @param string $status success|failed.
1704 * @param string $error_code Error code.
1705 * @return array
1706 */
1707 private function build_fetch_product_info_response( $product, $api, $full_item, $status, $error_code ) {
1708 return array(
1709 'product' => $product,
1710 'api' => $api,
1711 'full_item' => $full_item,
1712 'status' => $status,
1713 'error_code' => $error_code,
1714 );
1715 }
1716
1717 /**
1718 * Apply the same post-processing as the PA-API branch in fetch_product_info().
1719 *
1720 * @param array $product Product fields from API.
1721 * @param string $product_id Amazon ASIN.
1722 * @param bool $store_product Store in local DB.
1723 * @param bool|string $updated_at Optional updated timestamp.
1724 * @param string $amz_link Amazon URL.
1725 * @return array
1726 */
1727 private function finalize_fetch_product_info_product( $product, $product_id, $store_product, $updated_at, $amz_link ) {
1728 if ( empty( $product['product_id'] ) ) {
1729 $product['product_id'] = $product_id;
1730 }
1731
1732 return $this->enrich_fetched_amazon_product( $product, $store_product, $updated_at, $amz_link );
1733 }
1734
1735 /**
1736 * Shared post-fetch enrichment for PA-API and Creators product payloads.
1737 *
1738 * @param array $product Product fields.
1739 * @param bool $store_product Store in local DB.
1740 * @param bool|string $updated_at Optional updated timestamp.
1741 * @param string $amz_link Amazon URL.
1742 * @return array
1743 */
1744 private function enrich_fetched_amazon_product( $product, $store_product, $updated_at, $amz_link ) {
1745 global $wpdb;
1746
1747 $lasso_db = new Lasso_DB();
1748 $product_url = $amz_link ? $amz_link : ( $product['url'] ?? '' );
1749 $product['status_code'] = 200;
1750 $product['url'] = self::get_amazon_product_url( $product_url );
1751
1752 $amazon_product_id = (string) ( $product['product_id'] ?? '' );
1753 $query = $wpdb->prepare(
1754 'SELECT post_id FROM ' . $lasso_db->postmeta . " WHERE meta_key = 'amazon_product_id' AND meta_value = %s",
1755 $amazon_product_id
1756 );
1757 $lasso_id = $lasso_db->get_var( $query );
1758 $product['lasso_id'] = ( isset( $lasso_id ) ) ? $lasso_id : 0;
1759
1760 if ( $store_product ) {
1761 $amazon_tracking_id = Setting::get_setting( 'amazon_tracking_id', '' );
1762 $product['default_url'] = '' === $amazon_tracking_id ? $amz_link : ( $product['default_url'] ?? '' );
1763 $this->update_amazon_product_in_db( $product, $updated_at );
1764 }
1765
1766 return $product;
1767 }
1768
1769 /**
1770 * Get final url of the amazon shortlink from cache
1771 *
1772 * @param string $shortlink Amazon shortlink.
1773 * @return string|null
1774 */
1775 public static function get_shortlink_final_url_cached( $shortlink ) {
1776 if ( ! self::is_amazon_shortened_url( $shortlink ) ) {
1777 return null;
1778 }
1779 $final_url_cache = get_option( self::build_shortlink_cache_key( $shortlink ) );
1780
1781 if ( $final_url_cache === $shortlink ) {
1782 return null;
1783 }
1784
1785 return $final_url_cache ? $final_url_cache : null;
1786 }
1787
1788 /**
1789 * Build amazon shortlink cache key
1790 *
1791 * @param string $shortlink Amazon shortlink.
1792 * @return string|null
1793 */
1794 public static function build_shortlink_cache_key( $shortlink ) {
1795 if ( ! self::is_amazon_shortened_url( $shortlink ) ) {
1796 return null;
1797 }
1798
1799 $parse = wp_parse_url( $shortlink );
1800 $host = str_replace( '.', '_', $parse['host'] );
1801 $id = trim( $parse['path'] ?? '', '/' );
1802
1803 return $host . '_' . $id;
1804 }
1805
1806 /**
1807 * Format Amazon URLs
1808 *
1809 * @param string $url Amazon product url.
1810 * @return string URL.
1811 */
1812 public static function format_amazon_url( $url ) {
1813 $is_amazon_link = self::is_amazon_url( $url );
1814 $product_id = self::get_product_id_by_url( $url );
1815
1816 if ( $is_amazon_link && $product_id && strpos( $url, 'smile.amazon.' ) !== false ) {
1817 $url = str_replace( 'smile.amazon.', 'amazon.', $url );
1818 }
1819
1820 return $url;
1821 }
1822 }
1823