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

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