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('&amp;', '&', $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('&', '&amp;', 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 : '&ndash;';
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('&#44;', ',', 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