Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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 .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use-strict';

module.exports = {
'root': true,
'env': {
'browser': true,
'jquery': true
Expand Down
20 changes: 20 additions & 0 deletions tawkto/assets/js/tawk.admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,26 @@ jQuery(

jQuery( this ).addClass( 'reverse' );
});

if ( jQuery( '#enable-visitor-recognition' ).prop( 'checked' ) ) {
jQuery( '.tawk-selected-visitor' ).show();
jQuery( '#js-api-key' ).prop( 'disabled', false );
} else {
jQuery( '.tawk-selected-visitor' ).hide();
jQuery( '#js-api-key' ).prop( 'disabled', true );
}

jQuery( '#enable-visitor-recognition' ).change(
function() {
if ( this.checked ) {
jQuery( '.tawk-selected-visitor' ).fadeIn();
jQuery( '#js-api-key' ).prop( 'disabled', false );
} else {
jQuery( '.tawk-selected-visitor' ).fadeOut();
jQuery( '#js-api-key' ).prop( 'disabled', true );
}
}
);
}
);

Expand Down
1 change: 1 addition & 0 deletions tawkto/includes/default_config.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@
'display_on_productpage' => 0,
'display_on_producttag' => 0,
'enable_visitor_recognition' => 1,
'js_api_key' => '',
),
);
121 changes: 120 additions & 1 deletion tawkto/tawkto.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ class TawkTo_Settings {
const TAWK_VISIBILITY_OPTIONS = 'tawkto-visibility-options';
const TAWK_ACTION_SET_WIDGET = 'tawkto-set-widget';
const TAWK_ACTION_REMOVE_WIDGET = 'tawkto-remove-widget';
const CIPHER = 'AES-256-CBC';
const CIPHER_IV_LENGTH = 16;
const NO_CHANGE = 'nochange';
const TAWK_API_KEY = 'tawkto-js-api-key';

/**
* @var $plugin_ver Plugin version
Expand Down Expand Up @@ -249,12 +253,17 @@ public function validate_options( $input ) {
$visibility_text_fields = array(
'excluded_url_list',
'included_url_list',
'js_api_key',
);

self::validate_visibility_toggle_fields( $input, $visibility_toggle_fields );
self::validate_text_fields( $input, $visibility_text_fields );
self::validate_js_api_key( $input );

return $input;
$visibility = get_option( self::TAWK_VISIBILITY_OPTIONS, array() );
$visibility = array_merge( $visibility, $input );

return $visibility;
}

/**
Expand Down Expand Up @@ -303,6 +312,10 @@ public function create_plugin_settings_page() {
}
}

if ( ! empty( $visibility['js_api_key'] ) ) {
$visibility['js_api_key'] = self::NO_CHANGE;
}

include sprintf( '%s/templates/settings.php', dirname( __FILE__ ) );
}

Expand All @@ -318,6 +331,23 @@ public static function ids_are_correct( $page_id, $widget_id ) {
return 1 === preg_match( '/^[0-9A-Fa-f]{24}$/', $page_id ) && 1 === preg_match( '/^[a-z0-9]{1,50}$/i', $widget_id );
}

/**
* Validate JS API Key field
*
* @param array $fields - List of fields.
* @return void
*/
private static function validate_js_api_key( &$fields ) {
if ( self::NO_CHANGE === $fields['js_api_key'] ) {
unset( $fields['js_api_key'] );
return;
}

$fields['js_api_key'] = 40 === strlen( $fields['js_api_key'] ) ? self::get_encrypted_data( $fields['js_api_key'] ) : '';

delete_transient( self::TAWK_API_KEY );
}

/**
* Validates and sanitizes text fields
*
Expand Down Expand Up @@ -365,6 +395,87 @@ public static function get_default_visibility_options() {
return $config['visibility'];
}

/**
* Encrypt data
*
* @param string $data - Data to be encrypted.
* @return string
*/
private static function get_encrypted_data( $data ) {
if ( ! defined( 'SECURE_AUTH_KEY' ) ) {
return '';
}

try {
$iv = random_bytes( self::CIPHER_IV_LENGTH );
} catch ( Exception $e ) {
return '';
}

$encrypted_data = openssl_encrypt( $data, self::CIPHER, SECURE_AUTH_KEY, 0, $iv );

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

// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
$encrypted_data = base64_encode( $iv . $encrypted_data );

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

return $encrypted_data;
}

/**
* Decrypt data
*
* @param string $data - Data to be decrypted.
* @return string
*/
private static function get_decrypted_data( $data ) {
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
$decoded_data = base64_decode( $data );

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

$iv = substr( $decoded_data, 0, self::CIPHER_IV_LENGTH );
$encrypted_data = substr( $decoded_data, self::CIPHER_IV_LENGTH );

$decrypted_data = openssl_decrypt( $encrypted_data, self::CIPHER, SECURE_AUTH_KEY, 0, $iv );

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

return $decrypted_data;
}

/**
* Retrieves JS API Key
*
* @return string
*/
public static function get_js_api_key() {
if ( ! empty( get_transient( self::TAWK_API_KEY ) ) ) {
return get_transient( self::TAWK_API_KEY );
}

$visibility = get_option( self::TAWK_VISIBILITY_OPTIONS );

if ( ! isset( $visibility['js_api_key'] ) ) {
return '';
}

$key = self::get_decrypted_data( $visibility['js_api_key'] );

set_transient( self::TAWK_API_KEY, $key, 60 * 60 );

return $key;
}
}
}

Expand Down Expand Up @@ -442,6 +553,8 @@ public static function deactivate() {
delete_option( TawkTo_Settings::TAWK_WIDGET_ID_VARIABLE );
delete_option( TawkTo_Settings::TAWK_VISIBILITY_OPTIONS );
delete_option( self::PLUGIN_VERSION_VARIABLE );

delete_transient( TawkTo_Settings::TAWK_API_KEY );
}

/**
Expand All @@ -463,6 +576,12 @@ public function get_current_customer_details() {
'name' => $current_user->display_name,
'email' => $current_user->user_email,
);

$js_api_key = TawkTo_Settings::get_js_api_key();
if ( ! empty( $user_info['email'] ) && ! empty( $js_api_key ) ) {
$user_info['hash'] = hash_hmac( 'sha256', $user_info['email'], $js_api_key );
}

return wp_json_encode( $user_info );
}
return null;
Expand Down
23 changes: 23 additions & 0 deletions tawkto/templates/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,29 @@ class="slider round"
</td>
</tr>
</table>

<div class="tawk-selected-visitor">
<p class='tawk-notice'>
<?php esc_html_e( 'Note: If Secure Mode is enabled on your property, please enter your Javascript API Key to ensure visitor recognition works correctly.', 'tawk-to-live-chat' ); ?>
</p>

<table class="form-table">
<tr valign="top">
<th class="tawk-setting" scope="row">
<?php esc_html_e( 'Javascript API Key', 'tawk-to-live-chat' ); ?>
</th>
<td>
<input type="password"
id="js-api-key"
name="tawkto-visibility-options[js_api_key]"
value="<?php echo esc_attr( $visibility['js_api_key'] ); ?>"
pattern="^[a-zA-Z0-9]+$"
onfocus="this.select();"
autocomplete="off" />
</td>
</tr>
</table>
</div>
</div>
</form>
</div>
Expand Down
25 changes: 25 additions & 0 deletions tests/Coverages/PrivacyOptionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public static function setupBeforeClass(): void {

public function setup(): void {
self::$web->login();
self::$web->goto_privacy_options();
}

public static function tearDownAfterClass(): void {
Expand All @@ -60,6 +61,7 @@ public function should_have_data_on_visitor_object_if_logged_in(): void {
$this->assertNotEmpty( $visitor_data );
$this->assertEquals( self::$admin_name, $visitor_data['name'] );
$this->assertEquals( self::$admin_email, $visitor_data['email'] );
$this->assertArrayNotHasKey( 'hash', $visitor_data );
}

/**
Expand All @@ -76,4 +78,27 @@ public function should_not_have_data_on_visitor_object_if_not_logged_in(): void

$this->assertEmpty( $visitor_data );
}

/**
* @test
* @group privacy_options
*/
public function should_have_hash_on_visitor_object_if_logged_in_and_api_key_provided(): void {
self::$web->toggle_switch( '#enable-visitor-recognition', true );
self::$driver->find_element_and_input( '#js-api-key', str_repeat( 'a', 40 ) );

self::$driver->move_mouse_to( '#submit-header' )->click();
self::$driver->wait_for_seconds( 1 );
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace arbitrary wait with explicit wait condition.

Using a fixed wait time can make tests flaky. Instead, wait for a specific condition to be met.

-self::$driver->move_mouse_to('#submit-header')->click();
-self::$driver->wait_for_seconds(1);
+self::$driver->move_mouse_to('#submit-header')->click();
+self::$driver->wait_until_element_is_located('#setting-error-settings_updated');
📝 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
self::$driver->move_mouse_to( '#submit-header' )->click();
self::$driver->wait_for_seconds( 1 );
self::$driver->move_mouse_to('#submit-header')->click();
self::$driver->wait_until_element_is_located('#setting-error-settings_updated');


self::$driver->goto_page( self::$web->get_base_url() );

self::$driver->wait_until_element_is_located( self::$script_selector );

$visitor_data = self::$driver->get_driver()->executeScript( 'return Tawk_API.visitor' );

$this->assertNotEmpty( $visitor_data );
$this->assertEquals( self::$admin_name, $visitor_data['name'] );
$this->assertEquals( self::$admin_email, $visitor_data['email'] );
$this->assertArrayHasKey( 'hash', $visitor_data );
}
}
Loading