PluginProbe
Lasso Lite – Affiliate Link Manager & Product Displays / 150
Lasso Lite – Affiliate Link Manager & Product Displays v150
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 150, at classes/class-amazon-api.php

1,790 lines 56.5 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 * @return mixed
462 */
463 public function fetch_product_info( $product_id, $store_product = false, $updated_at = false, $amz_link = '' ) {
464 $lasso_db = new Lasso_DB();
465
466 $lasso_settings = Setting::get_settings();
467 $is_amazon_configured = $lasso_settings['amazon_access_key_id'] && $lasso_settings['amazon_secret_key'] && $lasso_settings['amazon_tracking_id'];
468
469 if (
470 self::is_amazon_creators_verified( $lasso_settings )
471 && $this->is_same_domain( $amz_link )
472 ) {
473 $creators_result = $this->fetch_product_info_from_creators_api(
474 $product_id,
475 $store_product,
476 $updated_at,
477 $amz_link,
478 $lasso_settings
479 );
480 if ( null !== $creators_result ) {
481 return $creators_result;
482 }
483 }
484
485 $result = $is_amazon_configured && $this->is_same_domain( $amz_link ) ? $this->get_product_by_id_v5( $product_id ) : false;
486 // phpcs:ignore
487 if ( is_object( $result ) && isset( $result->Errors[0] ) && (
488 "The ItemId $product_id provided in the request is invalid." === $result->Errors[0]->Message // phpcs:ignore
489 || "The value [$product_id] provided in the request for ItemIds is invalid." === $result->Errors[0]->Message // phpcs:ignore
490 )
491 ) {
492 return array(
493 'product' => array(),
494 'api' => 'yes',
495 'full_item' => array(),
496 'status' => 'failed',
497 'error_code' => 'NotFound',
498 );
499 } elseif ( is_object( $result ) && isset( $result->ItemsResult->Items[0] ) ) { // phpcs:ignore
500 $item = $result->ItemsResult->Items[0]; // phpcs:ignore
501
502 $product = $this->extract_search_result_v5( $item, true );
503 $product_url = $amz_link ? $amz_link : $product['url'];
504 $product['status_code'] = 200;
505 $product['url'] = self::get_amazon_product_url( $product_url );
506
507 // ? If $item->Offers is missing, we try to get the quantity from variation data
508 if ( ! isset( $item->Offers ) ) { // phpcs:ignore
509 sleep( 1 ); // ? Delay for a while before call the next request
510 $variation_product = $this->get_product_variation( $product_id, $product_url );
511 if ( $variation_product && isset( $variation_product['quantity'] ) && $variation_product['quantity'] ) {
512 $product = $variation_product;
513 }
514 }
515
516 $product = $this->enrich_fetched_amazon_product( $product, $store_product, $updated_at, $amz_link );
517
518 return array(
519 'product' => $product,
520 'api' => 'yes',
521 'full_item' => $item,
522 'status' => 'success',
523 'error_code' => '',
524 );
525 } elseif ( '' !== $lasso_settings['license_serial'] ) {
526 list( $product, $status ) = $this->fetch_product_from_bls( $product_id, $store_product, $updated_at, $amz_link );
527
528 return array(
529 'product' => $product,
530 'api' => 'no',
531 'full_item' => array(),
532 'status' => 200 === $status ? 'success' : 'fail',
533 'error_code' => 404 === $status ? 'NotFound' : '',
534 );
535 }
536
537 return array(
538 'product' => array(),
539 'api' => 'no',
540 'full_item' => array(),
541 'status' => 'failed',
542 'error_code' => 'NotFound',
543 );
544 }
545
546 /**
547 * Fetch amazon product from BLS (Lambda)
548 *
549 * @param string $product_id Amazon product id.
550 * @param bool $store_product Store product into DB or not. Default to false.
551 * @param bool|string $updated_at Set date time or not. Default to false.
552 * @param string $amz_link Amazon link. Default to empty.
553 */
554 public function fetch_product_from_bls( $product_id, $store_product = false, $updated_at = false, $amz_link = '' ) {
555 $url = strpos( $amz_link, 'amazon.' ) !== false ? $amz_link : $this->get_amazon_link_by_product_id( $product_id, $amz_link );
556 $m_link = self::get_amazon_product_url( $url );
557 $url = self::get_amazon_product_url( $url, false );
558
559 $amazon_product = array(
560 'title' => '',
561 'image' => '',
562 'url' => $m_link,
563 'price' => '',
564 'currency' => '',
565 'savings_amount' => 0,
566 'savings_percent' => 0,
567 'savings_basis' => 0,
568 );
569
570 try {
571 $res = Helper::get_url_status_code_by_broken_link_service( $url, true );
572 } catch ( \Throwable $e ) {
573 $res = array(
574 'status_code' => 500,
575 'response' => null,
576 );
577 }
578
579 $bls_response = ( isset( $res['response'] ) && is_object( $res['response'] ) ) ? $res['response'] : null;
580 $bls_status = ( null !== $bls_response && isset( $bls_response->status ) ) ? intval( $bls_response->status ) : 0;
581
582 if ( 200 === $res['status_code'] && 200 === $bls_status ) {
583 $img_url = $bls_response->imgUrl ?? '';
584 $img_url = '' !== $img_url ? $img_url : '';
585 $product_name = $bls_response->productName ?? '';
586 $product_name = '' === $product_name ? ( $bls_response->pageTitle ?? '' ) : $product_name;
587 $quantity = $bls_response->quantity ?? 200;
588 $price = $bls_response->price ?? '';
589 $product_id = self::get_product_id_by_url( $url );
590
591 $temp_url = $bls_response->finalUrl ?? $url;
592 $url = '' !== $temp_url ? $temp_url : $url;
593 $m_link = self::get_amazon_product_url( $amz_link ? $amz_link : $url );
594
595 if ( strpos( $product_name, 'Amazon.com:' ) === 0 ) {
596 $product_name = str_replace( 'Amazon.com:', '', $product_name );
597 $product_name = trim( $product_name );
598 }
599
600 if ( $product_id && $store_product ) {
601 $store_data = array(
602 'product_id' => $product_id,
603 'title' => $product_name,
604 'price' => $price,
605 'default_url' => $url,
606 'url' => $m_link,
607 'image' => trim( $img_url ),
608 'quantity' => intval( $quantity ), // Manual checks won't show out of stock for now. TODO: Add BLS to out of stock checks.
609 'is_manual' => 1,
610 );
611
612 // ? Set additional data for amazon product
613 if ( isset( $bls_response->additionalData ) && ! empty( $bls_response->additionalData ) ) {
614 $basis_price = $bls_response->additionalData->basis_price ?? '';
615 $basis_price = Helper::get_price_value_from_price_text( $basis_price );
616 $savings_amount = $bls_response->additionalData->saving_amount ?? '';
617 $savings_amount = Helper::get_price_value_from_price_text( $savings_amount );
618
619 $store_data['currency'] = $bls_response->additionalData->currency_name ?? '';
620 $store_data['savings_basis'] = $basis_price;
621 $store_data['savings_amount'] = $savings_amount;
622 $store_data['savings_percent'] = $bls_response->additionalData->saving_amount_percent ?? '';
623
624 $amazon_product['currency'] = $store_data['currency'];
625 $amazon_product['savings_basis'] = $store_data['savings_basis'];
626 $amazon_product['savings_amount'] = $store_data['savings_amount'];
627 $amazon_product['savings_percent'] = $store_data['savings_percent'];
628 }
629
630 $this->update_amazon_product_in_db( $store_data, $updated_at, true );
631 }
632
633 $amazon_product['title'] = $product_name;
634 $amazon_product['image'] = $img_url;
635 $amazon_product['url'] = $m_link;
636 $amazon_product['price'] = $price;
637 $amazon_product['quantity'] = $quantity;
638 $amazon_product['status_code'] = $bls_status;
639 }
640 if ( 404 === $res['status_code'] || ( 200 === $res['status_code'] && 404 === $bls_status ) ) {
641 $amz_model = new Amazon_Products();
642 $last_updated = gmdate( 'Y-m-d H:i:s', time() );
643 $amz_model->update_amazon_field( $product_id, 'last_updated', $last_updated );
644 $amz_model->update_amazon_field( $product_id, 'out_of_stock', 0 );
645 }
646
647 if ( $bls_status ) {
648 $status = $bls_status;
649 } elseif ( 200 === intval( $res['status_code'] ?? 0 ) ) {
650 $status = 500;
651 } else {
652 $status = intval( $res['status_code'] ?? 500 );
653 }
654
655 return array( $amazon_product, intval( $status ) );
656 }
657
658 /**
659 * Insert or Update Amazon Product Data
660 *
661 * @param array $product Amazon product.
662 * @param bool|string $updated_at Set update date time. Default to false.
663 */
664 public function update_amazon_product_in_db( $product, $updated_at = false ) {
665 global $wpdb;
666
667 $lasso_db = new Lasso_DB();
668
669 $amazon_id = $product['product_id'] ?? '';
670 $default_product_name = $product['title'] ?? '';
671 $latest_price = $product['price'] ?? '';
672 $latest_price = '0' === $latest_price || ( is_int( $latest_price ) && 0 === $latest_price ) ? '' : $latest_price;
673 $base_url = self::get_amazon_product_url( $product['default_url'] ?? '', false, false );
674 $monetized_url = self::get_amazon_product_url( $product['url'] ?? '', true, false );
675 $default_image = trim( $product['image'] ?? '' );
676 $last_updated = gmdate( 'Y-m-d H:i:s', time() );
677 $last_updated = $updated_at ? $updated_at : $last_updated;
678 $is_prime = $product['is_prime'] ?? '';
679 $currency = $product['currency'] ?? '';
680 $features = wp_json_encode( $product['features'] ?? array() );
681 $savings_amount = $product['savings_amount'] ?? '';
682 $savings_percent = $product['savings_percent'] ?? '';
683 $savings_basis = $product['savings_basis'] ?? '';
684 $is_manual = $product['is_manual'] ?? 0;
685 $quantity = intval( $product['quantity'] ?? 200 );
686 $out_of_stock = 0 === $quantity ? 1 : 0;
687
688 if ( '' === $amazon_id || '' === $default_product_name || '' === $default_image
689 || ( '' !== $default_image && Helper::validate_url( $default_image ) === false && strpos( $default_image, 'data:image' ) !== 0 )
690 ) {
691 return false;
692 }
693
694 $base_url = trim( $base_url );
695 $monetized_url = trim( $monetized_url );
696
697 $query = '
698 INSERT INTO ' . $lasso_db->amazon_products . "
699 (
700 amazon_id, default_product_name, latest_price, base_url,
701 monetized_url, default_image, last_updated, is_prime,
702 currency, features, savings_amount, savings_percent,
703 savings_basis, is_manual, out_of_stock
704 )
705 VALUES
706 (
707 %s, %s, %s, %s,
708 %s, %s, %s, %d,
709 %s, %s, %s, %d,
710 %s, %d, %d
711 )
712 ON DUPLICATE KEY UPDATE
713 amazon_id = %s,
714 default_product_name = %s,
715 latest_price = %s,
716 base_url = %s,
717 monetized_url = %s,
718 default_image = (CASE WHEN %s='' or %s IS NULL THEN `default_image` ELSE %s END),
719 last_updated = %s,
720 is_prime = %d,
721 currency = %s,
722 features = %s,
723 savings_amount = %s,
724 savings_percent = %d,
725 savings_basis = %s,
726 is_manual = %d,
727 out_of_stock = %d
728 ;
729 ";
730 $prepare = $wpdb->prepare(
731 // phpcs:ignore
732 $query,
733 // ? First for insert
734 $amazon_id,
735 $default_product_name,
736 $latest_price,
737 $base_url,
738 $monetized_url,
739 $default_image,
740 $last_updated,
741 $is_prime,
742 $currency,
743 $features,
744 $savings_amount,
745 $savings_percent,
746 $savings_basis,
747 $is_manual,
748 $out_of_stock,
749 // ? Second for update
750 $amazon_id,
751 $default_product_name,
752 $latest_price,
753 $base_url,
754 $monetized_url,
755 $default_image,
756 $default_image,
757 $default_image,
758 $last_updated,
759 $is_prime,
760 $currency,
761 $features,
762 $savings_amount,
763 $savings_percent,
764 $savings_basis,
765 $is_manual,
766 $out_of_stock
767 );
768
769 $lasso_db->query( $prepare );
770
771 return true;
772 }
773
774 /**
775 * Get amazon product from DB
776 *
777 * @param string $product_id Amazon Product id.
778 */
779 public function get_amazon_product_from_db( $product_id ) {
780 if ( empty( $product_id ) ) {
781 return false;
782 }
783
784 global $wpdb;
785
786 $lasso_db = new Lasso_DB();
787
788 $sql = '
789 SELECT *
790 FROM ' . $lasso_db->amazon_products . '
791 WHERE amazon_id = %s
792 ';
793
794 $prepare = $wpdb->prepare( $sql, $product_id ); // phpcs:ignore
795 $result = $lasso_db->get_row( $prepare, ARRAY_A );
796
797 if ( $result ) {
798 $result = apply_filters( self::FILTER_AMAZON_PRODUCT, $result );
799 $result['monetized_url'] = self::get_amazon_product_url( $result['monetized_url'] );
800 $result['base_url'] = self::get_amazon_product_url( $result['base_url'], false );
801 $result['features'] = json_decode( $result['features'] );
802 }
803
804 return $result;
805 }
806
807 /**
808 * Extract result to an array
809 *
810 * @param object $response Data from Amazon.
811 * @param bool $large_image Get large image size. Default to false.
812 * @param string $product_id Product Id. Default to empty.
813 * @param string $product_url Product url. Default to empty.
814 *
815 * @return array
816 */
817 private function extract_search_result_v5( $response, $large_image = false, $product_id = '', $product_url = '' ) {
818 $image = '';
819 if ( isset( $response->Images->Primary ) ) { // phpcs:ignore
820 $image = $large_image ? $response->Images->Primary->Large->URL : $response->Images->Primary->Small->URL; // phpcs:ignore
821 }
822
823 // @codingStandardsIgnoreStart
824 $result = array(
825 'product_id' => $product_id ? $product_id : ( $response->ASIN ?? 0 ),
826 'title' => $response->ItemInfo->Title->DisplayValue ?? '',
827 'url' => $product_url ? $product_url : ( $response->DetailPageURL ?? '' ),
828 'default_url' => $response->DetailPageURL ?? '',
829 'image' => $image,
830 'quantity' => $response->Offers->Summaries[0]->OfferCount ?? 0,
831 'is_prime' => $response->Offers->Listings[0]->DeliveryInfo->IsPrimeEligible ?? false,
832 'price' => $response->Offers->Listings[0]->Price->DisplayAmount ?? 0,
833 'amount' => $response->Offers->Listings[0]->Price->Amount ?? 0,
834 'currency' => $response->Offers->Listings[0]->Price->Currency ?? '',
835 'features' => $response->ItemInfo->Features->DisplayValues ?? array(),
836 'savings_amount' => $response->Offers->Listings[0]->Price->Savings->Amount ?? 0.0,
837 'savings_percent' => $response->Offers->Listings[0]->Price->Savings->Percentage ?? 0,
838 'savings_basis' => $response->Offers->Listings[0]->SavingBasis->Amount ?? 0.0,
839 );
840 // @codingStandardsIgnoreEnd
841
842 return $result;
843 }
844
845 /**
846 * Query amazon v5
847 *
848 * @param array $parameters Amazon API params.
849 * @param boolean $lasso_settings Lasso settings. Default to false.
850 *
851 * @return array
852 */
853 public function query_amazon_v5( $parameters, $lasso_settings = false ) {
854 try {
855 if ( ! $lasso_settings ) {
856 $lasso_settings = Setting::get_settings();
857 }
858
859 $this->amazon_access_key_id = $lasso_settings['amazon_access_key_id'] ?? '';
860 $this->amazon_secret_key = $lasso_settings['amazon_secret_key'] ?? '';
861 $this->amazon_tracking_id = $lasso_settings['amazon_tracking_id'] ?? '';
862
863 $result = $this->aws_signed_request_v5( $parameters, $this->amazon_access_key_id, $this->amazon_secret_key, $this->amazon_tracking_id );
864
865 if ( isset( $result->Errors ) ) { // phpcs:ignore
866 $error = $result->Errors[0]; // phpcs:ignore
867 }
868
869 return $result;
870 } catch ( \Exception $e ) {
871 return array();
872 }
873 }
874
875 /**
876 * Get Amazon product by product id
877 *
878 * @param string $product_id Amazon product id.
879 *
880 * @return object
881 */
882 public function get_product_by_id_v5( $product_id ) {
883 $parameters = array(
884 'Operation' => 'GetItems',
885 'ItemIds' => array( $product_id ),
886 'Resources' => array(
887 'Images.Primary.Small',
888 'Images.Primary.Large',
889 'ItemInfo.Title',
890 'ItemInfo.ContentRating',
891 'ItemInfo.Features',
892 'ItemInfo.ProductInfo',
893 'ItemInfo.TechnicalInfo',
894 'Offers.Listings.Price',
895 'Offers.Listings.SavingBasis',
896 'Offers.Summaries.OfferCount',
897 'Offers.Listings.DeliveryInfo.IsPrimeEligible',
898 ),
899 );
900
901 $json_response = $this->query_amazon_v5( $parameters );
902
903 return $json_response;
904 }
905
906 /**
907 * Get product from Amazon by product name
908 *
909 * @param string $keyword Keyword.
910 * @param string $product_type Product type.
911 *
912 * @return object
913 */
914 public function get_product_by_keyword_v5( $keyword, $product_type ) {
915 $parameters = array(
916 'Operation' => 'SearchItems',
917 'Keywords' => $keyword,
918 'SearchIndex' => $product_type,
919 'Resources' => array(
920 'Images.Primary.Small',
921 'Images.Primary.Large',
922 'ItemInfo.Title',
923 'ItemInfo.ContentRating',
924 'ItemInfo.Features',
925 'ItemInfo.ProductInfo',
926 'ItemInfo.TechnicalInfo',
927 'Offers.Listings.Price',
928 'Offers.Listings.SavingBasis',
929 'Offers.Summaries.OfferCount',
930 'Offers.Listings.DeliveryInfo.IsPrimeEligible',
931 ),
932 );
933
934 $json_response = $this->query_amazon_v5( $parameters );
935
936 return $json_response;
937 }
938
939 /**
940 * Sign request v5
941 *
942 * @param array $params Amazon params.
943 * @param string $amazon_access_key_id Amazon access key.
944 * @param string $amazon_secret_key Amazon secret key.
945 * @param string $amazon_tracking_id Amazon tracking id.
946 *
947 * @return object|bool
948 */
949 private function aws_signed_request_v5( $params, $amazon_access_key_id, $amazon_secret_key, $amazon_tracking_id ) {
950 // phpcs:ignore
951 // $amazon_domain = 'www.amazon.com';
952 // $pa_endpoint = 'webservices.amazon.com';
953
954 $country = Setting::get_setting( 'amazon_default_tracking_country', 'usa' );
955 $countries = self::get_amazon_api_countries();
956 $amazon_domain = $countries[ $country ]['amazon_domain'];
957 $pa_endpoint = $countries[ $country ]['pa_endpoint'];
958 $amazon_region = $countries[ $country ]['region'];
959
960 $params['Marketplace'] = $amazon_domain;
961 $params['PartnerType'] = 'Associates';
962 $params['PartnerTag'] = $amazon_tracking_id;
963 $post_fields = wp_json_encode( $params );
964
965 $aws_v5 = new AwsV5( $amazon_access_key_id, $amazon_secret_key );
966 $aws_v5->setHost( $pa_endpoint );
967 $aws_v5->setRegionName( $amazon_region );
968 $aws_v5->setPayload( $post_fields );
969 $aws_v5->addHeader( 'x-amz-target', 'com.amazon.paapi5.v1.ProductAdvertisingAPIv1.' . $params['Operation'] );
970 $headers = $aws_v5->getHeaders( true );
971 $url = "https://$pa_endpoint/paapi5/searchitems";
972
973 // @codingStandardsIgnoreStart
974 $ch = curl_init();
975 curl_setopt( $ch, CURLOPT_URL, $url );
976 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
977 curl_setopt( $ch, CURLOPT_POSTFIELDS, $post_fields );
978 curl_setopt( $ch, CURLOPT_POST, 1 );
979
980 curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
981
982 $result = curl_exec( $ch );
983 if ( curl_errno( $ch ) ) {
984 // phpcs:ignore
985 // $error = curl_error( $ch );
986 return false;
987 }
988 curl_close( $ch );
989 // @codingStandardsIgnoreEnd
990
991 return json_decode( $result );
992 }
993
994 /**
995 * Get Amazon product id by url
996 *
997 * @param string $url Amazon link.
998 * @return string|bool
999 */
1000 public static function get_product_id_by_url( $url ) {
1001 $url = Helper::add_https( $url );
1002
1003 if ( ! self::is_amazon_url( $url ) || strpos( $url, '.' ) === false ) {
1004 return false;
1005 }
1006
1007 $parse_url = wp_parse_url( $url );
1008 $amazon_domain = trim( $parse_url['host'] ?? '', 'www.' );
1009 $amazon_domain_regex = str_replace( '.', '\.', str_replace( 'www.', '', $amazon_domain ) );
1010
1011 $reg = '#(?:https?://(?:www\.){0,1}' . $amazon_domain_regex . '(?:/.*){0,1}(?:/dp/|/gp/product/|/ASIN/|/gp/video/detail/))([a-zA-Z0-9]*)(.*?)(?:/.*|$)#';
1012 $matches = array();
1013 preg_match( $reg, $url, $matches );
1014
1015 return isset( $matches[1] ) && ! empty( $matches[1] ) ? $matches[1] : false;
1016 }
1017
1018 /**
1019 * Keep URL arguments for Amazon URL
1020 *
1021 * @param string $product_url Amazon product url.
1022 *
1023 * @return array $results
1024 */
1025 private static function keep_args( $product_url ) {
1026 // ? amazon link but it is not product url
1027 if ( ! self::is_amazon_url( $product_url ) || self::is_amazon_shortened_url( $product_url ) ) {
1028 return array();
1029 }
1030
1031 $results = array();
1032 $url_params = array(
1033 'maas',
1034 'ref_',
1035 's',
1036 'aa_campaignid',
1037 'aa_creativeid',
1038 'aa_adgroupid',
1039 'campaignId',
1040 'linkCode',
1041 'linkId',
1042 );
1043
1044 foreach ( $url_params as $param_name ) {
1045 $param_value = Helper::get_argument_from_url( $product_url, $param_name );
1046 if ( $param_value ) {
1047 $results[] = $param_name . '=' . $param_value;
1048 }
1049 }
1050
1051 return $results;
1052 }
1053
1054 /**
1055 * Get amazon tracking id by url
1056 * This function will be deprecated in the future. Please use Helper::get_argument_from_url() instead
1057 *
1058 * @param string $link Amazon link.
1059 * @return string
1060 */
1061 public static function get_amazon_tracking_id_by_url( $link ) {
1062 $search = '/([?|&|&amp;|\/]{1})tag=([a-zA-Z0-9\-\_]*)/i';
1063 $matches = array();
1064 preg_match( $search, $link, $matches );
1065
1066 return $matches[2] ?? '';
1067 }
1068
1069 /**
1070 * Clean Amazon URL that contains maas parameter
1071 *
1072 * @param string $product_url Amazon product URL.
1073 * @return string
1074 */
1075 public static function clean_maas_url( $product_url ) {
1076 return $product_url;
1077 }
1078
1079 /**
1080 * Make product url to shorten url if this setting is enabled
1081 *
1082 * @param string $product_url Amazon product url.
1083 * @param bool $monetize Monetize link or not. Default to true.
1084 * @param bool $check_lasso_post Check Lasso post or ignore. Default to true.
1085 * @param string $custom_tracking_id Allow to use custom tracking id.
1086 */
1087 public static function get_amazon_product_url( $product_url, $monetize = true, $check_lasso_post = true, $custom_tracking_id = '' ) {
1088 // ? amazon link but it is not product url
1089 if ( ! self::is_amazon_url( $product_url ) || self::is_amazon_shortened_url( $product_url ) ) {
1090 return $product_url;
1091 }
1092
1093 // If MAAS param exists and the original tag is exactly "maas", leave the URL untouched
1094 $__maas_arg = Helper::get_argument_from_url( $product_url, 'maas' );
1095 $__orig_tag = Helper::get_argument_from_url( $product_url, 'tag' );
1096 if ( $__maas_arg && strtolower( $__orig_tag ) === 'maas' ) {
1097 return $product_url;
1098 }
1099
1100 $product_id = self::get_product_id_by_url( $product_url );
1101
1102 // ? remove all url queries, just keep needed args
1103 $url_without_params = explode( '?', $product_url )[0];
1104 if ( $product_id && ! $monetize ) {
1105 $keep_args = self::keep_args( $product_url );
1106 $args = Helper::build_url_parameter_string( $keep_args );
1107
1108 $product_url = $args ? $url_without_params . '?' . $args : $url_without_params;
1109 $product_url = self::clean_maas_url( $product_url );
1110
1111 return $product_url;
1112 }
1113
1114 $lasso_settings = Setting::get_settings();
1115 $amazon_tracking_id = trim( $lasso_settings['amazon_tracking_id'] ?? '' );
1116 $amazon_add_tracking_id_to_attribution = $lasso_settings['amazon_add_tracking_id_to_attribution'] ?? true;
1117
1118 $lasso_db = new Lasso_DB();
1119 $amz_cache_key = self::OBJECT_KEY . '_' . self::FUNCTION_NAME_GET_LASSO_ID_BY_PRODUCT_ID_AND_TYPE . '_' . $product_id . '_' . self::PRODUCT_TYPE;
1120 $lasso_id = Cache_Per_Process::get_instance()->get_cache( $amz_cache_key, null );
1121 if ( null === $lasso_id ) {
1122 $lasso_id = $lasso_db->get_lasso_id_by_product_id_and_type( $product_id, self::PRODUCT_TYPE, $product_url );
1123 Cache_Per_Process::get_instance()->set_cache( $amz_cache_key, $lasso_id );
1124 }
1125
1126 if ( $check_lasso_post && ! $lasso_id ) {
1127 $product_url = self::clean_maas_url( $product_url );
1128 return $product_url;
1129 }
1130
1131 $tag = Helper::get_argument_from_url( $product_url, 'tag' );
1132 $tag = $custom_tracking_id ? $custom_tracking_id : $tag;
1133 $tag = $tag ? $tag : '';
1134 $tag = ! empty( $tag ) ? $tag : $amazon_tracking_id;
1135
1136 // ? Return the remove all url queries for product url
1137 if ( $product_id ) {
1138 $tag_args = $tag ? 'tag=' . $tag : '';
1139 $keep_args = self::keep_args( $product_url );
1140 $maas = Helper::get_argument_from_url( $product_url, 'maas' );
1141 $add_tag_to_url = ( $maas && $amazon_add_tracking_id_to_attribution ) || ! $maas;
1142
1143 if ( $add_tag_to_url ) {
1144 array_unshift( $keep_args, $tag_args );
1145 }
1146
1147 $args = Helper::build_url_parameter_string( $keep_args );
1148 $product_url = $args ? $url_without_params . '?' . $args : $url_without_params;
1149 } else {
1150 $product_url = str_replace( '&amp;', '&', $product_url );
1151 $parse = wp_parse_url( $product_url );
1152 parse_str( $parse['query'] ?? '', $query );
1153
1154 // ? set tag id (tracking id) at the end of the url
1155 if ( $tag ) {
1156 $query['tag'] = $tag;
1157 } elseif ( ! empty( $amazon_tracking_id ) ) {
1158 $query['tag'] = $amazon_tracking_id;
1159 }
1160
1161 if ( ! $monetize ) {
1162 unset( $query['tag'] );
1163 }
1164
1165 $parse['query'] = Helper::get_query_from_array( $query );
1166 $product_url = Helper::get_url_from_parse( $parse );
1167 $product_url = trim( $product_url );
1168 $product_url = trim( $product_url, '?' );
1169 }
1170
1171 $product_url = self::clean_maas_url( $product_url );
1172
1173 return $product_url;
1174 }
1175
1176 /**
1177 * Get Amazon product info from url accept url or post_id
1178 *
1179 * @param string|int $url_or_post_id URL or post id.
1180 */
1181 public function get_amazon_product( $url_or_post_id ) {
1182
1183 if ( is_numeric( $url_or_post_id ) ) {
1184 // ? get amazon product using post_id
1185 $post_id = $url_or_post_id;
1186 $amazon_id = Affiliate_Link::get_amazon_id( $post_id );
1187 $product = $this->get_amazon_product_from_db( $amazon_id );
1188 return $product;
1189 } else {
1190 $url = $url_or_post_id;
1191 }
1192
1193 // ? get amazon prodcut using url
1194 if ( empty( $url ) ) {
1195 return '';
1196 }
1197
1198 $url = trim( $url, '/' );
1199 $product_id = self::get_product_id_by_url( $url );
1200 $product = $this->fetch_product_info( $product_id, true ); // ? Let's save all Amazon details as well
1201 $amazon_product = '';
1202
1203 if ( 'success' === $product['status'] ) {
1204 $product = $product['product'];
1205 $shorten_url = self::get_amazon_product_url( $product['url'] );
1206 $amazon_product = array(
1207 'id' => $product['product_id'],
1208 'name' => $product['title'],
1209 'price' => $product['price'],
1210 'url' => $shorten_url,
1211 'image' => $product['image'],
1212 'description' => '',
1213 );
1214 }
1215
1216 return $amazon_product;
1217 }
1218
1219 /**
1220 * Check whether a URL is amazon search page
1221 *
1222 * @param string $url URL.
1223 *
1224 * @return bool|string
1225 */
1226 public static function is_amazon_search_page( $url ) {
1227 $amazon_id = self::get_product_id_by_url( $url );
1228 $parse = wp_parse_url( $url );
1229 $path = $parse['path'] ?? '';
1230 $path = rtrim( $path, '/' );
1231 $keywords = Helper::get_argument_from_url( $url, 'keywords' );
1232 $field_keywords = Helper::get_argument_from_url( $url, 'field-keywords' );
1233 $k = Helper::get_argument_from_url( $url, 'k' );
1234 $k = $k ? $k : $field_keywords;
1235 $k = $k ? $k : $keywords;
1236
1237 if ( ! $amazon_id && ( '/s' === substr( $path, -2 ) || strpos( $path, '/s/' ) !== false ) && $k ) {
1238 return $k;
1239 }
1240
1241 return false;
1242 }
1243
1244 /**
1245 * Check whether a URL is amazon search page
1246 *
1247 * @param string $url URL.
1248 *
1249 * @return bool|string
1250 */
1251 public static function get_search_page_title( $url ) {
1252 $new_title = 'Amazon';
1253 $k = self::is_amazon_search_page( $url );
1254 if ( $k ) {
1255 $base_domain = Helper::get_base_domain( $url );
1256 $title_prefix = ucfirst( $base_domain );
1257 $new_title = $title_prefix . ' : ' . $k;
1258 }
1259
1260 return $new_title;
1261 }
1262
1263 /**
1264 * Validate Amazon tracking id
1265 *
1266 * @param string $tracking_id Amazon tracking id.
1267 * @return boolean
1268 */
1269 public static function validate_tracking_id( $tracking_id ) {
1270 return (bool) preg_match( '/' . self::TRACKING_ID_REGEX . '/i', $tracking_id );
1271 }
1272
1273 /**
1274 * Get Amazon link by product id
1275 *
1276 * @param string $product_id Amazon product id.
1277 * @param string $amz_link Amazon link. Default to empty.
1278 */
1279 public function get_amazon_link_by_product_id( $product_id, $amz_link = '' ) {
1280 if ( ! $product_id ) {
1281 return $amz_link;
1282 }
1283
1284 if ( '' !== $amz_link ) {
1285 $parse = wp_parse_url( $amz_link );
1286 $host = $parse['host'] ?? '';
1287 if ( '' !== $host ) {
1288 return 'https://' . $host . '/dp/' . $product_id;
1289 }
1290 }
1291
1292 $country = Setting::get_setting( 'amazon_default_tracking_country', 'usa' );
1293 $countries = self::get_amazon_api_countries();
1294 $amazon_domain = $countries[ $country ]['amazon_domain'];
1295
1296 return 'https://' . $amazon_domain . '/dp/' . $product_id;
1297 }
1298
1299 /**
1300 * Check whether a URL is Amazon redirect page
1301 *
1302 * @param string $url Amazon URL.
1303 */
1304 public static function is_amazon_redirect_page( $url ) {
1305 if ( ! self::is_amazon_url( $url ) || strpos( $url, '/gp/slredirect/' ) === false ) {
1306 return false;
1307 }
1308
1309 return true;
1310 }
1311
1312 /**
1313 * Get redirect url
1314 *
1315 * @param string $url Amazon URL.
1316 */
1317 public static function get_redirect_url( $url ) {
1318 if ( self::is_amazon_redirect_page( $url ) ) {
1319 $url_param = Helper::get_argument_from_url( $url, 'url' );
1320 $amazon_domain = Helper::get_base_domain( $url );
1321 $amazon_domain = Helper::add_https( $amazon_domain );
1322 $new_url = $amazon_domain . $url_param;
1323 $product_id = self::get_product_id_by_url( $new_url );
1324
1325 if ( $product_id ) {
1326 $url = $new_url;
1327 }
1328 }
1329
1330 return $url;
1331 }
1332
1333 /**
1334 * Format price
1335 * Ex: convert 19.89USD to $19.89
1336 *
1337 * @param string $price Price.
1338 * @param string $currency_iso Currency ISO.
1339 * @return string
1340 */
1341 public static function format_price( $price, $currency_iso = null ) {
1342 $currency_iso = $currency_iso ? $currency_iso : self::get_currency_iso_from_price_text( $price );
1343
1344 if ( $price && $currency_iso ) {
1345 return self::build_price_with_currency_iso( $price, $currency_iso );
1346 }
1347
1348 return $price;
1349 }
1350
1351 /**
1352 * Get Currency ISO from price text
1353 *
1354 * @param string $price Price text.
1355 * @return mixed|string
1356 */
1357 public static function get_currency_iso_from_price_text( $price ) {
1358 $result = '';
1359
1360 foreach ( self::CURRENCY_ISO as $currency_iso ) {
1361 if ( strpos( $price, $currency_iso ) !== false ) {
1362 return $currency_iso;
1363 }
1364 }
1365
1366 return $result;
1367 }
1368
1369 /**
1370 * Build price final format base on the currency ISO
1371 *
1372 * @param string $price_value Price value.
1373 * @param string $currency_iso Currency ISO.
1374 * @return string
1375 */
1376 public static function build_price_with_currency_iso( $price_value, $currency_iso ) {
1377 $currency_symbol = Helper::get_currency_symbol_from_iso_code( $currency_iso );
1378 $price_without_iso = str_replace( $currency_iso, '', $price_value );
1379 $price_value = Helper::get_price_value_from_price_text( $price_without_iso, $currency_symbol );
1380 $currency_position = preg_match( '/[]|R\$|TL|kr|zł/', $currency_symbol ) ? 'end' : 'begin';
1381 $price_format = preg_match( '/[]|R\$|TL|kr|zł/', $currency_symbol ) ? number_format( $price_value, 2, ',', '.' ) : number_format( $price_value, 2, '.', ',' );
1382
1383 return 'begin' === $currency_position ? $currency_symbol . $price_format : $price_format . $currency_symbol;
1384 }
1385
1386 /**
1387 * Get the variation item in stock
1388 *
1389 * @param string $product_id Product id.
1390 * @param string $product_url Product url.
1391 * @param int $variation_page Variation page.
1392 * @return array
1393 */
1394 public function get_product_variation( $product_id, $product_url, $variation_page = 1 ) {
1395 $result = $this->get_product_variations_by_id_v5( $product_id, $variation_page );
1396 $items = $result->VariationsResult->Items ?? array(); // phpcs:ignore
1397
1398 if ( ! empty( $items ) ) {
1399 $items = $result->VariationsResult->Items; // phpcs:ignore
1400 $product_variations = array();
1401
1402 // ? Get product variation list
1403 foreach ( $items as $item ) {
1404 $product_variations[] = $this->extract_search_result_v5( $item, true, $product_id, $product_url );
1405 }
1406
1407 // ? Sort price from lowest to highest
1408 usort(
1409 $product_variations,
1410 function( $a, $b ) {
1411 return strcmp( $a['amount'], $b['amount'] );
1412 }
1413 );
1414
1415 // ? Get the in-stock product
1416 foreach ( $product_variations as $product_variation ) {
1417 if ( $product_variation['quantity'] && $product_variation['price'] ) {
1418 return $product_variation;
1419 }
1420 }
1421
1422 // ? If all variation products in this page unavailable, we request to the next page
1423 $page_count = $result->VariationsResult->VariationSummary->PageCount ?? 1; // phpcs:ignore
1424 if ( $variation_page < self::VARIATION_PAGE_LIMIT && $variation_page < $page_count ) {
1425 sleep( 1 ); // ? Delay for a while before call the next request
1426 return $this->get_product_variation( $product_id, $product_url, $variation_page + 1 );
1427 }
1428 }
1429
1430 return array();
1431 }
1432
1433 /**
1434 * Get Amazon product variations by product id
1435 *
1436 * @param string $product_id Amazon product id.
1437 * @param int $variation_page Variation page.
1438 *
1439 * @return object
1440 */
1441 public function get_product_variations_by_id_v5( $product_id, $variation_page = 1 ) {
1442 $parameters = array(
1443 'Operation' => 'GetVariations',
1444 'ASIN' => $product_id,
1445 'Condition' => 'New',
1446 'VariationPage' => $variation_page,
1447 'Resources' => array(
1448 'Images.Primary.Small',
1449 'Images.Primary.Large',
1450 'ItemInfo.Title',
1451 'ItemInfo.ContentRating',
1452 'ItemInfo.Features',
1453 'ItemInfo.ProductInfo',
1454 'ItemInfo.TechnicalInfo',
1455 'Offers.Listings.Price',
1456 'Offers.Listings.SavingBasis',
1457 'Offers.Summaries.OfferCount',
1458 'Offers.Listings.DeliveryInfo.IsPrimeEligible',
1459 ),
1460 );
1461
1462 $json_response = $this->query_amazon_v5( $parameters );
1463
1464 return $json_response;
1465 }
1466
1467 /**
1468 * Build discount pricing html.
1469 *
1470 * @param string $latest_price Latest price.
1471 * @param mixed $basis_price Basis price value.
1472 * @param string $currency Currency ISO.
1473 */
1474 public static function build_discount_pricing_html( $latest_price, $basis_price, $currency ) {
1475 $result = '';
1476
1477 try {
1478 $latest_price_value = Helper::get_price_value_from_price_text( $latest_price );
1479 $basis_price_value = Helper::get_price_value_from_price_text( $basis_price );
1480
1481 if ( $basis_price_value && ( round( $latest_price_value, 2 ) < round( $basis_price_value, 2 ) ) ) {
1482 $currency_symbol = Helper::get_currency_symbol_from_iso_code( $currency );
1483 $currency_position = preg_match( '/[]|R\$|TL|kr|zł/', $latest_price ) ? 'end' : 'begin';
1484 $basis_price_format = preg_match( '/[]|R\$|TL|kr|zł/', $latest_price ) ? number_format( $basis_price_value, 2, ',', '.' ) : number_format( $basis_price_value, 2, '.', ',' );
1485 $format_price = 'begin' === $currency_position ? $currency_symbol . $basis_price_format : $basis_price_format . ' ' . $currency_symbol;
1486
1487 $result = "<strike>$format_price</strike>";
1488 }
1489 } catch ( \Exception $e ) {
1490 $result = '';
1491 }
1492
1493 return $result;
1494 }
1495
1496 /**
1497 * Check amazon setting is configured or not
1498 *
1499 * @return boolean
1500 */
1501 public static function is_amazon_setting_configured() {
1502 $lasso_settings = Setting::get_settings();
1503 $is_amazon_configured = $lasso_settings['amazon_access_key_id'] && $lasso_settings['amazon_secret_key'] && $lasso_settings['amazon_tracking_id'];
1504
1505 return $is_amazon_configured || self::is_amazon_creators_verified( $lasso_settings );
1506 }
1507
1508 /**
1509 * Whether Amazon Creators API credentials are complete in settings.
1510 *
1511 * @param array|false $lasso_settings Settings array. Default false loads current settings.
1512 * @return bool
1513 */
1514 public static function is_amazon_creators_configured( $lasso_settings = false ) {
1515 if ( ! is_array( $lasso_settings ) ) {
1516 $lasso_settings = Setting::get_settings();
1517 }
1518
1519 $fields = array(
1520 'amazon_creators_credential_id',
1521 'amazon_creators_secret',
1522 'amazon_creators_version',
1523 'amazon_creators_partner_tag',
1524 );
1525
1526 foreach ( $fields as $field ) {
1527 if ( '' === trim( (string) ( $lasso_settings[ $field ] ?? '' ) ) ) {
1528 return false;
1529 }
1530 }
1531
1532 return true;
1533 }
1534
1535 /**
1536 * Whether Creators credentials were verified successfully (signature matches stored settings).
1537 *
1538 * @param array|false $lasso_settings Settings array. Default false loads current settings.
1539 * @return bool
1540 */
1541 public static function is_amazon_creators_verified( $lasso_settings = false ) {
1542 if ( ! self::is_amazon_creators_configured( $lasso_settings ) ) {
1543 return false;
1544 }
1545
1546 if ( ! is_array( $lasso_settings ) ) {
1547 $lasso_settings = Setting::get_settings();
1548 }
1549
1550 $current_signature = self::get_amazon_creators_signature( $lasso_settings );
1551 $saved_signature = (string) Helper::get_option( Constant::LASSO_OPTION_AMAZON_CREATORS_VERIFIED_SIGNATURE, '' );
1552
1553 return '' !== $current_signature && '' !== $saved_signature && hash_equals( $saved_signature, $current_signature );
1554 }
1555
1556 /**
1557 * Build a stable signature for the Creators payload.
1558 *
1559 * @param array $settings Settings values.
1560 * @return string Empty when Creators credentials are incomplete.
1561 */
1562 public static function get_amazon_creators_signature( $settings ) {
1563 if ( ! self::is_amazon_creators_configured( $settings ) ) {
1564 return '';
1565 }
1566
1567 return hash(
1568 'sha256',
1569 wp_json_encode(
1570 array(
1571 'credential_id' => (string) ( $settings['amazon_creators_credential_id'] ?? '' ),
1572 'secret' => (string) ( $settings['amazon_creators_secret'] ?? '' ),
1573 'credential_version' => (string) ( $settings['amazon_creators_version'] ?? '' ),
1574 'partner_tag' => (string) ( $settings['amazon_creators_partner_tag'] ?? '' ),
1575 'country' => (string) ( $settings['amazon_default_tracking_country'] ?? '' ),
1576 )
1577 )
1578 );
1579 }
1580
1581 /**
1582 * Lite account token for lasso.link (md5 of linked account email).
1583 *
1584 * @return string Empty when Lite account email is not linked.
1585 */
1586 private static function get_lite_account_token() {
1587 $email = Helper::get_option( Constant::LASSO_ACCOUNT_EMAIL, '' );
1588 if ( ! is_string( $email ) || '' === trim( $email ) ) {
1589 return '';
1590 }
1591
1592 return md5( strtolower( trim( $email ) ) );
1593 }
1594
1595 /**
1596 * Fetch product data via lasso.link Creators API proxy (replaces deprecated PA-API GetItems).
1597 *
1598 * @param string $product_id Amazon ASIN.
1599 * @param bool $store_product Store in local DB.
1600 * @param bool|string $updated_at Optional updated timestamp.
1601 * @param string $amz_link Amazon URL.
1602 * @param array $lasso_settings Plugin settings.
1603 * @return array|null Same shape as fetch_product_info success/failure, or null to fall back.
1604 */
1605 private function fetch_product_info_from_creators_api( $product_id, $store_product, $updated_at, $amz_link, $lasso_settings ) {
1606 $token = self::get_lite_account_token();
1607 if ( '' === $token ) {
1608 return null;
1609 }
1610
1611 $headers = Helper::get_headers();
1612 $headers['token'] = $token;
1613
1614 $payload = array(
1615 'country' => $lasso_settings['amazon_default_tracking_country'] ?? '',
1616 'asin' => $product_id,
1617 'url' => $amz_link,
1618 );
1619
1620 $response = Helper::send_request(
1621 'post',
1622 rtrim( Constant::LASSO_LINK, '/' ) . '/amazon/creators/product',
1623 $payload,
1624 $headers
1625 );
1626
1627 $status_code = intval( $response['status_code'] ?? 500 );
1628 $response_body = $response['response'] ?? null;
1629
1630 if ( empty( $response_body ) ) {
1631 return null;
1632 }
1633
1634 if ( ! empty( $response_body->error_code ) && 'CredentialsNotFound' === $response_body->error_code ) {
1635 return null;
1636 }
1637
1638 $fetch_status = isset( $response_body->status ) ? (string) $response_body->status : '';
1639 $error_code = isset( $response_body->error_code ) ? (string) $response_body->error_code : '';
1640 $api_flag = isset( $response_body->api ) ? (string) $response_body->api : 'yes';
1641 $product_data = isset( $response_body->product ) ? json_decode( wp_json_encode( $response_body->product ), true ) : array();
1642 $product_data = is_array( $product_data ) ? $product_data : array();
1643 $full_item = isset( $response_body->full_item ) ? $response_body->full_item : array();
1644
1645 if ( 'failed' === $fetch_status || 'NotFound' === $error_code ) {
1646 return $this->build_fetch_product_info_response( array(), $api_flag, $full_item, 'failed', $error_code );
1647 }
1648
1649 if ( $status_code >= 400 || empty( $response_body->result ) || empty( $product_data ) ) {
1650 return null;
1651 }
1652
1653 $product = $this->finalize_fetch_product_info_product(
1654 $product_data,
1655 $product_id,
1656 $store_product,
1657 $updated_at,
1658 $amz_link
1659 );
1660
1661 return $this->build_fetch_product_info_response( $product, $api_flag, $full_item, 'success', '' );
1662 }
1663
1664 /**
1665 * Build fetch_product_info() return array (same contract as get_product_by_id_v5 path).
1666 *
1667 * @param array $product Product fields.
1668 * @param string $api API source flag.
1669 * @param array|object $full_item Raw item payload.
1670 * @param string $status success|failed.
1671 * @param string $error_code Error code.
1672 * @return array
1673 */
1674 private function build_fetch_product_info_response( $product, $api, $full_item, $status, $error_code ) {
1675 return array(
1676 'product' => $product,
1677 'api' => $api,
1678 'full_item' => $full_item,
1679 'status' => $status,
1680 'error_code' => $error_code,
1681 );
1682 }
1683
1684 /**
1685 * Apply the same post-processing as the PA-API branch in fetch_product_info().
1686 *
1687 * @param array $product Product fields from API.
1688 * @param string $product_id Amazon ASIN.
1689 * @param bool $store_product Store in local DB.
1690 * @param bool|string $updated_at Optional updated timestamp.
1691 * @param string $amz_link Amazon URL.
1692 * @return array
1693 */
1694 private function finalize_fetch_product_info_product( $product, $product_id, $store_product, $updated_at, $amz_link ) {
1695 if ( empty( $product['product_id'] ) ) {
1696 $product['product_id'] = $product_id;
1697 }
1698
1699 return $this->enrich_fetched_amazon_product( $product, $store_product, $updated_at, $amz_link );
1700 }
1701
1702 /**
1703 * Shared post-fetch enrichment for PA-API and Creators product payloads.
1704 *
1705 * @param array $product Product fields.
1706 * @param bool $store_product Store in local DB.
1707 * @param bool|string $updated_at Optional updated timestamp.
1708 * @param string $amz_link Amazon URL.
1709 * @return array
1710 */
1711 private function enrich_fetched_amazon_product( $product, $store_product, $updated_at, $amz_link ) {
1712 global $wpdb;
1713
1714 $lasso_db = new Lasso_DB();
1715 $product_url = $amz_link ? $amz_link : ( $product['url'] ?? '' );
1716 $product['status_code'] = 200;
1717 $product['url'] = self::get_amazon_product_url( $product_url );
1718
1719 $amazon_product_id = (string) ( $product['product_id'] ?? '' );
1720 $query = $wpdb->prepare(
1721 'SELECT post_id FROM ' . $lasso_db->postmeta . " WHERE meta_key = 'amazon_product_id' AND meta_value = %s",
1722 $amazon_product_id
1723 );
1724 $lasso_id = $lasso_db->get_var( $query );
1725 $product['lasso_id'] = ( isset( $lasso_id ) ) ? $lasso_id : 0;
1726
1727 if ( $store_product ) {
1728 $amazon_tracking_id = Setting::get_setting( 'amazon_tracking_id', '' );
1729 $product['default_url'] = '' === $amazon_tracking_id ? $amz_link : ( $product['default_url'] ?? '' );
1730 $this->update_amazon_product_in_db( $product, $updated_at );
1731 }
1732
1733 return $product;
1734 }
1735
1736 /**
1737 * Get final url of the amazon shortlink from cache
1738 *
1739 * @param string $shortlink Amazon shortlink.
1740 * @return string|null
1741 */
1742 public static function get_shortlink_final_url_cached( $shortlink ) {
1743 if ( ! self::is_amazon_shortened_url( $shortlink ) ) {
1744 return null;
1745 }
1746 $final_url_cache = get_option( self::build_shortlink_cache_key( $shortlink ) );
1747
1748 if ( $final_url_cache === $shortlink ) {
1749 return null;
1750 }
1751
1752 return $final_url_cache ? $final_url_cache : null;
1753 }
1754
1755 /**
1756 * Build amazon shortlink cache key
1757 *
1758 * @param string $shortlink Amazon shortlink.
1759 * @return string|null
1760 */
1761 public static function build_shortlink_cache_key( $shortlink ) {
1762 if ( ! self::is_amazon_shortened_url( $shortlink ) ) {
1763 return null;
1764 }
1765
1766 $parse = wp_parse_url( $shortlink );
1767 $host = str_replace( '.', '_', $parse['host'] );
1768 $id = trim( $parse['path'] ?? '', '/' );
1769
1770 return $host . '_' . $id;
1771 }
1772
1773 /**
1774 * Format Amazon URLs
1775 *
1776 * @param string $url Amazon product url.
1777 * @return string URL.
1778 */
1779 public static function format_amazon_url( $url ) {
1780 $is_amazon_link = self::is_amazon_url( $url );
1781 $product_id = self::get_product_id_by_url( $url );
1782
1783 if ( $is_amazon_link && $product_id && strpos( $url, 'smile.amazon.' ) !== false ) {
1784 $url = str_replace( 'smile.amazon.', 'amazon.', $url );
1785 }
1786
1787 return $url;
1788 }
1789 }
1790