Skip to content

Feature/secure mode #45

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 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ docker/bin
.phpcs.cache
node_modules/
.php_cs_fixer.cache
.aider*
2 changes: 2 additions & 0 deletions .phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
<rule ref="Squiz.Commenting.FunctionComment">
<exclude name="Squiz.Commenting.FunctionComment.ParamCommentFullStop"/>
<exclude name="Squiz.Commenting.FunctionComment.ParamCommentNotCapital"/>
<exclude name="Squiz.Commenting.FunctionComment.ThrowsNoFullStop"/>
<exclude name="Squiz.Commenting.FunctionComment.ThrowsNotCapital"/>
<exclude name="Squiz.Commenting.FunctionComment.SpacingAfterParamName"/>
<exclude name="Squiz.Commenting.FunctionComment.SpacingAfterParamType"/>
</rule>
Expand Down
162 changes: 130 additions & 32 deletions prestashop1.7/controllers/admin/AdminTawktoController.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
*/
class AdminTawktoController extends ModuleAdminController
{
public const NO_CHANGE = 'nochange';

/**
* __construct
*
Expand Down Expand Up @@ -87,11 +89,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 (!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);
Expand All @@ -113,7 +119,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,
Expand Down Expand Up @@ -221,8 +227,42 @@ 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) {
die(json_encode(['success' => false, 'message' => $e->getMessage()]));
}

// Override current options/fallback if not selected
$currentOpts = Configuration::get($key);
if (!empty($currentOpts)) {
$currentOpts = json_decode($currentOpts, true);
$jsonOpts = array_merge($currentOpts, $jsonOpts);
}

Configuration::updateValue($key, json_encode($jsonOpts));

die(json_encode(['success' => true]));
}

/**
* Process options
*
* @param string $options Selected options
*
* @return array
*
* @throws Exception error processing options
*/
private function processSetOptions(string $options): array
{
// default options
$jsonOpts = [
'always_display' => false,

Expand All @@ -239,40 +279,98 @@ 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($options)) {
return $jsonOpts;
}

$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);
}
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;
}
}

try {
if (strlen(trim($value)) !== 40) {
throw new Exception('Invalid API key. Please provide value with 40 characters');
}

$jsonOpts['js_api_key'] = $this->encryptData($value);
} catch (Exception $e) {
unset($jsonOpts['js_api_key']);

throw new Exception('Javascript API Key: ' . $e->getMessage());
}

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;
}
}
61 changes: 57 additions & 4 deletions prestashop1.7/tawkto.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,19 +168,29 @@ public function hookDisplayFooter()
}

// add customer details as visitor info
$customer_name = null;
$customer_email = null;
$customer_name = '';
$customer_email = '';
$hash = '';
if ($enable_visitor_recognition && !is_null($this->context->customer->id)) {
$customer = $this->context->customer;
$customer_name = $customer->firstname . ' ' . $customer->lastname;
$customer_email = $customer->email;

if (!empty($options->js_api_key)) {
$key = $this->getJsApiKey($options->js_api_key);

if (!empty($key)) {
$hash = hash_hmac('sha256', $customer_email, $key);
}
}
}

$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');
Expand Down Expand Up @@ -283,4 +293,47 @@ private function getArrayFromJson($data)

return $arr;
}

/**
* Retrieve JS API key
*
* @param string $js_api_key Encrypted JS API key
*
* @return string
*/
private function getJsApiKey(string $js_api_key)
{
// Cache::store & Cache::retrieve are not persistent

$key = $this->getDecryptedData($js_api_key);

return $key;
}

/**
* Decrypt data
*
* @param string $data Data to decrypt
*
* @return string
*/
private function getDecryptedData(string $data)
{
$decoded = base64_decode($data);

if ($decoded === false) {
return '';
}

$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) {
return '';
}

return $decrypted_data;
}
}
Loading