Skip to content

Feature/secure mode #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Api/ConfigInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php
/**
* Tawk.to
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to [email protected] so we can send you a copy immediately.
*
* @copyright Copyright (c) 2024 Tawk.to
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
*/

namespace Tawk\Widget\Api;

interface ConfigInterface
{
const JS_API_KEY_NO_CHANGE = 'no_change';
}
67 changes: 65 additions & 2 deletions Block/Embed.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@
use Magento\Framework\View\Element\Template;
use Magento\Framework\Escaper;
use Magento\Customer\Model\SessionFactory;
use Magento\Framework\Encryption\EncryptorInterface;
use Magento\Framework\Exception\LocalizedException;

use Tawk\Modules\UrlPatternMatcher;
use Tawk\Widget\Model\WidgetFactory;

class Embed extends Template
{
public const TAWKTO_VISITOR_SESSION = 'TAWKTO_VISITOR_SESSION';

/**
* Tawk.to Widget Model instance
*
Expand Down Expand Up @@ -76,20 +80,29 @@ class Embed extends Template
*/
protected $escaper;

/**
* Encryptor instance
*
* @var EncryptorInterface $encryptor
*/
protected $encryptor;

/**
* Constructor
*
* @param SessionFactory $sessionFactory Session Factory instance
* @param WidgetFactory $modelFactory Tawk.to Widget Model instance
* @param Template\Context $context Template Context
* @param Escaper $escaper Escaper instance
* @param EncryptorInterface $encryptor Encryptor instance
* @param array $data Template data
*/
public function __construct(
SessionFactory $sessionFactory,
WidgetFactory $modelFactory,
Template\Context $context,
Escaper $escaper,
EncryptorInterface $encryptor,
array $data = []
) {
parent::__construct($context, $data);
Expand All @@ -100,6 +113,7 @@ public function __construct(
$this->request = $context->getRequest();
$this->modelSessionFactory = $sessionFactory->create();
$this->escaper = $escaper;
$this->encryptor = $encryptor;
}

/**
Expand Down Expand Up @@ -146,7 +160,8 @@ private function getWidgetModel()
*
* @return array {
* name: string,
* email: string
* email: string,
* hash: string
* }
*/
public function getCurrentCustomerDetails()
Expand All @@ -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);
Comment on lines +219 to +221
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Additional error handling needed for decryption.

The decrypt operation could fail and throw an exception. Consider adding specific error handling for the decryption operation.

- $jsApiKey = $this->encryptor->decrypt($encryptedJsApiKey);
+ try {
+     $jsApiKey = $this->encryptor->decrypt($encryptedJsApiKey);
+ } catch (\Exception $e) {
+     $this->logger->error('Failed to decrypt JS API key: ' . $e->getMessage());
+     return null;
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$jsApiKey = $this->encryptor->decrypt($encryptedJsApiKey);
$hash = hash_hmac('sha256', $email, $jsApiKey);
try {
$jsApiKey = $this->encryptor->decrypt($encryptedJsApiKey);
} catch (\Exception $e) {
$this->logger->error('Failed to decrypt JS API key: ' . $e->getMessage());
return null;
}
$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.
*/
Expand Down
60 changes: 57 additions & 3 deletions Controller/Adminhtml/SaveWidget/Index.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -61,6 +66,13 @@ class Index extends \Magento\Backend\App\Action
*/
protected $helper;

/**
* Encryptor instance
*
* @var EncryptorInterface $encryptor
*/
protected $encryptor;

/**
* Constructor
*
Expand All @@ -69,20 +81,23 @@ 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;
$this->logger = $logger;
$this->modelWidgetFactory = $modelFactory->create();
$this->request = $this->getRequest();
$this->helper = $helper;
$this->encryptor = $encryptor;
}

/**
Expand All @@ -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);

Expand All @@ -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));
}
}
13 changes: 11 additions & 2 deletions Controller/Adminhtml/StoreWidget/Index.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -96,7 +98,8 @@ public function __construct(
* excludeurl: string,
* donotdisplay: int,
* includeurl: string,
* enableVisitorRecognition: int
* enableVisitorRecognition: int,
* jsApiKey: string
* }
*/
public function execute()
Expand Down Expand Up @@ -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,
Expand All @@ -134,7 +142,8 @@ public function execute()
'excludeurl' => $excludeurl,
'donotdisplay' => $donotdisplay,
'includeurl' => $includeurl,
'enableVisitorRecognition' => $enableVisitorRecognition
'enableVisitorRecognition' => $enableVisitorRecognition,
'jsApiKey' => $jsApiKey
]);
}
}
25 changes: 25 additions & 0 deletions Exception/SaveWidgetException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php
/**
* Tawk.to
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to [email protected] so we can send you a copy immediately.
*
* @copyright Copyright (c) 2024 Tawk.to
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
*/

namespace Tawk\Widget\Exception;

use Magento\Framework\Exception\LocalizedException;

class SaveWidgetException extends LocalizedException
{
}
19 changes: 19 additions & 0 deletions build/build-package.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/sh

Comment on lines +1 to +2
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Proper Shebang & Error Handling Consideration

The script begins with a proper shebang (#!/bin/sh). Consider adding error handling options (e.g., set -e, set -u, and/or set -o pipefail) immediately after the shebang to ensure the script exits on failure, which can help avoid silent failures in the build process.

composer run clean

mkdir -p ./tmp/tawkmagento2
cp -r ./view ./tmp/tawkmagento2
cp -r ./etc ./tmp/tawkmagento2
cp -r ./Setup ./tmp/tawkmagento2
cp -r ./Model ./tmp/tawkmagento2
cp -r ./Controller ./tmp/tawkmagento2
cp -r ./Block ./tmp/tawkmagento2
cp -r ./Helper ./tmp/tawkmagento2
cp -r ./Api ./tmp/tawkmagento2
cp -r ./Exception ./tmp/tawkmagento2
cp ./registration.php ./tmp/tawkmagento2
cp ./composer.json ./tmp/tawkmagento2
cp README.md ./tmp/tawkmagento2

(cd ./tmp && zip -9 -rq ./tawkmagento2.zip ./tawkmagento2)
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"build:prod": "composer install --no-dev",
"lint": "phpcs -p -s -v --runtime-set ignore_warnings_on_exit true .",
"lint:fix": "phpcbf -p -s -v .; err=$?; if [ $err -eq 1 ]; then exit 0; else exit $err; fi;",
"package": "composer run clean && mkdir -p ./tmp/tawkmagento2 && cp -r ./view ./tmp/tawkmagento2 && cp -r ./etc ./tmp/tawkmagento2 && cp -r ./Setup ./tmp/tawkmagento2 && cp -r ./Model ./tmp/tawkmagento2 && cp -r ./Controller ./tmp/tawkmagento2 && cp -r ./Block ./tmp/tawkmagento2 && cp -r ./Helper ./tmp/tawkmagento2 && cp ./registration.php ./tmp/tawkmagento2 && cp ./composer.json ./tmp/tawkmagento2 && cp README.md ./tmp/tawkmagento2 && (cd ./tmp && zip -9 -rq ./tawkmagento2.zip ./tawkmagento2)",
"package": "./build/build-package.sh",
"clean": "rm -rf ./tmp"
},
"repositories": {
Expand Down
2 changes: 2 additions & 0 deletions etc/db_schema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/et
<constraint xsi:type="primary" referenceId="PRIMARY">
<column name="id"/>
</constraint>
<column name="config_version" xsi:type="int" unsigned="true" nullable="false" default="0" comment="Config version"/>
<column name="for_store_id" xsi:type="int" unsigned="true" nullable="false" comment="Store View ID"/>
<column name="page_id" xsi:type="varchar" length="255" nullable="true" comment="Tawk.to Page ID"/>
<column name="widget_id" xsi:type="varchar" length="255" nullable="true" comment="Tawk.to Widget ID"/>
Expand All @@ -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"/>
<column name="enable_visitor_recognition" xsi:type="smallint" unsigned="true" nullable="false" default="1"
comment="Enable visitor recognition feature"/>
<column name="js_api_key" xsi:type="text" nullable="true" comment="JS API key"/>
</table>
</schema>
Loading