From 991d55dc9f288c026ed47cd8c0d6d7ade8893f00 Mon Sep 17 00:00:00 2001 From: Stanislav A Date: Tue, 11 Oct 2022 20:17:37 +0300 Subject: [PATCH] redo cookie parsing and matching, update tests --- .../{prepare-cookie.js => cookie-utils.js} | 29 ++++++++ src/helpers/index.js | 2 +- src/scriptlets/trusted-click-element.js | 74 ++++++++++++++----- .../scriptlets/trusted-click-element.test.js | 48 +++++++++--- 4 files changed, 123 insertions(+), 30 deletions(-) rename src/helpers/{prepare-cookie.js => cookie-utils.js} (59%) diff --git a/src/helpers/prepare-cookie.js b/src/helpers/cookie-utils.js similarity index 59% rename from src/helpers/prepare-cookie.js rename to src/helpers/cookie-utils.js index 29e1abb28..5de8ddfd9 100644 --- a/src/helpers/prepare-cookie.js +++ b/src/helpers/cookie-utils.js @@ -49,3 +49,32 @@ export const prepareCookie = (name, value) => { return cookieData; }; + +/** + * Parses cookie string into object + * @param {string} cookieString string that conforms to document.cookie format + * @returns {object} key:value object that corresponds with incoming cookies keys and values + */ +export const parseCookieString = (cookieString) => { + const COOKIE_DELIMITER = '='; + const COOKIE_PAIRS_DELIMITER = ';'; + + return cookieString + .split(COOKIE_PAIRS_DELIMITER) + .reduce((result, string) => { + const delimiterIndex = string.indexOf(COOKIE_DELIMITER); + let cookieKey; + let cookieValue; + if (delimiterIndex === -1) { + cookieKey = string.trim(); + } else { + cookieKey = string.slice(0, delimiterIndex).trim(); + cookieValue = string.slice(delimiterIndex + 1, string.length); + } + + return Object.defineProperty(result, cookieKey, { + value: cookieValue || null, + enumerable: true, + }); + }, {}); +}; diff --git a/src/helpers/index.js b/src/helpers/index.js index f1ea238c4..eeee05c94 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -14,7 +14,7 @@ export * from './observer'; export * from './match-stack'; export * from './open-shadow-dom-utils'; export * from './array-utils'; -export * from './prepare-cookie'; +export * from './cookie-utils'; export * from './number-utils'; export * from './adjust-set-utils'; export * from './fetch-utils'; diff --git a/src/scriptlets/trusted-click-element.js b/src/scriptlets/trusted-click-element.js index 319961c49..7fadb004f 100644 --- a/src/scriptlets/trusted-click-element.js +++ b/src/scriptlets/trusted-click-element.js @@ -1,6 +1,7 @@ import { hit, toRegExp, + parseCookieString, } from '../helpers/index'; /* eslint-disable max-len */ @@ -19,11 +20,11 @@ import { * - `selectors` — required, string with query selectors delimited by comma * - `extraMatch` — optional, extra condition to check on a page; allows to match `cookie` and `localStorage`; can be set as `name:key[=value]` where `value` is optional. * Multiple conditions are allowed inside one `extraMatch` but they should be delimited by comma and each of them should match the syntax. Possible `name`s: - * - `cookie` - test substring against document.cookie string + * - `cookie` - test string or regex against cookies on a page * - `localStorage` - check if localStorage item is present * - 'delay' - optional, time in ms to delay scriptlet execution, defaults to instant execution. * **Examples** - * 1. Click elements by selector + * 1. Click single element by selector * ``` * example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"]') * ``` @@ -35,22 +36,27 @@ import { * * 3. Click multiple elements by selector with a delay * ``` - * example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"], button[name='check"], input[type="submit"][value="akkoord"]', '500') + * example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"], button[name='check"], input[type="submit"][value="akkoord"]', '', '500') * ``` * - * 4. Match by cookie strings and click multiple elements by selector + * 4. Match cookies by keys using regex and string * ``` - * example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"], input[type="submit"][value="akkoord"]', '', 'cookie:userConsentCommunity, cookie:cmpconsent=1') + * example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"]', 'cookie:userConsentCommunity, cookie:/cmpconsent|cmp/', '') * ``` * - * 5. Match by localStorage item 'promo' key + * 5. Match by cookie key=value pairs using regex and string + * ``` + * example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"]', 'cookie:userConsentCommunity=true, cookie:/cmpconsent|cmp/=/[a-z]{1,5}/', '') + * ``` + * + * 6. Match by localStorage item 'promo' key * ``` * example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"]', '', 'localStorage:promo') * ``` * - * 6. Click multiple elements with delay and matching by both cookie string and localStorage item + * 7. Click multiple elements with delay and matching by both cookie string and localStorage item * ``` - * example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"], input[type="submit"][value="akkoord"]', '250', 'cookie:cmpconsent, localStorage:promo') + * example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"], input[type="submit"][value="akkoord"]', 'cookie:cmpconsent, localStorage:promo', '250') * ``` */ /* eslint-enable max-len */ @@ -66,6 +72,7 @@ export function trustedClickElement(source, selectors, extraMatch = '', delay = const COOKIE_MATCH_MARKER = 'cookie:'; const LOCAL_STORAGE_MATCH_MARKER = 'localStorage:'; const COMMON_DELIMITER = ','; + const COOKIE_STRING_DELIMITER = ';'; let parsedDelay; if (delay) { @@ -79,8 +86,8 @@ export function trustedClickElement(source, selectors, extraMatch = '', delay = let canClick = !parsedDelay; - const cookieMatch = []; - const localStorageMatch = []; + const cookieMatches = []; + const localStorageMatches = []; if (extraMatch) { // Get all match marker:value pairs from argument @@ -91,25 +98,53 @@ export function trustedClickElement(source, selectors, extraMatch = '', delay = // Filter match pairs by marker parsedExtraMatch.forEach((matchStr) => { if (matchStr.indexOf(COOKIE_MATCH_MARKER) > -1) { - const cookieMatchValue = matchStr.replace(COOKIE_MATCH_MARKER, ''); - cookieMatch.push(cookieMatchValue); + const cookieMatch = matchStr.replace(COOKIE_MATCH_MARKER, ''); + cookieMatches.push(cookieMatch); } if (matchStr.indexOf(LOCAL_STORAGE_MATCH_MARKER) > -1) { - const localStorageMatchValue = matchStr.replace(LOCAL_STORAGE_MATCH_MARKER, ''); - localStorageMatch.push(localStorageMatchValue); + const localStorageMatch = matchStr.replace(LOCAL_STORAGE_MATCH_MARKER, ''); + localStorageMatches.push(localStorageMatch); } }); } - if (cookieMatch.length > 0) { - const cookieMatched = cookieMatch.every((str) => document.cookie.indexOf(str) !== -1); - if (!cookieMatched) { + if (cookieMatches.length > 0) { + const parsedCookieMatches = parseCookieString(cookieMatches.join(COOKIE_STRING_DELIMITER)); + const parsedCookies = parseCookieString(document.cookie); + const cookieKeys = Object.keys(parsedCookies); + if (cookieKeys.length === 0) { + return; + } + + const cookiesMatched = Object.keys(parsedCookieMatches).every((key) => { + // Avoid getting /.?/ result from toRegExp on undefined + // as cookie may be set without value, + // on which cookie parsing will return cookieKey:undefined pair + const valueMatch = parsedCookieMatches[key] ? toRegExp(parsedCookieMatches[key]) : null; + const keyMatch = toRegExp(key); + + return cookieKeys.some((key) => { + const keysMatched = keyMatch.test(key); + if (!keysMatched) { + return false; + } + + // Key matching is enough if cookie value match is not specified + if (!valueMatch) { + return true; + } + + return valueMatch.test(parsedCookies[key]); + }); + }); + + if (!cookiesMatched) { return; } } - if (localStorageMatch.length > 0) { - const localStorageMatched = localStorageMatch + if (localStorageMatches.length > 0) { + const localStorageMatched = localStorageMatches .every((str) => { const itemValue = window.localStorage.getItem(str); return itemValue || itemValue === ''; @@ -260,4 +295,5 @@ trustedClickElement.names = [ trustedClickElement.injections = [ hit, toRegExp, + parseCookieString, ]; diff --git a/tests/scriptlets/trusted-click-element.test.js b/tests/scriptlets/trusted-click-element.test.js index 25ea0a791..5d5fb485b 100644 --- a/tests/scriptlets/trusted-click-element.test.js +++ b/tests/scriptlets/trusted-click-element.test.js @@ -161,10 +161,10 @@ test('Multiple elements clicked, non-ordered render', (assert) => { }); test('extraMatch - single cookie match, matched', (assert) => { - const cName = 'first'; - const cookieData = prepareCookie(cName, 'true'); + const cookieKey1 = 'first'; + const cookieData = prepareCookie(cookieKey1, 'true'); document.cookie = cookieData; - const EXTRA_MATCH_STR = `cookie:${cName}`; + const EXTRA_MATCH_STR = `cookie:${cookieKey1}`; const ELEM_COUNT = 1; // Check elements for being clicked and hit func execution @@ -184,15 +184,15 @@ test('extraMatch - single cookie match, matched', (assert) => { assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); done(); }, 150); - clearCookie(); + clearCookie(cookieKey1); }); test('extraMatch - single cookie match, not matched', (assert) => { - const cName = 'first'; - const cName2 = 'second'; - const cookieData = prepareCookie(cName, 'true'); + const cookieKey1 = 'first'; + const cookieKey2 = 'second'; + const cookieData = prepareCookie(cookieKey1, 'true'); document.cookie = cookieData; - const EXTRA_MATCH_STR = `cookie:${cName2}`; + const EXTRA_MATCH_STR = `cookie:${cookieKey2}`; const ELEM_COUNT = 1; // Check elements for being clicked and hit func execution @@ -209,10 +209,38 @@ test('extraMatch - single cookie match, not matched', (assert) => { setTimeout(() => { assert.notOk(clickable.getAttribute('clicked'), 'Element should not be clicked'); - assert.strictEqual(window.hit, undefined, 'hit shoud not fire'); + assert.strictEqual(window.hit, undefined, 'hit should not fire'); + done(); + }, 150); + clearCookie(cookieKey1); +}); + +test('extraMatch - string+regex cookie input, matched', (assert) => { + const cookieKey1 = 'first'; + const cookieVal1 = 'true'; + const cookieData1 = prepareCookie(cookieKey1, cookieVal1); + document.cookie = cookieData1; + const EXTRA_MATCH_STR = 'cookie:/firs/=true'; + + const ELEM_COUNT = 1; + // Check elements for being clicked and hit func execution + const ASSERTIONS = ELEM_COUNT + 1; + assert.expect(ASSERTIONS); + const done = assert.async(); + + const selectorsString = `#${PANEL_ID} > #${CLICKABLE_NAME}${ELEM_COUNT}`; + + runScriptlet(name, [selectorsString, EXTRA_MATCH_STR]); + const panel = createPanel(); + const clickable = createClickable(1); + panel.appendChild(clickable); + + setTimeout(() => { + assert.ok(clickable.getAttribute('clicked'), 'Element should be clicked'); + assert.strictEqual(window.hit, 'FIRED', 'hit func executed'); done(); }, 150); - clearCookie(); + clearCookie(cookieKey1); }); test('extraMatch - single localStorage match, matched', (assert) => {