diff --git a/Api/ConfigInterface.php b/Api/ConfigInterface.php new file mode 100644 index 0000000..8882b2d --- /dev/null +++ b/Api/ConfigInterface.php @@ -0,0 +1,24 @@ +request = $context->getRequest(); $this->modelSessionFactory = $sessionFactory->create(); $this->escaper = $escaper; + $this->encryptor = $encryptor; } /** @@ -146,7 +160,8 @@ private function getWidgetModel() * * @return array { * name: string, - * email: string + * email: string, + * hash: string * } */ public function getCurrentCustomerDetails() @@ -160,12 +175,60 @@ public function getCurrentCustomerDetails() } $customerSession = $this->modelSessionFactory->getCustomer(); + + $hash = null; + try { + $hash = $this->getVisitorHash($customerSession->getEmail()); + } catch (LocalizedException $e) { + error_log($e->getMessage()); + } + return [ 'name' => $customerSession->getName(), - 'email' => $customerSession->getEmail() + 'email' => $customerSession->getEmail(), + 'hash' => $hash ]; } + /** + * Get visitor hash + * + * @param string $email Visitor email + * @return string + */ + private function getVisitorHash(string $email) + { + $encryptedJsApiKey = $this->model->getJsApiKey(); + + if (empty($encryptedJsApiKey)) { + return null; + } + + $configVersion = $this->model->getConfigVersion(); + + if ($this->modelSessionFactory->hasData(self::TAWKTO_VISITOR_SESSION)) { + $currentSession = $this->modelSessionFactory->getData(self::TAWKTO_VISITOR_SESSION); + + if (isset($currentSession['hash']) && + $currentSession['email'] === $email && + $currentSession['config_version'] === $configVersion) { + return $currentSession['hash']; + } + } + + $jsApiKey = $this->encryptor->decrypt($encryptedJsApiKey); + + $hash = hash_hmac('sha256', $email, $jsApiKey); + + $this->modelSessionFactory->setData(self::TAWKTO_VISITOR_SESSION, [ + 'hash' => $hash, + 'email' => $email, + 'config_version' => $configVersion, + ]); + + return $hash; + } + /** * To or to not display the selected widget. */ diff --git a/Controller/Adminhtml/SaveWidget/Index.php b/Controller/Adminhtml/SaveWidget/Index.php index b41324c..5f80da1 100755 --- a/Controller/Adminhtml/SaveWidget/Index.php +++ b/Controller/Adminhtml/SaveWidget/Index.php @@ -20,9 +20,14 @@ use Magento\Framework\Controller\Result\JsonFactory; use Magento\Backend\App\Action\Context; +use Magento\Framework\Encryption\EncryptorInterface; +use Magento\Framework\Exception\LocalizedException; + use Psr\Log\LoggerInterface; use Tawk\Widget\Model\WidgetFactory; use Tawk\Widget\Helper\StringUtil; +use Tawk\Widget\Api\ConfigInterface; +use Tawk\Widget\Exception\SaveWidgetException; class Index extends \Magento\Backend\App\Action { @@ -61,6 +66,13 @@ class Index extends \Magento\Backend\App\Action */ protected $helper; + /** + * Encryptor instance + * + * @var EncryptorInterface $encryptor + */ + protected $encryptor; + /** * Constructor * @@ -69,13 +81,15 @@ class Index extends \Magento\Backend\App\Action * @param JsonFactory $resultJsonFactory Json Factory instance * @param LoggerInterface $logger PSR Logger * @param StringUtil $helper String util helper + * @param EncryptorInterface $encryptor Encryptor instance */ public function __construct( WidgetFactory $modelFactory, Context $context, JsonFactory $resultJsonFactory, LoggerInterface $logger, - StringUtil $helper + StringUtil $helper, + EncryptorInterface $encryptor ) { parent::__construct($context); $this->resultJsonFactory = $resultJsonFactory; @@ -83,6 +97,7 @@ public function __construct( $this->modelWidgetFactory = $modelFactory->create(); $this->request = $this->getRequest(); $this->helper = $helper; + $this->encryptor = $encryptor; } /** @@ -106,13 +121,14 @@ public function execute() } $alwaysdisplay = filter_var($this->request->getParam('alwaysdisplay'), FILTER_SANITIZE_NUMBER_INT); - $excludeurl = $this->request->getParam('excludeurl'); + $excludeurl = $this->helper->stripTagsandQuotes($this->request->getParam('excludeurl')); $donotdisplay = filter_var($this->request->getParam('donotdisplay'), FILTER_SANITIZE_NUMBER_INT); - $includeurl = $this->request->getParam('includeurl'); + $includeurl = $this->helper->stripTagsAndQuotes($this->request->getParam('includeurl')); $enableVisitorRecognition = filter_var( $this->request->getParam('enableVisitorRecognition'), FILTER_SANITIZE_NUMBER_INT ); + $jsApiKey = $this->helper->stripTagsandQuotes($this->request->getParam('jsApiKey')); $model = $this->modelWidgetFactory->loadByForStoreId($storeId); @@ -134,8 +150,46 @@ public function execute() $model->setEnableVisitorRecognition($enableVisitorRecognition); + try { + $this->setJsApiKey($model, $jsApiKey); + } catch (LocalizedException $e) { + if ($e instanceof SaveWidgetException) { + return $response->setData(['success' => false, 'message' => $e->getMessage()]); + } + + return $response->setData(['success' => false, 'message' => 'An error occurred while saving the widget']); + } + + $model->setConfigVersion($model->getConfigVersion() + 1); + $model->save(); return $response->setData(['success' => true]); } + + /** + * Sets the JS API key for the widget. + * + * @param \Tawk\Widget\Model\Widget $model The widget model + * @param string $jsApiKey The JS API key + * @return void + */ + private function setJsApiKey($model, $jsApiKey) + { + if ($jsApiKey === ConfigInterface::JS_API_KEY_NO_CHANGE) { + return; + } + + if ($jsApiKey === '') { + return $model->setJsApiKey(null); + } + + $jsApiKey = trim($jsApiKey); + + if (strlen($jsApiKey) !== 40) { + throw new SaveWidgetException(__('Invalid API key')); + } + + return $model->setJsApiKey($this->encryptor->encrypt($jsApiKey)); + } } diff --git a/Controller/Adminhtml/StoreWidget/Index.php b/Controller/Adminhtml/StoreWidget/Index.php index 27758c4..8eeeb8f 100755 --- a/Controller/Adminhtml/StoreWidget/Index.php +++ b/Controller/Adminhtml/StoreWidget/Index.php @@ -20,9 +20,11 @@ use Magento\Backend\App\Action\Context; use Magento\Framework\Controller\Result\JsonFactory; + use Psr\Log\LoggerInterface; use Tawk\Widget\Model\WidgetFactory; use Tawk\Widget\Helper\StringUtil; +use Tawk\Widget\Api\ConfigInterface; class Index extends \Magento\Backend\App\Action { @@ -96,7 +98,8 @@ public function __construct( * excludeurl: string, * donotdisplay: int, * includeurl: string, - * enableVisitorRecognition: int + * enableVisitorRecognition: int, + * jsApiKey: string * } */ public function execute() @@ -126,6 +129,11 @@ public function execute() $enableVisitorRecognition = $model->getEnableVisitorRecognition(); + $jsApiKey = $model->getJsApiKey(); + if (!empty($jsApiKey)) { + $jsApiKey = ConfigInterface::JS_API_KEY_NO_CHANGE; + } + return $response->setData([ 'success' => true, 'pageid' => $pageId, @@ -134,7 +142,8 @@ public function execute() 'excludeurl' => $excludeurl, 'donotdisplay' => $donotdisplay, 'includeurl' => $includeurl, - 'enableVisitorRecognition' => $enableVisitorRecognition + 'enableVisitorRecognition' => $enableVisitorRecognition, + 'jsApiKey' => $jsApiKey ]); } } diff --git a/Exception/SaveWidgetException.php b/Exception/SaveWidgetException.php new file mode 100644 index 0000000..d5651d7 --- /dev/null +++ b/Exception/SaveWidgetException.php @@ -0,0 +1,25 @@ + + @@ -18,5 +19,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/et comment="Comma-separated list of url patterns where widget should be displayed"/> + diff --git a/view/adminhtml/templates/selectwidget.phtml b/view/adminhtml/templates/selectwidget.phtml index c86dc0a..46a3110 100755 --- a/view/adminhtml/templates/selectwidget.phtml +++ b/view/adminhtml/templates/selectwidget.phtml @@ -147,7 +147,30 @@ +
+

Security Options

+
+

+ Note: If Secure Mode is enabled on your property, please enter + your Javascript API Key to ensure visitor recognition works correctly. +

+ + +
+
+
Save Settings +
+ Settings saved successfully +
+
+
diff --git a/view/adminhtml/web/css/tawk-widget-select.css b/view/adminhtml/web/css/tawk-widget-select.css index 9d2ea48..9f9536b 100644 --- a/view/adminhtml/web/css/tawk-widget-select.css +++ b/view/adminhtml/web/css/tawk-widget-select.css @@ -1,11 +1,6 @@ -.websiteids:focus { - box-shadow: 0 1px 1px rgba(0, 0, 0, .075) inset, 0 0 8px rgba(102, 175, 233, .6); - border-color: #66afe9; - outline: 0 none; -} - -.websiteids { - width: 350px; +input, +textarea, +select { background-color: #fff; background-image: none; border: 1px solid #ccc; @@ -13,11 +8,24 @@ box-shadow: 0 1px 1px rgba(0, 0, 0, .075) inset; color: #555; font-size: 14px; - height: 34px; line-height: 1.42857; padding: 6px 12px; - transition: border-color .15s ease-in-out 0s, box-shadow .15s ease-in-out 0s; +} +input, +select { + height: 34px; +} + +.websiteids:focus { + box-shadow: 0 1px 1px rgba(0, 0, 0, .075) inset, 0 0 8px rgba(102, 175, 233, .6); + border-color: #66afe9; + outline: 0 none; +} + +.websiteids { + width: 350px; + transition: border-color .15s ease-in-out 0s, box-shadow .15s ease-in-out 0s; } .websiteids-label { @@ -158,3 +166,12 @@ input:checked + .slider:before { .tawk-tooltip:hover .tawk-tooltiptext { visibility: visible; } + +.options-alert { + width: 50%; + font-weight: bold; + display: none; + padding: 10px; + border-radius: 5px; + margin: 5px 0; +} diff --git a/view/adminhtml/web/js/tawk-widget-select.js b/view/adminhtml/web/js/tawk-widget-select.js index 157d4e0..5f5185a 100644 --- a/view/adminhtml/web/js/tawk-widget-select.js +++ b/view/adminhtml/web/js/tawk-widget-select.js @@ -5,11 +5,11 @@ define(['jquery', 'jquery/ui'], function ($) { return function (config) { var domain = config.domain, - baseUrl = config.baseUrl, - removeWidgetUrl = config.removeWidgetUrl, - storedWidgetUrl = config.storedWidgetUrl, - formAction = config.form.action, - formKey = config.form.key; + baseUrl = config.baseUrl, + removeWidgetUrl = config.removeWidgetUrl, + storedWidgetUrl = config.storedWidgetUrl, + formAction = config.form.action, + formKey = config.form.key; function displayWidget(websiteId) { jQuery.get(storedWidgetUrl + '?id=' + websiteId, function (response) { @@ -43,15 +43,16 @@ define(['jquery', 'jquery/ui'], function ($) { } jQuery('#enable_visitor_recognition').prop('checked', response.enableVisitorRecognition === '1'); + jQuery('#js_api_key').val(response.jsApiKey); }); } function setWidget(e) { var alwaysdisplay = jQuery('#alwaysdisplay').is(':checked'), - alwaysdisplayvalue = alwaysdisplay ? 1 : 0, + alwaysdisplayvalue = alwaysdisplay ? 1 : 0, - donotdisplay = jQuery('#donotdisplay').is(':checked'), - donotdisplayvalue = donotdisplay ? 1 : 0; + donotdisplay = jQuery('#donotdisplay').is(':checked'), + donotdisplayvalue = donotdisplay ? 1 : 0; jQuery.post(formAction, { pageId : e.data.pageId, @@ -62,9 +63,14 @@ define(['jquery', 'jquery/ui'], function ($) { alwaysdisplay : alwaysdisplayvalue, donotdisplay: donotdisplayvalue, enableVisitorRecognition : jQuery('#enable_visitor_recognition').is(':checked') ? 1 : 0, + jsApiKey: jQuery('#js_api_key').val(), form_key : formKey - }, function () { - e.source.postMessage({action : 'setDone'}, baseUrl); + }, function (response) { + if (response.success) { + e.source.postMessage({action : 'setDone'}, baseUrl); + } else { + e.source.postMessage({action : 'setFail'}, baseUrl); + } }); } @@ -76,9 +82,9 @@ define(['jquery', 'jquery/ui'], function ($) { function saveVisibilityOptions(e) { var alwaysdisplay = jQuery('#alwaysdisplay').is(':checked'), - alwaysdisplayvalue = alwaysdisplay ? 1 : 0, - donotdisplay = jQuery('#donotdisplay').is(':checked'), - donotdisplayvalue = donotdisplay ? 1 : 0; + alwaysdisplayvalue = alwaysdisplay ? 1 : 0, + donotdisplay = jQuery('#donotdisplay').is(':checked'), + donotdisplayvalue = donotdisplay ? 1 : 0; e.preventDefault(); @@ -91,11 +97,14 @@ define(['jquery', 'jquery/ui'], function ($) { alwaysdisplay : alwaysdisplayvalue, donotdisplay: donotdisplayvalue, enableVisitorRecognition : jQuery('#enable_visitor_recognition').is(':checked') ? 1 : 0, + jsApiKey: jQuery('#js_api_key').val(), form_key : formKey - }, function () { - /* TODO: convert this to a different type of alert that doesn't use the browser alert func */ - /* eslint-disable-next-line no-alert */ - alert('Visibility options Saved'); + }, function (response) { + if (response.success) { + jQuery('#optionsSuccessMessage').toggle().delay(3000).fadeOut(); + } else { + jQuery('#optionsFailureMessage').text(response.message).toggle().delay(3000).fadeOut(); + } }); } diff --git a/view/frontend/templates/embed.phtml b/view/frontend/templates/embed.phtml index e25c3f4..e1f56e8 100755 --- a/view/frontend/templates/embed.phtml +++ b/view/frontend/templates/embed.phtml @@ -35,6 +35,9 @@ "visitor" : { "name" : "escapeJs($customer_details['name']); ?>", "email" : "escapeJs($customer_details['email']); ?>" + + , "hash" : "escapeJs($customer_details['hash']); ?>" + } } } diff --git a/view/frontend/web/js/tawk-embed.js b/view/frontend/web/js/tawk-embed.js index 8951ae0..c9f470e 100644 --- a/view/frontend/web/js/tawk-embed.js +++ b/view/frontend/web/js/tawk-embed.js @@ -3,9 +3,9 @@ define([], function () { return function (config) { var visitor = config.visitor, - embedUrl = config.embedUrl, - /* eslint-disable-next-line no-unused-vars */ - Tawk_LoadStart = new Date(); + embedUrl = config.embedUrl, + /* eslint-disable-next-line no-unused-vars */ + Tawk_LoadStart = new Date(); window.Tawk_API = window.Tawk_API || {}; @@ -14,6 +14,10 @@ define([], function () { name : visitor.name, email : visitor.email }; + + if (visitor.hash) { + window.Tawk_API.visitor.hash = visitor.hash; + } } (function () {