Skip to content

Commit 9d0a4a3

Browse files
authored
[NOCSL] Adds extra url validation for android referrers (#420)
* add url validation for android referres * lint spell check * add url trimming * remove mutation
1 parent f123f36 commit 9d0a4a3

File tree

3 files changed

+227
-6
lines changed

3 files changed

+227
-6
lines changed

cspell.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"atcs",
4949
"testdata",
5050
"Bytespider",
51-
"Timespans"
51+
"Timespans",
52+
"googlequicksearchbox"
5253
]
5354
}

spec/src/utils/helpers.js

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const {
1515
isNil,
1616
getWindowLocation,
1717
getCanonicalUrl,
18+
getDocumentReferrer,
1819
dispatchEvent,
1920
createCustomEvent,
2021
hasOrderIdRecord,
@@ -23,6 +24,8 @@ const {
2324
stringify,
2425
convertResponseToJson,
2526
addHTTPSToString,
27+
trimUrl,
28+
cleanAndValidateUrl,
2629
} = require('../../../test/utils/helpers'); // eslint-disable-line import/extensions
2730
const jsdom = require('./jsdom-global');
2831
const store = require('../../../test/utils/store'); // eslint-disable-line import/extensions
@@ -215,6 +218,18 @@ describe('ConstructorIO - Utils - Helpers', () => {
215218
});
216219

217220
describe('getCanonicalUrl', () => {
221+
it('Should return android app referrers in a valid url structure', () => {
222+
const cleanup = jsdom();
223+
224+
const canonicalUrl = 'android-app://com.google.android.googlequicksearchbox/';
225+
const canonicalEle = document.querySelector('[rel=canonical]');
226+
canonicalEle.setAttribute('href', canonicalUrl);
227+
228+
expect(getCanonicalUrl()).to.equal('https://com.google.android.googlequicksearchbox/');
229+
230+
cleanup();
231+
});
232+
218233
it('Should return the canonical URL from the DOM link element', () => {
219234
const cleanup = jsdom();
220235

@@ -264,6 +279,67 @@ describe('ConstructorIO - Utils - Helpers', () => {
264279
});
265280
});
266281

282+
describe('getDocumentReferrer', () => {
283+
it('Should return android app referrers in a valid url structure', () => {
284+
const cleanup = jsdom();
285+
286+
const referrerUrl = 'android-app://com.google.android.googlequicksearchbox/';
287+
Object.defineProperty(document, 'referrer', {
288+
value: referrerUrl,
289+
configurable: true,
290+
});
291+
292+
expect(getDocumentReferrer()).to.equal('https://com.google.android.googlequicksearchbox/');
293+
294+
cleanup();
295+
});
296+
297+
it('Should return the referrer URL from the document', () => {
298+
const cleanup = jsdom();
299+
300+
const referrerUrl = 'https://constructor.io/products/item';
301+
Object.defineProperty(document, 'referrer', {
302+
value: referrerUrl,
303+
configurable: true,
304+
});
305+
306+
expect(getDocumentReferrer()).to.equal(referrerUrl);
307+
308+
cleanup();
309+
});
310+
311+
it('Should return null for a relative url', () => {
312+
const cleanup = jsdom();
313+
314+
const relativeUrl = '/products/item';
315+
Object.defineProperty(document, 'referrer', {
316+
value: relativeUrl,
317+
configurable: true,
318+
});
319+
320+
const result = getDocumentReferrer();
321+
expect(result).to.be.null;
322+
323+
cleanup();
324+
});
325+
326+
it('Should return null when referrer is empty', () => {
327+
const cleanup = jsdom();
328+
329+
Object.defineProperty(document, 'referrer', {
330+
value: '',
331+
configurable: true,
332+
});
333+
334+
expect(getDocumentReferrer()).to.be.null;
335+
cleanup();
336+
});
337+
338+
it('Should return null when not in a DOM context', () => {
339+
expect(getDocumentReferrer()).to.be.null;
340+
});
341+
});
342+
267343
describe('dispatchEvent', () => {
268344
it('Should dispatch an event if in a DOM context', () => {
269345
const cleanup = jsdom();
@@ -539,5 +615,88 @@ describe('ConstructorIO - Utils - Helpers', () => {
539615
expect(addHTTPSToString(testUrl)).to.equal(null);
540616
});
541617
});
618+
619+
describe('trimUrl', () => {
620+
it('Should return the URL as-is if it is under the max length', () => {
621+
const testUrl = new URL('https://www.constructor.io/search?q=test');
622+
const result = trimUrl(testUrl);
623+
624+
expect(result).to.equal('https://www.constructor.io/search?q=test');
625+
});
626+
627+
it('Should remove the longest parameter when URL exceeds max length', () => {
628+
const longValue = 'a'.repeat(2000);
629+
const testUrl = new URL(`https://www.constructor.io/search?short=b&long=${longValue}`);
630+
const result = trimUrl(testUrl, 100);
631+
632+
expect(result).to.include('short=b');
633+
expect(result).to.not.include('long=');
634+
expect(result.length).to.be.at.most(100);
635+
});
636+
637+
it('Should remove multiple parameters starting with the longest', () => {
638+
const testUrl = new URL('https://www.constructor.io/search?a=1&b=22&c=333&d=4444');
639+
const result = trimUrl(testUrl, 50);
640+
641+
expect(result.length).to.be.at.most(50);
642+
});
643+
644+
it('Should truncate URL if removing all parameters is not enough', () => {
645+
const longPath = 'a'.repeat(2000);
646+
const testUrl = new URL(`https://www.constructor.io/${longPath}`);
647+
const result = trimUrl(testUrl, 100);
648+
649+
expect(result.length).to.equal(100);
650+
});
651+
652+
it('Should use custom maxLen parameter', () => {
653+
const testUrl = new URL('https://www.constructor.io/search?param=value');
654+
const customMaxLen = 30;
655+
const result = trimUrl(testUrl, customMaxLen);
656+
657+
expect(result.length).to.be.at.most(customMaxLen);
658+
});
659+
});
660+
661+
describe('cleanAndValidateUrl', () => {
662+
it('Should return a valid URL string', () => {
663+
const testUrl = 'https://www.constructor.io/search?q=test';
664+
const result = cleanAndValidateUrl(testUrl);
665+
666+
expect(result).to.equal(testUrl);
667+
});
668+
669+
it('Should handle android-app referrers by converting to https', () => {
670+
const androidUrl = 'android-app://com.google.android.googlequicksearchbox/path';
671+
const result = cleanAndValidateUrl(androidUrl);
672+
673+
expect(result).to.include('https://');
674+
expect(result).to.include('com.google.android.googlequicksearchbox');
675+
});
676+
677+
it('Should return null for invalid URLs', () => {
678+
const invalidUrl = 'not a valid url';
679+
const result = cleanAndValidateUrl(invalidUrl);
680+
681+
expect(result).to.be.null;
682+
});
683+
684+
it('Should handle relative URLs with baseUrl', () => {
685+
const relativeUrl = '/search?q=test';
686+
const baseUrl = 'https://www.constructor.io';
687+
const result = cleanAndValidateUrl(relativeUrl, baseUrl);
688+
689+
expect(result).to.include('https://www.constructor.io/search?q=test');
690+
});
691+
692+
it('Should trim URLs that exceed max length', () => {
693+
const longValue = 'a'.repeat(2000);
694+
const testUrl = `https://www.constructor.io/search?param=${longValue}`;
695+
const result = cleanAndValidateUrl(testUrl);
696+
697+
expect(result).to.not.be.null;
698+
expect(result.length).to.be.at.most(2000);
699+
});
700+
});
542701
}
543702
});

src/utils/helpers.js

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const PII_REGEX = [
1919
},
2020
// Add more PII REGEX
2121
];
22+
const URL_MAX_LEN = 2000;
2223

2324
const utils = {
2425
trimNonBreakingSpaces: (string) => string.replace(/\s/g, ' ').trim(),
@@ -43,6 +44,61 @@ const utils = {
4344

4445
return cleanedParams;
4546
},
47+
trimUrl: (urlObj, maxLen = URL_MAX_LEN) => {
48+
let urlString = urlObj.toString();
49+
50+
if (urlString.length <= maxLen) {
51+
return urlString;
52+
}
53+
54+
const urlCopy = new URL(urlObj.toString());
55+
const { searchParams } = urlCopy;
56+
const paramEntries = Array.from(searchParams.entries());
57+
58+
if (paramEntries.length === 0) {
59+
return utils.truncateString(urlString, maxLen);
60+
}
61+
62+
paramEntries.sort((a, b) => {
63+
const aLength = a[0].length + a[1].length;
64+
const bLength = b[0].length + b[1].length;
65+
return bLength - aLength;
66+
});
67+
68+
for (let i = 0; i < paramEntries.length; i += 1) {
69+
if (urlString.length <= maxLen) {
70+
break;
71+
}
72+
searchParams.delete(paramEntries[i][0]);
73+
urlString = urlCopy.toString();
74+
}
75+
76+
if (urlString.length > maxLen) {
77+
return utils.truncateString(urlString, maxLen);
78+
}
79+
80+
return urlString;
81+
},
82+
83+
cleanAndValidateUrl: (url, baseUrl = undefined) => {
84+
let validatedUrl = null;
85+
86+
try {
87+
// Handle android app referrers
88+
if (url?.startsWith('android-app')) {
89+
url = url?.replace('android-app', 'https');
90+
}
91+
92+
const urlObj = new URL(url, baseUrl);
93+
const trimmedUrl = new URL(utils.trimUrl(urlObj));
94+
95+
validatedUrl = trimmedUrl.toString();
96+
} catch (e) {
97+
// do nothing
98+
}
99+
100+
return validatedUrl;
101+
},
46102

47103
throwHttpErrorFromResponse: (error, response) => response.json().then((json) => {
48104
error.message = json.message;
@@ -92,11 +148,17 @@ const utils = {
92148
},
93149

94150
getDocumentReferrer: () => {
95-
if (utils.canUseDOM()) {
96-
return document?.referrer;
151+
let documentReferrer = null;
152+
153+
try {
154+
if (utils.canUseDOM()) {
155+
documentReferrer = utils.cleanAndValidateUrl(document.referrer);
156+
}
157+
} catch (e) {
158+
// do nothing
97159
}
98160

99-
return null;
161+
return documentReferrer;
100162
},
101163

102164
getCanonicalUrl: () => {
@@ -108,8 +170,7 @@ const utils = {
108170
const href = linkEle?.getAttribute('href');
109171

110172
if (href) {
111-
const url = new URL(href, document.location.href);
112-
canonicalURL = url.toString();
173+
canonicalURL = utils.cleanAndValidateUrl(href, document.location.href);
113174
}
114175
}
115176
} catch (e) {

0 commit comments

Comments
 (0)