diff --git a/.gitignore b/.gitignore index a05346c..81b95e7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ docker/bin .phpcs.cache node_modules/ .php_cs_fixer.cache +.aider* diff --git a/.phpcs.xml b/.phpcs.xml index 6ee615f..356faf3 100644 --- a/.phpcs.xml +++ b/.phpcs.xml @@ -17,6 +17,8 @@ + + diff --git a/prestashop1.7/controllers/admin/AdminTawktoController.php b/prestashop1.7/controllers/admin/AdminTawktoController.php index e52b218..9f4c007 100644 --- a/prestashop1.7/controllers/admin/AdminTawktoController.php +++ b/prestashop1.7/controllers/admin/AdminTawktoController.php @@ -21,11 +21,20 @@ exit; } +/** + * Tawkto exception + */ +class TawktoException extends Exception +{ +} + /** * Admin settings controller */ class AdminTawktoController extends ModuleAdminController { + public const NO_CHANGE = 'nochange'; + /** * __construct * @@ -87,11 +96,15 @@ public function renderView() $optKey = TawkTo::TAWKTO_WIDGET_OPTS; // returns 'false' if retrieved none. - $displayOpts = Configuration::get($optKey); - if (!$displayOpts) { - $displayOpts = null; + $widgetOpts = Configuration::get($optKey); + if (!$widgetOpts) { + $widgetOpts = null; + } + $widgetOpts = Tools::jsonDecode($widgetOpts); + + if ($widgetOpts && !empty($widgetOpts->js_api_key)) { + $widgetOpts->js_api_key = self::NO_CHANGE; } - $displayOpts = Tools::jsonDecode($displayOpts); $sameUser = true; // assuming there is only one admin by default $empId = Configuration::get(TawkTo::TAWKTO_WIDGET_USER); @@ -113,7 +126,7 @@ public function renderView() 'controller' => $this->context->link->getAdminLink('AdminTawkto'), 'tab_id' => (int) $this->context->controller->id, 'domain' => $domain, - 'display_opts' => $displayOpts, + 'widget_opts' => $widgetOpts, 'page_id' => $pageId, 'widget_id' => $widgetId, 'same_user' => $sameUser, @@ -221,8 +234,54 @@ public function ajaxProcessRemoveWidget() * * @return void */ - public function ajaxProcessSetVisibility() + public function ajaxProcessSetOptions() + { + $key = TawkTo::TAWKTO_WIDGET_OPTS; + $jsonOpts = []; + + try { + // Process selected options + $jsonOpts = $this->processSetOptions(Tools::getValue('options')); + } catch (Exception $e) { + if ($e instanceof TawktoException) { + die(json_encode(['success' => false, 'message' => $e->getMessage()])); + } + + die(json_encode(['success' => false, 'message' => 'An error occurred while saving options'])); + } + + // Override current options/fallback if not selected + $currentOpts = Configuration::get($key); + if (!empty($currentOpts)) { + $currentOpts = json_decode($currentOpts, true); + if (is_array($currentOpts)) { + $jsonOpts = array_merge($currentOpts, $jsonOpts); + } + } + + if (!isset($jsonOpts['config_version'])) { + $jsonOpts['config_version'] = 0; + } else { + ++$jsonOpts['config_version']; + } + + Configuration::updateValue($key, json_encode($jsonOpts)); + + die(json_encode(['success' => true])); + } + + /** + * Process options + * + * @param string $params Selected options + * + * @return array + * + * @throws TawktoException Error processing options + */ + private function processSetOptions(string $params): array { + // default options $jsonOpts = [ 'always_display' => false, @@ -239,40 +298,99 @@ public function ajaxProcessSetVisibility() 'show_oncustom' => json_encode([]), 'enable_visitor_recognition' => false, + 'js_api_key' => '', ]; - $options = Tools::getValue('options'); - if (!empty($options)) { - $options = explode('&', $options); - foreach ($options as $post) { - [$column, $value] = explode('=', $post); - switch ($column) { - case 'hide_oncustom': - case 'show_oncustom': - // replace newlines and returns with comma, and convert to array for - // saving - $value = urldecode($value); - $value = str_ireplace(["\r\n", "\r", "\n"], ',', $value); - if (!empty($value)) { - $value = explode(',', $value); - $jsonOpts[$column] = json_encode($value); - } + if (empty($params)) { + return $jsonOpts; + } + + parse_str($params, $options); + foreach ($options as $column => $value) { + switch ($column) { + case 'hide_oncustom': + case 'show_oncustom': + // replace newlines and returns with comma, and convert to array for saving + $value = urldecode($value); + $value = str_ireplace(["\r\n", "\r", "\n"], ',', $value); + if (!empty($value)) { + $value = explode(',', $value); + $jsonOpts[$column] = json_encode($value); + } + break; + + case 'show_onfrontpage': + case 'show_oncategory': + case 'show_onproduct': + case 'always_display': + case 'enable_visitor_recognition': + $jsonOpts[$column] = ($value == 1); + break; + + case 'js_api_key': + if ($value === self::NO_CHANGE) { + unset($jsonOpts['js_api_key']); break; + } - case 'show_onfrontpage': - case 'show_oncategory': - case 'show_onproduct': - case 'always_display': - case 'enable_visitor_recognition': - $jsonOpts[$column] = ($value == 1); + if ($value === '') { break; - } + } + + $value = trim($value); + + if (strlen($value) !== 40) { + throw new TawktoException('Invalid API key.'); + } + + try { + $jsonOpts['js_api_key'] = $this->encryptData($value); + } catch (Exception $e) { + error_log($e->getMessage()); + + throw new TawktoException('Error saving Javascript API Key.'); + } + + break; } } - $key = TawkTo::TAWKTO_WIDGET_OPTS; - Configuration::updateValue($key, json_encode($jsonOpts)); + return $jsonOpts; + } - die(Tools::jsonEncode(['success' => true])); + /** + * Encrypt data + * + * @param string $data Data to encrypt + * + * @return string Encrypted data + * + * @throws Exception error encrypting data + */ + private function encryptData(string $data) + { + if (!defined('_COOKIE_KEY_')) { + throw new Exception('Cookie key not defined'); + } + + try { + $iv = random_bytes(16); + } catch (Exception $e) { + throw new Exception('Failed to generate IV'); + } + + $encrypted = openssl_encrypt($data, 'AES-256-CBC', _COOKIE_KEY_, 0, $iv); + + if ($encrypted === false) { + throw new Exception('Failed to encrypt data'); + } + + $encrypted = base64_encode($iv . $encrypted); + + if ($encrypted === false) { + throw new Exception('Failed to encode data'); + } + + return $encrypted; } } diff --git a/prestashop1.7/tawkto.php b/prestashop1.7/tawkto.php index 6c93e88..555c7dd 100644 --- a/prestashop1.7/tawkto.php +++ b/prestashop1.7/tawkto.php @@ -35,6 +35,7 @@ class Tawkto extends Module public const TAWKTO_WIDGET_OPTS = 'TAWKTO_WIDGET_OPTS'; public const TAWKTO_WIDGET_USER = 'TAWKTO_WIDGET_USER'; public const TAWKTO_SELECTED_WIDGET = 'TAWKTO_SELECTED_WIDGET'; + public const TAWKTO_VISITOR_SESSION = 'TAWKTO_VISITOR_SESSION'; /** * __construct @@ -110,7 +111,11 @@ public function hookDisplayFooter() $widgetId = $current_widget['widget_id']; $result = Configuration::get(self::TAWKTO_WIDGET_OPTS); - $enable_visitor_recognition = true; // default value + // default values + $enable_visitor_recognition = true; + $js_api_key = ''; + $config_version = 0; + if ($result) { $options = json_decode($result); $current_page = (string) $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; @@ -119,6 +124,14 @@ public function hookDisplayFooter() $enable_visitor_recognition = $options->enable_visitor_recognition; } + if (isset($options->js_api_key)) { + $js_api_key = $options->js_api_key; + } + + if (isset($options->config_version)) { + $config_version = $options->config_version; + } + // prepare visibility if (false == $options->always_display) { // show on specified urls @@ -168,19 +181,23 @@ public function hookDisplayFooter() } // add customer details as visitor info - $customer_name = null; - $customer_email = null; + $customer_name = ''; + $customer_email = ''; + $hash = null; if ($enable_visitor_recognition && !is_null($this->context->customer->id)) { $customer = $this->context->customer; $customer_name = $customer->firstname . ' ' . $customer->lastname; $customer_email = $customer->email; + + $hash = $this->getVisitorHash($customer_email, $js_api_key, $config_version); } $this->context->smarty->assign([ 'widget_id' => $widgetId, 'page_id' => $pageId, - 'customer_name' => (!is_null($customer_name)) ? $customer_name : '', - 'customer_email' => (!is_null($customer_email)) ? $customer_email : '', + 'customer_name' => $customer_name, + 'customer_email' => $customer_email, + 'hash' => $hash, ]); return $this->display(__FILE__, 'widget.tpl'); @@ -283,4 +300,85 @@ private function getArrayFromJson($data) return $arr; } + + /** + * Get visitor hash + * + * @param string $email Visitor email + * @param string $js_api_key JS API key + * @param int $config_version Config version + * + * @return string|null + */ + private function getVisitorHash(string $email, string $js_api_key, int $config_version) + { + if (empty($js_api_key)) { + return null; + } + + if (session_status() === PHP_SESSION_NONE && !headers_sent()) { + session_start(); + } + + if (isset($_SESSION[self::TAWKTO_VISITOR_SESSION])) { + $current_session = $_SESSION[self::TAWKTO_VISITOR_SESSION]; + + if (isset($current_session['hash']) + && $current_session['email'] === $email + && $current_session['config_version'] === $config_version) { + return $current_session['hash']; + } + } + + try { + $key = $this->getDecryptedData($js_api_key); + } catch (Exception $e) { + error_log($e->getMessage()); + + return null; + } + + $hash = hash_hmac('sha256', $email, $key); + + $_SESSION[self::TAWKTO_VISITOR_SESSION] = [ + 'hash' => $hash, + 'email' => $email, + 'config_version' => $config_version, + ]; + + return $hash; + } + + /** + * Decrypt data + * + * @param string $data Data to decrypt + * + * @return string Decrypted data + * + * @throws Exception error decrypting data + */ + private function getDecryptedData(string $data) + { + if (!defined('_COOKIE_KEY_')) { + throw new Exception('Cookie key not defined'); + } + + $decoded = base64_decode($data); + + if ($decoded === false) { + throw new Exception('Failed to decode data'); + } + + $iv = substr($decoded, 0, 16); + $encrypted_data = substr($decoded, 16); + + $decrypted_data = openssl_decrypt($encrypted_data, 'AES-256-CBC', _COOKIE_KEY_, 0, $iv); + + if ($decrypted_data === false) { + throw new Exception('Failed to decrypt data'); + } + + return $decrypted_data; + } } diff --git a/prestashop1.7/views/templates/admin/tawkto/helpers/view/view.tpl b/prestashop1.7/views/templates/admin/tawkto/helpers/view/view.tpl index 981ca48..80d1a44 100644 --- a/prestashop1.7/views/templates/admin/tawkto/helpers/view/view.tpl +++ b/prestashop1.7/views/templates/admin/tawkto/helpers/view/view.tpl @@ -67,6 +67,13 @@ .tawk-tooltip:hover .tawk-tooltiptext { visibility: visible; } + +.options-alert { + width: 50%; + float: left; + font-weight: bold; + display: none; +}