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 @@
+
+
+
+
@@ -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 @@