1<?php 2 3namespace googleshopping; 4 5use \googleshopping\traits\StoreLoader; 6use \googleshopping\exception\Connection as ConnectionException; 7use \googleshopping\exception\AccessForbidden as AccessForbiddenException; 8 9class Googleshopping extends Library { 10 use StoreLoader; 11 12 const API_URL = 'https://campaigns.opencart.com/'; 13 const CACHE_CAMPAIGN_REPORT = 21600; // In seconds 14 const CACHE_PRODUCT_REPORT = 21600; // In seconds 15 const ROAS_WAIT_INTERVAL = 1209600; // In seconds 16 const MICROAMOUNT = 1000000; 17 const DEBUG_LOG_FILENAME = 'googleshopping.%s.log'; 18 const ENDPOINT_ACCESS_TOKEN = 'api/access_token'; 19 const ENDPOINT_ACCESS_TOKEN_TEST = 'api/access_token/test'; 20 const ENDPOINT_CAMPAIGN_DELETE = 'api/campaign/delete'; 21 const ENDPOINT_CAMPAIGN_STATUS = 'api/campaign/status'; 22 const ENDPOINT_CAMPAIGN_TEST = 'api/campaign/test'; 23 const ENDPOINT_CAMPAIGN_UPDATE = 'api/campaign/update'; 24 const ENDPOINT_CONVERSION_TRACKER = 'api/conversion_tracker'; 25 const ENDPOINT_DATAFEED_CLOSE = 'api/datafeed/close'; 26 const ENDPOINT_DATAFEED_INIT = 'api/datafeed/init'; 27 const ENDPOINT_DATAFEED_PUSH = 'api/datafeed/push'; 28 const ENDPOINT_MERCHANT_AUTH_URL = 'api/merchant/authorize_url'; 29 const ENDPOINT_MERCHANT_AVAILABLE_CARRIERS = 'api/merchant/available_carriers'; 30 const ENDPOINT_MERCHANT_DISCONNECT = 'api/merchant/disconnect'; 31 const ENDPOINT_MERCHANT_PRODUCT_STATUSES = 'api/merchant/product_statuses'; 32 const ENDPOINT_MERCHANT_SHIPPING_TAXES = 'api/merchant/shipping_taxes'; 33 const ENDPOINT_REPORT_AD = 'api/report/ad&interval=%s'; 34 const ENDPOINT_REPORT_CAMPAIGN = 'api/report/campaign&interval=%s'; 35 const ENDPOINT_VERIFY_IS_CLAIMED = 'api/verify/is_claimed'; 36 const ENDPOINT_VERIFY_SITE = 'api/verify/site'; 37 const ENDPOINT_VERIFY_TOKEN = 'api/verify/token'; 38 const SCOPES = 'OC_FEED REPORT ADVERTISE'; 39 40 private $event_snippet; 41 private $purchase_data; 42 private $store_url; 43 private $store_name; 44 private $endpoint_url; 45 private $store_id = 0; 46 private $debug_log; 47 48 public function __construct($registry, $store_id) { 49 parent::__construct($registry); 50 51 $this->store_id = $store_id; 52 53 $this->load->model('setting/setting'); 54 55 if ($this->store_id === 0) { 56 $this->store_url = basename(DIR_TEMPLATE) == 'template' ? HTTPS_CATALOG : HTTPS_SERVER; 57 $this->store_name = $this->config->get('config_name'); 58 } else { 59 $this->store_url = $this->model_setting_setting->getSettingValue('config_ssl', $store_id); 60 $this->store_name = $this->model_setting_setting->getSettingValue('config_name', $store_id); 61 } 62 63 $this->endpoint_url = self::API_URL . 'index.php?route=%s'; 64 65 $this->loadStore($this->store_id); 66 67 $this->debug_log = new Log(sprintf(self::DEBUG_LOG_FILENAME, $this->store_id)); 68 } 69 70 public function getStoreUrl() { 71 return $this->store_url; 72 } 73 74 public function getStoreName() { 75 return $this->store_name; 76 } 77 78 public function getSupportedLanguageId($code) { 79 $this->load->model('localisation/language'); 80 81 foreach ($this->model_localisation_language->getLanguages() as $language) { 82 $language_code = current(explode("-", $language['code'])); 83 84 if ($this->compareTrimmedLowercase($code, $language_code) === 0) { 85 return (int)$language['language_id']; 86 } 87 } 88 89 return 0; 90 } 91 92 public function getSupportedCurrencyId($code) { 93 $this->load->model('localisation/currency'); 94 95 foreach ($this->model_localisation_currency->getCurrencies() as $currency) { 96 if ($this->compareTrimmedLowercase($code, $currency['code']) === 0) { 97 return (int)$currency['currency_id']; 98 } 99 } 100 101 return 0; 102 } 103 104 public function getCountryName($code) { 105 $this->load->config('googleshopping/googleshopping'); 106 107 $this->load->model('localisation/country'); 108 109 $countries = $this->config->get('advertise_google_countries'); 110 111 // Default value 112 $result = $countries[$code]; 113 114 // Override with store value, if present 115 foreach ($this->model_localisation_country->getCountries() as $store_country) { 116 if ($this->compareTrimmedLowercase($store_country['iso_code_2'], $code) === 0) { 117 $result = $store_country['name']; 118 break; 119 } 120 } 121 122 return $result; 123 } 124 125 public function compareTrimmedLowercase($text1, $text2) { 126 return strcmp(strtolower(trim($text1)), strtolower(trim($text2))); 127 } 128 129 public function getTargets($store_id) { 130 $sql = "SELECT * FROM `" . DB_PREFIX . "googleshopping_target` WHERE store_id=" . $store_id; 131 132 return array_map(array($this, 'target'), $this->db->query($sql)->rows); 133 } 134 135 public function getTarget($advertise_google_target_id) { 136 $sql = "SELECT * FROM `" . DB_PREFIX . "googleshopping_target` WHERE advertise_google_target_id=" . (int)$advertise_google_target_id; 137 138 return $this->target($this->db->query($sql)->row); 139 } 140 141 public function editTarget($target_id, $target) { 142 $sql = "UPDATE `" . DB_PREFIX . "googleshopping_target` SET `campaign_name`='" . $this->db->escape($target['campaign_name']) . "', `country`='" . $this->db->escape($target['country']) . "', `budget`='" . (float)$target['budget'] . "', `feeds`='" . $this->db->escape(json_encode($target['feeds'])) . "', `roas`='" . (int)$target['roas'] . "', `status`='" . $this->db->escape($target['status']) . "' WHERE `advertise_google_target_id`='" . (int)$target_id . "'"; 143 144 $this->db->query($sql); 145 146 return $target; 147 } 148 149 public function deleteTarget($target_id) { 150 $sql = "DELETE FROM `" . DB_PREFIX . "googleshopping_target` WHERE `advertise_google_target_id`='" . (int)$target_id . "'"; 151 152 $this->db->query($sql); 153 154 $sql = "DELETE FROM `" . DB_PREFIX . "googleshopping_product_target` WHERE `advertise_google_target_id`='" . (int)$target_id . "'"; 155 156 $this->db->query($sql); 157 158 return true; 159 } 160 161 public function doJob($job) { 162 $product_count = 0; 163 164 // Initialize push 165 $init_request = array( 166 'type' => 'POST', 167 'endpoint' => self::ENDPOINT_DATAFEED_INIT, 168 'use_access_token' => true, 169 'content_type' => 'multipart/form-data', 170 'data' => array( 171 'work_id' => $job['work_id'] 172 ) 173 ); 174 175 $response = $this->api($init_request); 176 177 // At this point, the job has been initialized and we can start pushing the datafeed 178 $page = 0; 179 180 while (null !== $products = $this->getFeedProducts(++$page, $job['language_id'], $job['currency'])) { 181 $post = array(); 182 183 $post_data = array( 184 'product' => $products, 185 'work_id' => $job['work_id'], 186 'work_step' => $response['work_step'] 187 ); 188 189 $this->curlPostQuery($post_data, $post); 190 191 $push_request = array( 192 'type' => 'POST', 193 'endpoint' => self::ENDPOINT_DATAFEED_PUSH, 194 'use_access_token' => true, 195 'content_type' => 'multipart/form-data', 196 'data' => $post 197 ); 198 199 $response = $this->api($push_request); 200 201 $product_count += count($products); 202 } 203 204 // Finally, close the file to finish the job 205 $close_request = array( 206 'type' => 'POST', 207 'endpoint' => self::ENDPOINT_DATAFEED_CLOSE, 208 'use_access_token' => true, 209 'content_type' => 'multipart/form-data', 210 'data' => array( 211 'work_id' => $job['work_id'], 212 'work_step' => $response['work_step'] 213 ) 214 ); 215 216 $this->api($close_request); 217 218 return $product_count; 219 } 220 221 public function getProductVariationIds($page) { 222 $this->load->config('googleshopping/googleshopping'); 223 224 $sql = "SELECT DISTINCT pag.product_id, pag.color, pag.size FROM `" . DB_PREFIX . "googleshopping_product` pag LEFT JOIN `" . DB_PREFIX . "product` p ON (p.product_id = pag.product_id) LEFT JOIN `" . DB_PREFIX . "product_to_store` p2s ON (p2s.product_id = p.product_id AND p2s.store_id=" . (int)$this->store_id . ") WHERE p2s.store_id IS NOT NULL AND p.status = 1 AND p.date_available <= NOW() AND p.price > 0 ORDER BY p.product_id ASC LIMIT " . (int)(($page - 1) * $this->config->get('advertise_google_report_limit')) . ', ' . (int)$this->config->get('advertise_google_report_limit'); 225 226 $result = array(); 227 228 $this->load->model('localisation/language'); 229 230 foreach ($this->db->query($sql)->rows as $row) { 231 foreach ($this->model_localisation_language->getLanguages() as $language) { 232 $groups = $this->getGroups($row['product_id'], $language['language_id'], $row['color'], $row['size']); 233 234 foreach (array_keys($groups) as $id) { 235 if (!in_array($id, $result)) { 236 $result[] = $id; 237 } 238 } 239 } 240 } 241 242 return !empty($result) ? $result : null; 243 } 244 245 // A copy of the OpenCart SEO URL rewrite method. 246 public function rewrite($link) { 247 $url_info = parse_url(str_replace('&', '&', $link)); 248 249 $url = ''; 250 251 $data = array(); 252 253 parse_str($url_info['query'], $data); 254 255 foreach ($data as $key => $value) { 256 if (isset($data['route'])) { 257 if (($data['route'] == 'product/product' && $key == 'product_id') || (($data['route'] == 'product/manufacturer/info' || $data['route'] == 'product/product') && $key == 'manufacturer_id') || ($data['route'] == 'information/information' && $key == 'information_id')) { 258 $query = $this->db->query("SELECT * FROM " . DB_PREFIX . "seo_url WHERE `query` = '" . $this->db->escape($key . '=' . (int)$value) . "' AND store_id = '" . (int)$this->config->get('config_store_id') . "' AND language_id = '" . (int)$this->config->get('config_language_id') . "'"); 259 260 if ($query->num_rows && $query->row['keyword']) { 261 $url .= '/' . $query->row['keyword']; 262 263 unset($data[$key]); 264 } 265 } elseif ($key == 'path') { 266 $categories = explode('_', $value); 267 268 foreach ($categories as $category) { 269 $query = $this->db->query("SELECT * FROM " . DB_PREFIX . "seo_url WHERE `query` = 'category_id=" . (int)$category . "' AND store_id = '" . (int)$this->config->get('config_store_id') . "' AND language_id = '" . (int)$this->config->get('config_language_id') . "'"); 270 271 if ($query->num_rows && $query->row['keyword']) { 272 $url .= '/' . $query->row['keyword']; 273 } else { 274 $url = ''; 275 276 break; 277 } 278 } 279 280 unset($data[$key]); 281 } 282 } 283 } 284 285 if ($url) { 286 unset($data['route']); 287 288 $query = ''; 289 290 if ($data) { 291 foreach ($data as $key => $value) { 292 $query .= '&' . rawurlencode((string)$key) . '=' . rawurlencode((is_array($value) ? http_build_query($value) : (string)$value)); 293 } 294 295 if ($query) { 296 $query = '?' . str_replace('&', '&', trim($query, '&')); 297 } 298 } 299 300 return $url_info['scheme'] . '://' . $url_info['host'] . (isset($url_info['port']) ? ':' . $url_info['port'] : '') . str_replace('/index.php', '', $url_info['path']) . $url . $query; 301 } else { 302 return $link; 303 } 304 } 305 306 protected function convertedTaxedPrice($value, $tax_class_id, $currency) { 307 return number_format($this->currency->convert($this->tax->calculate($value, $tax_class_id, $this->config->get('config_tax')), $this->config->get('config_currency'), $currency), 2, '.', ''); 308 } 309 310 protected function getFeedProducts($page, $language_id, $currency) { 311 $sql = $this->getFeedProductsQuery($page, $language_id); 312 313 $result = array(); 314 315 $this->setRuntimeExceptionErrorHandler(); 316 317 foreach ($this->db->query($sql)->rows as $row) { 318 try { 319 if (!empty($row['image']) && is_file(DIR_IMAGE . $row['image']) && is_readable(DIR_IMAGE . $row['image'])) { 320 $image = $this->resize($row['image'], 250, 250); 321 } else { 322 throw new \RuntimeException("Image does not exist or cannot be read."); 323 } 324 } catch (\RuntimeException $e) { 325 $this->output(sprintf("Error for product %s: %s", $row['model'], $e->getMessage())); 326 327 $image = $this->resize('no_image.png', 250, 250); 328 } 329 330 $url = new \Url($this->store_url, $this->store_url); 331 332 if ($this->config->get('config_seo_url')) { 333 $url->addRewrite($this); 334 } 335 336 $price = $this->convertedTaxedPrice($row['price'], $row['tax_class_id'], $currency); 337 338 $special_price = null; 339 340 if ($row['special_price'] !== null) { 341 $parts = explode('<[S]>', $row['special_price']); 342 343 $special_price = array( 344 'value' => $this->convertedTaxedPrice($parts[0], $row['tax_class_id'], $currency), 345 'currency' => $currency 346 ); 347 348 if ($parts[1] >= '1970-01-01') { 349 $special_price['start'] = $parts[1]; 350 } 351 352 if ($parts[2] >= '1970-01-01') { 353 $special_price['end'] = $parts[2]; 354 } 355 } 356 357 $campaigns = array(); 358 $custom_label_0 = ''; 359 $custom_label_1 = ''; 360 $custom_label_2 = ''; 361 $custom_label_3 = ''; 362 $custom_label_4 = ''; 363 364 if (!empty($row['campaign_names'])) { 365 $campaigns = explode('<[S]>', $row['campaign_names']); 366 $i = 0; 367 368 do { 369 ${'custom_label_' . ($i++)} = trim(strtolower(array_pop($campaigns))); 370 } while (!empty($campaigns)); 371 } 372 373 $mpn = !empty($row['mpn']) ? $row['mpn'] : ''; 374 375 if (!empty($row['upc'])) { 376 $gtin = $row['upc']; 377 } else if (!empty($row['ean'])) { 378 $gtin = $row['ean']; 379 } else if (!empty($row['jan'])) { 380 $gtin = $row['jan']; 381 } else if (!empty($row['isbn'])) { 382 $gtin = $row['isbn']; 383 } else { 384 $gtin = ''; 385 } 386 387 $base_row = array( 388 'adult' => !empty($row['adult']) ? 'yes' : 'no', 389 'age_group' => !empty($row['age_group']) ? $row['age_group'] : '', 390 'availability' => (int)$row['quantity'] > 0 && !$this->config->get('config_maintenance') ? 'in stock' : 'out of stock', 391 'brand' => $this->sanitizeText($row['brand'], 70), 392 'color' => '', 393 'condition' => !empty($row['condition']) ? $row['condition'] : '', 394 'custom_label_0' => $this->sanitizeText($custom_label_0, 100), 395 'custom_label_1' => $this->sanitizeText($custom_label_1, 100), 396 'custom_label_2' => $this->sanitizeText($custom_label_2, 100), 397 'custom_label_3' => $this->sanitizeText($custom_label_3, 100), 398 'custom_label_4' => $this->sanitizeText($custom_label_4, 100), 399 'description' => $this->sanitizeText($row['description'], 5000), 400 'gender' => !empty($row['gender']) ? $row['gender'] : '', 401 'google_product_category' => !empty($row['google_product_category']) ? $row['google_product_category'] : '', 402 'id' => $this->sanitizeText($row['product_id'], 50), 403 'identifier_exists' => !empty($row['brand']) && !empty($mpn) ? 'yes' : 'no', 404 'image_link' => $this->sanitizeText($image, 2000), 405 'is_bundle' => !empty($row['is_bundle']) ? 'yes' : 'no', 406 'item_group_id' => $this->sanitizeText($row['product_id'], 50), 407 'link' => $this->sanitizeText(html_entity_decode($url->link('product/product', 'product_id=' . $row['product_id'], true), ENT_QUOTES, 'UTF-8'), 2000), 408 'mpn' => $this->sanitizeText($mpn, 70), 409 'gtin' => $this->sanitizeText($gtin, 14), 410 'multipack' => !empty($row['multipack']) && (int)$row['multipack'] >= 2 ? (int)$row['multipack'] : '', // Cannot be 1!!! 411 'price' => array( 412 'value' => $price, 413 'currency' => $currency 414 ), 415 'size' => '', 416 'size_system' => !empty($row['size_system']) ? $row['size_system'] : '', 417 'size_type' => !empty($row['size_type']) ? $row['size_type'] : '', 418 'title' => $this->sanitizeText($row['name'], 150) 419 ); 420 421 // Provide optional special price 422 if ($special_price !== null) { 423 $base_row['special_price'] = $special_price; 424 } 425 426 $groups = $this->getGroups($row['product_id'], $language_id, $row['color'], $row['size']); 427 428 foreach ($groups as $id => $group) { 429 $base_row['id'] = $id; 430 $base_row['color'] = $this->sanitizeText($group['color'], 40); 431 $base_row['size'] = $this->sanitizeText($group['size'], 100); 432 433 $result[] = $base_row; 434 } 435 } 436 437 $this->restoreErrorHandler(); 438 439 return !empty($result) ? $result : null; 440 } 441 442 public function getGroups($product_id, $language_id, $color_id, $size_id) { 443 $options = array( 444 'color' => $this->getProductOptionValueNames($product_id, $language_id, $color_id), 445 'size' => $this->getProductOptionValueNames($product_id, $language_id, $size_id) 446 ); 447 448 $result = array(); 449 450 foreach ($this->combineOptions($options) as $group) { 451 $key = $product_id . '-' . md5(json_encode(array('color' => $group['color'], 'size' => $group['size']))); 452 453 $result[$key] = $group; 454 } 455 456 return $result; 457 } 458 459 public function getProductOptionValueNames($product_id, $language_id, $option_id) { 460 $sql = "SELECT DISTINCT pov.product_option_value_id, ovd.name FROM `" . DB_PREFIX . "product_option_value` pov LEFT JOIN `" . DB_PREFIX . "option_value_description` ovd ON (ovd.option_value_id = pov.option_value_id) WHERE pov.product_id=" . (int)$product_id . " AND pov.option_id=" . (int)$option_id . " AND ovd.language_id=" . (int)$language_id; 461 462 $result = $this->db->query($sql); 463 464 if ($result->num_rows > 0) { 465 $return = array(); 466 467 foreach ($result->rows as $row) { 468 $text = $this->sanitizeText($row['name'], 100); 469 $name = implode('/', array_slice(array_filter(array_map('trim', preg_split('~[,/;]+~i', $text))), 0, 3)); 470 471 $return[$row['product_option_value_id']] = $name; 472 } 473 474 return $return; 475 } 476 477 return array(''); 478 } 479 480 public function applyFilter(&$sql, &$data) { 481 if (!empty($data['filter_product_name'])) { 482 $sql .= " AND pd.name LIKE '" . $this->db->escape($data['filter_product_name']) . "%'"; 483 } 484 485 if (!empty($data['filter_product_model'])) { 486 $sql .= " AND p.model LIKE '" . $this->db->escape($data['filter_product_model']) . "%'"; 487 } 488 489 if (!empty($data['filter_category_id'])) { 490 $sql .= " AND p.product_id IN (SELECT p2c_t.product_id FROM `" . DB_PREFIX . "category_path` cp_t LEFT JOIN `" . DB_PREFIX . "product_to_category` p2c_t ON (p2c_t.category_id=cp_t.category_id) WHERE cp_t.path_id=" . (int)$data['filter_category_id'] . ")"; 491 } 492 493 if (isset($data['filter_is_modified']) && $data['filter_is_modified'] !== "") { 494 $sql .= " AND p.product_id IN (SELECT pag_t.product_id FROM `" . DB_PREFIX . "googleshopping_product` pag_t WHERE pag_t.is_modified=" . (int)$data['filter_is_modified'] . ")"; 495 } 496 497 if (!empty($data['filter_store_id'])) { 498 $sql .= " AND p.product_id IN (SELECT p2s_t.product_id FROM `" . DB_PREFIX . "product_to_store` p2s_t WHERE p2s_t.store_id=" . (int)$data['filter_store_id'] . ")"; 499 } 500 } 501 502 public function getProducts($data, $store_id) { 503 $sql = "SELECT pag.*, p.product_id, p.image, pd.name, p.model FROM `" . DB_PREFIX . "product` p LEFT JOIN `" . DB_PREFIX . "product_description` pd ON (p.product_id = pd.product_id) LEFT JOIN `" . DB_PREFIX . "googleshopping_product` pag ON (pag.product_id = p.product_id AND pag.store_id = " . (int)$store_id . ") WHERE pag.store_id IS NOT NULL AND pd.language_id = '" . (int)$this->config->get('config_language_id') . "'"; 504 505 $this->applyFilter($sql, $data); 506 507 $sql .= " GROUP BY p.product_id"; 508 509 $sort_data = array( 510 'name', 511 'model', 512 'impressions', 513 'clicks', 514 'cost', 515 'conversions', 516 'conversion_value', 517 'has_issues', 518 'destination_status' 519 ); 520 521 if (isset($data['sort']) && in_array($data['sort'], $sort_data)) { 522 $sql .= " ORDER BY " . $data['sort']; 523 } else { 524 $sql .= " ORDER BY name"; 525 } 526 527 if (isset($data['order']) && ($data['order'] == 'DESC')) { 528 $sql .= " DESC"; 529 } else { 530 $sql .= " ASC"; 531 } 532 533 if (isset($data['start']) || isset($data['limit'])) { 534 if ($data['start'] < 0) { 535 $data['start'] = 0; 536 } 537 538 if ($data['limit'] < 1) { 539 $data['limit'] = 20; 540 } 541 542 $sql .= " LIMIT " . (int)$data['start'] . "," . (int)$data['limit']; 543 } 544 545 return $this->db->query($sql)->rows; 546 } 547 548 public function getTotalProducts($data, $store_id) { 549 $sql = "SELECT COUNT(*) as total FROM `" . DB_PREFIX . "product` p LEFT JOIN `" . DB_PREFIX . "product_description` pd ON (p.product_id = pd.product_id) LEFT JOIN `" . DB_PREFIX . "googleshopping_product` pag ON (pag.product_id = p.product_id AND pag.store_id = " . (int)$store_id . ") WHERE pag.store_id IS NOT NULL AND pd.language_id = '" . (int)$this->config->get('config_language_id') . "'"; 550 551 $this->applyFilter($sql, $data); 552 553 return (int)$this->db->query($sql)->row['total']; 554 } 555 556 public function getProductIds($data, $store_id) { 557 $result = array(); 558 559 $this->load->model('localisation/language'); 560 561 foreach ($this->getProducts($data, $store_id) as $row) { 562 $product_id = (int)$row['product_id']; 563 564 if (!in_array($product_id, $result)) { 565 $result[] = $product_id; 566 } 567 } 568 569 return $result; 570 } 571 572 public function clearProductStatuses($product_ids, $store_id) { 573 $sql = "UPDATE `" . DB_PREFIX . "googleshopping_product_status` SET `destination_statuses`='', `data_quality_issues`='', `item_level_issues`='', `google_expiration_date`=0 WHERE `product_id` IN (" . $this->productIdsToIntegerExpression($product_ids) . ") AND `store_id`=" . (int)$store_id; 574 575 $this->db->query($sql); 576 577 $sql = "UPDATE `" . DB_PREFIX . "googleshopping_product` SET `has_issues`=0, `destination_status`='pending' WHERE `product_id` IN (" . $this->productIdsToIntegerExpression($product_ids) . ") AND `store_id`=" . (int)$store_id; 578 579 $this->db->query($sql); 580 } 581 582 public function productIdsToIntegerExpression($product_ids) { 583 return implode(",", array_map(array($this, 'integer'), $product_ids)); 584 } 585 586 public function integer(&$product_id) { 587 if (!is_numeric($product_id)) { 588 return 0; 589 } else { 590 return (int)$product_id; 591 } 592 } 593 594 public function cron() { 595 $this->enableErrorReporting(); 596 597 $this->load->config('googleshopping/googleshopping'); 598 599 $report = array(); 600 601 $report[] = $this->output("Starting CRON task for " . $this->getStoreUrl()); 602 603 try { 604 $report[] = $this->output("Refreshing access token."); 605 606 $this->isConnected(); 607 } catch (\RuntimeException $e) { 608 $report[] = $this->output($e->getMessage()); 609 } 610 611 $default_config_tax = $this->config->get("config_tax"); 612 $default_config_store_id = $this->config->get("config_store_id"); 613 $default_config_language_id = $this->config->get("config_language_id"); 614 $default_config_seo_url = $this->config->get("config_seo_url"); 615 616 // Do product feed uploads 617 foreach ($this->getJobs() as $job) { 618 try { 619 $report[] = $this->output("Uploading product feed. Work ID: " . $job['work_id']); 620 621 // Set the tax context for the job 622 if (in_array("US", $job['countries'])) { 623 // In case the feed is for the US, disable taxes because they are already configured on the merchant level by the extension 624 $this->config->set("config_tax", 0); 625 } 626 627 // Set the store and language context for the job 628 $this->config->set("config_store_id", $this->store_id); 629 $this->config->set("config_language_id", $job['language_id']); 630 $this->config->set("config_seo_url", $this->model_setting_setting->getSettingValue("config_seo_url", $this->store_id)); 631 632 // Do the CRON job 633 $count = $this->doJob($job); 634 635 // Reset the taxes, store, and language to their original state 636 $this->config->set("config_tax", $default_config_tax); 637 $this->config->set("config_store_id", $default_config_store_id); 638 $this->config->set("config_language_id", $default_config_language_id); 639 $this->config->set("config_seo_url", $default_config_seo_url); 640 641 $report[] = $this->output("Uploaded count: " . $count); 642 } catch (\RuntimeException $e) { 643 $report[] = $this->output($e->getMessage()); 644 } 645 } 646 647 // Reset the taxes, store, and language to their original state 648 $this->config->set("config_tax", $default_config_tax); 649 $this->config->set("config_store_id", $default_config_store_id); 650 $this->config->set("config_language_id", $default_config_language_id); 651 $this->config->set("config_seo_url", $default_config_seo_url); 652 653 // Pull product reports 654 $report[] = $this->output("Fetching product reports."); 655 656 try { 657 $report_count = 0; 658 659 $page = 0; 660 661 $this->clearReports(); 662 663 while (null !== $product_variation_ids = $this->getProductVariationIds(++$page)) { 664 foreach (array_chunk($product_variation_ids, (int)$this->config->get('advertise_google_report_limit')) as $chunk) { 665 $product_reports = $this->getProductReports($chunk); 666 667 if (!empty($product_reports)) { 668 $this->updateProductReports($product_reports, $this->store_id); 669 $report_count += count($product_reports); 670 } 671 } 672 } 673 } catch (\RuntimeException $e) { 674 $report[] = $this->output($e->getMessage()); 675 } 676 677 $report[] = $this->output("Fetched report count: " . $report_count); 678 679 // Pull product statuses 680 $report[] = $this->output("Fetching product statuses."); 681 682 $page = 1; 683 $status_count = 0; 684 685 do { 686 $filter_data = array( 687 'start' => ($page - 1) * $this->config->get('advertise_google_product_status_limit'), 688 'limit' => $this->config->get('advertise_google_product_status_limit') 689 ); 690 691 $page++; 692 693 $product_variation_target_specific_ids = $this->getProductVariationTargetSpecificIds($filter_data); 694 695 try { 696 // Fetch latest statuses from the API 697 if (!empty($product_variation_target_specific_ids)) { 698 $product_ids = $this->getProductIds($filter_data, $this->store_id); 699 700 $this->clearProductStatuses($product_ids, $this->store_id); 701 702 foreach (array_chunk($product_variation_target_specific_ids, (int)$this->config->get('advertise_google_product_status_limit')) as $chunk) { 703 $product_statuses = $this->getProductStatuses($chunk); 704 705 if (!empty($product_statuses)) { 706 $this->updateProductStatuses($product_statuses); 707 $status_count += count($product_statuses); 708 } 709 } 710 } 711 } catch (\RuntimeException $e) { 712 $report[] = $this->output($e->getMessage()); 713 } 714 } while (!empty($product_variation_target_specific_ids)); 715 716 $report[] = $this->output("Fetched status count: " . $status_count); 717 718 $report[] = $this->output("CRON finished!"); 719 720 $this->applyNewSetting('advertise_google_cron_last_executed', time()); 721 722 $this->sendEmailReport($report); 723 } 724 725 public function getProductVariationTargetSpecificIds($data) { 726 $result = array(); 727 728 $targets = $this->getTargets($this->store_id); 729 730 foreach ($this->getProducts($data, $this->store_id) as $row) { 731 foreach ($targets as $target) { 732 foreach ($target['feeds'] as $feed) { 733 $language_code = $feed['language']; 734 735 $language_id = $this->getSupportedLanguageId($language_code); 736 737 $groups = $this->getGroups($row['product_id'], $language_id, $row['color'], $row['size']); 738 739 foreach (array_keys($groups) as $id) { 740 $id_parts = array(); 741 $id_parts[] = 'online'; 742 $id_parts[] = $language_code; 743 $id_parts[] = $target['country']['code']; 744 $id_parts[] = $id; 745 746 $result_id = implode(':', $id_parts); 747 748 if (!in_array($result_id, $result)) { 749 $result[] = $result_id; 750 } 751 } 752 } 753 } 754 } 755 756 return $result; 757 } 758 759 public function updateProductReports($reports) { 760 $values = array(); 761 762 foreach ($reports as $report) { 763 $entry = array(); 764 $entry['product_id'] = $this->getProductIdFromOfferId($report['offer_id']); 765 $entry['store_id'] = (int)$this->store_id; 766 $entry['impressions'] = (int)$report['impressions']; 767 $entry['clicks'] = (int)$report['clicks']; 768 $entry['conversions'] = (int)$report['conversions']; 769 $entry['cost'] = ((int)$report['cost']) / self::MICROAMOUNT; 770 $entry['conversion_value'] = (float)$report['conversion_value']; 771 772 $values[] = "(" . implode(",", $entry) . ")"; 773 } 774 775 $sql = "INSERT INTO `" . DB_PREFIX . "googleshopping_product` (`product_id`, `store_id`, `impressions`, `clicks`, `conversions`, `cost`, `conversion_value`) VALUES " . implode(',', $values) . " ON DUPLICATE KEY UPDATE `impressions`=`impressions` + VALUES(`impressions`), `clicks`=`clicks` + VALUES(`clicks`), `conversions`=`conversions` + VALUES(`conversions`), `cost`=`cost` + VALUES(`cost`), `conversion_value`=`conversion_value` + VALUES(`conversion_value`)"; 776 777 $this->db->query($sql); 778 } 779 780 public function updateProductStatuses($statuses) { 781 $product_advertise_google = array(); 782 $product_advertise_google_status = array(); 783 $product_level_entries = array(); 784 $entry_statuses = array(); 785 786 foreach ($statuses as $status) { 787 $product_id = $this->getProductIdFromTargetSpecificId($status['productId']); 788 $product_variation_id = $this->getProductVariationIdFromTargetSpecificId($status['productId']); 789 790 if (!isset($product_level_entries[$product_id])) { 791 $product_level_entries[$product_id] = array( 792 'product_id' => (int)$product_id, 793 'store_id' => (int)$this->store_id, 794 'has_issues' => 0, 795 'destination_status' => 'pending' 796 ); 797 } 798 799 foreach ($status['destinationStatuses'] as $destination_status) { 800 if (!$destination_status['approvalPending']) { 801 switch ($destination_status['approvalStatus']) { 802 case 'approved' : 803 if ($product_level_entries[$product_id]['destination_status'] == 'pending') { 804 $product_level_entries[$product_id]['destination_status'] = 'approved'; 805 } 806 break; 807 case 'disapproved' : 808 $product_level_entries[$product_id]['destination_status'] = 'disapproved'; 809 break; 810 } 811 } 812 } 813 814 if (!$product_level_entries[$product_id]['has_issues']) { 815 if (!empty($status['dataQualityIssues']) || !empty($status['itemLevelIssues'])) { 816 $product_level_entries[$product_id]['has_issues'] = 1; 817 } 818 } 819 820 if (!isset($entry_statuses[$product_variation_id])) { 821 $entry_statuses[$product_variation_id] = array(); 822 823 $entry_statuses[$product_variation_id]['product_id'] = (int)$product_id; 824 $entry_statuses[$product_variation_id]['store_id'] = (int)$this->store_id; 825 $entry_statuses[$product_variation_id]['product_variation_id'] = "'" . $this->db->escape($product_variation_id) . "'"; 826 $entry_statuses[$product_variation_id]['destination_statuses'] = array(); 827 $entry_statuses[$product_variation_id]['data_quality_issues'] = array(); 828 $entry_statuses[$product_variation_id]['item_level_issues'] = array(); 829 $entry_statuses[$product_variation_id]['google_expiration_date'] = (int)strtotime($status['googleExpirationDate']); 830 } 831 832 $entry_statuses[$product_variation_id]['destination_statuses'] = array_merge( 833 $entry_statuses[$product_variation_id]['destination_statuses'], 834 !empty($status['destinationStatuses']) ? $status['destinationStatuses'] : array() 835 ); 836 837 $entry_statuses[$product_variation_id]['data_quality_issues'] = array_merge( 838 $entry_statuses[$product_variation_id]['data_quality_issues'], 839 !empty($status['dataQualityIssues']) ? $status['dataQualityIssues'] : array() 840 ); 841 842 $entry_statuses[$product_variation_id]['item_level_issues'] = array_merge( 843 $entry_statuses[$product_variation_id]['item_level_issues'], 844 !empty($status['itemLevelIssues']) ? $status['itemLevelIssues'] : array() 845 ); 846 } 847 848 foreach ($entry_statuses as &$entry_status) { 849 $entry_status['destination_statuses'] = "'" . $this->db->escape(json_encode($entry_status['destination_statuses'])) . "'"; 850 $entry_status['data_quality_issues'] = "'" . $this->db->escape(json_encode($entry_status['data_quality_issues'])) . "'"; 851 $entry_status['item_level_issues'] = "'" . $this->db->escape(json_encode($entry_status['item_level_issues'])) . "'"; 852 853 $product_advertise_google_status[] = "(" . implode(",", $entry_status) . ")"; 854 } 855 856 $sql = "INSERT INTO `" . DB_PREFIX . "googleshopping_product_status` (`product_id`, `store_id`, `product_variation_id`, `destination_statuses`, `data_quality_issues`, `item_level_issues`, `google_expiration_date`) VALUES " . implode(',', $product_advertise_google_status) . " ON DUPLICATE KEY UPDATE `destination_statuses`=VALUES(`destination_statuses`), `data_quality_issues`=VALUES(`data_quality_issues`), `item_level_issues`=VALUES(`item_level_issues`), `google_expiration_date`=VALUES(`google_expiration_date`)"; 857 858 $this->db->query($sql); 859 860 foreach ($product_level_entries as $entry) { 861 $entry['destination_status'] = "'" . $this->db->escape($entry['destination_status']) . "'"; 862 863 $product_advertise_google[] = "(" . implode(",", $entry) . ")"; 864 } 865 866 $sql = "INSERT INTO `" . DB_PREFIX . "googleshopping_product` (`product_id`, `store_id`, `has_issues`, `destination_status`) VALUES " . implode(',', $product_advertise_google) . " ON DUPLICATE KEY UPDATE `has_issues`=VALUES(`has_issues`), `destination_status`=VALUES(`destination_status`)"; 867 868 $this->db->query($sql); 869 } 870 871 protected function memoryLimitInBytes() { 872 $memory_limit = ini_get('memory_limit'); 873 874 if (preg_match('/^(\d+)(.)$/', $memory_limit, $matches)) { 875 if ($matches[2] == 'G') { 876 $memory_limit = (int)$matches[1] * 1024 * 1024 * 1024; // nnnG -> nnn GB 877 } else if ($matches[2] == 'M') { 878 $memory_limit = (int)$matches[1] * 1024 * 1024; // nnnM -> nnn MB 879 } else if ($matches[2] == 'K') { 880 $memory_limit = (int)$matches[1] * 1024; // nnnK -> nnn KB 881 } 882 } 883 884 return (int)$memory_limit; 885 } 886 887 protected function enableErrorReporting() { 888 ini_set('display_errors', 1); 889 ini_set('display_startup_errors', 1); 890 error_reporting(E_ALL); 891 } 892 893 protected function getProductIdFromTargetSpecificId($target_specific_id) { 894 return (int)preg_replace('/^online:[a-z]{2}:[A-Z]{2}:(\d+)-[a-f0-9]{32}$/', '$1', $target_specific_id); 895 } 896 897 protected function getProductVariationIdFromTargetSpecificId($target_specific_id) { 898 return preg_replace('/^online:[a-z]{2}:[A-Z]{2}:(\d+-[a-f0-9]{32})$/', '$1', $target_specific_id); 899 } 900 901 protected function getProductIdFromOfferId($offer_id) { 902 return (int)preg_replace('/^(\d+)-[a-f0-9]{32}$/', '$1', $offer_id); 903 } 904 905 protected function clearReports() { 906 $sql = "UPDATE `" . DB_PREFIX . "googleshopping_product` SET `impressions`=0, `clicks`=0, `conversions`=0, `cost`=0.0000, `conversion_value`=0.0000 WHERE `store_id`=" . (int)$this->store_id; 907 908 $this->db->query($sql); 909 } 910 911 protected function getJobs() { 912 $jobs = array(); 913 914 if ($this->setting->has('advertise_google_work') && is_array($this->setting->get('advertise_google_work'))) { 915 $this->load->model('extension/advertise/google'); 916 917 foreach ($this->setting->get('advertise_google_work') as $work) { 918 $supported_language_id = $this->getSupportedLanguageId($work['language']); 919 $supported_currency_id = $this->getSupportedCurrencyId($work['currency']); 920 921 if (!empty($supported_language_id) && !empty($supported_currency_id)) { 922 $currency_info = $this->getCurrency($supported_currency_id); 923 924 $jobs[] = array( 925 'work_id' => $work['work_id'], 926 'countries' => isset($work['countries']) && is_array($work['countries']) ? $work['countries'] : array(), 927 'language_id' => $supported_language_id, 928 'currency' => $currency_info['code'] 929 ); 930 } 931 } 932 } 933 934 return $jobs; 935 } 936 937 protected function output($message) { 938 $log_message = date('Y-m-d H:i:s - ') . $message; 939 940 if (defined('STDOUT')) { 941 fwrite(STDOUT, $log_message . PHP_EOL); 942 } else { 943 echo $log_message . '<br /><hr />'; 944 } 945 946 return $log_message; 947 } 948 949 protected function sendEmailReport(&$report) { 950 if (!$this->setting->get('advertise_google_cron_email_status')) { 951 return; //Do nothing 952 } 953 954 $this->load->language('extension/advertise/google'); 955 956 $subject = $this->language->get('text_cron_email_subject'); 957 $message = sprintf($this->language->get('text_cron_email_message'), implode('<br/>', $report)); 958 959 $mail = new \Mail(); 960 961 $mail->protocol = $this->config->get('config_mail_protocol'); 962 $mail->parameter = $this->config->get('config_mail_parameter'); 963 964 $mail->smtp_hostname = $this->config->get('config_mail_smtp_hostname'); 965 $mail->smtp_username = $this->config->get('config_mail_smtp_username'); 966 $mail->smtp_password = html_entity_decode($this->config->get('config_mail_smtp_password'), ENT_QUOTES, "UTF-8"); 967 $mail->smtp_port = $this->config->get('config_mail_smtp_port'); 968 $mail->smtp_timeout = $this->config->get('config_mail_smtp_timeout'); 969 970 $mail->setTo($this->setting->get('advertise_google_cron_email')); 971 $mail->setFrom($this->config->get('config_email')); 972 $mail->setSender($this->config->get('config_name')); 973 $mail->setSubject(html_entity_decode($subject, ENT_QUOTES, "UTF-8")); 974 $mail->setText(strip_tags($message)); 975 $mail->setHtml($message); 976 977 $mail->send(); 978 } 979 980 protected function getOptionValueName($row) { 981 $text = $this->sanitizeText($row['name'], 100); 982 983 return implode('/', array_slice(array_filter(array_map('trim', preg_split('~[,/;]+~i', $text))), 0, 3)); 984 } 985 986 protected function combineOptions($arrays) { 987 // Based on: https://gist.github.com/cecilemuller/4688876 988 $result = array(array()); 989 990 foreach ($arrays as $property => $property_values) { 991 $tmp = array(); 992 foreach ($result as $result_item) { 993 foreach ($property_values as $property_value) { 994 $tmp[] = array_merge($result_item, array($property => $property_value)); 995 } 996 } 997 $result = $tmp; 998 } 999 1000 return $result; 1001 } 1002 1003 protected function resize($filename, $width, $height) { 1004 if (!is_file(DIR_IMAGE . $filename) || substr(str_replace('\\', '/', realpath(DIR_IMAGE . $filename)), 0, strlen(DIR_IMAGE)) != str_replace('\\', '/', DIR_IMAGE)) { 1005 throw new \RuntimeException("Invalid image filename: " . DIR_IMAGE . $filename); 1006 } 1007 1008 $extension = pathinfo($filename, PATHINFO_EXTENSION); 1009 1010 $image_old = $filename; 1011 $image_new = 'cache/' . utf8_substr($filename, 0, utf8_strrpos($filename, '.')) . '-' . (int)$width . 'x' . (int)$height . '.' . $extension; 1012 1013 if (!is_file(DIR_IMAGE . $image_new) || (filemtime(DIR_IMAGE . $image_old) > filemtime(DIR_IMAGE . $image_new))) { 1014 list($width_orig, $height_orig, $image_type) = getimagesize(DIR_IMAGE . $image_old); 1015 1016 if ($width_orig * $height_orig * 4 > $this->memoryLimitInBytes() * 0.4) { 1017 throw new \RuntimeException("Image too large, skipping: " . $image_old); 1018 } 1019 1020 if (!in_array($image_type, array(IMAGETYPE_PNG, IMAGETYPE_JPEG, IMAGETYPE_GIF))) { 1021 throw new \RuntimeException("Unexpected image type, skipping: " . $image_old); 1022 } 1023 1024 $path = ''; 1025 1026 $directories = explode('/', dirname($image_new)); 1027 1028 foreach ($directories as $directory) { 1029 $path = $path . '/' . $directory; 1030 1031 if (!is_dir(DIR_IMAGE . $path)) { 1032 @mkdir(DIR_IMAGE . $path, 0777); 1033 } 1034 } 1035 1036 if ($width_orig != $width || $height_orig != $height) { 1037 $image = new \Image(DIR_IMAGE . $image_old); 1038 $image->resize($width, $height); 1039 $image->save(DIR_IMAGE . $image_new); 1040 } else { 1041 copy(DIR_IMAGE . $image_old, DIR_IMAGE . $image_new); 1042 } 1043 } 1044 1045 $image_new = str_replace(array(' ', ','), array('%20', '%2C'), $image_new); // fix bug when attach image on email (gmail.com). it is automatic changing space " " to + 1046 1047 return $this->store_url . 'image/' . $image_new; 1048 } 1049 1050 protected function sanitizeText($text, $limit) { 1051 return utf8_substr( 1052 trim( 1053 preg_replace( 1054 '~\s+~', 1055 ' ', 1056 strip_tags( 1057 html_entity_decode(htmlspecialchars_decode($text, ENT_QUOTES), ENT_QUOTES, 'UTF-8') 1058 ) 1059 ) 1060 ), 1061 0, 1062 $limit 1063 ); 1064 } 1065 1066 protected function setRuntimeExceptionErrorHandler() { 1067 set_error_handler(function($code, $message, $file, $line) { 1068 if (error_reporting() === 0) { 1069 return false; 1070 } 1071 1072 switch ($code) { 1073 case E_NOTICE: 1074 case E_USER_NOTICE: 1075 $error = 'Notice'; 1076 break; 1077 case E_WARNING: 1078 case E_USER_WARNING: 1079 $error = 'Warning'; 1080 break; 1081 case E_ERROR: 1082 case E_USER_ERROR: 1083 $error = 'Fatal Error'; 1084 break; 1085 default: 1086 $error = 'Unknown'; 1087 break; 1088 } 1089 1090 $message = 'PHP ' . $error . ': ' . $message . ' in ' . $file . ' on line ' . $line; 1091 1092 throw new \RuntimeException($message); 1093 }); 1094 } 1095 1096 protected function restoreErrorHandler() { 1097 restore_error_handler(); 1098 } 1099 1100 protected function getFeedProductsQuery($page, $language_id) { 1101 $this->load->config('googleshopping/googleshopping'); 1102 1103 $sql = "SELECT p.product_id, pd.name, pd.description, p.image, p.quantity, p.price, p.mpn, p.ean, p.jan, p.isbn, p.upc, p.model, p.tax_class_id, IFNULL((SELECT m.name FROM `" . DB_PREFIX . "manufacturer` m WHERE m.manufacturer_id = p.manufacturer_id), '') as brand, (SELECT GROUP_CONCAT(agt.campaign_name SEPARATOR '<[S]>') FROM `" . DB_PREFIX . "googleshopping_product_target` pagt LEFT JOIN `" . DB_PREFIX . "googleshopping_target` agt ON (agt.advertise_google_target_id = pagt.advertise_google_target_id) WHERE pagt.product_id = p.product_id AND pagt.store_id = p2s.store_id GROUP BY pagt.product_id) as campaign_names, (SELECT CONCAT_WS('<[S]>', ps.price, ps.date_start, ps.date_end) FROM `" . DB_PREFIX . "product_special` ps WHERE ps.product_id=p.product_id AND ps.customer_group_id=" . (int)$this->config->get('config_customer_group_id') . " AND ((ps.date_start = '0000-00-00' OR ps.date_start < NOW()) AND (ps.date_end = '0000-00-00' OR ps.date_end > NOW())) ORDER BY ps.priority ASC, ps.price ASC LIMIT 1) as special_price, pag.google_product_category, pag.condition, pag.adult, pag.multipack, pag.is_bundle, pag.age_group, pag.color, pag.gender, pag.size_type, pag.size_system, pag.size FROM `" . DB_PREFIX . "product` p LEFT JOIN `" . DB_PREFIX . "product_to_store` p2s ON (p2s.product_id = p.product_id AND p2s.store_id=" . (int)$this->store_id . ") LEFT JOIN `" . DB_PREFIX . "product_description` pd ON (pd.product_id = p.product_id) LEFT JOIN `" . DB_PREFIX . "googleshopping_product` pag ON (pag.product_id = p.product_id AND pag.store_id = p2s.store_id) WHERE p2s.store_id IS NOT NULL AND pd.language_id=" . (int)$language_id . " AND pd.name != '' AND pd.description != '' AND pd.name IS NOT NULL AND pd.description IS NOT NULL AND p.image != '' AND p.status = 1 AND p.date_available <= NOW() AND p.price > 0 ORDER BY p.product_id ASC LIMIT " . (int)(($page - 1) * $this->config->get('advertise_google_push_limit')) . ', ' . (int)$this->config->get('advertise_google_push_limit'); 1104 1105 return $sql; 1106 } 1107 1108 public function setEventSnippet($snippet) { 1109 $this->event_snippet = $snippet; 1110 } 1111 1112 public function getEventSnippet() { 1113 return $this->event_snippet; 1114 } 1115 1116 public function getEventSnippetSendTo() { 1117 $tracker = $this->setting->get('advertise_google_conversion_tracker'); 1118 1119 if (!empty($tracker['google_event_snippet'])) { 1120 $matches = array(); 1121 1122 preg_match('~send_to\': \'([a-zA-Z0-9-]*).*\'~', $tracker['google_event_snippet'], $matches); 1123 1124 return $matches[1]; 1125 } 1126 1127 return null; 1128 } 1129 1130 public function setPurchaseData($total) { 1131 $this->purchase_data = $total; 1132 } 1133 1134 public function getPurchaseData() { 1135 return $this->purchase_data; 1136 } 1137 1138 public function convertAndFormat($price, $currency) { 1139 $currency_converter = new \Cart\Currency($this->registry); 1140 $converted_price = $currency_converter->convert((float)$price, $this->config->get('config_currency'), $currency); 1141 return (float)number_format($converted_price, 2, '.', ''); 1142 } 1143 1144 public function getMerchantAuthUrl($data) { 1145 $request = array( 1146 'type' => 'POST', 1147 'endpoint' => self::ENDPOINT_MERCHANT_AUTH_URL, 1148 'use_access_token' => true, 1149 'content_type' => 'multipart/form-data', 1150 'data' => $data 1151 ); 1152 1153 $response = $this->api($request); 1154 1155 return $response['url']; 1156 } 1157 1158 public function isConnected() { 1159 $settings_exist = 1160 $this->setting->has('advertise_google_access_token') && 1161 $this->setting->has('advertise_google_refresh_token') && 1162 $this->setting->has('advertise_google_app_id') && 1163 $this->setting->has('advertise_google_app_secret'); 1164 1165 if ($settings_exist) { 1166 if ($this->testAccessToken() || $this->getAccessToken()) { 1167 return true; 1168 } 1169 } 1170 1171 throw new ConnectionException("Access unavailable. Please re-connect."); 1172 } 1173 1174 public function isStoreUrlClaimed() { 1175 // No need to check the connection here - this method is called immediately after checking it 1176 1177 $request = array( 1178 'type' => 'POST', 1179 'endpoint' => self::ENDPOINT_VERIFY_IS_CLAIMED, 1180 'use_access_token' => true, 1181 'content_type' => 'multipart/form-data', 1182 'data' => array( 1183 'url_website' => $this->store_url 1184 ) 1185 ); 1186 1187 $response = $this->api($request); 1188 1189 return $response['is_claimed']; 1190 } 1191 1192 public function currencyFormat($value) { 1193 return '$' . number_format($value, 2, '.', ','); 1194 } 1195 1196 public function getCampaignReports() { 1197 $targets = array(); 1198 $statuses = array(); 1199 1200 foreach ($this->getTargets($this->store_id) as $target) { 1201 $targets[] = $target['campaign_name']; 1202 $statuses[$target['campaign_name']] = $target['status']; 1203 } 1204 $targets[] = 'Total'; 1205 1206 $cache = new \Cache($this->config->get('cache_engine'), self::CACHE_CAMPAIGN_REPORT); 1207 $cache_key = 'advertise_google.' . $this->store_id . '.campaign_reports.' . md5(json_encode(array_keys($statuses)) . $this->setting->get('advertise_google_reporting_interval')); 1208 1209 $cache_result = $cache->get($cache_key); 1210 1211 if (empty($cache_result['result']) || (isset($cache_result['timestamp']) && $cache_result['timestamp'] >= time() + self::CACHE_CAMPAIGN_REPORT)) { 1212 $request = array( 1213 'endpoint' => sprintf(self::ENDPOINT_REPORT_CAMPAIGN, $this->setting->get('advertise_google_reporting_interval')), 1214 'use_access_token' => true 1215 ); 1216 1217 $csv = $this->api($request); 1218 1219 $lines = explode("\n", trim($csv['campaign_report'])); 1220 1221 $result = array( 1222 'date_range' => null, 1223 'reports' => array() 1224 ); 1225 1226 // Get date range 1227 $matches = array(); 1228 preg_match('~CAMPAIGN_PERFORMANCE_REPORT \((.*?)\)~', $lines[0], $matches); 1229 $result['date_range'] = $matches[1]; 1230 1231 $header = explode(',', $lines[1]); 1232 $data = array(); 1233 $total = array(); 1234 $value_keys = array(); 1235 1236 $campaign_keys = array_flip($targets); 1237 1238 $expected = array( 1239 'Campaign' => 'campaign_name', 1240 'Impressions' => 'impressions', 1241 'Clicks' => 'clicks', 1242 'Cost' => 'cost', 1243 'Conversions' => 'conversions', 1244 'Total conv. value' => 'conversion_value' 1245 ); 1246 1247 foreach ($header as $i => $title) { 1248 if (!in_array($title, array_keys($expected))) { 1249 continue; 1250 } 1251 1252 $value_keys[$i] = $expected[$title]; 1253 } 1254 1255 // Fill blank values 1256 foreach ($campaign_keys as $campaign_name => $l) { 1257 foreach ($value_keys as $i => $key) { 1258 $result['reports'][$l][$key] = $key == 'campaign_name' ? $campaign_name : '–'; 1259 } 1260 } 1261 1262 // Fill actual values 1263 for ($j = 2; $j < count($lines); $j++) { 1264 $line_items = explode(',', $lines[$j]); 1265 $l = null; 1266 1267 // Identify campaign key 1268 foreach ($line_items as $k => $line_item_value) { 1269 if (array_key_exists($k, $value_keys) && array_key_exists($line_item_value, $campaign_keys) && $value_keys[$k] == 'campaign_name') { 1270 $l = $campaign_keys[$line_item_value]; 1271 } 1272 } 1273 1274 // Fill campaign values 1275 if (!is_null($l)) { 1276 foreach ($line_items as $k => $line_item_value) { 1277 if (!array_key_exists($k, $value_keys)) { 1278 continue; 1279 } 1280 1281 if (in_array($value_keys[$k], array('cost'))) { 1282 $line_item_value = $this->currencyFormat((float)$line_item_value / self::MICROAMOUNT); 1283 } else if (in_array($value_keys[$k], array('conversion_value'))) { 1284 $line_item_value = $this->currencyFormat((float)$line_item_value); 1285 } else if ($value_keys[$k] == 'conversions') { 1286 $line_item_value = (int)$line_item_value; 1287 } 1288 1289 $result['reports'][$l][$value_keys[$k]] = $line_item_value; 1290 } 1291 } 1292 } 1293 1294 $cache->set($cache_key, array( 1295 'timestamp' => time(), 1296 'result' => $result 1297 )); 1298 } else { 1299 $result = $cache_result['result']; 1300 } 1301 1302 // Fill campaign statuses 1303 foreach ($result['reports'] as &$report) { 1304 if ($report['campaign_name'] == 'Total') { 1305 $report['status'] = ''; 1306 } else { 1307 $report['status'] = $statuses[$report['campaign_name']]; 1308 } 1309 } 1310 1311 $this->applyNewSetting('advertise_google_report_campaigns', $result); 1312 } 1313 1314 public function getProductReports($product_ids) { 1315 $cache = new \Cache($this->config->get('cache_engine'), self::CACHE_PRODUCT_REPORT); 1316 $cache_key = 'advertise_google.' . $this->store_id . '.product_reports.' . md5(json_encode($product_ids) . $this->setting->get('advertise_google_reporting_interval')); 1317 1318 $cache_result = $cache->get($cache_key); 1319 1320 if (!empty($cache_result['result']) && isset($cache_result['timestamp']) && (time() - self::CACHE_PRODUCT_REPORT <= $cache_result['timestamp'])) { 1321 return $cache_result['result']; 1322 } 1323 1324 $post = array(); 1325 $post_data = array( 1326 'product_ids' => $product_ids 1327 ); 1328 1329 $this->curlPostQuery($post_data, $post); 1330 1331 $request = array( 1332 'type' => 'POST', 1333 'endpoint' => sprintf(self::ENDPOINT_REPORT_AD, $this->setting->get('advertise_google_reporting_interval')), 1334 'use_access_token' => true, 1335 'content_type' => 'multipart/form-data', 1336 'data' => $post 1337 ); 1338 1339 $response = $this->api($request); 1340 1341 $result = array(); 1342 1343 if (!empty($response['ad_report'])) { 1344 $lines = explode("\n", trim($response['ad_report'])); 1345 1346 $header = explode(',', $lines[1]); 1347 $data = array(); 1348 $keys = array(); 1349 1350 $expected = array( 1351 'Item Id' => 'offer_id', 1352 'Impressions' => 'impressions', 1353 'Clicks' => 'clicks', 1354 'Cost' => 'cost', 1355 'Conversions' => 'conversions', 1356 'Total conv. value' => 'conversion_value' 1357 ); 1358 1359 foreach ($header as $i => $title) { 1360 if (!in_array($title, array_keys($expected))) { 1361 continue; 1362 } 1363 1364 $data[$i] = 0.0; 1365 $keys[$i] = $expected[$title]; 1366 } 1367 1368 // We want to omit the last line because it does not include the total number of impressions for all campaigns 1369 for ($j = 2; $j < count($lines) - 1; $j++) { 1370 $line_items = explode(',', $lines[$j]); 1371 1372 $result[$j] = array(); 1373 1374 foreach ($line_items as $k => $line_item) { 1375 if (in_array($k, array_keys($data))) { 1376 $result[$j][$keys[$k]] = (float)$line_item; 1377 } 1378 } 1379 } 1380 } 1381 1382 $cache->set($cache_key, array( 1383 'result' => $result, 1384 'timestamp' => time() 1385 )); 1386 1387 return $result; 1388 } 1389 1390 public function getProductStatuses($product_ids) { 1391 $post_data = array( 1392 'product_ids' => $product_ids 1393 ); 1394 1395 $this->curlPostQuery($post_data, $post); 1396 1397 $request = array( 1398 'type' => 'POST', 1399 'endpoint' => self::ENDPOINT_MERCHANT_PRODUCT_STATUSES, 1400 'use_access_token' => true, 1401 'content_type' => 'multipart/form-data', 1402 'data' => $post 1403 ); 1404 1405 $response = $this->api($request); 1406 1407 return $response['statuses']; 1408 } 1409 1410 public function getConversionTracker() { 1411 $request = array( 1412 'endpoint' => self::ENDPOINT_CONVERSION_TRACKER, 1413 'use_access_token' => true 1414 ); 1415 1416 $result = $this->api($request); 1417 1418 // Amend the conversion snippet by replacing the default values with placeholders. 1419 $search = array( 1420 "'value': 0.0", 1421 "'currency': 'USD'" 1422 ); 1423 1424 $replace = array( 1425 "'value': {VALUE}", 1426 "'currency': '{CURRENCY}'" 1427 ); 1428 1429 $result['conversion_tracker']['google_event_snippet'] = str_replace($search, $replace, $result['conversion_tracker']['google_event_snippet']); 1430 1431 return $result['conversion_tracker']; 1432 } 1433 1434 public function testCampaigns() { 1435 $request = array( 1436 'endpoint' => self::ENDPOINT_CAMPAIGN_TEST, 1437 'use_access_token' => true 1438 ); 1439 1440 $result = $this->api($request); 1441 1442 return $result['status'] === true; 1443 } 1444 1445 public function testAccessToken() { 1446 $request = array( 1447 'endpoint' => self::ENDPOINT_ACCESS_TOKEN_TEST, 1448 'use_access_token' => true 1449 ); 1450 1451 try { 1452 $result = $this->api($request); 1453 1454 return $result['status'] === true; 1455 } catch (AccessForbiddenException $e) { 1456 throw $e; 1457 } catch (\RuntimeException $e) { 1458 // Do nothing 1459 } 1460 1461 return false; 1462 } 1463 1464 public function getAccessToken() { 1465 $request = array( 1466 'type' => 'POST', 1467 'endpoint' => self::ENDPOINT_ACCESS_TOKEN, 1468 'use_access_token' => false, 1469 'content_type' => 'multipart/form-data', 1470 'data' => array( 1471 'grant_type' => 'refresh_token', 1472 'refresh_token' => $this->setting->get('advertise_google_refresh_token'), 1473 'client_id' => $this->setting->get('advertise_google_app_id'), 1474 'client_secret' => $this->setting->get('advertise_google_app_secret'), 1475 'scope' => self::SCOPES 1476 ) 1477 ); 1478 1479 $access = $this->api($request); 1480 1481 $this->applyNewSetting('advertise_google_access_token', $access['access_token']); 1482 $this->applyNewSetting('advertise_google_refresh_token', $access['refresh_token']); 1483 1484 return true; 1485 } 1486 1487 public function access($data, $code) { 1488 $request = array( 1489 'type' => 'POST', 1490 'endpoint' => self::ENDPOINT_ACCESS_TOKEN, 1491 'use_access_token' => false, 1492 'content_type' => 'multipart/form-data', 1493 'data' => array( 1494 'grant_type' => 'authorization_code', 1495 'client_id' => $data['app_id'], 1496 'client_secret' => $data['app_secret'], 1497 'redirect_uri' => $data['redirect_uri'], 1498 'code' => $code 1499 ) 1500 ); 1501 1502 return $this->api($request); 1503 } 1504 1505 public function authorize($data) { 1506 $query = array(); 1507 1508 $query['response_type'] = 'code'; 1509 $query['client_id'] = $data['app_id']; 1510 $query['redirect_uri'] = $data['redirect_uri']; 1511 $query['scope'] = self::SCOPES; 1512 $query['state'] = $data['state']; 1513 1514 return sprintf($this->endpoint_url, 'api/authorize/login') . '&' . http_build_query($query); 1515 } 1516 1517 public function verifySite() { 1518 $request = array( 1519 'type' => 'POST', 1520 'endpoint' => self::ENDPOINT_VERIFY_TOKEN, 1521 'use_access_token' => true, 1522 'content_type' => 'multipart/form-data', 1523 'data' => array( 1524 'url_website' => $this->store_url 1525 ) 1526 ); 1527 1528 $response = $this->api($request); 1529 1530 $token = $response['token']; 1531 1532 $this->createVerificationToken($token); 1533 1534 $request = array( 1535 'type' => 'POST', 1536 'endpoint' => self::ENDPOINT_VERIFY_SITE, 1537 'use_access_token' => true, 1538 'content_type' => 'multipart/form-data', 1539 'data' => array( 1540 'url_website' => $this->store_url 1541 ) 1542 ); 1543 1544 try { 1545 $this->api($request); 1546 1547 $this->deleteVerificationToken($token); 1548 } catch (\RuntimeException $e) { 1549 $this->deleteVerificationToken($token); 1550 1551 throw $e; 1552 } 1553 } 1554 1555 public function deleteCampaign($name) { 1556 $post = array(); 1557 $data = array( 1558 'delete' => array( 1559 $name 1560 ) 1561 ); 1562 1563 $this->curlPostQuery($data, $post); 1564 1565 $request = array( 1566 'type' => 'POST', 1567 'endpoint' => self::ENDPOINT_CAMPAIGN_DELETE, 1568 'use_access_token' => true, 1569 'content_type' => 'multipart/form-data', 1570 'data' => $post 1571 ); 1572 1573 $this->api($request); 1574 } 1575 1576 public function pushTargets() { 1577 $post = array(); 1578 $targets = array(); 1579 1580 foreach ($this->getTargets($this->store_id) as $target) { 1581 $targets[] = array( 1582 'campaign_name' => $target['campaign_name_raw'], 1583 'country' => $target['country']['code'], 1584 'status' => $this->setting->get('advertise_google_status') ? $target['status'] : 'paused', 1585 'budget' => (float)$target['budget']['value'], 1586 'roas' => ((int)$target['roas']) / 100, 1587 'feeds' => $target['feeds_raw'] 1588 ); 1589 } 1590 1591 $data = array( 1592 'target' => $targets 1593 ); 1594 1595 $this->curlPostQuery($data, $post); 1596 1597 $request = array( 1598 'type' => 'POST', 1599 'endpoint' => self::ENDPOINT_CAMPAIGN_UPDATE, 1600 'use_access_token' => true, 1601 'content_type' => 'multipart/form-data', 1602 'data' => $post 1603 ); 1604 1605 $response = $this->api($request); 1606 1607 $this->applyNewSetting('advertise_google_work', $response['work']); 1608 } 1609 1610 public function pushShippingAndTaxes() { 1611 $post = array(); 1612 $data = $this->setting->get('advertise_google_shipping_taxes'); 1613 1614 $this->curlPostQuery($data, $post); 1615 1616 $request = array( 1617 'type' => 'POST', 1618 'endpoint' => self::ENDPOINT_MERCHANT_SHIPPING_TAXES, 1619 'use_access_token' => true, 1620 'content_type' => 'multipart/form-data', 1621 'data' => $post 1622 ); 1623 1624 $this->api($request); 1625 } 1626 1627 public function disconnect() { 1628 $request = array( 1629 'type' => 'GET', 1630 'endpoint' => self::ENDPOINT_MERCHANT_DISCONNECT, 1631 'use_access_token' => true 1632 ); 1633 1634 $this->api($request); 1635 } 1636 1637 public function pushCampaignStatus() { 1638 $post = array(); 1639 $targets = array(); 1640 1641 foreach ($this->getTargets($this->store_id) as $target) { 1642 $targets[] = array( 1643 'campaign_name' => $target['campaign_name_raw'], 1644 'status' => $this->setting->get('advertise_google_status') ? $target['status'] : 'paused' 1645 ); 1646 } 1647 1648 $data = array( 1649 'target' => $targets 1650 ); 1651 1652 $this->curlPostQuery($data, $post); 1653 1654 $request = array( 1655 'type' => 'POST', 1656 'endpoint' => self::ENDPOINT_CAMPAIGN_STATUS, 1657 'use_access_token' => true, 1658 'content_type' => 'multipart/form-data', 1659 'data' => $post 1660 ); 1661 1662 $this->api($request); 1663 } 1664 1665 public function getAvailableCarriers() { 1666 $request = array( 1667 'type' => 'GET', 1668 'endpoint' => self::ENDPOINT_MERCHANT_AVAILABLE_CARRIERS, 1669 'use_access_token' => true 1670 ); 1671 1672 $result = $this->api($request); 1673 1674 return $result['available_carriers']; 1675 } 1676 1677 public function getLanguages($language_codes) { 1678 $this->load->config('googleshopping/googleshopping'); 1679 1680 $result = array(); 1681 1682 foreach ($this->config->get('advertise_google_languages') as $code => $name) { 1683 if (in_array($code, $language_codes)) { 1684 $supported_language_id = $this->getSupportedLanguageId($code); 1685 1686 $result[] = array( 1687 'status' => $supported_language_id !== 0, 1688 'language_id' => $supported_language_id, 1689 'code' => $code, 1690 'name' => $this->getLanguageName($supported_language_id, $name) 1691 ); 1692 } 1693 } 1694 1695 return $result; 1696 } 1697 1698 public function getLanguageName($language_id, $default) { 1699 $this->load->model('localisation/language'); 1700 1701 $language_info = $this->model_localisation_language->getLanguage($language_id); 1702 1703 if (isset($language_info['name']) && trim($language_info['name']) != "") { 1704 return $language_info['name']; 1705 } 1706 1707 // We do not expect to get to this point, but just in case... 1708 return $default; 1709 } 1710 1711 public function getCurrencies($currency_codes) { 1712 $result = array(); 1713 1714 $this->load->config('googleshopping/googleshopping'); 1715 1716 $result = array(); 1717 1718 foreach ($this->config->get('advertise_google_currencies') as $code => $name) { 1719 if (in_array($code, $currency_codes)) { 1720 $supported_currency_id = $this->getSupportedCurrencyId($code); 1721 1722 $result[] = array( 1723 'status' => $supported_currency_id !== 0, 1724 'code' => $code, 1725 'name' => $this->getCurrencyName($supported_currency_id, $name) . ' (' . $code . ')' 1726 ); 1727 } 1728 } 1729 1730 return $result; 1731 } 1732 1733 public function getCurrencyName($currency_id, $default) { 1734 $this->load->model('extension/advertise/google'); 1735 1736 $currency_info = $this->getCurrency($currency_id); 1737 1738 if (isset($currency_info['title']) && trim($currency_info['title']) != "") { 1739 return $currency_info['title']; 1740 } 1741 1742 // We do not expect to get to this point, but just in case... 1743 return $default; 1744 } 1745 1746 public function getCurrency($currency_id) { 1747 $query = $this->db->query("SELECT DISTINCT * FROM " . DB_PREFIX . "currency WHERE currency_id = '" . (int)$currency_id . "'"); 1748 1749 return $query->row; 1750 } 1751 1752 public function debugLog($text) { 1753 if ($this->setting->get('advertise_google_debug_log')) { 1754 $this->debug_log->write($text); 1755 } 1756 } 1757 1758 protected function target($target) { 1759 $feeds_raw = json_decode($target['feeds'], true); 1760 1761 $feeds = array_map(function($feed) { 1762 $language = current($this->getLanguages(array($feed['language']))); 1763 $currency = current($this->getCurrencies(array($feed['currency']))); 1764 1765 return array( 1766 'text' => $language['name'] . ', ' . $currency['name'], 1767 'language' => $feed['language'], 1768 'currency' => $feed['currency'] 1769 ); 1770 }, $feeds_raw); 1771 1772 return array( 1773 'target_id' => $target['advertise_google_target_id'], 1774 'campaign_name' => str_replace(',', ',', trim($target['campaign_name'])), 1775 'campaign_name_raw' => $target['campaign_name'], 1776 'country' => array( 1777 'code' => $target['country'], 1778 'name' => $this->getCountryName($target['country']) 1779 ), 1780 'budget' => array( 1781 'formatted' => sprintf($this->language->get('text_per_day'), number_format((float)$target['budget'], 2)), 1782 'value' => (float)$target['budget'] 1783 ), 1784 'feeds' => $feeds, 1785 'status' => $target['status'], 1786 'roas' => $target['roas'], 1787 'roas_status' => $target['date_added'] <= date('Y-m-d', time() - self::ROAS_WAIT_INTERVAL), 1788 'roas_available_on' => strtotime($target['date_added']) + self::ROAS_WAIT_INTERVAL, 1789 'feeds_raw' => $feeds_raw 1790 ); 1791 } 1792 1793 private function curlPostQuery($arrays, &$new = array(), $prefix = null) { 1794 foreach ($arrays as $key => $value) { 1795 $k = isset($prefix) ? $prefix . '[' . $key . ']' : $key; 1796 if (is_array($value)) { 1797 $this->curlPostQuery($value, $new, $k); 1798 } else { 1799 $new[$k] = $value; 1800 } 1801 } 1802 } 1803 1804 private function createVerificationToken($token) { 1805 $dir = dirname(DIR_SYSTEM); 1806 1807 if (!is_dir($dir) || !is_writable($dir)) { 1808 throw new \RuntimeException("Not a directory, or no permissions to write to: " . $dir); 1809 } 1810 1811 if (!file_put_contents($dir . '/' . $token, 'google-site-verification: ' . $token)) { 1812 throw new \RuntimeException("Could not write to: " . $dir . '/' . $token); 1813 } 1814 } 1815 1816 private function deleteVerificationToken($token) { 1817 $dir = dirname(DIR_SYSTEM); 1818 1819 if (!is_dir($dir) || !is_writable($dir)) { 1820 throw new \RuntimeException("Not a directory, or no permissions to write to: " . $dir); 1821 } 1822 1823 $file = $dir . '/' . $token; 1824 1825 if (is_file($file) && is_writable($file)) { 1826 @unlink($file); 1827 } 1828 } 1829 1830 private function applyNewSetting($key, $value) { 1831 $sql = "SELECT * FROM `" . DB_PREFIX . "setting` WHERE `code`='advertise_google' AND `key`='" . $this->db->escape($key) . "'"; 1832 $result = $this->db->query($sql); 1833 1834 if (is_array($value)) { 1835 $encoded = json_encode($value); 1836 $serialized = 1; 1837 } else { 1838 $encoded = $value; 1839 $serialized = 0; 1840 } 1841 1842 if ($result->num_rows == 0) { 1843 $this->db->query("INSERT INTO `" . DB_PREFIX . "setting` SET `value`='" . $this->db->escape($encoded) . "', `code`='advertise_google', `key`='" . $this->db->escape($key) . "', serialized='" . $serialized . "', store_id='0'"); 1844 1845 $this->setting->set($key, $value); 1846 } else { 1847 $this->db->query("UPDATE `" . DB_PREFIX . "setting` SET `value`='" . $this->db->escape($encoded) . "', serialized='" . $serialized . "' WHERE `code`='advertise_google' AND `key`='" . $this->db->escape($key) . "'"); 1848 1849 $this->setting->set($key, $value); 1850 } 1851 } 1852 1853 private function api($request) { 1854 $this->debugLog("REQUEST: " . json_encode($request)); 1855 1856 $url = sprintf($this->endpoint_url, $request['endpoint']); 1857 1858 $headers = array(); 1859 1860 if (isset($request['content_type'])) { 1861 $headers[] = 'Content-Type: ' . $request['content_type']; 1862 } else { 1863 $headers[] = 'Content-Type: application/json'; 1864 } 1865 1866 if (!empty($request['use_access_token'])) { 1867 $headers[] = 'Authorization: Bearer ' . $this->setting->get('advertise_google_access_token'); 1868 } 1869 1870 $curl_options = array(); 1871 1872 if (isset($request['type']) && $request['type'] == 'POST') { 1873 $curl_options[CURLOPT_POST] = true; 1874 $curl_options[CURLOPT_POSTFIELDS] = $request['data']; 1875 } 1876 1877 $curl_options[CURLOPT_URL] = $url; 1878 $curl_options[CURLOPT_RETURNTRANSFER] = true; 1879 $curl_options[CURLOPT_HTTPHEADER] = $headers; 1880 1881 $ch = curl_init(); 1882 curl_setopt_array($ch, $curl_options); 1883 $result = curl_exec($ch); 1884 $info = curl_getinfo($ch); 1885 curl_close($ch); 1886 1887 $this->debugLog("RESPONSE: " . $result); 1888 1889 if (!empty($result) && $info['http_code'] == 200) { 1890 $return = json_decode($result, true); 1891 1892 if ($return['error']) { 1893 throw new \RuntimeException($return['message']); 1894 } else { 1895 return $return['result']; 1896 } 1897 } else if (in_array($info['http_code'], array(400, 401, 403))) { 1898 $return = json_decode($result, true); 1899 1900 if ($info['http_code'] != 401 && $return['error']) { 1901 throw new \RuntimeException($return['message']); 1902 } else { 1903 throw new ConnectionException("Access unavailable. Please re-connect."); 1904 } 1905 } else if ($info['http_code'] == 402) { 1906 $return = json_decode($result, true); 1907 1908 if ($return['error']) { 1909 throw new AccessForbiddenException($return['message']); 1910 } else { 1911 throw new ConnectionException("Access unavailable. Please re-connect."); 1912 } 1913 } else { 1914 $this->debugLog("CURL ERROR! CURL INFO: " . print_r($info, true)); 1915 1916 throw new \RuntimeException("A temporary error was encountered. Please try again later."); 1917 } 1918 } 1919} 1920