diff --git a/inc/cleantalk-common.php b/inc/cleantalk-common.php index d60727c53..50f724cf2 100644 --- a/inc/cleantalk-common.php +++ b/inc/cleantalk-common.php @@ -1877,12 +1877,10 @@ function apbct__bot_detector_get_fd_log() $result = array( 'plugin_status' => 'OK', 'error_msg' => '', - 'frontend_data_log' => '' + 'apbct_browser_state' => apbct__bot_detector_get_browser_state_default(), ); - // Initialize result array with default values if (Constant::is(Constant::APBCT_SERVICE__DO_NOT_COLLECT_FRONTEND_DATA_LOGS)) { - $result['plugin_status'] = 'OK'; $result['error_msg'] = 'bot detector logs collection is disabled via constant definition'; return json_encode($result); } @@ -1891,28 +1889,81 @@ function apbct__bot_detector_get_fd_log() if ( ! apbct__is_bot_detector_enabled() ) { throw new \Exception('bot detector library usage is disabled'); } - // Retrieve bot detector frontend data log from Alt Sessions - $alt_sessions_fd_log = AltSessions::get('ct_bot_detector_frontend_data_log'); - // Check if the retrieved data is a string - if ( !is_string($alt_sessions_fd_log) || '' === $alt_sessions_fd_log ) { - throw new \Exception('no log found in alt sessions'); - } - // Encode the retrieved data to JSON format - $param_bot_detector_fd_log = json_decode($alt_sessions_fd_log, true); - // Check if the JSON encoding was successful - if ( empty($param_bot_detector_fd_log) ) { - throw new \Exception('can not decode data from alt sessions'); + + $browser_state = apbct__bot_detector_get_browser_state(); + + if ( empty($browser_state) ) { + throw new \Exception('no browser state provided by the transport'); } + + $result['apbct_browser_state'] = $browser_state; } catch (Exception $e) { $result['plugin_status'] = 'ERROR'; $result['error_msg'] = $e->getMessage(); - return json_encode($result); } - $result['frontend_data_log'] = $param_bot_detector_fd_log; + // Return the result as a JSON encoded string return json_encode($result); } +/** + * Default browser state - nothing is known about the service in the visitor browser. + * + * @return array + */ +function apbct__bot_detector_get_browser_state_default() +{ + return array( + 'frontend_data_log' => '', + 'botd_logic_loaded' => 0, + 'botd_wrapper_loaded' => 0, + ); +} + +/** + * Get the browser state gathered by the plugin JS. + * + * The state comes with the transport the site is currently configured to use: + * NoCookie hidden field or alternative sessions are both covered by RequestParameters, + * the XHR interception passes the state as a plain POST field. + * Native cookies do not store the state in cookies (cookie size limits), but the JS interceptors may still POST it. + * + * @return array Empty array if no state provided. + */ +function apbct__bot_detector_get_browser_state() +{ + $raw_state = RequestParameters::get('apbct_browser_state'); + + // XHR interception transport - look at the POST directly + if ( empty($raw_state) ) { + $raw_state = Post::get('apbct_browser_state'); + } + + // XHR interception transport - the state could be wrapped to the data[] array + if ( empty($raw_state) ) { + $post_data = Post::get('data'); + if ( is_array($post_data) && ! empty($post_data['apbct_browser_state']) ) { + $raw_state = $post_data['apbct_browser_state']; + } + } + + if ( empty($raw_state) ) { + return array(); + } + + $state = is_string($raw_state) ? json_decode($raw_state, true) : $raw_state; + + if ( ! is_array($state) ) { + return array(); + } + + return array( + 'frontend_data_log' => TT::getArrayValueAsString($state, 'frontend_data_log'), + 'botd_logic_loaded' => TT::getArrayValueAsInt($state, 'botd_logic_loaded'), + 'botd_wrapper_loaded' => TT::getArrayValueAsInt($state, 'botd_wrapper_loaded'), + ); +} + function apbct__bot_detector_get_custom_exclusion_from_settings() { global $apbct; diff --git a/js/prebuild/apbct-public-bundle.js b/js/prebuild/apbct-public-bundle.js index f8fa38be9..b1054010b 100644 --- a/js/prebuild/apbct-public-bundle.js +++ b/js/prebuild/apbct-public-bundle.js @@ -2642,6 +2642,15 @@ function getNoCookieData() { // eslint-disable-line no-unused-vars let noCookieDataLocal = apbctLocalStorage.getCleanTalkData(); let noCookieDataSession = apbctSessionStorage.getCleanTalkData(); let noCookieData = {...noCookieDataLocal, ...noCookieDataSession}; + + // Bot detector browser state is transferred with the hidden field on the NoCookie mode + if (typeof apbctGetBrowserStatePair === 'function') { + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + noCookieData[browserState.key] = browserState.value; + } + } + noCookieData = JSON.stringify(noCookieData); return '_ct_no_cookie_data_' + btoa(unescape(encodeURIComponent(noCookieData))); @@ -2669,7 +2678,21 @@ function getCleanTalkStorageDataArray() { // eslint-disable-line no-unused-vars noCookieDataFromUserActivity = {collecting_user_activity_data: collectingUserActivityData}; } - return {...noCookieDataLocal, ...noCookieDataSession, ...noCookieDataTypo, ...noCookieDataFromUserActivity}; + let browserStateData = {}; + if (typeof apbctGetBrowserStatePair === 'function') { + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + browserStateData[browserState.key] = browserState.value; + } + } + + return { + ...noCookieDataLocal, + ...noCookieDataSession, + ...noCookieDataTypo, + ...noCookieDataFromUserActivity, + ...browserStateData, + }; } /** @@ -3314,6 +3337,9 @@ class ApbctHandler { const eventTokenKey = useUnwrappedCleantalkKeys ? 'ct_bot_detector_event_token' : 'data%5Bct_bot_detector_event_token%5D'; + const browserStateKey = useUnwrappedCleantalkKeys ? + 'apbct_browser_state' : + 'data%5Bapbct_browser_state%5D'; if (!( +ctPublic.bot_detector_enabled && @@ -3338,6 +3364,12 @@ class ApbctHandler { } } + const browserState = apbctGetBrowserStatePair(); + if (browserState && body.indexOf('apbct_browser_state') === -1) { + addidionalCleantalkData += '&' + browserStateKey + '=' + + encodeURIComponent(browserState.value); + } + body += addidionalCleantalkData; } @@ -3354,6 +3386,11 @@ class ApbctHandler { body.append('ct_bot_detector_event_token', eventToken); } } + + const browserState = apbctGetBrowserStatePair(); + if (browserState && !body.has(browserState.key)) { + body.append(browserState.key, browserState.value); + } } if (isNeedToAddCleantalkDataCheckJsonData) { @@ -3391,6 +3428,22 @@ class ApbctHandler { } } } + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + try { + const bodyObj = JSON.parse(body); + if ( + bodyObj && typeof bodyObj === 'object' && + !Object.prototype.hasOwnProperty.call(bodyObj, browserState.key) + ) { + bodyObj[browserState.key] = browserState.value; + body = JSON.stringify(bodyObj); + } + } catch (e) { + // body is not JSON — leave as is + } + } } return originalSend.apply(this, [body]); @@ -3448,6 +3501,11 @@ class ApbctHandler { } else { args[1].body.append('ct_no_cookie_hidden_field', getNoCookieData()); } + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + args[1].body.append(browserState.key, browserState.value); + } } } } catch (e) { @@ -3504,6 +3562,22 @@ class ApbctHandler { * @return {string|FormData} Modified body. */ const attachFieldsToBody = function(body, fieldPair = false) { + const fieldPairs = [fieldPair, apbctGetBrowserStatePair()]; + + for (const pair of fieldPairs) { + body = attachFieldPairToBody(body, pair); + } + + return body; + }; + + /** + * Attach a single key/value pair to fetch request body + * @param {string|FormData} body Fetch request data body. + * @param {object|bool} fieldPair Key value to inject. + * @return {string|FormData} Modified body. + */ + const attachFieldPairToBody = function(body, fieldPair = false) { if (fieldPair) { if (body instanceof FormData || typeof body.append === 'function') { body.append(fieldPair.key, fieldPair.value); @@ -3800,11 +3874,15 @@ class ApbctHandler { const batchPayload = JSON.parse(args[1].body); if (batchPayload.requests && Array.isArray(batchPayload.requests)) { const fieldPair = selectFieldsData(+ctPublic.bot_detector_enabled); + const browserState = apbctGetBrowserStatePair(); for (const req of batchPayload.requests) { const isAddItem = req.path === '/wc/store/v1/cart/add-item'; if (isAddItem && req.body && fieldPair && fieldPair.key) { req.body[fieldPair.key] = fieldPair.value; } + if (isAddItem && req.body && browserState) { + req.body[browserState.key] = browserState.value; + } } args[1].body = JSON.stringify(batchPayload); } @@ -4053,6 +4131,16 @@ class ApbctHandler { } } + // Bot detector browser state + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + if (sourceSign.keepUnwrapped) { + ajaxDataFormData.append(browserState.key, browserState.value); + } else { + ajaxDataFormData.append('data[' + browserState.key + ']', browserState.value); + } + } + // Visible fields if (sourceSign.attachVisibleFieldsData) { let visibleFieldsSearchResult = false; @@ -4111,6 +4199,15 @@ class ApbctHandler { let eventToken = ''; let noCookieData = ''; let visibleFieldsString = ''; + let browserStateString = ''; + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + const browserStateKey = sourceSign.keepUnwrapped ? + 'apbct_browser_state' : + 'data%5Bapbct_browser_state%5D'; + browserStateString = browserStateKey + '=' + encodeURIComponent(browserState.value) + '&'; + } if ( +ctPublic.bot_detector_enabled && @@ -4152,7 +4249,7 @@ class ApbctHandler { } } - return noCookieData + eventToken + visibleFieldsString + ajaxDataString; + return noCookieData + eventToken + browserStateString + visibleFieldsString + ajaxDataString; } /** @@ -4888,67 +4985,78 @@ if (document.readyState !== 'loading') { */ new ApbctHandler().cronFormsHandler(2000); -let botDetectorLogLastUpdate = 0; -let botDetectorLogEventTypesCollected = []; - -// bot_detector frontend_data log alt session saving cron -if ( - ctPublicFunctions.hasOwnProperty('bot_detector_enabled') && - +ctPublicFunctions.bot_detector_enabled && - ctPublicFunctions.hasOwnProperty('data__frontend_data_log_enabled') && - ctPublicFunctions.data__frontend_data_log_enabled == 1 -) { - sendBotDetectorLogToAltSessions(1000); -} - /** - * Send BotDetector logs data to alternative sessions. - * If log_last_update has changed and log contains new event types, the log will be sent to the alternative sessions. - * @param {int} cronStartTimeout delay before cron start - * @param {int} interval check fires on interval + * Bot detector browser state. + * + * The state is passed to the site backend with the current JS->PHP transport and goes + * to the moderate request as sender_info.bot_detector_frontend_data_log. */ -function sendBotDetectorLogToAltSessions(cronStartTimeout = 3000, interval = 1000) { - setTimeout(function() { - setInterval(function() { - const currentLog = apbctLocalStorage.get('ct_bot_detector_frontend_data_log'); - if (needsSaveLogToAltSessions(currentLog)) { - botDetectorLogLastUpdate = currentLog.log_last_update; - // the log will be taken from javascriptclientdata - ctSetAlternativeCookie([], {forceAltCookies: true}); - } - }, interval); - }, cronStartTimeout); -} +class ApbctBrowserState { + static STATE_KEY = 'apbct_browser_state'; + static LOG_KEY = 'ct_bot_detector_frontend_data_log'; + static SCRIPT_SIGNS = { + botd_wrapper_loaded: 'ct-bot-detector-wrapper', + botd_logic_loaded: 'ct-bot-detector.min', + }; + static botdWrapperLoaded = 0; + static botdLogicLoaded = 0; -/** - * Check if the log needs to be saved to the alt sessions. If the log has new event types, it will be saved. - * @param {object} currentLog - * @return {boolean} - */ -function needsSaveLogToAltSessions(currentLog) { - if ( - currentLog && currentLog.hasOwnProperty('log_last_update') && - botDetectorLogLastUpdate !== currentLog.log_last_update - ) { + /** + * Get the current browser state as a JSON string ready to be transferred. + * @return {string} Empty string if the bot detector is disabled or on any collecting error. + */ + static toJson() { try { - for (let i = 0; i < currentLog.records.length; i++) { - const currentType = currentLog.records[i].frontend_data.js_event; - // check if this event type was already collected - if (currentType !== undefined && botDetectorLogEventTypesCollected.includes(currentType)) { - continue; - } - // add new event type to collection, this type will be sent to the alt sessions further - botDetectorLogEventTypesCollected.push(currentType); - return true; - } + const log = localStorage.getItem(ApbctBrowserState.LOG_KEY); + return JSON.stringify({ + botd_logic_loaded: ApbctBrowserState.botdLogicLoaded, + botd_wrapper_loaded: ApbctBrowserState.botdWrapperLoaded, + frontend_data_log: typeof log === 'string' ? log : '', + }); } catch (e) { - console.log('APBCT: bot detector log collection error: ' . e.toString()); + return ''; } } - return false; + + /** + * Look for the bot detector scripts in the page DOM. + * @return {boolean} True if both scripts are found. + */ + static detectScripts() { + const scripts = document.getElementsByTagName('script'); + + for (let i = 0; i < scripts.length; i++) { + const src = scripts[i].getAttribute('src'); + if (!src) { + continue; + } + if (src.indexOf(ApbctBrowserState.SCRIPT_SIGNS.botd_wrapper_loaded) !== -1) { + ApbctBrowserState.botdWrapperLoaded = 1; + } else if (src.indexOf(ApbctBrowserState.SCRIPT_SIGNS.botd_logic_loaded) !== -1) { + ApbctBrowserState.botdLogicLoaded = 1; + } + } + + // The bot detector logic sets this flag on its own start + + return !!ApbctBrowserState.botdWrapperLoaded && !!ApbctBrowserState.botdLogicLoaded; + } } +/** + * Get the browser state as a key/value pair to attach it to an intercepted request. + * @return {{key: string, value: string}|false} False if there is nothing to attach. + */ +function apbctGetBrowserStatePair() { // eslint-disable-line no-unused-vars + try { + ApbctBrowserState.detectScripts(); + const state = ApbctBrowserState.toJson(); + return state ? {key: ApbctBrowserState.STATE_KEY, value: state} : false; + } catch (e) { + return false; + } +} let ctCheckedEmails = {}; let ctCheckedEmailsExist = {}; diff --git a/js/prebuild/apbct-public-bundle_ext-protection.js b/js/prebuild/apbct-public-bundle_ext-protection.js index 176785df6..7b3194053 100644 --- a/js/prebuild/apbct-public-bundle_ext-protection.js +++ b/js/prebuild/apbct-public-bundle_ext-protection.js @@ -2642,6 +2642,15 @@ function getNoCookieData() { // eslint-disable-line no-unused-vars let noCookieDataLocal = apbctLocalStorage.getCleanTalkData(); let noCookieDataSession = apbctSessionStorage.getCleanTalkData(); let noCookieData = {...noCookieDataLocal, ...noCookieDataSession}; + + // Bot detector browser state is transferred with the hidden field on the NoCookie mode + if (typeof apbctGetBrowserStatePair === 'function') { + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + noCookieData[browserState.key] = browserState.value; + } + } + noCookieData = JSON.stringify(noCookieData); return '_ct_no_cookie_data_' + btoa(unescape(encodeURIComponent(noCookieData))); @@ -2669,7 +2678,21 @@ function getCleanTalkStorageDataArray() { // eslint-disable-line no-unused-vars noCookieDataFromUserActivity = {collecting_user_activity_data: collectingUserActivityData}; } - return {...noCookieDataLocal, ...noCookieDataSession, ...noCookieDataTypo, ...noCookieDataFromUserActivity}; + let browserStateData = {}; + if (typeof apbctGetBrowserStatePair === 'function') { + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + browserStateData[browserState.key] = browserState.value; + } + } + + return { + ...noCookieDataLocal, + ...noCookieDataSession, + ...noCookieDataTypo, + ...noCookieDataFromUserActivity, + ...browserStateData, + }; } /** @@ -3314,6 +3337,9 @@ class ApbctHandler { const eventTokenKey = useUnwrappedCleantalkKeys ? 'ct_bot_detector_event_token' : 'data%5Bct_bot_detector_event_token%5D'; + const browserStateKey = useUnwrappedCleantalkKeys ? + 'apbct_browser_state' : + 'data%5Bapbct_browser_state%5D'; if (!( +ctPublic.bot_detector_enabled && @@ -3338,6 +3364,12 @@ class ApbctHandler { } } + const browserState = apbctGetBrowserStatePair(); + if (browserState && body.indexOf('apbct_browser_state') === -1) { + addidionalCleantalkData += '&' + browserStateKey + '=' + + encodeURIComponent(browserState.value); + } + body += addidionalCleantalkData; } @@ -3354,6 +3386,11 @@ class ApbctHandler { body.append('ct_bot_detector_event_token', eventToken); } } + + const browserState = apbctGetBrowserStatePair(); + if (browserState && !body.has(browserState.key)) { + body.append(browserState.key, browserState.value); + } } if (isNeedToAddCleantalkDataCheckJsonData) { @@ -3391,6 +3428,22 @@ class ApbctHandler { } } } + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + try { + const bodyObj = JSON.parse(body); + if ( + bodyObj && typeof bodyObj === 'object' && + !Object.prototype.hasOwnProperty.call(bodyObj, browserState.key) + ) { + bodyObj[browserState.key] = browserState.value; + body = JSON.stringify(bodyObj); + } + } catch (e) { + // body is not JSON — leave as is + } + } } return originalSend.apply(this, [body]); @@ -3448,6 +3501,11 @@ class ApbctHandler { } else { args[1].body.append('ct_no_cookie_hidden_field', getNoCookieData()); } + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + args[1].body.append(browserState.key, browserState.value); + } } } } catch (e) { @@ -3504,6 +3562,22 @@ class ApbctHandler { * @return {string|FormData} Modified body. */ const attachFieldsToBody = function(body, fieldPair = false) { + const fieldPairs = [fieldPair, apbctGetBrowserStatePair()]; + + for (const pair of fieldPairs) { + body = attachFieldPairToBody(body, pair); + } + + return body; + }; + + /** + * Attach a single key/value pair to fetch request body + * @param {string|FormData} body Fetch request data body. + * @param {object|bool} fieldPair Key value to inject. + * @return {string|FormData} Modified body. + */ + const attachFieldPairToBody = function(body, fieldPair = false) { if (fieldPair) { if (body instanceof FormData || typeof body.append === 'function') { body.append(fieldPair.key, fieldPair.value); @@ -3800,11 +3874,15 @@ class ApbctHandler { const batchPayload = JSON.parse(args[1].body); if (batchPayload.requests && Array.isArray(batchPayload.requests)) { const fieldPair = selectFieldsData(+ctPublic.bot_detector_enabled); + const browserState = apbctGetBrowserStatePair(); for (const req of batchPayload.requests) { const isAddItem = req.path === '/wc/store/v1/cart/add-item'; if (isAddItem && req.body && fieldPair && fieldPair.key) { req.body[fieldPair.key] = fieldPair.value; } + if (isAddItem && req.body && browserState) { + req.body[browserState.key] = browserState.value; + } } args[1].body = JSON.stringify(batchPayload); } @@ -4053,6 +4131,16 @@ class ApbctHandler { } } + // Bot detector browser state + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + if (sourceSign.keepUnwrapped) { + ajaxDataFormData.append(browserState.key, browserState.value); + } else { + ajaxDataFormData.append('data[' + browserState.key + ']', browserState.value); + } + } + // Visible fields if (sourceSign.attachVisibleFieldsData) { let visibleFieldsSearchResult = false; @@ -4111,6 +4199,15 @@ class ApbctHandler { let eventToken = ''; let noCookieData = ''; let visibleFieldsString = ''; + let browserStateString = ''; + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + const browserStateKey = sourceSign.keepUnwrapped ? + 'apbct_browser_state' : + 'data%5Bapbct_browser_state%5D'; + browserStateString = browserStateKey + '=' + encodeURIComponent(browserState.value) + '&'; + } if ( +ctPublic.bot_detector_enabled && @@ -4152,7 +4249,7 @@ class ApbctHandler { } } - return noCookieData + eventToken + visibleFieldsString + ajaxDataString; + return noCookieData + eventToken + browserStateString + visibleFieldsString + ajaxDataString; } /** @@ -6229,67 +6326,78 @@ function apbctIsFormInDiv(formObj, divClassName) { return false; } -let botDetectorLogLastUpdate = 0; -let botDetectorLogEventTypesCollected = []; - -// bot_detector frontend_data log alt session saving cron -if ( - ctPublicFunctions.hasOwnProperty('bot_detector_enabled') && - +ctPublicFunctions.bot_detector_enabled && - ctPublicFunctions.hasOwnProperty('data__frontend_data_log_enabled') && - ctPublicFunctions.data__frontend_data_log_enabled == 1 -) { - sendBotDetectorLogToAltSessions(1000); -} - /** - * Send BotDetector logs data to alternative sessions. - * If log_last_update has changed and log contains new event types, the log will be sent to the alternative sessions. - * @param {int} cronStartTimeout delay before cron start - * @param {int} interval check fires on interval + * Bot detector browser state. + * + * The state is passed to the site backend with the current JS->PHP transport and goes + * to the moderate request as sender_info.bot_detector_frontend_data_log. */ -function sendBotDetectorLogToAltSessions(cronStartTimeout = 3000, interval = 1000) { - setTimeout(function() { - setInterval(function() { - const currentLog = apbctLocalStorage.get('ct_bot_detector_frontend_data_log'); - if (needsSaveLogToAltSessions(currentLog)) { - botDetectorLogLastUpdate = currentLog.log_last_update; - // the log will be taken from javascriptclientdata - ctSetAlternativeCookie([], {forceAltCookies: true}); - } - }, interval); - }, cronStartTimeout); -} +class ApbctBrowserState { + static STATE_KEY = 'apbct_browser_state'; + static LOG_KEY = 'ct_bot_detector_frontend_data_log'; + static SCRIPT_SIGNS = { + botd_wrapper_loaded: 'ct-bot-detector-wrapper', + botd_logic_loaded: 'ct-bot-detector.min', + }; + static botdWrapperLoaded = 0; + static botdLogicLoaded = 0; -/** - * Check if the log needs to be saved to the alt sessions. If the log has new event types, it will be saved. - * @param {object} currentLog - * @return {boolean} - */ -function needsSaveLogToAltSessions(currentLog) { - if ( - currentLog && currentLog.hasOwnProperty('log_last_update') && - botDetectorLogLastUpdate !== currentLog.log_last_update - ) { + /** + * Get the current browser state as a JSON string ready to be transferred. + * @return {string} Empty string if the bot detector is disabled or on any collecting error. + */ + static toJson() { try { - for (let i = 0; i < currentLog.records.length; i++) { - const currentType = currentLog.records[i].frontend_data.js_event; - // check if this event type was already collected - if (currentType !== undefined && botDetectorLogEventTypesCollected.includes(currentType)) { - continue; - } - // add new event type to collection, this type will be sent to the alt sessions further - botDetectorLogEventTypesCollected.push(currentType); - return true; - } + const log = localStorage.getItem(ApbctBrowserState.LOG_KEY); + return JSON.stringify({ + botd_logic_loaded: ApbctBrowserState.botdLogicLoaded, + botd_wrapper_loaded: ApbctBrowserState.botdWrapperLoaded, + frontend_data_log: typeof log === 'string' ? log : '', + }); } catch (e) { - console.log('APBCT: bot detector log collection error: ' . e.toString()); + return ''; } } - return false; + + /** + * Look for the bot detector scripts in the page DOM. + * @return {boolean} True if both scripts are found. + */ + static detectScripts() { + const scripts = document.getElementsByTagName('script'); + + for (let i = 0; i < scripts.length; i++) { + const src = scripts[i].getAttribute('src'); + if (!src) { + continue; + } + if (src.indexOf(ApbctBrowserState.SCRIPT_SIGNS.botd_wrapper_loaded) !== -1) { + ApbctBrowserState.botdWrapperLoaded = 1; + } else if (src.indexOf(ApbctBrowserState.SCRIPT_SIGNS.botd_logic_loaded) !== -1) { + ApbctBrowserState.botdLogicLoaded = 1; + } + } + + // The bot detector logic sets this flag on its own start + + return !!ApbctBrowserState.botdWrapperLoaded && !!ApbctBrowserState.botdLogicLoaded; + } } +/** + * Get the browser state as a key/value pair to attach it to an intercepted request. + * @return {{key: string, value: string}|false} False if there is nothing to attach. + */ +function apbctGetBrowserStatePair() { // eslint-disable-line no-unused-vars + try { + ApbctBrowserState.detectScripts(); + const state = ApbctBrowserState.toJson(); + return state ? {key: ApbctBrowserState.STATE_KEY, value: state} : false; + } catch (e) { + return false; + } +} let ctCheckedEmails = {}; let ctCheckedEmailsExist = {}; diff --git a/js/prebuild/apbct-public-bundle_ext-protection_gathering.js b/js/prebuild/apbct-public-bundle_ext-protection_gathering.js index 4820dfe97..32e938412 100644 --- a/js/prebuild/apbct-public-bundle_ext-protection_gathering.js +++ b/js/prebuild/apbct-public-bundle_ext-protection_gathering.js @@ -2642,6 +2642,15 @@ function getNoCookieData() { // eslint-disable-line no-unused-vars let noCookieDataLocal = apbctLocalStorage.getCleanTalkData(); let noCookieDataSession = apbctSessionStorage.getCleanTalkData(); let noCookieData = {...noCookieDataLocal, ...noCookieDataSession}; + + // Bot detector browser state is transferred with the hidden field on the NoCookie mode + if (typeof apbctGetBrowserStatePair === 'function') { + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + noCookieData[browserState.key] = browserState.value; + } + } + noCookieData = JSON.stringify(noCookieData); return '_ct_no_cookie_data_' + btoa(unescape(encodeURIComponent(noCookieData))); @@ -2669,7 +2678,21 @@ function getCleanTalkStorageDataArray() { // eslint-disable-line no-unused-vars noCookieDataFromUserActivity = {collecting_user_activity_data: collectingUserActivityData}; } - return {...noCookieDataLocal, ...noCookieDataSession, ...noCookieDataTypo, ...noCookieDataFromUserActivity}; + let browserStateData = {}; + if (typeof apbctGetBrowserStatePair === 'function') { + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + browserStateData[browserState.key] = browserState.value; + } + } + + return { + ...noCookieDataLocal, + ...noCookieDataSession, + ...noCookieDataTypo, + ...noCookieDataFromUserActivity, + ...browserStateData, + }; } /** @@ -3314,6 +3337,9 @@ class ApbctHandler { const eventTokenKey = useUnwrappedCleantalkKeys ? 'ct_bot_detector_event_token' : 'data%5Bct_bot_detector_event_token%5D'; + const browserStateKey = useUnwrappedCleantalkKeys ? + 'apbct_browser_state' : + 'data%5Bapbct_browser_state%5D'; if (!( +ctPublic.bot_detector_enabled && @@ -3338,6 +3364,12 @@ class ApbctHandler { } } + const browserState = apbctGetBrowserStatePair(); + if (browserState && body.indexOf('apbct_browser_state') === -1) { + addidionalCleantalkData += '&' + browserStateKey + '=' + + encodeURIComponent(browserState.value); + } + body += addidionalCleantalkData; } @@ -3354,6 +3386,11 @@ class ApbctHandler { body.append('ct_bot_detector_event_token', eventToken); } } + + const browserState = apbctGetBrowserStatePair(); + if (browserState && !body.has(browserState.key)) { + body.append(browserState.key, browserState.value); + } } if (isNeedToAddCleantalkDataCheckJsonData) { @@ -3391,6 +3428,22 @@ class ApbctHandler { } } } + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + try { + const bodyObj = JSON.parse(body); + if ( + bodyObj && typeof bodyObj === 'object' && + !Object.prototype.hasOwnProperty.call(bodyObj, browserState.key) + ) { + bodyObj[browserState.key] = browserState.value; + body = JSON.stringify(bodyObj); + } + } catch (e) { + // body is not JSON — leave as is + } + } } return originalSend.apply(this, [body]); @@ -3448,6 +3501,11 @@ class ApbctHandler { } else { args[1].body.append('ct_no_cookie_hidden_field', getNoCookieData()); } + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + args[1].body.append(browserState.key, browserState.value); + } } } } catch (e) { @@ -3504,6 +3562,22 @@ class ApbctHandler { * @return {string|FormData} Modified body. */ const attachFieldsToBody = function(body, fieldPair = false) { + const fieldPairs = [fieldPair, apbctGetBrowserStatePair()]; + + for (const pair of fieldPairs) { + body = attachFieldPairToBody(body, pair); + } + + return body; + }; + + /** + * Attach a single key/value pair to fetch request body + * @param {string|FormData} body Fetch request data body. + * @param {object|bool} fieldPair Key value to inject. + * @return {string|FormData} Modified body. + */ + const attachFieldPairToBody = function(body, fieldPair = false) { if (fieldPair) { if (body instanceof FormData || typeof body.append === 'function') { body.append(fieldPair.key, fieldPair.value); @@ -3800,11 +3874,15 @@ class ApbctHandler { const batchPayload = JSON.parse(args[1].body); if (batchPayload.requests && Array.isArray(batchPayload.requests)) { const fieldPair = selectFieldsData(+ctPublic.bot_detector_enabled); + const browserState = apbctGetBrowserStatePair(); for (const req of batchPayload.requests) { const isAddItem = req.path === '/wc/store/v1/cart/add-item'; if (isAddItem && req.body && fieldPair && fieldPair.key) { req.body[fieldPair.key] = fieldPair.value; } + if (isAddItem && req.body && browserState) { + req.body[browserState.key] = browserState.value; + } } args[1].body = JSON.stringify(batchPayload); } @@ -4053,6 +4131,16 @@ class ApbctHandler { } } + // Bot detector browser state + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + if (sourceSign.keepUnwrapped) { + ajaxDataFormData.append(browserState.key, browserState.value); + } else { + ajaxDataFormData.append('data[' + browserState.key + ']', browserState.value); + } + } + // Visible fields if (sourceSign.attachVisibleFieldsData) { let visibleFieldsSearchResult = false; @@ -4111,6 +4199,15 @@ class ApbctHandler { let eventToken = ''; let noCookieData = ''; let visibleFieldsString = ''; + let browserStateString = ''; + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + const browserStateKey = sourceSign.keepUnwrapped ? + 'apbct_browser_state' : + 'data%5Bapbct_browser_state%5D'; + browserStateString = browserStateKey + '=' + encodeURIComponent(browserState.value) + '&'; + } if ( +ctPublic.bot_detector_enabled && @@ -4152,7 +4249,7 @@ class ApbctHandler { } } - return noCookieData + eventToken + visibleFieldsString + ajaxDataString; + return noCookieData + eventToken + browserStateString + visibleFieldsString + ajaxDataString; } /** @@ -7073,67 +7170,78 @@ function apbctCancelAutocomplete(element) { })); } -let botDetectorLogLastUpdate = 0; -let botDetectorLogEventTypesCollected = []; - -// bot_detector frontend_data log alt session saving cron -if ( - ctPublicFunctions.hasOwnProperty('bot_detector_enabled') && - +ctPublicFunctions.bot_detector_enabled && - ctPublicFunctions.hasOwnProperty('data__frontend_data_log_enabled') && - ctPublicFunctions.data__frontend_data_log_enabled == 1 -) { - sendBotDetectorLogToAltSessions(1000); -} - /** - * Send BotDetector logs data to alternative sessions. - * If log_last_update has changed and log contains new event types, the log will be sent to the alternative sessions. - * @param {int} cronStartTimeout delay before cron start - * @param {int} interval check fires on interval + * Bot detector browser state. + * + * The state is passed to the site backend with the current JS->PHP transport and goes + * to the moderate request as sender_info.bot_detector_frontend_data_log. */ -function sendBotDetectorLogToAltSessions(cronStartTimeout = 3000, interval = 1000) { - setTimeout(function() { - setInterval(function() { - const currentLog = apbctLocalStorage.get('ct_bot_detector_frontend_data_log'); - if (needsSaveLogToAltSessions(currentLog)) { - botDetectorLogLastUpdate = currentLog.log_last_update; - // the log will be taken from javascriptclientdata - ctSetAlternativeCookie([], {forceAltCookies: true}); - } - }, interval); - }, cronStartTimeout); -} +class ApbctBrowserState { + static STATE_KEY = 'apbct_browser_state'; + static LOG_KEY = 'ct_bot_detector_frontend_data_log'; + static SCRIPT_SIGNS = { + botd_wrapper_loaded: 'ct-bot-detector-wrapper', + botd_logic_loaded: 'ct-bot-detector.min', + }; + static botdWrapperLoaded = 0; + static botdLogicLoaded = 0; -/** - * Check if the log needs to be saved to the alt sessions. If the log has new event types, it will be saved. - * @param {object} currentLog - * @return {boolean} - */ -function needsSaveLogToAltSessions(currentLog) { - if ( - currentLog && currentLog.hasOwnProperty('log_last_update') && - botDetectorLogLastUpdate !== currentLog.log_last_update - ) { + /** + * Get the current browser state as a JSON string ready to be transferred. + * @return {string} Empty string if the bot detector is disabled or on any collecting error. + */ + static toJson() { try { - for (let i = 0; i < currentLog.records.length; i++) { - const currentType = currentLog.records[i].frontend_data.js_event; - // check if this event type was already collected - if (currentType !== undefined && botDetectorLogEventTypesCollected.includes(currentType)) { - continue; - } - // add new event type to collection, this type will be sent to the alt sessions further - botDetectorLogEventTypesCollected.push(currentType); - return true; - } + const log = localStorage.getItem(ApbctBrowserState.LOG_KEY); + return JSON.stringify({ + botd_logic_loaded: ApbctBrowserState.botdLogicLoaded, + botd_wrapper_loaded: ApbctBrowserState.botdWrapperLoaded, + frontend_data_log: typeof log === 'string' ? log : '', + }); } catch (e) { - console.log('APBCT: bot detector log collection error: ' . e.toString()); + return ''; } } - return false; + + /** + * Look for the bot detector scripts in the page DOM. + * @return {boolean} True if both scripts are found. + */ + static detectScripts() { + const scripts = document.getElementsByTagName('script'); + + for (let i = 0; i < scripts.length; i++) { + const src = scripts[i].getAttribute('src'); + if (!src) { + continue; + } + if (src.indexOf(ApbctBrowserState.SCRIPT_SIGNS.botd_wrapper_loaded) !== -1) { + ApbctBrowserState.botdWrapperLoaded = 1; + } else if (src.indexOf(ApbctBrowserState.SCRIPT_SIGNS.botd_logic_loaded) !== -1) { + ApbctBrowserState.botdLogicLoaded = 1; + } + } + + // The bot detector logic sets this flag on its own start + + return !!ApbctBrowserState.botdWrapperLoaded && !!ApbctBrowserState.botdLogicLoaded; + } } +/** + * Get the browser state as a key/value pair to attach it to an intercepted request. + * @return {{key: string, value: string}|false} False if there is nothing to attach. + */ +function apbctGetBrowserStatePair() { // eslint-disable-line no-unused-vars + try { + ApbctBrowserState.detectScripts(); + const state = ApbctBrowserState.toJson(); + return state ? {key: ApbctBrowserState.STATE_KEY, value: state} : false; + } catch (e) { + return false; + } +} let ctCheckedEmails = {}; let ctCheckedEmailsExist = {}; diff --git a/js/prebuild/apbct-public-bundle_full-protection.js b/js/prebuild/apbct-public-bundle_full-protection.js index 26ea6d110..fb16da51b 100644 --- a/js/prebuild/apbct-public-bundle_full-protection.js +++ b/js/prebuild/apbct-public-bundle_full-protection.js @@ -2642,6 +2642,15 @@ function getNoCookieData() { // eslint-disable-line no-unused-vars let noCookieDataLocal = apbctLocalStorage.getCleanTalkData(); let noCookieDataSession = apbctSessionStorage.getCleanTalkData(); let noCookieData = {...noCookieDataLocal, ...noCookieDataSession}; + + // Bot detector browser state is transferred with the hidden field on the NoCookie mode + if (typeof apbctGetBrowserStatePair === 'function') { + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + noCookieData[browserState.key] = browserState.value; + } + } + noCookieData = JSON.stringify(noCookieData); return '_ct_no_cookie_data_' + btoa(unescape(encodeURIComponent(noCookieData))); @@ -2669,7 +2678,21 @@ function getCleanTalkStorageDataArray() { // eslint-disable-line no-unused-vars noCookieDataFromUserActivity = {collecting_user_activity_data: collectingUserActivityData}; } - return {...noCookieDataLocal, ...noCookieDataSession, ...noCookieDataTypo, ...noCookieDataFromUserActivity}; + let browserStateData = {}; + if (typeof apbctGetBrowserStatePair === 'function') { + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + browserStateData[browserState.key] = browserState.value; + } + } + + return { + ...noCookieDataLocal, + ...noCookieDataSession, + ...noCookieDataTypo, + ...noCookieDataFromUserActivity, + ...browserStateData, + }; } /** @@ -3314,6 +3337,9 @@ class ApbctHandler { const eventTokenKey = useUnwrappedCleantalkKeys ? 'ct_bot_detector_event_token' : 'data%5Bct_bot_detector_event_token%5D'; + const browserStateKey = useUnwrappedCleantalkKeys ? + 'apbct_browser_state' : + 'data%5Bapbct_browser_state%5D'; if (!( +ctPublic.bot_detector_enabled && @@ -3338,6 +3364,12 @@ class ApbctHandler { } } + const browserState = apbctGetBrowserStatePair(); + if (browserState && body.indexOf('apbct_browser_state') === -1) { + addidionalCleantalkData += '&' + browserStateKey + '=' + + encodeURIComponent(browserState.value); + } + body += addidionalCleantalkData; } @@ -3354,6 +3386,11 @@ class ApbctHandler { body.append('ct_bot_detector_event_token', eventToken); } } + + const browserState = apbctGetBrowserStatePair(); + if (browserState && !body.has(browserState.key)) { + body.append(browserState.key, browserState.value); + } } if (isNeedToAddCleantalkDataCheckJsonData) { @@ -3391,6 +3428,22 @@ class ApbctHandler { } } } + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + try { + const bodyObj = JSON.parse(body); + if ( + bodyObj && typeof bodyObj === 'object' && + !Object.prototype.hasOwnProperty.call(bodyObj, browserState.key) + ) { + bodyObj[browserState.key] = browserState.value; + body = JSON.stringify(bodyObj); + } + } catch (e) { + // body is not JSON — leave as is + } + } } return originalSend.apply(this, [body]); @@ -3448,6 +3501,11 @@ class ApbctHandler { } else { args[1].body.append('ct_no_cookie_hidden_field', getNoCookieData()); } + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + args[1].body.append(browserState.key, browserState.value); + } } } } catch (e) { @@ -3504,6 +3562,22 @@ class ApbctHandler { * @return {string|FormData} Modified body. */ const attachFieldsToBody = function(body, fieldPair = false) { + const fieldPairs = [fieldPair, apbctGetBrowserStatePair()]; + + for (const pair of fieldPairs) { + body = attachFieldPairToBody(body, pair); + } + + return body; + }; + + /** + * Attach a single key/value pair to fetch request body + * @param {string|FormData} body Fetch request data body. + * @param {object|bool} fieldPair Key value to inject. + * @return {string|FormData} Modified body. + */ + const attachFieldPairToBody = function(body, fieldPair = false) { if (fieldPair) { if (body instanceof FormData || typeof body.append === 'function') { body.append(fieldPair.key, fieldPair.value); @@ -3800,11 +3874,15 @@ class ApbctHandler { const batchPayload = JSON.parse(args[1].body); if (batchPayload.requests && Array.isArray(batchPayload.requests)) { const fieldPair = selectFieldsData(+ctPublic.bot_detector_enabled); + const browserState = apbctGetBrowserStatePair(); for (const req of batchPayload.requests) { const isAddItem = req.path === '/wc/store/v1/cart/add-item'; if (isAddItem && req.body && fieldPair && fieldPair.key) { req.body[fieldPair.key] = fieldPair.value; } + if (isAddItem && req.body && browserState) { + req.body[browserState.key] = browserState.value; + } } args[1].body = JSON.stringify(batchPayload); } @@ -4053,6 +4131,16 @@ class ApbctHandler { } } + // Bot detector browser state + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + if (sourceSign.keepUnwrapped) { + ajaxDataFormData.append(browserState.key, browserState.value); + } else { + ajaxDataFormData.append('data[' + browserState.key + ']', browserState.value); + } + } + // Visible fields if (sourceSign.attachVisibleFieldsData) { let visibleFieldsSearchResult = false; @@ -4111,6 +4199,15 @@ class ApbctHandler { let eventToken = ''; let noCookieData = ''; let visibleFieldsString = ''; + let browserStateString = ''; + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + const browserStateKey = sourceSign.keepUnwrapped ? + 'apbct_browser_state' : + 'data%5Bapbct_browser_state%5D'; + browserStateString = browserStateKey + '=' + encodeURIComponent(browserState.value) + '&'; + } if ( +ctPublic.bot_detector_enabled && @@ -4152,7 +4249,7 @@ class ApbctHandler { } } - return noCookieData + eventToken + visibleFieldsString + ajaxDataString; + return noCookieData + eventToken + browserStateString + visibleFieldsString + ajaxDataString; } /** @@ -6473,67 +6570,78 @@ function ctCheckInternalIsExcludedForm(action) { }); } -let botDetectorLogLastUpdate = 0; -let botDetectorLogEventTypesCollected = []; - -// bot_detector frontend_data log alt session saving cron -if ( - ctPublicFunctions.hasOwnProperty('bot_detector_enabled') && - +ctPublicFunctions.bot_detector_enabled && - ctPublicFunctions.hasOwnProperty('data__frontend_data_log_enabled') && - ctPublicFunctions.data__frontend_data_log_enabled == 1 -) { - sendBotDetectorLogToAltSessions(1000); -} - /** - * Send BotDetector logs data to alternative sessions. - * If log_last_update has changed and log contains new event types, the log will be sent to the alternative sessions. - * @param {int} cronStartTimeout delay before cron start - * @param {int} interval check fires on interval + * Bot detector browser state. + * + * The state is passed to the site backend with the current JS->PHP transport and goes + * to the moderate request as sender_info.bot_detector_frontend_data_log. */ -function sendBotDetectorLogToAltSessions(cronStartTimeout = 3000, interval = 1000) { - setTimeout(function() { - setInterval(function() { - const currentLog = apbctLocalStorage.get('ct_bot_detector_frontend_data_log'); - if (needsSaveLogToAltSessions(currentLog)) { - botDetectorLogLastUpdate = currentLog.log_last_update; - // the log will be taken from javascriptclientdata - ctSetAlternativeCookie([], {forceAltCookies: true}); - } - }, interval); - }, cronStartTimeout); -} +class ApbctBrowserState { + static STATE_KEY = 'apbct_browser_state'; + static LOG_KEY = 'ct_bot_detector_frontend_data_log'; + static SCRIPT_SIGNS = { + botd_wrapper_loaded: 'ct-bot-detector-wrapper', + botd_logic_loaded: 'ct-bot-detector.min', + }; + static botdWrapperLoaded = 0; + static botdLogicLoaded = 0; -/** - * Check if the log needs to be saved to the alt sessions. If the log has new event types, it will be saved. - * @param {object} currentLog - * @return {boolean} - */ -function needsSaveLogToAltSessions(currentLog) { - if ( - currentLog && currentLog.hasOwnProperty('log_last_update') && - botDetectorLogLastUpdate !== currentLog.log_last_update - ) { + /** + * Get the current browser state as a JSON string ready to be transferred. + * @return {string} Empty string if the bot detector is disabled or on any collecting error. + */ + static toJson() { try { - for (let i = 0; i < currentLog.records.length; i++) { - const currentType = currentLog.records[i].frontend_data.js_event; - // check if this event type was already collected - if (currentType !== undefined && botDetectorLogEventTypesCollected.includes(currentType)) { - continue; - } - // add new event type to collection, this type will be sent to the alt sessions further - botDetectorLogEventTypesCollected.push(currentType); - return true; - } + const log = localStorage.getItem(ApbctBrowserState.LOG_KEY); + return JSON.stringify({ + botd_logic_loaded: ApbctBrowserState.botdLogicLoaded, + botd_wrapper_loaded: ApbctBrowserState.botdWrapperLoaded, + frontend_data_log: typeof log === 'string' ? log : '', + }); } catch (e) { - console.log('APBCT: bot detector log collection error: ' . e.toString()); + return ''; } } - return false; + + /** + * Look for the bot detector scripts in the page DOM. + * @return {boolean} True if both scripts are found. + */ + static detectScripts() { + const scripts = document.getElementsByTagName('script'); + + for (let i = 0; i < scripts.length; i++) { + const src = scripts[i].getAttribute('src'); + if (!src) { + continue; + } + if (src.indexOf(ApbctBrowserState.SCRIPT_SIGNS.botd_wrapper_loaded) !== -1) { + ApbctBrowserState.botdWrapperLoaded = 1; + } else if (src.indexOf(ApbctBrowserState.SCRIPT_SIGNS.botd_logic_loaded) !== -1) { + ApbctBrowserState.botdLogicLoaded = 1; + } + } + + // The bot detector logic sets this flag on its own start + + return !!ApbctBrowserState.botdWrapperLoaded && !!ApbctBrowserState.botdLogicLoaded; + } } +/** + * Get the browser state as a key/value pair to attach it to an intercepted request. + * @return {{key: string, value: string}|false} False if there is nothing to attach. + */ +function apbctGetBrowserStatePair() { // eslint-disable-line no-unused-vars + try { + ApbctBrowserState.detectScripts(); + const state = ApbctBrowserState.toJson(); + return state ? {key: ApbctBrowserState.STATE_KEY, value: state} : false; + } catch (e) { + return false; + } +} let ctCheckedEmails = {}; let ctCheckedEmailsExist = {}; diff --git a/js/prebuild/apbct-public-bundle_full-protection_gathering.js b/js/prebuild/apbct-public-bundle_full-protection_gathering.js index 98230542b..ecd798687 100644 --- a/js/prebuild/apbct-public-bundle_full-protection_gathering.js +++ b/js/prebuild/apbct-public-bundle_full-protection_gathering.js @@ -2642,6 +2642,15 @@ function getNoCookieData() { // eslint-disable-line no-unused-vars let noCookieDataLocal = apbctLocalStorage.getCleanTalkData(); let noCookieDataSession = apbctSessionStorage.getCleanTalkData(); let noCookieData = {...noCookieDataLocal, ...noCookieDataSession}; + + // Bot detector browser state is transferred with the hidden field on the NoCookie mode + if (typeof apbctGetBrowserStatePair === 'function') { + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + noCookieData[browserState.key] = browserState.value; + } + } + noCookieData = JSON.stringify(noCookieData); return '_ct_no_cookie_data_' + btoa(unescape(encodeURIComponent(noCookieData))); @@ -2669,7 +2678,21 @@ function getCleanTalkStorageDataArray() { // eslint-disable-line no-unused-vars noCookieDataFromUserActivity = {collecting_user_activity_data: collectingUserActivityData}; } - return {...noCookieDataLocal, ...noCookieDataSession, ...noCookieDataTypo, ...noCookieDataFromUserActivity}; + let browserStateData = {}; + if (typeof apbctGetBrowserStatePair === 'function') { + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + browserStateData[browserState.key] = browserState.value; + } + } + + return { + ...noCookieDataLocal, + ...noCookieDataSession, + ...noCookieDataTypo, + ...noCookieDataFromUserActivity, + ...browserStateData, + }; } /** @@ -3314,6 +3337,9 @@ class ApbctHandler { const eventTokenKey = useUnwrappedCleantalkKeys ? 'ct_bot_detector_event_token' : 'data%5Bct_bot_detector_event_token%5D'; + const browserStateKey = useUnwrappedCleantalkKeys ? + 'apbct_browser_state' : + 'data%5Bapbct_browser_state%5D'; if (!( +ctPublic.bot_detector_enabled && @@ -3338,6 +3364,12 @@ class ApbctHandler { } } + const browserState = apbctGetBrowserStatePair(); + if (browserState && body.indexOf('apbct_browser_state') === -1) { + addidionalCleantalkData += '&' + browserStateKey + '=' + + encodeURIComponent(browserState.value); + } + body += addidionalCleantalkData; } @@ -3354,6 +3386,11 @@ class ApbctHandler { body.append('ct_bot_detector_event_token', eventToken); } } + + const browserState = apbctGetBrowserStatePair(); + if (browserState && !body.has(browserState.key)) { + body.append(browserState.key, browserState.value); + } } if (isNeedToAddCleantalkDataCheckJsonData) { @@ -3391,6 +3428,22 @@ class ApbctHandler { } } } + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + try { + const bodyObj = JSON.parse(body); + if ( + bodyObj && typeof bodyObj === 'object' && + !Object.prototype.hasOwnProperty.call(bodyObj, browserState.key) + ) { + bodyObj[browserState.key] = browserState.value; + body = JSON.stringify(bodyObj); + } + } catch (e) { + // body is not JSON — leave as is + } + } } return originalSend.apply(this, [body]); @@ -3448,6 +3501,11 @@ class ApbctHandler { } else { args[1].body.append('ct_no_cookie_hidden_field', getNoCookieData()); } + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + args[1].body.append(browserState.key, browserState.value); + } } } } catch (e) { @@ -3504,6 +3562,22 @@ class ApbctHandler { * @return {string|FormData} Modified body. */ const attachFieldsToBody = function(body, fieldPair = false) { + const fieldPairs = [fieldPair, apbctGetBrowserStatePair()]; + + for (const pair of fieldPairs) { + body = attachFieldPairToBody(body, pair); + } + + return body; + }; + + /** + * Attach a single key/value pair to fetch request body + * @param {string|FormData} body Fetch request data body. + * @param {object|bool} fieldPair Key value to inject. + * @return {string|FormData} Modified body. + */ + const attachFieldPairToBody = function(body, fieldPair = false) { if (fieldPair) { if (body instanceof FormData || typeof body.append === 'function') { body.append(fieldPair.key, fieldPair.value); @@ -3800,11 +3874,15 @@ class ApbctHandler { const batchPayload = JSON.parse(args[1].body); if (batchPayload.requests && Array.isArray(batchPayload.requests)) { const fieldPair = selectFieldsData(+ctPublic.bot_detector_enabled); + const browserState = apbctGetBrowserStatePair(); for (const req of batchPayload.requests) { const isAddItem = req.path === '/wc/store/v1/cart/add-item'; if (isAddItem && req.body && fieldPair && fieldPair.key) { req.body[fieldPair.key] = fieldPair.value; } + if (isAddItem && req.body && browserState) { + req.body[browserState.key] = browserState.value; + } } args[1].body = JSON.stringify(batchPayload); } @@ -4053,6 +4131,16 @@ class ApbctHandler { } } + // Bot detector browser state + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + if (sourceSign.keepUnwrapped) { + ajaxDataFormData.append(browserState.key, browserState.value); + } else { + ajaxDataFormData.append('data[' + browserState.key + ']', browserState.value); + } + } + // Visible fields if (sourceSign.attachVisibleFieldsData) { let visibleFieldsSearchResult = false; @@ -4111,6 +4199,15 @@ class ApbctHandler { let eventToken = ''; let noCookieData = ''; let visibleFieldsString = ''; + let browserStateString = ''; + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + const browserStateKey = sourceSign.keepUnwrapped ? + 'apbct_browser_state' : + 'data%5Bapbct_browser_state%5D'; + browserStateString = browserStateKey + '=' + encodeURIComponent(browserState.value) + '&'; + } if ( +ctPublic.bot_detector_enabled && @@ -4152,7 +4249,7 @@ class ApbctHandler { } } - return noCookieData + eventToken + visibleFieldsString + ajaxDataString; + return noCookieData + eventToken + browserStateString + visibleFieldsString + ajaxDataString; } /** @@ -7317,67 +7414,78 @@ function ctCheckInternalIsExcludedForm(action) { }); } -let botDetectorLogLastUpdate = 0; -let botDetectorLogEventTypesCollected = []; - -// bot_detector frontend_data log alt session saving cron -if ( - ctPublicFunctions.hasOwnProperty('bot_detector_enabled') && - +ctPublicFunctions.bot_detector_enabled && - ctPublicFunctions.hasOwnProperty('data__frontend_data_log_enabled') && - ctPublicFunctions.data__frontend_data_log_enabled == 1 -) { - sendBotDetectorLogToAltSessions(1000); -} - /** - * Send BotDetector logs data to alternative sessions. - * If log_last_update has changed and log contains new event types, the log will be sent to the alternative sessions. - * @param {int} cronStartTimeout delay before cron start - * @param {int} interval check fires on interval + * Bot detector browser state. + * + * The state is passed to the site backend with the current JS->PHP transport and goes + * to the moderate request as sender_info.bot_detector_frontend_data_log. */ -function sendBotDetectorLogToAltSessions(cronStartTimeout = 3000, interval = 1000) { - setTimeout(function() { - setInterval(function() { - const currentLog = apbctLocalStorage.get('ct_bot_detector_frontend_data_log'); - if (needsSaveLogToAltSessions(currentLog)) { - botDetectorLogLastUpdate = currentLog.log_last_update; - // the log will be taken from javascriptclientdata - ctSetAlternativeCookie([], {forceAltCookies: true}); - } - }, interval); - }, cronStartTimeout); -} +class ApbctBrowserState { + static STATE_KEY = 'apbct_browser_state'; + static LOG_KEY = 'ct_bot_detector_frontend_data_log'; + static SCRIPT_SIGNS = { + botd_wrapper_loaded: 'ct-bot-detector-wrapper', + botd_logic_loaded: 'ct-bot-detector.min', + }; + static botdWrapperLoaded = 0; + static botdLogicLoaded = 0; -/** - * Check if the log needs to be saved to the alt sessions. If the log has new event types, it will be saved. - * @param {object} currentLog - * @return {boolean} - */ -function needsSaveLogToAltSessions(currentLog) { - if ( - currentLog && currentLog.hasOwnProperty('log_last_update') && - botDetectorLogLastUpdate !== currentLog.log_last_update - ) { + /** + * Get the current browser state as a JSON string ready to be transferred. + * @return {string} Empty string if the bot detector is disabled or on any collecting error. + */ + static toJson() { try { - for (let i = 0; i < currentLog.records.length; i++) { - const currentType = currentLog.records[i].frontend_data.js_event; - // check if this event type was already collected - if (currentType !== undefined && botDetectorLogEventTypesCollected.includes(currentType)) { - continue; - } - // add new event type to collection, this type will be sent to the alt sessions further - botDetectorLogEventTypesCollected.push(currentType); - return true; - } + const log = localStorage.getItem(ApbctBrowserState.LOG_KEY); + return JSON.stringify({ + botd_logic_loaded: ApbctBrowserState.botdLogicLoaded, + botd_wrapper_loaded: ApbctBrowserState.botdWrapperLoaded, + frontend_data_log: typeof log === 'string' ? log : '', + }); } catch (e) { - console.log('APBCT: bot detector log collection error: ' . e.toString()); + return ''; } } - return false; + + /** + * Look for the bot detector scripts in the page DOM. + * @return {boolean} True if both scripts are found. + */ + static detectScripts() { + const scripts = document.getElementsByTagName('script'); + + for (let i = 0; i < scripts.length; i++) { + const src = scripts[i].getAttribute('src'); + if (!src) { + continue; + } + if (src.indexOf(ApbctBrowserState.SCRIPT_SIGNS.botd_wrapper_loaded) !== -1) { + ApbctBrowserState.botdWrapperLoaded = 1; + } else if (src.indexOf(ApbctBrowserState.SCRIPT_SIGNS.botd_logic_loaded) !== -1) { + ApbctBrowserState.botdLogicLoaded = 1; + } + } + + // The bot detector logic sets this flag on its own start + + return !!ApbctBrowserState.botdWrapperLoaded && !!ApbctBrowserState.botdLogicLoaded; + } } +/** + * Get the browser state as a key/value pair to attach it to an intercepted request. + * @return {{key: string, value: string}|false} False if there is nothing to attach. + */ +function apbctGetBrowserStatePair() { // eslint-disable-line no-unused-vars + try { + ApbctBrowserState.detectScripts(); + const state = ApbctBrowserState.toJson(); + return state ? {key: ApbctBrowserState.STATE_KEY, value: state} : false; + } catch (e) { + return false; + } +} let ctCheckedEmails = {}; let ctCheckedEmailsExist = {}; diff --git a/js/prebuild/apbct-public-bundle_gathering.js b/js/prebuild/apbct-public-bundle_gathering.js index 7384cd88c..dde5388f4 100644 --- a/js/prebuild/apbct-public-bundle_gathering.js +++ b/js/prebuild/apbct-public-bundle_gathering.js @@ -2642,6 +2642,15 @@ function getNoCookieData() { // eslint-disable-line no-unused-vars let noCookieDataLocal = apbctLocalStorage.getCleanTalkData(); let noCookieDataSession = apbctSessionStorage.getCleanTalkData(); let noCookieData = {...noCookieDataLocal, ...noCookieDataSession}; + + // Bot detector browser state is transferred with the hidden field on the NoCookie mode + if (typeof apbctGetBrowserStatePair === 'function') { + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + noCookieData[browserState.key] = browserState.value; + } + } + noCookieData = JSON.stringify(noCookieData); return '_ct_no_cookie_data_' + btoa(unescape(encodeURIComponent(noCookieData))); @@ -2669,7 +2678,21 @@ function getCleanTalkStorageDataArray() { // eslint-disable-line no-unused-vars noCookieDataFromUserActivity = {collecting_user_activity_data: collectingUserActivityData}; } - return {...noCookieDataLocal, ...noCookieDataSession, ...noCookieDataTypo, ...noCookieDataFromUserActivity}; + let browserStateData = {}; + if (typeof apbctGetBrowserStatePair === 'function') { + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + browserStateData[browserState.key] = browserState.value; + } + } + + return { + ...noCookieDataLocal, + ...noCookieDataSession, + ...noCookieDataTypo, + ...noCookieDataFromUserActivity, + ...browserStateData, + }; } /** @@ -3314,6 +3337,9 @@ class ApbctHandler { const eventTokenKey = useUnwrappedCleantalkKeys ? 'ct_bot_detector_event_token' : 'data%5Bct_bot_detector_event_token%5D'; + const browserStateKey = useUnwrappedCleantalkKeys ? + 'apbct_browser_state' : + 'data%5Bapbct_browser_state%5D'; if (!( +ctPublic.bot_detector_enabled && @@ -3338,6 +3364,12 @@ class ApbctHandler { } } + const browserState = apbctGetBrowserStatePair(); + if (browserState && body.indexOf('apbct_browser_state') === -1) { + addidionalCleantalkData += '&' + browserStateKey + '=' + + encodeURIComponent(browserState.value); + } + body += addidionalCleantalkData; } @@ -3354,6 +3386,11 @@ class ApbctHandler { body.append('ct_bot_detector_event_token', eventToken); } } + + const browserState = apbctGetBrowserStatePair(); + if (browserState && !body.has(browserState.key)) { + body.append(browserState.key, browserState.value); + } } if (isNeedToAddCleantalkDataCheckJsonData) { @@ -3391,6 +3428,22 @@ class ApbctHandler { } } } + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + try { + const bodyObj = JSON.parse(body); + if ( + bodyObj && typeof bodyObj === 'object' && + !Object.prototype.hasOwnProperty.call(bodyObj, browserState.key) + ) { + bodyObj[browserState.key] = browserState.value; + body = JSON.stringify(bodyObj); + } + } catch (e) { + // body is not JSON — leave as is + } + } } return originalSend.apply(this, [body]); @@ -3448,6 +3501,11 @@ class ApbctHandler { } else { args[1].body.append('ct_no_cookie_hidden_field', getNoCookieData()); } + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + args[1].body.append(browserState.key, browserState.value); + } } } } catch (e) { @@ -3504,6 +3562,22 @@ class ApbctHandler { * @return {string|FormData} Modified body. */ const attachFieldsToBody = function(body, fieldPair = false) { + const fieldPairs = [fieldPair, apbctGetBrowserStatePair()]; + + for (const pair of fieldPairs) { + body = attachFieldPairToBody(body, pair); + } + + return body; + }; + + /** + * Attach a single key/value pair to fetch request body + * @param {string|FormData} body Fetch request data body. + * @param {object|bool} fieldPair Key value to inject. + * @return {string|FormData} Modified body. + */ + const attachFieldPairToBody = function(body, fieldPair = false) { if (fieldPair) { if (body instanceof FormData || typeof body.append === 'function') { body.append(fieldPair.key, fieldPair.value); @@ -3800,11 +3874,15 @@ class ApbctHandler { const batchPayload = JSON.parse(args[1].body); if (batchPayload.requests && Array.isArray(batchPayload.requests)) { const fieldPair = selectFieldsData(+ctPublic.bot_detector_enabled); + const browserState = apbctGetBrowserStatePair(); for (const req of batchPayload.requests) { const isAddItem = req.path === '/wc/store/v1/cart/add-item'; if (isAddItem && req.body && fieldPair && fieldPair.key) { req.body[fieldPair.key] = fieldPair.value; } + if (isAddItem && req.body && browserState) { + req.body[browserState.key] = browserState.value; + } } args[1].body = JSON.stringify(batchPayload); } @@ -4053,6 +4131,16 @@ class ApbctHandler { } } + // Bot detector browser state + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + if (sourceSign.keepUnwrapped) { + ajaxDataFormData.append(browserState.key, browserState.value); + } else { + ajaxDataFormData.append('data[' + browserState.key + ']', browserState.value); + } + } + // Visible fields if (sourceSign.attachVisibleFieldsData) { let visibleFieldsSearchResult = false; @@ -4111,6 +4199,15 @@ class ApbctHandler { let eventToken = ''; let noCookieData = ''; let visibleFieldsString = ''; + let browserStateString = ''; + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + const browserStateKey = sourceSign.keepUnwrapped ? + 'apbct_browser_state' : + 'data%5Bapbct_browser_state%5D'; + browserStateString = browserStateKey + '=' + encodeURIComponent(browserState.value) + '&'; + } if ( +ctPublic.bot_detector_enabled && @@ -4152,7 +4249,7 @@ class ApbctHandler { } } - return noCookieData + eventToken + visibleFieldsString + ajaxDataString; + return noCookieData + eventToken + browserStateString + visibleFieldsString + ajaxDataString; } /** @@ -5732,67 +5829,78 @@ function apbctCancelAutocomplete(element) { })); } -let botDetectorLogLastUpdate = 0; -let botDetectorLogEventTypesCollected = []; - -// bot_detector frontend_data log alt session saving cron -if ( - ctPublicFunctions.hasOwnProperty('bot_detector_enabled') && - +ctPublicFunctions.bot_detector_enabled && - ctPublicFunctions.hasOwnProperty('data__frontend_data_log_enabled') && - ctPublicFunctions.data__frontend_data_log_enabled == 1 -) { - sendBotDetectorLogToAltSessions(1000); -} - /** - * Send BotDetector logs data to alternative sessions. - * If log_last_update has changed and log contains new event types, the log will be sent to the alternative sessions. - * @param {int} cronStartTimeout delay before cron start - * @param {int} interval check fires on interval + * Bot detector browser state. + * + * The state is passed to the site backend with the current JS->PHP transport and goes + * to the moderate request as sender_info.bot_detector_frontend_data_log. */ -function sendBotDetectorLogToAltSessions(cronStartTimeout = 3000, interval = 1000) { - setTimeout(function() { - setInterval(function() { - const currentLog = apbctLocalStorage.get('ct_bot_detector_frontend_data_log'); - if (needsSaveLogToAltSessions(currentLog)) { - botDetectorLogLastUpdate = currentLog.log_last_update; - // the log will be taken from javascriptclientdata - ctSetAlternativeCookie([], {forceAltCookies: true}); - } - }, interval); - }, cronStartTimeout); -} +class ApbctBrowserState { + static STATE_KEY = 'apbct_browser_state'; + static LOG_KEY = 'ct_bot_detector_frontend_data_log'; + static SCRIPT_SIGNS = { + botd_wrapper_loaded: 'ct-bot-detector-wrapper', + botd_logic_loaded: 'ct-bot-detector.min', + }; + static botdWrapperLoaded = 0; + static botdLogicLoaded = 0; -/** - * Check if the log needs to be saved to the alt sessions. If the log has new event types, it will be saved. - * @param {object} currentLog - * @return {boolean} - */ -function needsSaveLogToAltSessions(currentLog) { - if ( - currentLog && currentLog.hasOwnProperty('log_last_update') && - botDetectorLogLastUpdate !== currentLog.log_last_update - ) { + /** + * Get the current browser state as a JSON string ready to be transferred. + * @return {string} Empty string if the bot detector is disabled or on any collecting error. + */ + static toJson() { try { - for (let i = 0; i < currentLog.records.length; i++) { - const currentType = currentLog.records[i].frontend_data.js_event; - // check if this event type was already collected - if (currentType !== undefined && botDetectorLogEventTypesCollected.includes(currentType)) { - continue; - } - // add new event type to collection, this type will be sent to the alt sessions further - botDetectorLogEventTypesCollected.push(currentType); - return true; - } + const log = localStorage.getItem(ApbctBrowserState.LOG_KEY); + return JSON.stringify({ + botd_logic_loaded: ApbctBrowserState.botdLogicLoaded, + botd_wrapper_loaded: ApbctBrowserState.botdWrapperLoaded, + frontend_data_log: typeof log === 'string' ? log : '', + }); } catch (e) { - console.log('APBCT: bot detector log collection error: ' . e.toString()); + return ''; } } - return false; + + /** + * Look for the bot detector scripts in the page DOM. + * @return {boolean} True if both scripts are found. + */ + static detectScripts() { + const scripts = document.getElementsByTagName('script'); + + for (let i = 0; i < scripts.length; i++) { + const src = scripts[i].getAttribute('src'); + if (!src) { + continue; + } + if (src.indexOf(ApbctBrowserState.SCRIPT_SIGNS.botd_wrapper_loaded) !== -1) { + ApbctBrowserState.botdWrapperLoaded = 1; + } else if (src.indexOf(ApbctBrowserState.SCRIPT_SIGNS.botd_logic_loaded) !== -1) { + ApbctBrowserState.botdLogicLoaded = 1; + } + } + + // The bot detector logic sets this flag on its own start + + return !!ApbctBrowserState.botdWrapperLoaded && !!ApbctBrowserState.botdLogicLoaded; + } } +/** + * Get the browser state as a key/value pair to attach it to an intercepted request. + * @return {{key: string, value: string}|false} False if there is nothing to attach. + */ +function apbctGetBrowserStatePair() { // eslint-disable-line no-unused-vars + try { + ApbctBrowserState.detectScripts(); + const state = ApbctBrowserState.toJson(); + return state ? {key: ApbctBrowserState.STATE_KEY, value: state} : false; + } catch (e) { + return false; + } +} let ctCheckedEmails = {}; let ctCheckedEmailsExist = {}; diff --git a/js/prebuild/apbct-public-bundle_int-protection.js b/js/prebuild/apbct-public-bundle_int-protection.js index 33c495707..d1a06c5e7 100644 --- a/js/prebuild/apbct-public-bundle_int-protection.js +++ b/js/prebuild/apbct-public-bundle_int-protection.js @@ -2642,6 +2642,15 @@ function getNoCookieData() { // eslint-disable-line no-unused-vars let noCookieDataLocal = apbctLocalStorage.getCleanTalkData(); let noCookieDataSession = apbctSessionStorage.getCleanTalkData(); let noCookieData = {...noCookieDataLocal, ...noCookieDataSession}; + + // Bot detector browser state is transferred with the hidden field on the NoCookie mode + if (typeof apbctGetBrowserStatePair === 'function') { + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + noCookieData[browserState.key] = browserState.value; + } + } + noCookieData = JSON.stringify(noCookieData); return '_ct_no_cookie_data_' + btoa(unescape(encodeURIComponent(noCookieData))); @@ -2669,7 +2678,21 @@ function getCleanTalkStorageDataArray() { // eslint-disable-line no-unused-vars noCookieDataFromUserActivity = {collecting_user_activity_data: collectingUserActivityData}; } - return {...noCookieDataLocal, ...noCookieDataSession, ...noCookieDataTypo, ...noCookieDataFromUserActivity}; + let browserStateData = {}; + if (typeof apbctGetBrowserStatePair === 'function') { + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + browserStateData[browserState.key] = browserState.value; + } + } + + return { + ...noCookieDataLocal, + ...noCookieDataSession, + ...noCookieDataTypo, + ...noCookieDataFromUserActivity, + ...browserStateData, + }; } /** @@ -3314,6 +3337,9 @@ class ApbctHandler { const eventTokenKey = useUnwrappedCleantalkKeys ? 'ct_bot_detector_event_token' : 'data%5Bct_bot_detector_event_token%5D'; + const browserStateKey = useUnwrappedCleantalkKeys ? + 'apbct_browser_state' : + 'data%5Bapbct_browser_state%5D'; if (!( +ctPublic.bot_detector_enabled && @@ -3338,6 +3364,12 @@ class ApbctHandler { } } + const browserState = apbctGetBrowserStatePair(); + if (browserState && body.indexOf('apbct_browser_state') === -1) { + addidionalCleantalkData += '&' + browserStateKey + '=' + + encodeURIComponent(browserState.value); + } + body += addidionalCleantalkData; } @@ -3354,6 +3386,11 @@ class ApbctHandler { body.append('ct_bot_detector_event_token', eventToken); } } + + const browserState = apbctGetBrowserStatePair(); + if (browserState && !body.has(browserState.key)) { + body.append(browserState.key, browserState.value); + } } if (isNeedToAddCleantalkDataCheckJsonData) { @@ -3391,6 +3428,22 @@ class ApbctHandler { } } } + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + try { + const bodyObj = JSON.parse(body); + if ( + bodyObj && typeof bodyObj === 'object' && + !Object.prototype.hasOwnProperty.call(bodyObj, browserState.key) + ) { + bodyObj[browserState.key] = browserState.value; + body = JSON.stringify(bodyObj); + } + } catch (e) { + // body is not JSON — leave as is + } + } } return originalSend.apply(this, [body]); @@ -3448,6 +3501,11 @@ class ApbctHandler { } else { args[1].body.append('ct_no_cookie_hidden_field', getNoCookieData()); } + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + args[1].body.append(browserState.key, browserState.value); + } } } } catch (e) { @@ -3504,6 +3562,22 @@ class ApbctHandler { * @return {string|FormData} Modified body. */ const attachFieldsToBody = function(body, fieldPair = false) { + const fieldPairs = [fieldPair, apbctGetBrowserStatePair()]; + + for (const pair of fieldPairs) { + body = attachFieldPairToBody(body, pair); + } + + return body; + }; + + /** + * Attach a single key/value pair to fetch request body + * @param {string|FormData} body Fetch request data body. + * @param {object|bool} fieldPair Key value to inject. + * @return {string|FormData} Modified body. + */ + const attachFieldPairToBody = function(body, fieldPair = false) { if (fieldPair) { if (body instanceof FormData || typeof body.append === 'function') { body.append(fieldPair.key, fieldPair.value); @@ -3800,11 +3874,15 @@ class ApbctHandler { const batchPayload = JSON.parse(args[1].body); if (batchPayload.requests && Array.isArray(batchPayload.requests)) { const fieldPair = selectFieldsData(+ctPublic.bot_detector_enabled); + const browserState = apbctGetBrowserStatePair(); for (const req of batchPayload.requests) { const isAddItem = req.path === '/wc/store/v1/cart/add-item'; if (isAddItem && req.body && fieldPair && fieldPair.key) { req.body[fieldPair.key] = fieldPair.value; } + if (isAddItem && req.body && browserState) { + req.body[browserState.key] = browserState.value; + } } args[1].body = JSON.stringify(batchPayload); } @@ -4053,6 +4131,16 @@ class ApbctHandler { } } + // Bot detector browser state + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + if (sourceSign.keepUnwrapped) { + ajaxDataFormData.append(browserState.key, browserState.value); + } else { + ajaxDataFormData.append('data[' + browserState.key + ']', browserState.value); + } + } + // Visible fields if (sourceSign.attachVisibleFieldsData) { let visibleFieldsSearchResult = false; @@ -4111,6 +4199,15 @@ class ApbctHandler { let eventToken = ''; let noCookieData = ''; let visibleFieldsString = ''; + let browserStateString = ''; + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + const browserStateKey = sourceSign.keepUnwrapped ? + 'apbct_browser_state' : + 'data%5Bapbct_browser_state%5D'; + browserStateString = browserStateKey + '=' + encodeURIComponent(browserState.value) + '&'; + } if ( +ctPublic.bot_detector_enabled && @@ -4152,7 +4249,7 @@ class ApbctHandler { } } - return noCookieData + eventToken + visibleFieldsString + ajaxDataString; + return noCookieData + eventToken + browserStateString + visibleFieldsString + ajaxDataString; } /** @@ -4992,67 +5089,78 @@ function ctCheckInternalIsExcludedForm(action) { }); } -let botDetectorLogLastUpdate = 0; -let botDetectorLogEventTypesCollected = []; - -// bot_detector frontend_data log alt session saving cron -if ( - ctPublicFunctions.hasOwnProperty('bot_detector_enabled') && - +ctPublicFunctions.bot_detector_enabled && - ctPublicFunctions.hasOwnProperty('data__frontend_data_log_enabled') && - ctPublicFunctions.data__frontend_data_log_enabled == 1 -) { - sendBotDetectorLogToAltSessions(1000); -} - /** - * Send BotDetector logs data to alternative sessions. - * If log_last_update has changed and log contains new event types, the log will be sent to the alternative sessions. - * @param {int} cronStartTimeout delay before cron start - * @param {int} interval check fires on interval + * Bot detector browser state. + * + * The state is passed to the site backend with the current JS->PHP transport and goes + * to the moderate request as sender_info.bot_detector_frontend_data_log. */ -function sendBotDetectorLogToAltSessions(cronStartTimeout = 3000, interval = 1000) { - setTimeout(function() { - setInterval(function() { - const currentLog = apbctLocalStorage.get('ct_bot_detector_frontend_data_log'); - if (needsSaveLogToAltSessions(currentLog)) { - botDetectorLogLastUpdate = currentLog.log_last_update; - // the log will be taken from javascriptclientdata - ctSetAlternativeCookie([], {forceAltCookies: true}); - } - }, interval); - }, cronStartTimeout); -} +class ApbctBrowserState { + static STATE_KEY = 'apbct_browser_state'; + static LOG_KEY = 'ct_bot_detector_frontend_data_log'; + static SCRIPT_SIGNS = { + botd_wrapper_loaded: 'ct-bot-detector-wrapper', + botd_logic_loaded: 'ct-bot-detector.min', + }; + static botdWrapperLoaded = 0; + static botdLogicLoaded = 0; -/** - * Check if the log needs to be saved to the alt sessions. If the log has new event types, it will be saved. - * @param {object} currentLog - * @return {boolean} - */ -function needsSaveLogToAltSessions(currentLog) { - if ( - currentLog && currentLog.hasOwnProperty('log_last_update') && - botDetectorLogLastUpdate !== currentLog.log_last_update - ) { + /** + * Get the current browser state as a JSON string ready to be transferred. + * @return {string} Empty string if the bot detector is disabled or on any collecting error. + */ + static toJson() { try { - for (let i = 0; i < currentLog.records.length; i++) { - const currentType = currentLog.records[i].frontend_data.js_event; - // check if this event type was already collected - if (currentType !== undefined && botDetectorLogEventTypesCollected.includes(currentType)) { - continue; - } - // add new event type to collection, this type will be sent to the alt sessions further - botDetectorLogEventTypesCollected.push(currentType); - return true; - } + const log = localStorage.getItem(ApbctBrowserState.LOG_KEY); + return JSON.stringify({ + botd_logic_loaded: ApbctBrowserState.botdLogicLoaded, + botd_wrapper_loaded: ApbctBrowserState.botdWrapperLoaded, + frontend_data_log: typeof log === 'string' ? log : '', + }); } catch (e) { - console.log('APBCT: bot detector log collection error: ' . e.toString()); + return ''; } } - return false; + + /** + * Look for the bot detector scripts in the page DOM. + * @return {boolean} True if both scripts are found. + */ + static detectScripts() { + const scripts = document.getElementsByTagName('script'); + + for (let i = 0; i < scripts.length; i++) { + const src = scripts[i].getAttribute('src'); + if (!src) { + continue; + } + if (src.indexOf(ApbctBrowserState.SCRIPT_SIGNS.botd_wrapper_loaded) !== -1) { + ApbctBrowserState.botdWrapperLoaded = 1; + } else if (src.indexOf(ApbctBrowserState.SCRIPT_SIGNS.botd_logic_loaded) !== -1) { + ApbctBrowserState.botdLogicLoaded = 1; + } + } + + // The bot detector logic sets this flag on its own start + + return !!ApbctBrowserState.botdWrapperLoaded && !!ApbctBrowserState.botdLogicLoaded; + } } +/** + * Get the browser state as a key/value pair to attach it to an intercepted request. + * @return {{key: string, value: string}|false} False if there is nothing to attach. + */ +function apbctGetBrowserStatePair() { // eslint-disable-line no-unused-vars + try { + ApbctBrowserState.detectScripts(); + const state = ApbctBrowserState.toJson(); + return state ? {key: ApbctBrowserState.STATE_KEY, value: state} : false; + } catch (e) { + return false; + } +} let ctCheckedEmails = {}; let ctCheckedEmailsExist = {}; diff --git a/js/prebuild/apbct-public-bundle_int-protection_gathering.js b/js/prebuild/apbct-public-bundle_int-protection_gathering.js index 792acf4ad..0942ec211 100644 --- a/js/prebuild/apbct-public-bundle_int-protection_gathering.js +++ b/js/prebuild/apbct-public-bundle_int-protection_gathering.js @@ -2642,6 +2642,15 @@ function getNoCookieData() { // eslint-disable-line no-unused-vars let noCookieDataLocal = apbctLocalStorage.getCleanTalkData(); let noCookieDataSession = apbctSessionStorage.getCleanTalkData(); let noCookieData = {...noCookieDataLocal, ...noCookieDataSession}; + + // Bot detector browser state is transferred with the hidden field on the NoCookie mode + if (typeof apbctGetBrowserStatePair === 'function') { + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + noCookieData[browserState.key] = browserState.value; + } + } + noCookieData = JSON.stringify(noCookieData); return '_ct_no_cookie_data_' + btoa(unescape(encodeURIComponent(noCookieData))); @@ -2669,7 +2678,21 @@ function getCleanTalkStorageDataArray() { // eslint-disable-line no-unused-vars noCookieDataFromUserActivity = {collecting_user_activity_data: collectingUserActivityData}; } - return {...noCookieDataLocal, ...noCookieDataSession, ...noCookieDataTypo, ...noCookieDataFromUserActivity}; + let browserStateData = {}; + if (typeof apbctGetBrowserStatePair === 'function') { + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + browserStateData[browserState.key] = browserState.value; + } + } + + return { + ...noCookieDataLocal, + ...noCookieDataSession, + ...noCookieDataTypo, + ...noCookieDataFromUserActivity, + ...browserStateData, + }; } /** @@ -3314,6 +3337,9 @@ class ApbctHandler { const eventTokenKey = useUnwrappedCleantalkKeys ? 'ct_bot_detector_event_token' : 'data%5Bct_bot_detector_event_token%5D'; + const browserStateKey = useUnwrappedCleantalkKeys ? + 'apbct_browser_state' : + 'data%5Bapbct_browser_state%5D'; if (!( +ctPublic.bot_detector_enabled && @@ -3338,6 +3364,12 @@ class ApbctHandler { } } + const browserState = apbctGetBrowserStatePair(); + if (browserState && body.indexOf('apbct_browser_state') === -1) { + addidionalCleantalkData += '&' + browserStateKey + '=' + + encodeURIComponent(browserState.value); + } + body += addidionalCleantalkData; } @@ -3354,6 +3386,11 @@ class ApbctHandler { body.append('ct_bot_detector_event_token', eventToken); } } + + const browserState = apbctGetBrowserStatePair(); + if (browserState && !body.has(browserState.key)) { + body.append(browserState.key, browserState.value); + } } if (isNeedToAddCleantalkDataCheckJsonData) { @@ -3391,6 +3428,22 @@ class ApbctHandler { } } } + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + try { + const bodyObj = JSON.parse(body); + if ( + bodyObj && typeof bodyObj === 'object' && + !Object.prototype.hasOwnProperty.call(bodyObj, browserState.key) + ) { + bodyObj[browserState.key] = browserState.value; + body = JSON.stringify(bodyObj); + } + } catch (e) { + // body is not JSON — leave as is + } + } } return originalSend.apply(this, [body]); @@ -3448,6 +3501,11 @@ class ApbctHandler { } else { args[1].body.append('ct_no_cookie_hidden_field', getNoCookieData()); } + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + args[1].body.append(browserState.key, browserState.value); + } } } } catch (e) { @@ -3504,6 +3562,22 @@ class ApbctHandler { * @return {string|FormData} Modified body. */ const attachFieldsToBody = function(body, fieldPair = false) { + const fieldPairs = [fieldPair, apbctGetBrowserStatePair()]; + + for (const pair of fieldPairs) { + body = attachFieldPairToBody(body, pair); + } + + return body; + }; + + /** + * Attach a single key/value pair to fetch request body + * @param {string|FormData} body Fetch request data body. + * @param {object|bool} fieldPair Key value to inject. + * @return {string|FormData} Modified body. + */ + const attachFieldPairToBody = function(body, fieldPair = false) { if (fieldPair) { if (body instanceof FormData || typeof body.append === 'function') { body.append(fieldPair.key, fieldPair.value); @@ -3800,11 +3874,15 @@ class ApbctHandler { const batchPayload = JSON.parse(args[1].body); if (batchPayload.requests && Array.isArray(batchPayload.requests)) { const fieldPair = selectFieldsData(+ctPublic.bot_detector_enabled); + const browserState = apbctGetBrowserStatePair(); for (const req of batchPayload.requests) { const isAddItem = req.path === '/wc/store/v1/cart/add-item'; if (isAddItem && req.body && fieldPair && fieldPair.key) { req.body[fieldPair.key] = fieldPair.value; } + if (isAddItem && req.body && browserState) { + req.body[browserState.key] = browserState.value; + } } args[1].body = JSON.stringify(batchPayload); } @@ -4053,6 +4131,16 @@ class ApbctHandler { } } + // Bot detector browser state + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + if (sourceSign.keepUnwrapped) { + ajaxDataFormData.append(browserState.key, browserState.value); + } else { + ajaxDataFormData.append('data[' + browserState.key + ']', browserState.value); + } + } + // Visible fields if (sourceSign.attachVisibleFieldsData) { let visibleFieldsSearchResult = false; @@ -4111,6 +4199,15 @@ class ApbctHandler { let eventToken = ''; let noCookieData = ''; let visibleFieldsString = ''; + let browserStateString = ''; + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + const browserStateKey = sourceSign.keepUnwrapped ? + 'apbct_browser_state' : + 'data%5Bapbct_browser_state%5D'; + browserStateString = browserStateKey + '=' + encodeURIComponent(browserState.value) + '&'; + } if ( +ctPublic.bot_detector_enabled && @@ -4152,7 +4249,7 @@ class ApbctHandler { } } - return noCookieData + eventToken + visibleFieldsString + ajaxDataString; + return noCookieData + eventToken + browserStateString + visibleFieldsString + ajaxDataString; } /** @@ -5836,67 +5933,78 @@ function apbctCancelAutocomplete(element) { })); } -let botDetectorLogLastUpdate = 0; -let botDetectorLogEventTypesCollected = []; - -// bot_detector frontend_data log alt session saving cron -if ( - ctPublicFunctions.hasOwnProperty('bot_detector_enabled') && - +ctPublicFunctions.bot_detector_enabled && - ctPublicFunctions.hasOwnProperty('data__frontend_data_log_enabled') && - ctPublicFunctions.data__frontend_data_log_enabled == 1 -) { - sendBotDetectorLogToAltSessions(1000); -} - /** - * Send BotDetector logs data to alternative sessions. - * If log_last_update has changed and log contains new event types, the log will be sent to the alternative sessions. - * @param {int} cronStartTimeout delay before cron start - * @param {int} interval check fires on interval + * Bot detector browser state. + * + * The state is passed to the site backend with the current JS->PHP transport and goes + * to the moderate request as sender_info.bot_detector_frontend_data_log. */ -function sendBotDetectorLogToAltSessions(cronStartTimeout = 3000, interval = 1000) { - setTimeout(function() { - setInterval(function() { - const currentLog = apbctLocalStorage.get('ct_bot_detector_frontend_data_log'); - if (needsSaveLogToAltSessions(currentLog)) { - botDetectorLogLastUpdate = currentLog.log_last_update; - // the log will be taken from javascriptclientdata - ctSetAlternativeCookie([], {forceAltCookies: true}); - } - }, interval); - }, cronStartTimeout); -} +class ApbctBrowserState { + static STATE_KEY = 'apbct_browser_state'; + static LOG_KEY = 'ct_bot_detector_frontend_data_log'; + static SCRIPT_SIGNS = { + botd_wrapper_loaded: 'ct-bot-detector-wrapper', + botd_logic_loaded: 'ct-bot-detector.min', + }; + static botdWrapperLoaded = 0; + static botdLogicLoaded = 0; -/** - * Check if the log needs to be saved to the alt sessions. If the log has new event types, it will be saved. - * @param {object} currentLog - * @return {boolean} - */ -function needsSaveLogToAltSessions(currentLog) { - if ( - currentLog && currentLog.hasOwnProperty('log_last_update') && - botDetectorLogLastUpdate !== currentLog.log_last_update - ) { + /** + * Get the current browser state as a JSON string ready to be transferred. + * @return {string} Empty string if the bot detector is disabled or on any collecting error. + */ + static toJson() { try { - for (let i = 0; i < currentLog.records.length; i++) { - const currentType = currentLog.records[i].frontend_data.js_event; - // check if this event type was already collected - if (currentType !== undefined && botDetectorLogEventTypesCollected.includes(currentType)) { - continue; - } - // add new event type to collection, this type will be sent to the alt sessions further - botDetectorLogEventTypesCollected.push(currentType); - return true; - } + const log = localStorage.getItem(ApbctBrowserState.LOG_KEY); + return JSON.stringify({ + botd_logic_loaded: ApbctBrowserState.botdLogicLoaded, + botd_wrapper_loaded: ApbctBrowserState.botdWrapperLoaded, + frontend_data_log: typeof log === 'string' ? log : '', + }); } catch (e) { - console.log('APBCT: bot detector log collection error: ' . e.toString()); + return ''; } } - return false; + + /** + * Look for the bot detector scripts in the page DOM. + * @return {boolean} True if both scripts are found. + */ + static detectScripts() { + const scripts = document.getElementsByTagName('script'); + + for (let i = 0; i < scripts.length; i++) { + const src = scripts[i].getAttribute('src'); + if (!src) { + continue; + } + if (src.indexOf(ApbctBrowserState.SCRIPT_SIGNS.botd_wrapper_loaded) !== -1) { + ApbctBrowserState.botdWrapperLoaded = 1; + } else if (src.indexOf(ApbctBrowserState.SCRIPT_SIGNS.botd_logic_loaded) !== -1) { + ApbctBrowserState.botdLogicLoaded = 1; + } + } + + // The bot detector logic sets this flag on its own start + + return !!ApbctBrowserState.botdWrapperLoaded && !!ApbctBrowserState.botdLogicLoaded; + } } +/** + * Get the browser state as a key/value pair to attach it to an intercepted request. + * @return {{key: string, value: string}|false} False if there is nothing to attach. + */ +function apbctGetBrowserStatePair() { // eslint-disable-line no-unused-vars + try { + ApbctBrowserState.detectScripts(); + const state = ApbctBrowserState.toJson(); + return state ? {key: ApbctBrowserState.STATE_KEY, value: state} : false; + } catch (e) { + return false; + } +} let ctCheckedEmails = {}; let ctCheckedEmailsExist = {}; diff --git a/js/src/public-1-functions.js b/js/src/public-1-functions.js index 88ce4356f..81a185f31 100644 --- a/js/src/public-1-functions.js +++ b/js/src/public-1-functions.js @@ -487,6 +487,15 @@ function getNoCookieData() { // eslint-disable-line no-unused-vars let noCookieDataLocal = apbctLocalStorage.getCleanTalkData(); let noCookieDataSession = apbctSessionStorage.getCleanTalkData(); let noCookieData = {...noCookieDataLocal, ...noCookieDataSession}; + + // Bot detector browser state is transferred with the hidden field on the NoCookie mode + if (typeof apbctGetBrowserStatePair === 'function') { + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + noCookieData[browserState.key] = browserState.value; + } + } + noCookieData = JSON.stringify(noCookieData); return '_ct_no_cookie_data_' + btoa(unescape(encodeURIComponent(noCookieData))); @@ -514,7 +523,21 @@ function getCleanTalkStorageDataArray() { // eslint-disable-line no-unused-vars noCookieDataFromUserActivity = {collecting_user_activity_data: collectingUserActivityData}; } - return {...noCookieDataLocal, ...noCookieDataSession, ...noCookieDataTypo, ...noCookieDataFromUserActivity}; + let browserStateData = {}; + if (typeof apbctGetBrowserStatePair === 'function') { + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + browserStateData[browserState.key] = browserState.value; + } + } + + return { + ...noCookieDataLocal, + ...noCookieDataSession, + ...noCookieDataTypo, + ...noCookieDataFromUserActivity, + ...browserStateData, + }; } /** diff --git a/js/src/public-1-main.js b/js/src/public-1-main.js index 1a2ec6d1a..466afc479 100644 --- a/js/src/public-1-main.js +++ b/js/src/public-1-main.js @@ -590,6 +590,9 @@ class ApbctHandler { const eventTokenKey = useUnwrappedCleantalkKeys ? 'ct_bot_detector_event_token' : 'data%5Bct_bot_detector_event_token%5D'; + const browserStateKey = useUnwrappedCleantalkKeys ? + 'apbct_browser_state' : + 'data%5Bapbct_browser_state%5D'; if (!( +ctPublic.bot_detector_enabled && @@ -614,6 +617,12 @@ class ApbctHandler { } } + const browserState = apbctGetBrowserStatePair(); + if (browserState && body.indexOf('apbct_browser_state') === -1) { + addidionalCleantalkData += '&' + browserStateKey + '=' + + encodeURIComponent(browserState.value); + } + body += addidionalCleantalkData; } @@ -630,6 +639,11 @@ class ApbctHandler { body.append('ct_bot_detector_event_token', eventToken); } } + + const browserState = apbctGetBrowserStatePair(); + if (browserState && !body.has(browserState.key)) { + body.append(browserState.key, browserState.value); + } } if (isNeedToAddCleantalkDataCheckJsonData) { @@ -667,6 +681,22 @@ class ApbctHandler { } } } + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + try { + const bodyObj = JSON.parse(body); + if ( + bodyObj && typeof bodyObj === 'object' && + !Object.prototype.hasOwnProperty.call(bodyObj, browserState.key) + ) { + bodyObj[browserState.key] = browserState.value; + body = JSON.stringify(bodyObj); + } + } catch (e) { + // body is not JSON — leave as is + } + } } return originalSend.apply(this, [body]); @@ -724,6 +754,11 @@ class ApbctHandler { } else { args[1].body.append('ct_no_cookie_hidden_field', getNoCookieData()); } + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + args[1].body.append(browserState.key, browserState.value); + } } } } catch (e) { @@ -780,6 +815,22 @@ class ApbctHandler { * @return {string|FormData} Modified body. */ const attachFieldsToBody = function(body, fieldPair = false) { + const fieldPairs = [fieldPair, apbctGetBrowserStatePair()]; + + for (const pair of fieldPairs) { + body = attachFieldPairToBody(body, pair); + } + + return body; + }; + + /** + * Attach a single key/value pair to fetch request body + * @param {string|FormData} body Fetch request data body. + * @param {object|bool} fieldPair Key value to inject. + * @return {string|FormData} Modified body. + */ + const attachFieldPairToBody = function(body, fieldPair = false) { if (fieldPair) { if (body instanceof FormData || typeof body.append === 'function') { body.append(fieldPair.key, fieldPair.value); @@ -1076,11 +1127,15 @@ class ApbctHandler { const batchPayload = JSON.parse(args[1].body); if (batchPayload.requests && Array.isArray(batchPayload.requests)) { const fieldPair = selectFieldsData(+ctPublic.bot_detector_enabled); + const browserState = apbctGetBrowserStatePair(); for (const req of batchPayload.requests) { const isAddItem = req.path === '/wc/store/v1/cart/add-item'; if (isAddItem && req.body && fieldPair && fieldPair.key) { req.body[fieldPair.key] = fieldPair.value; } + if (isAddItem && req.body && browserState) { + req.body[browserState.key] = browserState.value; + } } args[1].body = JSON.stringify(batchPayload); } @@ -1329,6 +1384,16 @@ class ApbctHandler { } } + // Bot detector browser state + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + if (sourceSign.keepUnwrapped) { + ajaxDataFormData.append(browserState.key, browserState.value); + } else { + ajaxDataFormData.append('data[' + browserState.key + ']', browserState.value); + } + } + // Visible fields if (sourceSign.attachVisibleFieldsData) { let visibleFieldsSearchResult = false; @@ -1387,6 +1452,15 @@ class ApbctHandler { let eventToken = ''; let noCookieData = ''; let visibleFieldsString = ''; + let browserStateString = ''; + + const browserState = apbctGetBrowserStatePair(); + if (browserState) { + const browserStateKey = sourceSign.keepUnwrapped ? + 'apbct_browser_state' : + 'data%5Bapbct_browser_state%5D'; + browserStateString = browserStateKey + '=' + encodeURIComponent(browserState.value) + '&'; + } if ( +ctPublic.bot_detector_enabled && @@ -1428,7 +1502,7 @@ class ApbctHandler { } } - return noCookieData + eventToken + visibleFieldsString + ajaxDataString; + return noCookieData + eventToken + browserStateString + visibleFieldsString + ajaxDataString; } /** diff --git a/js/src/public-3-bot-detector-log.js b/js/src/public-3-bot-detector-log.js index 6630a29e6..03c239635 100644 --- a/js/src/public-3-bot-detector-log.js +++ b/js/src/public-3-bot-detector-log.js @@ -1,61 +1,76 @@ -let botDetectorLogLastUpdate = 0; -let botDetectorLogEventTypesCollected = []; - -// bot_detector frontend_data log alt session saving cron -if ( - ctPublicFunctions.hasOwnProperty('bot_detector_enabled') && - +ctPublicFunctions.bot_detector_enabled && - ctPublicFunctions.hasOwnProperty('data__frontend_data_log_enabled') && - ctPublicFunctions.data__frontend_data_log_enabled == 1 -) { - sendBotDetectorLogToAltSessions(1000); -} - /** - * Send BotDetector logs data to alternative sessions. - * If log_last_update has changed and log contains new event types, the log will be sent to the alternative sessions. - * @param {int} cronStartTimeout delay before cron start - * @param {int} interval check fires on interval + * Bot detector browser state. + * + * The state is passed to the site backend with the current JS->PHP transport and goes + * to the moderate request as sender_info.bot_detector_frontend_data_log. */ -function sendBotDetectorLogToAltSessions(cronStartTimeout = 3000, interval = 1000) { - setTimeout(function() { - setInterval(function() { - const currentLog = apbctLocalStorage.get('ct_bot_detector_frontend_data_log'); - if (needsSaveLogToAltSessions(currentLog)) { - botDetectorLogLastUpdate = currentLog.log_last_update; - // the log will be taken from javascriptclientdata - ctSetAlternativeCookie([], {forceAltCookies: true}); - } - }, interval); - }, cronStartTimeout); -} +class ApbctBrowserState { + static STATE_KEY = 'apbct_browser_state'; + static LOG_KEY = 'ct_bot_detector_frontend_data_log'; + static SCRIPT_SIGNS = { + botd_wrapper_loaded: 'ct-bot-detector-wrapper', + botd_logic_loaded: 'ct-bot-detector.min', + }; + static botdWrapperLoaded = 0; + static botdLogicLoaded = 0; -/** - * Check if the log needs to be saved to the alt sessions. If the log has new event types, it will be saved. - * @param {object} currentLog - * @return {boolean} - */ -function needsSaveLogToAltSessions(currentLog) { - if ( - currentLog && currentLog.hasOwnProperty('log_last_update') && - botDetectorLogLastUpdate !== currentLog.log_last_update - ) { + /** + * Get the current browser state as a JSON string ready to be transferred. + * @return {string} Empty string if the bot detector is disabled or on any collecting error. + */ + static toJson() { try { - for (let i = 0; i < currentLog.records.length; i++) { - const currentType = currentLog.records[i].frontend_data.js_event; - // check if this event type was already collected - if (currentType !== undefined && botDetectorLogEventTypesCollected.includes(currentType)) { - continue; - } - // add new event type to collection, this type will be sent to the alt sessions further - botDetectorLogEventTypesCollected.push(currentType); - return true; + if (!+ctPublicFunctions.bot_detector_enabled) { + return ''; } + + const log = localStorage.getItem(ApbctBrowserState.LOG_KEY); + return JSON.stringify({ + botd_logic_loaded: ApbctBrowserState.botdLogicLoaded, + botd_wrapper_loaded: ApbctBrowserState.botdWrapperLoaded, + frontend_data_log: typeof log === 'string' ? log : '', + }); } catch (e) { - console.log('APBCT: bot detector log collection error: ' . e.toString()); + return ''; } } - return false; + + /** + * Look for the bot detector scripts in the page DOM. + * @return {boolean} True if both scripts are found. + */ + static detectScripts() { + const scripts = document.getElementsByTagName('script'); + + for (let i = 0; i < scripts.length; i++) { + const src = scripts[i].getAttribute('src'); + if (!src) { + continue; + } + if (src.indexOf(ApbctBrowserState.SCRIPT_SIGNS.botd_wrapper_loaded) !== -1) { + ApbctBrowserState.botdWrapperLoaded = 1; + } else if (src.indexOf(ApbctBrowserState.SCRIPT_SIGNS.botd_logic_loaded) !== -1) { + ApbctBrowserState.botdLogicLoaded = 1; + } + } + + // The bot detector logic sets this flag on its own start + + return !!ApbctBrowserState.botdWrapperLoaded && !!ApbctBrowserState.botdLogicLoaded; + } } +/** + * Get the browser state as a key/value pair to attach it to an intercepted request. + * @return {{key: string, value: string}|false} False if there is nothing to attach. + */ +function apbctGetBrowserStatePair() { // eslint-disable-line no-unused-vars + try { + ApbctBrowserState.detectScripts(); + const state = ApbctBrowserState.toJson(); + return state ? {key: ApbctBrowserState.STATE_KEY, value: state} : false; + } catch (e) { + return false; + } +} diff --git a/lib/Cleantalk/ApbctWP/Variables/AltSessions.php b/lib/Cleantalk/ApbctWP/Variables/AltSessions.php index 20c41f687..bfd21850b 100644 --- a/lib/Cleantalk/ApbctWP/Variables/AltSessions.php +++ b/lib/Cleantalk/ApbctWP/Variables/AltSessions.php @@ -36,6 +36,7 @@ class AltSessions 'ct_sfw_passed' => 'int', 'ct_gathering_loaded' => 'bool', 'apbct_search_form__honeypot_value' => 'string', + 'apbct_browser_state' => 'json', ]; /** diff --git a/tests/StandaloneFunctions/TestBotDetectorLogGathering.php b/tests/StandaloneFunctions/TestBotDetectorLogGathering.php index 133e0a9a3..926f7c901 100644 --- a/tests/StandaloneFunctions/TestBotDetectorLogGathering.php +++ b/tests/StandaloneFunctions/TestBotDetectorLogGathering.php @@ -1,89 +1,184 @@ $log, + 'botd_logic_loaded' => 1, + 'botd_wrapper_loaded' => 1, + )); + } + + private function getExpected($status, $error_msg, $state) + { + return json_encode(array( + 'plugin_status' => $status, + 'error_msg' => $error_msg, + 'apbct_browser_state' => $state, + )); + } + + private function getDefaultState() + { + return array( + 'frontend_data_log' => '', + 'botd_logic_loaded' => 0, + 'botd_wrapper_loaded' => 0, + ); + } + + protected function setUp(): void { global $apbct; + $apbct->data['cookies_type'] = 'alternative'; $apbct->data['bot_detector_enabled'] = '1'; - AltSessions::set('ct_bot_detector_frontend_data_log', json_encode(['log' => 'data'])); + $this->resetTransports(); + } - $result = apbct__bot_detector_get_fd_log(); - $expected = json_encode([ - 'plugin_status' => 'OK', - 'error_msg' => '', - 'frontend_data_log' => ['log' => 'data'] - ]); + protected function tearDown(): void + { + $this->resetTransports(); + } - $this->assertEquals($expected, $result); + /** + * Every transport keeps its own state between the calls: + * Post caches each variable in the singleton, NoCookie holds a static array, + * AltSessions store the values in the DB. Without the full reset the tests + * would depend on the execution order. + */ + private function resetTransports() + { + unset($_POST['apbct_browser_state'], $_POST['data']); + Post::getInstance()->variables = array(); + NoCookie::$no_cookies_data = array(); + AltSessions::wipe(); } - public function test_returnsErrorWhenDisabled() + public function test_returnsBrowserStateFromAltSessions() { - global $apbct; - $apbct->data['bot_detector_enabled'] = '0'; + AltSessions::set('apbct_browser_state', $this->getBrowserState()); - $result = apbct__bot_detector_get_fd_log(); - $expected = json_encode([ - 'plugin_status' => 'ERROR', - 'error_msg' => 'bot detector library usage is disabled', - 'frontend_data_log' => '' - ]); + $expected = $this->getExpected('OK', '', array( + 'frontend_data_log' => '[["success"],[true]]', + 'botd_logic_loaded' => 1, + 'botd_wrapper_loaded' => 1, + )); - $this->assertEquals($expected, $result); + $this->assertEquals($expected, apbct__bot_detector_get_fd_log()); } - public function test_returnsErrorWhenNoLogFound() + public function test_returnsBrowserStateFromNoCookie() { global $apbct; - $apbct->data['bot_detector_enabled'] = '1'; - AltSessions::wipe(); + $apbct->data['cookies_type'] = 'none'; + NoCookie::set('apbct_browser_state', $this->getBrowserState()); - $result = apbct__bot_detector_get_fd_log(); - $expected = json_encode([ - 'plugin_status' => 'ERROR', - 'error_msg' => 'no log found in alt sessions', - 'frontend_data_log' => '' - ]); + $expected = $this->getExpected('OK', '', array( + 'frontend_data_log' => '[["success"],[true]]', + 'botd_logic_loaded' => 1, + 'botd_wrapper_loaded' => 1, + )); - $this->assertEquals($expected, $result); + $this->assertEquals($expected, apbct__bot_detector_get_fd_log()); } - public function test_returnsErrorWhenLogNotString() + public function test_returnsBrowserStateFromPost() { - global $apbct; - $apbct->data['bot_detector_enabled'] = '1'; - AltSessions::set('ct_bot_detector_frontend_data_log', false); + $_POST['apbct_browser_state'] = $this->getBrowserState(); + + $result = json_decode(apbct__bot_detector_get_fd_log(), true); + + $this->assertEquals('OK', $result['plugin_status']); + $this->assertEquals('[["success"],[true]]', $result['apbct_browser_state']['frontend_data_log']); + $this->assertEquals(1, $result['apbct_browser_state']['botd_logic_loaded']); + $this->assertEquals(1, $result['apbct_browser_state']['botd_wrapper_loaded']); + } + + public function test_returnsBrowserStateFromPostDataArray() + { + $_POST['data'] = array('apbct_browser_state' => $this->getBrowserState()); - $result = apbct__bot_detector_get_fd_log(); - $expected = json_encode([ - 'plugin_status' => 'ERROR', - 'error_msg' => 'no log found in alt sessions', - 'frontend_data_log' => '' - ]); + $result = json_decode(apbct__bot_detector_get_fd_log(), true); - $this->assertEquals($expected, $result); + $this->assertEquals('OK', $result['plugin_status']); + $this->assertEquals('[["success"],[true]]', $result['apbct_browser_state']['frontend_data_log']); + $this->assertEquals(1, $result['apbct_browser_state']['botd_logic_loaded']); + $this->assertEquals(1, $result['apbct_browser_state']['botd_wrapper_loaded']); } - public function test_returnsErrorWhenLogNotJson() + public function test_returnsErrorWhenDisabled() { global $apbct; - $apbct->data['bot_detector_enabled'] = '1'; - AltSessions::set('ct_bot_detector_frontend_data_log', 'invalid json'); + $apbct->data['bot_detector_enabled'] = '0'; + AltSessions::set('apbct_browser_state', $this->getBrowserState()); + + $expected = $this->getExpected( + 'ERROR', + 'bot detector library usage is disabled', + $this->getDefaultState() + ); + + $this->assertEquals($expected, apbct__bot_detector_get_fd_log()); + } + + public function test_returnsErrorWhenNoStateProvided() + { + $expected = $this->getExpected( + 'ERROR', + 'no browser state provided by the transport', + $this->getDefaultState() + ); + + $this->assertEquals($expected, apbct__bot_detector_get_fd_log()); + } + + public function test_returnsErrorWhenStateNotJson() + { + AltSessions::set('apbct_browser_state', 'invalid json'); + + $expected = $this->getExpected( + 'ERROR', + 'no browser state provided by the transport', + $this->getDefaultState() + ); + + $this->assertEquals($expected, apbct__bot_detector_get_fd_log()); + } + + public function test_returnsEmptyLogWhenBotDetectorDidNotLoad() + { + AltSessions::set('apbct_browser_state', json_encode(array( + 'frontend_data_log' => '', + 'botd_logic_loaded' => 0, + 'botd_wrapper_loaded' => 1, + ))); + + $result = json_decode(apbct__bot_detector_get_fd_log(), true); + + $this->assertEquals('OK', $result['plugin_status']); + $this->assertEquals('', $result['apbct_browser_state']['frontend_data_log']); + $this->assertEquals(0, $result['apbct_browser_state']['botd_logic_loaded']); + $this->assertEquals(1, $result['apbct_browser_state']['botd_wrapper_loaded']); + } + + public function test_partialStateIsFilledWithDefaults() + { + AltSessions::set('apbct_browser_state', json_encode(array( + 'botd_wrapper_loaded' => 1, + ))); - $result = apbct__bot_detector_get_fd_log(); - $expected = json_encode([ - 'plugin_status' => 'ERROR', - 'error_msg' => 'can not decode data from alt sessions', - 'frontend_data_log' => '' - ]); + $expected = $this->getExpected('OK', '', array( + 'frontend_data_log' => '', + 'botd_logic_loaded' => 0, + 'botd_wrapper_loaded' => 1, + )); - $this->assertEquals($expected, $result); + $this->assertEquals($expected, apbct__bot_detector_get_fd_log()); } }