diff --git a/admin/controller/module/tawkto.php b/admin/controller/module/tawkto.php index 1f69f88..bb2b439 100644 --- a/admin/controller/module/tawkto.php +++ b/admin/controller/module/tawkto.php @@ -13,6 +13,9 @@ class Tawkto extends Controller { + public const CREDENTIALS_FILE = DIR_EXTENSION . 'tawkto/system/config/credentials.json'; + public const NO_CHANGE = 'nochange'; + /** * __construct */ @@ -148,6 +151,7 @@ private function getStoreHierarchy() 'display_opts' => $this->getDisplayOpts($currentSettings), 'privacy_opts' => $this->getPrivacyOpts($currentSettings), 'cart_opts' => $this->getCartOpts($currentSettings), + 'security_opts' => $this->getSecurityOpts($currentSettings), ); foreach ($stores as $store) { @@ -160,6 +164,7 @@ private function getStoreHierarchy() 'display_opts' => $this->getDisplayOpts($currentSettings), 'privacy_opts' => $this->getPrivacyOpts($currentSettings), 'cart_opts' => $this->getCartOpts($currentSettings), + 'security_opts' => $this->getSecurityOpts($currentSettings), ); } @@ -250,6 +255,8 @@ public function setoptions() $cartOpts = $this->config->get('tawkto_cart'); + $securityOpts = $this->config->get('tawkto_security'); + if (isset($_POST['options']) && !empty($_POST['options'])) { $options = explode('&', $_POST['options']); @@ -286,14 +293,63 @@ public function setoptions() case 'monitor_customer_cart': $cartOpts[$key] = true; break; + + case 'secure_mode_enabled': + $securityOpts[$key] = true; + break; + + case 'js_api_key': + if ($value === self::NO_CHANGE) { + unset($securityOpts['js_api_key']); + break; + } + + if ($value === '') { + break; + } + + $value = trim($value); + + if (strlen($value) !== 40) { + throw new \Exception('Invalid API key.'); + } + + try { + $securityOpts['js_api_key'] = $this->encryptData($value); + } catch (\Exception $e) { + error_log($e->getMessage()); + + unset($securityOpts['js_api_key']); + + echo json_encode(array('success' => false, 'message' => 'Error saving Javascript API Key.')); + die(); + } } } } $currentSettings = $this->getCurrentSettingsFor($store_id); - $currentSettings['module_tawkto_visibility'] = $visibilityOpts; - $currentSettings['module_tawkto_privacy'] = $privacyOpts; - $currentSettings['module_tawkto_cart'] = $cartOpts; + if (!isset($currentSettings['module_tawkto_visibility'])) { + $currentSettings['module_tawkto_visibility'] = array(); + } + if (!isset($currentSettings['module_tawkto_privacy'])) { + $currentSettings['module_tawkto_privacy'] = array(); + } + if (!isset($currentSettings['module_tawkto_cart'])) { + $currentSettings['module_tawkto_cart'] = array(); + } + if (!isset($currentSettings['module_tawkto_security'])) { + $currentSettings['module_tawkto_security'] = array(); + } + if (!isset($currentSettings['module_tawkto_config_version'])) { + $currentSettings['module_tawkto_config_version'] = $this->config->get('tawkto_config_version'); + } + + $currentSettings['module_tawkto_visibility'] = array_merge($currentSettings['module_tawkto_visibility'], $visibilityOpts); + $currentSettings['module_tawkto_privacy'] = array_merge($currentSettings['module_tawkto_privacy'], $privacyOpts); + $currentSettings['module_tawkto_cart'] = array_merge($currentSettings['module_tawkto_cart'], $cartOpts); + $currentSettings['module_tawkto_security'] = array_merge($currentSettings['module_tawkto_security'], $securityOpts); + $currentSettings['module_tawkto_config_version'] = $currentSettings['module_tawkto_config_version'] + 1; $this->model_setting_setting->editSetting('module_tawkto', $currentSettings, $store_id); echo json_encode(array('success' => true)); @@ -396,4 +452,80 @@ public function getCartOpts($settings) return $options; } + + /** + * Get security options from setting + * + * @return Array + */ + public function getSecurityOpts($settings) { + $options = $this->config->get('tawkto_security'); + + if (isset($settings['module_tawkto_security'])) { + $options = $settings['module_tawkto_security']; + } + + if (!empty($options['js_api_key'])) { + $options['js_api_key'] = self::NO_CHANGE; + } + + return $options; + } + + + /** + * Get credentials + * + * @return Array + */ + private function getCredentials() { + if (file_exists(self::CREDENTIALS_FILE)) { + return json_decode(file_get_contents(self::CREDENTIALS_FILE), true); + } + + $credentials = array( + 'encryption_key' => bin2hex(random_bytes(32)), + ); + + file_put_contents(self::CREDENTIALS_FILE, json_encode($credentials)); + + return $credentials; + } + + /** + * Encrypt data + * + * @param string $data Data to encrypt + * + * @return string Encrypted data + * + * @throws \Exception Error encrypting data + */ + private function encryptData($data) { + try { + $encryptionKey = $this->getCredentials()['encryption_key']; + } catch (\Exception $e) { + throw new \Exception('Failed to get encryption key'); + } + + try { + $iv = random_bytes(16); + } catch (\Exception $e) { + throw new \Exception('Failed to generate IV'); + } + + $encrypted = openssl_encrypt($data, 'AES-256-CBC', $encryptionKey, 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/admin/view/stylesheet/index.css b/admin/view/stylesheet/index.css index ff73bcb..948547c 100644 --- a/admin/view/stylesheet/index.css +++ b/admin/view/stylesheet/index.css @@ -29,13 +29,16 @@ html { display: none; } -#optionsSuccessMessage { +.alert { position: absolute; + font-weight: bold; + display: none; +} + +#optionsSuccessMessage { background-color: #dff0d8; color: #3c763d; border-color: #d6e9c6; - font-weight: bold; - display: none; } .pull-right { @@ -50,7 +53,7 @@ html { } @media only screen and (max-width: 1200px) { - #optionsSuccessMessage { + .alert { position: relative; margin-top: 1rem; } diff --git a/admin/view/template/module/tawkto.twig b/admin/view/template/module/tawkto.twig index 7db82e3..4ea1111 100644 --- a/admin/view/template/module/tawkto.twig +++ b/admin/view/template/module/tawkto.twig @@ -195,6 +195,34 @@

+
+
Security Settings
+
+
+ +
+ +
+
+
+ +
+ +
+
+

Cart Integration
@@ -219,6 +247,9 @@
Successfully set widget options to your site
+
+ Failed to set widget options to your site +
@@ -402,59 +433,62 @@ store: store_id, options: form.serialize(), }, function (r) { - if (r.success) { - $('#optionsSuccessMessage').toggle().delay(3000).fadeOut(); + if (!r.success) { + $('#optionsErrorMessage').text(r.message).toggle().delay(3000).fadeOut(); + return; + } - // Update saved options - var fields = form.serializeArray(); - for (store of storeHierarchy) { - if (store.id !== store_id) { - continue; - } + $('#optionsSuccessMessage').toggle().delay(3000).fadeOut(); + + // Update saved options + var fields = form.serializeArray(); + for (store of storeHierarchy) { + if (store.id !== store_id) { + continue; + } + + store.display_opts = { + 'always_display': false, + 'show_onfrontpage': false, + 'show_oncategory': false, + 'show_oncustom': [], + 'hide_oncustom': [], + }; - store.display_opts = { - 'always_display': false, - 'show_onfrontpage': false, - 'show_oncategory': false, - 'show_oncustom': [], - 'hide_oncustom': [], - }; + store.privacy_opts = { + 'enable_visitor_recognition': false, + } - store.privacy_opts = { - 'enable_visitor_recognition': false, + store.cart_opts = { + 'monitor_customer_cart': false, + } + + for (field of fields) { + if (field.name === 'show_oncustom') { + store.display_opts['show_oncustom'] = field.value.replaceAll('\r', '\n').split('\n').filter(Boolean); + continue; } - store.cart_opts = { - 'monitor_customer_cart': false, + if (field.name === 'hide_oncustom') { + store.display_opts['hide_oncustom'] = field.value.replaceAll('\r', '\n').split('\n').filter(Boolean); + continue; } - for (field of fields) { - if (field.name === 'show_oncustom') { - store.display_opts['show_oncustom'] = field.value.replaceAll('\r', '\n').split('\n').filter(Boolean); - continue; - } - - if (field.name === 'hide_oncustom') { - store.display_opts['hide_oncustom'] = field.value.replaceAll('\r', '\n').split('\n').filter(Boolean); - continue; - } - - // serializeArray() only includes "successful controls" - switch (field.name) { - case 'always_display': - case 'show_onfrontpage': - case 'show_oncategory': - store.display_opts[field.name] = true; - break; - - case 'enable_visitor_recognition': - store.privacy_opts[field.name] = true; - break; - - case 'monitor_customer_cart': - store.cart_opts[field.name] = true; - break; - } + // serializeArray() only includes "successful controls" + switch (field.name) { + case 'always_display': + case 'show_onfrontpage': + case 'show_oncategory': + store.display_opts[field.name] = true; + break; + + case 'enable_visitor_recognition': + store.privacy_opts[field.name] = true; + break; + + case 'monitor_customer_cart': + store.cart_opts[field.name] = true; + break; } } } diff --git a/catalog/controller/module/tawkto.php b/catalog/controller/module/tawkto.php index c765cea..d9cb190 100644 --- a/catalog/controller/module/tawkto.php +++ b/catalog/controller/module/tawkto.php @@ -16,6 +16,8 @@ class Tawkto extends Controller { + public const CREDENTIALS_FILE = DIR_EXTENSION . 'tawkto/system/config/credentials.json'; + /** * __construct */ @@ -34,10 +36,11 @@ public function index() $this->load->model('setting/setting'); $data = array(); - $data['visitor'] = $this->getVisitor(); $privacy_opts = $this->config->get('tawkto_privacy'); $cart_opts = $this->config->get('tawkto_cart'); + $security_opts = $this->config->get('tawkto_security'); + $config_version = $this->config->get('tawkto_config_version'); $settings = $this->getCurrentSettings(); if (isset($settings['module_tawkto_privacy'])) { @@ -46,8 +49,19 @@ public function index() if (isset($settings['module_tawkto_cart'])) { $cart_opts = $settings['module_tawkto_cart']; } + if (isset($settings['module_tawkto_security'])) { + $security_opts = $settings['module_tawkto_security']; + } + if (isset($settings['module_tawkto_config_version'])) { + $config_version = $settings['module_tawkto_config_version']; + } - $data['enable_visitor_recognition'] = $privacy_opts['enable_visitor_recognition']; + $data['visitor'] = $this->getVisitor(array( + 'enable_visitor_recognition' => $privacy_opts['enable_visitor_recognition'], + 'secure_mode_enabled' => $security_opts['secure_mode_enabled'], + 'js_api_key' => $security_opts['js_api_key'], + 'config_version' => $config_version, + )); $data['can_monitor_customer_cart'] = $cart_opts['monitor_customer_cart']; $widget = $this->getWidget(); @@ -143,16 +157,34 @@ private function matchPatterns($current_page, $pages) /** * Get visitor details * + * @param array $params * @return string|null */ - private function getVisitor() + private function getVisitor($params) { + if ($params['enable_visitor_recognition'] === false) { + return null; + } + + $secure_mode_enabled = $params['secure_mode_enabled']; + $encrypted_js_api_key = $params['js_api_key']; + $config_version = $params['config_version']; + $logged_in = $this->customer->isLogged(); if ($logged_in) { $data = array( 'name' => $this->customer->getFirstName().' '.$this->customer->getLastName(), 'email' => $this->customer->getEmail(), ); + + if ($secure_mode_enabled && !is_null($encrypted_js_api_key)) { + $data['hash'] = $this->getVisitorHash(array( + 'email' => $this->customer->getEmail(), + 'js_api_key' => $encrypted_js_api_key, + 'config_version' => $config_version, + )); + } + return json_encode($data); } @@ -169,4 +201,89 @@ private function getCurrentSettings() $store_id = $this->config->get('config_store_id'); return $this->model_setting_setting->getSetting('module_tawkto', $store_id); } + + /** + * Get visitor hash + * + * @param array $params + * @return string + */ + private function getVisitorHash($params) + { + $js_api_key = $params['js_api_key']; + + if (empty($js_api_key)) { + return ''; + } + + if (session_status() === PHP_SESSION_NONE && !headers_sent()) { + session_start(); + } + + $configVersion = $params['config_version']; + $email = $params['email']; + + if (isset($_SESSION['tawkto_visitor_hash'])) { + $currentSession = $_SESSION['tawkto_visitor_hash']; + + if (isset($currentSession['hash']) && + $currentSession['email'] === $email && + $currentSession['config_version'] === $configVersion) { + return $currentSession['hash']; + } + } + + try { + $jsApiKey = $this->decryptData($js_api_key); + } catch (\Exception $e) { + error_log($e->getMessage()); + + return ''; + } + + $hash = hash_hmac('sha256', $email, $jsApiKey); + + $_SESSION['tawkto_visitor_hash'] = array( + 'hash' => $hash, + 'email' => $email, + 'config_version' => $configVersion, + ); + + return $hash; + } + + /** + * Decrypt data + * @param mixed $data + * @return string Decrypted data + */ + private function decryptData($data) + { + if (!file_exists(self::CREDENTIALS_FILE)) { + throw new \Exception('Credentials file not found'); + } + + $credentials = json_decode(file_get_contents(self::CREDENTIALS_FILE), true); + + if (!isset($credentials['encryption_key'])) { + throw new \Exception('Encryption key not found'); + } + + $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', $credentials['encryption_key'], 0, $iv); + + if ($decrypted_data === false) { + throw new \Exception('Failed to decrypt data'); + } + + return $decrypted_data; + } } diff --git a/catalog/view/template/module/tawkto.twig b/catalog/view/template/module/tawkto.twig index 23f1302..6f02633 100644 --- a/catalog/view/template/module/tawkto.twig +++ b/catalog/view/template/module/tawkto.twig @@ -9,7 +9,7 @@