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 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
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
182 changes: 150 additions & 32 deletions prestashop1.7/controllers/admin/AdminTawktoController.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,20 @@
exit;
}

/**
* Tawkto exception
*/
class TawktoException extends Exception
{
}

/**
* Admin settings controller
*/
class AdminTawktoController extends ModuleAdminController
{
public const NO_CHANGE = 'nochange';

/**
* __construct
*
Expand Down Expand Up @@ -87,11 +96,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 ($widgetOpts && !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 +126,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 +234,54 @@ 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) {
if ($e instanceof TawktoException) {
die(json_encode(['success' => false, 'message' => $e->getMessage()]));
}

die(json_encode(['success' => false, 'message' => 'An error occurred while saving options']));
}

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

if (!isset($jsonOpts['config_version'])) {
$jsonOpts['config_version'] = 0;
} else {
++$jsonOpts['config_version'];
}

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

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

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

Expand All @@ -239,40 +298,99 @@ 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($params)) {
return $jsonOpts;
}

parse_str($params, $options);
foreach ($options as $column => $value) {
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;
}
}

$value = trim($value);

if (strlen($value) !== 40) {
throw new TawktoException('Invalid API key.');
}

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

throw new TawktoException('Error saving Javascript API Key.');
}

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;
}
}
108 changes: 103 additions & 5 deletions prestashop1.7/tawkto.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Tawkto extends Module
public const TAWKTO_WIDGET_OPTS = 'TAWKTO_WIDGET_OPTS';
public const TAWKTO_WIDGET_USER = 'TAWKTO_WIDGET_USER';
public const TAWKTO_SELECTED_WIDGET = 'TAWKTO_SELECTED_WIDGET';
public const TAWKTO_VISITOR_SESSION = 'TAWKTO_VISITOR_SESSION';

/**
* __construct
Expand Down Expand Up @@ -110,7 +111,11 @@ public function hookDisplayFooter()
$widgetId = $current_widget['widget_id'];

$result = Configuration::get(self::TAWKTO_WIDGET_OPTS);
$enable_visitor_recognition = true; // default value
// default values
$enable_visitor_recognition = true;
$js_api_key = '';
$config_version = 0;

if ($result) {
$options = json_decode($result);
$current_page = (string) $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
Expand All @@ -119,6 +124,14 @@ public function hookDisplayFooter()
$enable_visitor_recognition = $options->enable_visitor_recognition;
}

if (isset($options->js_api_key)) {
$js_api_key = $options->js_api_key;
}

if (isset($options->config_version)) {
$config_version = $options->config_version;
}

// prepare visibility
if (false == $options->always_display) {
// show on specified urls
Expand Down Expand Up @@ -168,19 +181,23 @@ public function hookDisplayFooter()
}

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

$hash = $this->getVisitorHash($customer_email, $js_api_key, $config_version);
}

$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 +300,85 @@ private function getArrayFromJson($data)

return $arr;
}

/**
* Get visitor hash
*
* @param string $email Visitor email
* @param string $js_api_key JS API key
* @param int $config_version Config version
*
* @return string|null
*/
private function getVisitorHash(string $email, string $js_api_key, int $config_version)
{
if (empty($js_api_key)) {
return null;
}

if (session_status() === PHP_SESSION_NONE && !headers_sent()) {
session_start();
}

if (isset($_SESSION[self::TAWKTO_VISITOR_SESSION])) {
$current_session = $_SESSION[self::TAWKTO_VISITOR_SESSION];

if (isset($current_session['hash'])
&& $current_session['email'] === $email
&& $current_session['config_version'] === $config_version) {
return $current_session['hash'];
}
}

try {
$key = $this->getDecryptedData($js_api_key);
} catch (Exception $e) {
error_log($e->getMessage());

return null;
}

$hash = hash_hmac('sha256', $email, $key);

$_SESSION[self::TAWKTO_VISITOR_SESSION] = [
'hash' => $hash,
'email' => $email,
'config_version' => $config_version,
];

return $hash;
}

/**
* Decrypt data
*
* @param string $data Data to decrypt
*
* @return string Decrypted data
*
* @throws Exception error decrypting data
*/
private function getDecryptedData(string $data)
{
if (!defined('_COOKIE_KEY_')) {
throw new Exception('Cookie key not defined');
}

$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', _COOKIE_KEY_, 0, $iv);

if ($decrypted_data === false) {
throw new Exception('Failed to decrypt data');
}

return $decrypted_data;
}
}
Loading