@govtechsg/oobee 0.10.84 → 0.10.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/.github/workflows/image.yml +3 -2
  2. package/.github/workflows/publish.yml +10 -0
  3. package/DETAILS.md +29 -0
  4. package/dist/cli.js +7 -6
  5. package/dist/combine.js +1 -1
  6. package/dist/constants/common.js +15 -4
  7. package/dist/constants/constants.js +604 -1
  8. package/dist/crawlers/commonCrawlerFunc.js +3 -2
  9. package/dist/crawlers/crawlSitemap.js +98 -80
  10. package/dist/crawlers/custom/utils.js +218 -71
  11. package/dist/crawlers/guards/urlGuard.js +8 -15
  12. package/dist/crawlers/runCustom.js +24 -15
  13. package/dist/generateOobeeClientScanner.js +570 -0
  14. package/dist/mergeAxeResults.js +49 -29
  15. package/dist/npmIndex.js +10 -2
  16. package/dist/proxyService.js +18 -3
  17. package/dist/services/s3Uploader.js +21 -10
  18. package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +2 -2
  19. package/dist/static/ejs/partials/scripts/ruleModal/constants.ejs +1 -761
  20. package/dist/static/ejs/summary.ejs +10 -5
  21. package/oobee-client-scanner.js +34992 -0
  22. package/package.json +3 -3
  23. package/src/cli.ts +20 -15
  24. package/src/combine.ts +3 -1
  25. package/src/constants/common.ts +22 -10
  26. package/src/constants/constants.ts +602 -1
  27. package/src/crawlers/commonCrawlerFunc.ts +4 -3
  28. package/src/crawlers/crawlSitemap.ts +116 -98
  29. package/src/crawlers/custom/utils.ts +244 -84
  30. package/src/crawlers/guards/urlGuard.ts +24 -31
  31. package/src/crawlers/runCustom.ts +38 -15
  32. package/src/generateOobeeClientScanner.ts +591 -0
  33. package/src/mergeAxeResults.ts +48 -29
  34. package/src/npmIndex.ts +12 -2
  35. package/src/proxyService.ts +25 -4
  36. package/src/services/s3Uploader.ts +23 -11
  37. package/src/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +2 -2
  38. package/src/static/ejs/partials/scripts/ruleModal/constants.ejs +1 -761
  39. package/src/static/ejs/summary.ejs +10 -5
  40. package/testStaticJSScanner.html +534 -0
@@ -1,12 +1,44 @@
1
- /* eslint-disable no-shadow */
2
1
  /* eslint-disable no-alert */
3
2
  /* eslint-disable no-param-reassign */
4
3
  /* eslint-env browser */
5
4
  import path from 'path';
5
+ import { getDomain } from 'tldts';
6
6
  import { runAxeScript } from '../commonCrawlerFunc.js';
7
7
  import { consoleLogger, guiInfoLog } from '../../logs.js';
8
8
  import { guiInfoStatusTypes } from '../../constants/constants.js';
9
9
  import { isSkippedUrl, validateCustomFlowLabel } from '../../constants/common.js';
10
+ const sameRegistrableDomain = (hostA, hostB) => {
11
+ const domainA = getDomain(hostA);
12
+ const domainB = getDomain(hostB);
13
+ if (!domainA || !domainB)
14
+ return hostA === hostB;
15
+ return domainA === domainB;
16
+ };
17
+ const parseBoolEnv = (val, defaultVal) => {
18
+ if (val == null)
19
+ return defaultVal;
20
+ const v = String(val).trim().toLowerCase();
21
+ if (['1', 'true', 'yes', 'y', 'on'].includes(v))
22
+ return true;
23
+ if (['0', 'false', 'no', 'n', 'off'].includes(v))
24
+ return false;
25
+ return defaultVal;
26
+ };
27
+ const RESTRICT_OVERLAY_TO_ENTRY_DOMAIN = parseBoolEnv(process.env.RESTRICT_OVERLAY_TO_ENTRY_DOMAIN, false);
28
+ const isOverlayAllowed = (currentUrl, entryUrl) => {
29
+ try {
30
+ const cur = new URL(currentUrl);
31
+ if (cur.protocol !== 'http:' && cur.protocol !== 'https:')
32
+ return false;
33
+ if (!RESTRICT_OVERLAY_TO_ENTRY_DOMAIN)
34
+ return true;
35
+ const base = new URL(entryUrl);
36
+ return sameRegistrableDomain(cur.hostname, base.hostname);
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ };
10
42
  //! For Cypress Test
11
43
  // env to check if Cypress test is running
12
44
  const isCypressTest = process.env.IS_CYPRESS_TEST === 'true';
@@ -30,16 +62,19 @@ export const screenshotFullPage = async (page, screenshotsDir, screenshotIdx) =>
30
62
  await page.evaluate(() => {
31
63
  window.scrollTo(0, document.body.scrollHeight);
32
64
  });
33
- const isLoadMoreContent = async () => new Promise(resolve => {
34
- setTimeout(async () => {
65
+ const isLoadMoreContent = async () => {
66
+ await new Promise(resolve => setTimeout(resolve, 2500));
67
+ if (page.isClosed())
68
+ return false;
69
+ try {
35
70
  await page.waitForLoadState('domcontentloaded');
36
- const newHeight = await page.evaluate(
37
- // eslint-disable-next-line no-shadow
38
- () => document.body.scrollHeight);
39
- const result = newHeight > prevHeight;
40
- resolve(result);
41
- }, 2500);
42
- });
71
+ const newHeight = await page.evaluate(() => document.body.scrollHeight);
72
+ return newHeight > prevHeight;
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ };
43
78
  const result = await isLoadMoreContent();
44
79
  return result;
45
80
  };
@@ -157,7 +192,7 @@ export const MENU_POSITION = {
157
192
  export const updateMenu = async (page, urlsCrawled) => {
158
193
  log(`Overlay menu: updating: ${page.url()}`);
159
194
  await page.evaluate(vars => {
160
- const shadowHost = document.querySelector('#oobee-shadow-host');
195
+ const shadowHost = document.querySelector('#oobeeShadowHost');
161
196
  if (shadowHost) {
162
197
  const p = shadowHost.shadowRoot.querySelector('#oobee-p-pages-scanned');
163
198
  if (p) {
@@ -200,7 +235,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
200
235
  </svg>
201
236
  `;
202
237
  minBtn.innerHTML = MINBTN_SVG;
203
- let currentPos = (vars.menuPos || 'RIGHT');
238
+ let currentPos = vars.menuPos || 'RIGHT';
204
239
  const isCollapsed = () => panel.classList.contains('collapsed');
205
240
  const setPosClass = (pos) => {
206
241
  panel.classList.remove('pos-left', 'pos-right');
@@ -217,7 +252,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
217
252
  setDraggableSidebarMenu();
218
253
  };
219
254
  const toggleCollapsed = (force) => {
220
- const willCollapse = (typeof force === 'boolean') ? force : !isCollapsed();
255
+ const willCollapse = typeof force === 'boolean' ? force : !isCollapsed();
221
256
  if (willCollapse) {
222
257
  panel.classList.add('collapsed');
223
258
  localStorage.setItem('oobee:overlay-collapsed', '1');
@@ -261,22 +296,60 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
261
296
  const h2 = document.createElement('h2');
262
297
  h2.id = 'oobeeHPagesScanned';
263
298
  h2.className = 'oobee-section-title';
264
- h2.textContent = 'Pages Scanned';
299
+ h2.textContent = `Pages Scanned (${vars.urlsCrawled.scanned.length || 0})`;
300
+ const scanIcon = document.createElement('span');
301
+ scanIcon.className = 'oobee-btn-icon';
302
+ const SCAN_SVG = `
303
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
304
+ <g clip-path="url(#clip0_1421_431)">
305
+ <path d="M12.5763 11.5472L12.2958 11.2857L12.1037 11.1005C12.776 10.3183 12.9194 9.56432 12.9194 8.45969C12.9194 5.99657 10.9228 4 8.45969 4C5.99657 4 4 5.99657 4 8.45969C4 10.9228 5.99657 12.9194 8.45969 12.9194C9.56432 12.9194 10.3183 12.776 11.1005 12.1037L11.2857 12.2958L11.5472 12.5763L14.9777 16L16 14.9777L12.5763 11.5472ZM8.45969 11.5472C6.75129 11.5472 5.37221 10.1681 5.37221 8.45969C5.37221 6.75129 6.75129 5.37221 8.45969 5.37221C10.1681 5.37221 11.5472 6.75129 11.5472 8.45969C11.5472 10.1681 10.1681 11.5472 8.45969 11.5472Z" fill="white"/>
306
+ <path d="M18.5 0H19.5C19.7761 0 20 0.223858 20 0.5V5H18.5V0Z" fill="white"/>
307
+ <path d="M19.5 2.18552e-08L19.5 1.5L15 1.5L15 -2.18556e-07L19.5 2.18552e-08Z" fill="white"/>
308
+ <path d="M1.5 0H0.5C0.223858 0 0 0.223858 0 0.5V5H1.5V0Z" fill="white"/>
309
+ <path d="M0.5 2.18552e-08L0.5 1.5L5 1.5L5 -2.18556e-07L0.5 2.18552e-08Z" fill="white"/>
310
+ <path d="M1.5 20H0.5C0.223858 20 0 19.7761 0 19.5V15H1.5V20Z" fill="white"/>
311
+ <path d="M0.5 20L0.5 18.5L5 18.5L5 20L0.5 20Z" fill="white"/>
312
+ <path d="M18.5 20H19.5C19.7761 20 20 19.7761 20 19.5V15H18.5V20Z" fill="white"/>
313
+ <path d="M19.5 20L19.5 18.5L15 18.5L15 20L19.5 20Z" fill="white"/>
314
+ </g>
315
+ <defs>
316
+ <clipPath id="clip0_1421_431">
317
+ <rect width="20" height="20" fill="white"/>
318
+ </clipPath>
319
+ </defs>
320
+ </svg>
321
+ `;
322
+ scanIcon.innerHTML = SCAN_SVG;
265
323
  const scanBtn = document.createElement('button');
266
324
  scanBtn.id = 'oobeeBtnScan';
267
325
  scanBtn.className = 'oobee-btn oobee-btn-primary';
268
- scanBtn.innerText = 'Scan this page';
269
326
  scanBtn.disabled = inProgress;
327
+ scanBtn.appendChild(scanIcon);
328
+ const scanText = document.createElement('span');
329
+ scanText.className = 'oobee-btn-text';
330
+ scanText.innerText = 'Scan page';
331
+ scanBtn.appendChild(scanText);
270
332
  scanBtn.addEventListener('click', async () => customWindow.handleOnScanClick?.());
271
- const stopBtn = document.createElement('button');
272
- stopBtn.id = 'oobeeBtnStop';
273
- stopBtn.className = 'oobee-btn oobee-btn-secondary';
274
- stopBtn.innerText = 'Stop scan';
275
- stopBtn.addEventListener('click', async () => customWindow.handleOnStopClick?.());
333
+ const endScanIcon = document.createElement('span');
334
+ endScanIcon.className = 'oobee-btn-icon';
335
+ const ENDSCAN_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
336
+ <path d="M10 0C4.47 0 0 4.47 0 10C0 15.53 4.47 20 10 20C15.53 20 20 15.53 20 10C20 4.47 15.53 0 10 0ZM10 18C5.59 18 2 14.41 2 10C2 5.59 5.59 2 10 2C14.41 2 18 5.59 18 10C18 14.41 14.41 18 10 18ZM13.59 5L10 8.59L6.41 5L5 6.41L8.59 10L5 13.59L6.41 15L10 11.41L13.59 15L15 13.59L11.41 10L15 6.41L13.59 5Z" fill="#9021A6"/>
337
+ </svg>
338
+ `;
339
+ endScanIcon.innerHTML = ENDSCAN_SVG;
340
+ const endScanBtn = document.createElement('button');
341
+ endScanBtn.id = 'oobeeBtnEndScan';
342
+ endScanBtn.className = 'oobee-btn oobee-btn-secondary';
343
+ endScanBtn.appendChild(endScanIcon);
344
+ const endScanText = document.createElement('span');
345
+ endScanText.className = 'oobee-btn-text';
346
+ endScanText.innerText = 'End scan';
347
+ endScanBtn.appendChild(endScanText);
348
+ endScanBtn.addEventListener('click', async () => customWindow.handleOnStopClick?.());
276
349
  const btnGroup = document.createElement('div');
277
350
  btnGroup.className = 'oobee-actions';
278
351
  btnGroup.appendChild(scanBtn);
279
- btnGroup.appendChild(stopBtn);
352
+ btnGroup.appendChild(endScanBtn);
280
353
  const listWrap = document.createElement('div');
281
354
  listWrap.id = 'oobeeList';
282
355
  listWrap.className = 'oobee-list';
@@ -292,12 +365,12 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
292
365
  }
293
366
  const ol = document.createElement('ol');
294
367
  ol.className = 'oobee-ol';
295
- scanned.forEach((item) => {
368
+ scanned.forEach(item => {
296
369
  const li = document.createElement('li');
297
370
  li.className = 'oobee-li';
298
371
  const title = document.createElement('div');
299
372
  title.className = 'oobee-item-title';
300
- title.textContent = (item.pageTitle && item.pageTitle.trim()) ? item.pageTitle : item.url;
373
+ title.textContent = item.pageTitle && item.pageTitle.trim() ? item.pageTitle : item.url;
301
374
  const url = document.createElement('div');
302
375
  url.className = 'oobee-item-url';
303
376
  url.textContent = item.url;
@@ -340,7 +413,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
340
413
  border-right: 1px solid rgba(0,0,0,.08)
341
414
  }
342
415
  .oobee-panel.collapsed {
343
- width: 56px;
416
+ width: 58px;
344
417
  overflow: hidden
345
418
  }
346
419
 
@@ -417,6 +490,12 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
417
490
  padding: 1rem;
418
491
  }
419
492
 
493
+ .oobee-panel.collapsed .oobee-actions {
494
+ display: flex;
495
+ justify-content: center;
496
+ padding: 1rem 0.7rem;
497
+ }
498
+
420
499
  /* Base button */
421
500
  .oobee-btn {
422
501
  width: 100%;
@@ -427,6 +506,10 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
427
506
  line-height: 1.2;
428
507
  font-weight: 400;
429
508
  cursor: pointer;
509
+ display: flex;
510
+ align-items: center;
511
+ justify-content: center;
512
+ gap: 10px;
430
513
  transition: {
431
514
  box-shadow .12s ease,
432
515
  transform .02s ease,
@@ -440,6 +523,19 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
440
523
  cursor:not-allowed
441
524
  }
442
525
 
526
+ .oobee-panel.collapsed .oobee-btn {
527
+ width: 44px !important;
528
+ height: 44px !important;
529
+ min-width: 44px !important;
530
+ min-height: 44px !important;
531
+ max-width: 44px !important;
532
+ max-height: 44px !important;
533
+ border-radius: 50% !important;
534
+ padding: 0 !important;
535
+ justify-content: center;
536
+ gap: 0;
537
+ }
538
+
443
539
  /* Primary (filled) */
444
540
  .oobee-btn-primary {
445
541
  background: #9021a6;
@@ -495,6 +591,25 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
495
591
  display: none;
496
592
  }
497
593
 
594
+ .oobee-btn-icon {
595
+ display: inline-flex;
596
+ align-items: center;
597
+ justify-content: center;
598
+ width: 20px;
599
+ height: 20px;
600
+ vertical-align: middle;
601
+ }
602
+
603
+ .oobee-btn-text {
604
+ display: inline;
605
+ white-space: nowrap;
606
+ vertical-align: middle;
607
+ }
608
+
609
+ .oobee-panel.collapsed .oobee-btn-text {
610
+ display: none;
611
+ }
612
+
498
613
  #oobeeStopOverlay[hidden] {
499
614
  display:none !important;
500
615
  }
@@ -512,7 +627,10 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
512
627
  }
513
628
 
514
629
  .oobee-panel.collapsed .oobee-section-title {
515
- display: none;
630
+ font-size: 14px;
631
+ display: flex;
632
+ justify-content: center;
633
+ text-align: center;
516
634
  }
517
635
 
518
636
  .oobee-ol {
@@ -596,8 +714,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
596
714
  if (!icon)
597
715
  return;
598
716
  const closed = isCollapsed();
599
- const arrowPointsRight = (currentPos === 'RIGHT' && !closed) ||
600
- (currentPos === 'LEFT' && closed);
717
+ const arrowPointsRight = (currentPos === 'RIGHT' && !closed) || (currentPos === 'LEFT' && closed);
601
718
  icon.classList.toggle('is-left', !arrowPointsRight);
602
719
  minBtn.setAttribute('aria-label', closed ? 'Expand panel' : 'Collapse panel');
603
720
  }
@@ -652,7 +769,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
652
769
  borderRadius: '16px',
653
770
  overflow: 'hidden',
654
771
  boxShadow: '0 10px 40px rgba(0,0,0,.35)',
655
- fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif'
772
+ fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif',
656
773
  });
657
774
  const dialogSheet = new CSSStyleSheet();
658
775
  dialogSheet.replaceSync(`
@@ -689,12 +806,17 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
689
806
  display: 'flex',
690
807
  alignItems: 'center',
691
808
  justifyContent: 'space-between',
692
- gap: '8px'
809
+ gap: '8px',
693
810
  });
694
811
  const title = document.createElement('h2');
695
812
  title.id = 'oobee-stop-title';
696
813
  title.textContent = 'Are you sure you want to stop this scan?';
697
- Object.assign(title.style, { margin: '0', fontSize: '22px', fontWeight: '700', lineHeight: '1.25' });
814
+ Object.assign(title.style, {
815
+ margin: '0',
816
+ fontSize: '22px',
817
+ fontWeight: '700',
818
+ lineHeight: '1.25',
819
+ });
698
820
  const closeX = document.createElement('button');
699
821
  closeX.type = 'button';
700
822
  closeX.setAttribute('aria-label', 'Close');
@@ -711,13 +833,13 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
711
833
  height: '36px',
712
834
  borderRadius: '12px',
713
835
  display: 'grid',
714
- placeItems: 'center'
836
+ placeItems: 'center',
715
837
  });
716
838
  head.appendChild(title);
717
839
  head.appendChild(closeX);
718
840
  const bodyWrap = document.createElement('div');
719
841
  Object.assign(bodyWrap.style, {
720
- padding: '12px 20px 20px 20px'
842
+ padding: '12px 20px 20px 20px',
721
843
  });
722
844
  const form = document.createElement('form');
723
845
  form.noValidate = true;
@@ -725,7 +847,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
725
847
  Object.assign(form.style, {
726
848
  display: 'grid',
727
849
  gridTemplateColumns: '1fr',
728
- rowGap: '12px'
850
+ rowGap: '12px',
729
851
  });
730
852
  const label = document.createElement('label');
731
853
  label.setAttribute('for', 'oobee-stop-input');
@@ -741,7 +863,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
741
863
  padding: '12px 14px',
742
864
  fontSize: '14px',
743
865
  outline: 'none',
744
- boxSizing: 'border-box'
866
+ boxSizing: 'border-box',
745
867
  });
746
868
  input.addEventListener('focus', () => {
747
869
  input.style.borderColor = '#7b4dff';
@@ -765,7 +887,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
765
887
  fontWeight: '600',
766
888
  color: '#fff',
767
889
  background: '#9021A6',
768
- cursor: 'pointer'
890
+ cursor: 'pointer',
769
891
  });
770
892
  const cancel = document.createElement('button');
771
893
  cancel.type = 'button';
@@ -777,7 +899,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
777
899
  fontSize: '14px',
778
900
  justifySelf: 'center',
779
901
  cursor: 'pointer',
780
- padding: '6px'
902
+ padding: '6px',
781
903
  });
782
904
  actions.appendChild(primary);
783
905
  actions.appendChild(cancel);
@@ -792,10 +914,13 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
792
914
  stopDialog.appendChild(bodyWrap);
793
915
  shadowRoot.appendChild(stopDialog);
794
916
  let stopResolver = null;
795
- const hideStop = () => { try {
796
- stopDialog.close();
797
- }
798
- catch { } stopResolver = null; };
917
+ const hideStop = () => {
918
+ try {
919
+ stopDialog.close();
920
+ }
921
+ catch { }
922
+ stopResolver = null;
923
+ };
799
924
  const showStop = () => {
800
925
  if (!shouldHideInput)
801
926
  input.value = '';
@@ -813,7 +938,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
813
938
  });
814
939
  }
815
940
  };
816
- form.addEventListener('submit', (e) => {
941
+ form.addEventListener('submit', e => {
817
942
  e.preventDefault();
818
943
  const v = (input.value || '').trim();
819
944
  if (stopResolver)
@@ -830,13 +955,13 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
830
955
  stopResolver({ confirmed: false, label: '' });
831
956
  hideStop();
832
957
  });
833
- stopDialog.addEventListener('cancel', (e) => {
958
+ stopDialog.addEventListener('cancel', e => {
834
959
  e.preventDefault();
835
960
  if (stopResolver)
836
961
  stopResolver({ confirmed: false, label: '' });
837
962
  hideStop();
838
963
  });
839
- customWindow.oobeeShowStopModal = () => new Promise((resolve) => {
964
+ customWindow.oobeeShowStopModal = () => new Promise(resolve => {
840
965
  stopResolver = resolve;
841
966
  showStop();
842
967
  });
@@ -861,7 +986,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
861
986
  log('Overlay menu: successfully added');
862
987
  })
863
988
  .catch(error => {
864
- error('Overlay menu: failed to add', error);
989
+ consoleLogger.error('Overlay menu: failed to add', error);
865
990
  });
866
991
  };
867
992
  export const removeOverlayMenu = async (page) => {
@@ -884,7 +1009,14 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
884
1009
  let menuPos = MENU_POSITION.right;
885
1010
  // eslint-disable-next-line no-underscore-dangle
886
1011
  const pageId = page._guid;
887
- page.on('dialog', () => { });
1012
+ page.on('dialog', async (dialog) => {
1013
+ try {
1014
+ await dialog.dismiss();
1015
+ }
1016
+ catch {
1017
+ // dialog may already be closed
1018
+ }
1019
+ });
888
1020
  const pageClosePromise = new Promise(resolve => {
889
1021
  page.on('close', () => {
890
1022
  log(`Page: close detected: ${page.url()}`);
@@ -910,11 +1042,19 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
910
1042
  await processPage(page, processPageParams);
911
1043
  log('Scan: success');
912
1044
  pagesDict[pageId].isScanning = false;
913
- await addOverlayMenu(page, processPageParams.urlsCrawled, menuPos, {
914
- inProgress: false,
915
- collapsed: !!pagesDict[pageId]?.collapsed,
916
- hideStopInput: !!processPageParams.customFlowLabel,
917
- });
1045
+ if (page.isClosed())
1046
+ return;
1047
+ const allowed = isOverlayAllowed(page.url(), processPageParams.entryUrl);
1048
+ if (allowed) {
1049
+ await addOverlayMenu(page, processPageParams.urlsCrawled, menuPos, {
1050
+ inProgress: false,
1051
+ collapsed: !!pagesDict[pageId]?.collapsed,
1052
+ hideStopInput: !!processPageParams.customFlowLabel,
1053
+ });
1054
+ }
1055
+ else {
1056
+ await removeOverlayMenu(page);
1057
+ }
918
1058
  }
919
1059
  catch (error) {
920
1060
  log(`Scan failed ${error}`);
@@ -944,10 +1084,10 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
944
1084
  });
945
1085
  if (!inputValue?.confirmed) {
946
1086
  await page.evaluate(() => {
947
- const stopBtn = document.getElementById('oobeeBtnStop');
948
- if (stopBtn) {
949
- stopBtn.disabled = false;
950
- stopBtn.textContent = 'Stop';
1087
+ const endScanBtn = document.getElementById('oobeeBtnEndScan');
1088
+ if (endScanBtn) {
1089
+ endScanBtn.disabled = false;
1090
+ endScanBtn.textContent = 'Stop';
951
1091
  }
952
1092
  });
953
1093
  return;
@@ -976,7 +1116,14 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
976
1116
  }
977
1117
  };
978
1118
  page.on('domcontentloaded', async () => {
1119
+ if (page.isClosed())
1120
+ return;
979
1121
  try {
1122
+ const allowed = isOverlayAllowed(page.url(), processPageParams.entryUrl);
1123
+ if (!allowed) {
1124
+ await removeOverlayMenu(page);
1125
+ return;
1126
+ }
980
1127
  const existingOverlay = await page.evaluate(() => {
981
1128
  return document.querySelector('#oobeeShadowHost');
982
1129
  });
@@ -989,11 +1136,6 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
989
1136
  hideStopInput: !!processPageParams.customFlowLabel,
990
1137
  });
991
1138
  }
992
- setTimeout(() => {
993
- // Timeout here to slow things down a little
994
- }, 1000);
995
- //! For Cypress Test
996
- // Auto-clicks 'Scan this page' button only once
997
1139
  if (isCypressTest) {
998
1140
  try {
999
1141
  await handleOnScanClick();
@@ -1003,22 +1145,27 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
1003
1145
  consoleLogger.info(`Error in calling handleOnScanClick, isCypressTest: ${isCypressTest}`);
1004
1146
  }
1005
1147
  }
1006
- consoleLogger.info(`Overlay state: ${existingOverlay}`);
1007
1148
  }
1008
1149
  catch {
1009
1150
  consoleLogger.info('Error in adding overlay menu to page');
1010
- consoleLogger.info('Error in adding overlay menu to page');
1011
1151
  }
1012
1152
  });
1013
- await page.exposeFunction('handleOnScanClick', handleOnScanClick);
1014
- await page.exposeFunction('handleOnStopClick', handleOnStopClick);
1015
- // Define the updateMenuPos function
1016
- const updateMenuPos = newPos => {
1017
- const prevPos = menuPos;
1018
- if (prevPos !== newPos) {
1019
- menuPos = newPos;
1020
- }
1021
- };
1022
- await page.exposeFunction('updateMenuPos', updateMenuPos);
1153
+ try {
1154
+ if (page.isClosed())
1155
+ return page;
1156
+ await page.exposeFunction('handleOnScanClick', handleOnScanClick);
1157
+ await page.exposeFunction('handleOnStopClick', handleOnStopClick);
1158
+ // Define the updateMenuPos function
1159
+ const updateMenuPos = newPos => {
1160
+ const prevPos = menuPos;
1161
+ if (prevPos !== newPos) {
1162
+ menuPos = newPos;
1163
+ }
1164
+ };
1165
+ await page.exposeFunction('updateMenuPos', updateMenuPos);
1166
+ }
1167
+ catch (e) {
1168
+ log(`Error exposing functions on page: ${e}`);
1169
+ }
1023
1170
  return page;
1024
1171
  };
@@ -2,15 +2,16 @@ const ALLOWED_PROTOCOLS = new Set(['http:', 'https:']);
2
2
  export function addUrlGuardScript(context, opts = {}) {
3
3
  const { fallbackUrl } = opts;
4
4
  const lastAllowedUrlByPage = new WeakMap();
5
- const attachGuardsToPage = (page) => {
5
+ const attachGuardsToPage = page => {
6
6
  if (!lastAllowedUrlByPage.has(page) && fallbackUrl) {
7
7
  lastAllowedUrlByPage.set(page, String(fallbackUrl));
8
8
  }
9
- page.addInitScript(() => {
10
- const isAllowedProtocol = (value) => {
9
+ page
10
+ .addInitScript(() => {
11
+ const isAllowedProtocol = value => {
11
12
  try {
12
13
  const s = value instanceof URL ? value.toString() : String(value);
13
- const protocol = new URL(s, window.location.href).protocol;
14
+ const { protocol } = new URL(s, window.location.href);
14
15
  return protocol === 'http:' || protocol === 'https:';
15
16
  }
16
17
  catch {
@@ -24,17 +25,9 @@ export function addUrlGuardScript(context, opts = {}) {
24
25
  return null;
25
26
  return openOriginal.call(this, targetUrl, ...args);
26
27
  };
27
- const assignOriginal = win.location.assign.bind(win.location);
28
- const replaceOriginal = win.location.replace.bind(win.location);
29
- win.location.assign = (nextUrl) => { if (isAllowedProtocol(nextUrl))
30
- assignOriginal(nextUrl); };
31
- win.location.replace = (nextUrl) => { if (isAllowedProtocol(nextUrl))
32
- replaceOriginal(nextUrl); };
33
- Object.defineProperty(win.location, 'href', {
34
- get() { return String(win.location.toString()); },
35
- set(nextUrl) { if (isAllowedProtocol(nextUrl))
36
- assignOriginal(nextUrl); },
37
- });
28
+ })
29
+ .catch(() => {
30
+ // page may have closed before addInitScript completed; safe to ignore
38
31
  });
39
32
  const restoreToSafeUrl = async (page, attemptedUrl) => {
40
33
  try {
@@ -1,12 +1,11 @@
1
1
  /* eslint-env browser */
2
- import { chromium } from 'playwright';
3
2
  import { createCrawleeSubFolders } from './commonCrawlerFunc.js';
4
3
  import { cleanUpAndExit, register, registerSoftClose } from '../utils.js';
5
4
  import constants, { getIntermediateScreenshotsPath, guiInfoStatusTypes, } from '../constants/constants.js';
6
5
  import { initNewPage, log } from './custom/utils.js';
7
6
  import { guiInfoLog } from '../logs.js';
8
7
  import { addUrlGuardScript } from './guards/urlGuard.js';
9
- import { getPlaywrightLaunchOptions } from '../constants/common.js';
8
+ import { getBrowserToRun, getPlaywrightLaunchOptions, initModifiedUserAgent, } from '../constants/common.js';
10
9
  // Export of classes
11
10
  export class ProcessPageParams {
12
11
  constructor(scannedIdx, blacklistedPatterns, includeScreenshots, dataset, intermediateScreenshotsPath, urlsCrawled, randomToken) {
@@ -19,7 +18,7 @@ export class ProcessPageParams {
19
18
  this.randomToken = randomToken;
20
19
  }
21
20
  }
22
- const runCustom = async (url, randomToken, viewportSettings, blacklistedPatterns, includeScreenshots, initialCustomFlowLabel) => {
21
+ const runCustom = async (url, randomToken, browserToRun, userDataDirectory, viewportSettings, blacklistedPatterns, includeScreenshots, initialCustomFlowLabel) => {
23
22
  // checks and delete datasets path if it already exists
24
23
  process.env.CRAWLEE_STORAGE_DIR = randomToken;
25
24
  const urlsCrawled = { ...constants.urlsCrawledObj };
@@ -27,48 +26,58 @@ const runCustom = async (url, randomToken, viewportSettings, blacklistedPatterns
27
26
  const intermediateScreenshotsPath = getIntermediateScreenshotsPath(randomToken);
28
27
  const processPageParams = new ProcessPageParams(0, // scannedIdx
29
28
  blacklistedPatterns, includeScreenshots, dataset, intermediateScreenshotsPath, urlsCrawled, randomToken);
29
+ processPageParams.entryUrl = url;
30
30
  if (initialCustomFlowLabel && initialCustomFlowLabel.trim()) {
31
31
  processPageParams.customFlowLabel = initialCustomFlowLabel.trim();
32
32
  }
33
33
  const pagesDict = {};
34
34
  const pageClosePromises = [];
35
35
  try {
36
+ const { browserToRun: resolvedBrowserToRun } = getBrowserToRun(randomToken, browserToRun, false);
36
37
  const deviceConfig = viewportSettings.playwrightDeviceDetailsObject;
37
38
  const hasCustomViewport = !!deviceConfig;
38
- const baseLaunchOptions = getPlaywrightLaunchOptions('chrome');
39
+ const rawDevice = (deviceConfig || {});
40
+ const { userAgent: deviceUserAgent, ...contextDeviceOptions } = rawDevice;
41
+ await initModifiedUserAgent(resolvedBrowserToRun, viewportSettings.playwrightDeviceDetailsObject);
42
+ const baseLaunchOptions = getPlaywrightLaunchOptions(resolvedBrowserToRun);
39
43
  // Merge base args with custom flow specific args
40
44
  const baseArgs = baseLaunchOptions.args || [];
41
45
  const customArgs = hasCustomViewport ? ['--window-size=1920,1040'] : ['--start-maximized'];
42
- const mergedArgs = [...baseArgs.filter(a => !a.startsWith('--window-size') && a !== '--start-maximized'), ...customArgs];
43
- const browser = await chromium.launch({
46
+ const mergedArgs = [
47
+ ...baseArgs.filter(a => !a.startsWith('--window-size') && a !== '--start-maximized'),
48
+ ...customArgs,
49
+ ];
50
+ const context = await constants.launcher.launchPersistentContext(userDataDirectory, {
44
51
  ...baseLaunchOptions,
45
52
  args: mergedArgs,
46
53
  headless: false,
47
- channel: 'chrome',
48
- });
49
- const context = await browser.newContext({
50
54
  ignoreHTTPSErrors: true,
51
55
  serviceWorkers: 'block',
52
56
  viewport: null,
53
- ...(hasCustomViewport ? deviceConfig : {}),
57
+ ...(hasCustomViewport ? contextDeviceOptions : {}),
58
+ userAgent: process.env.OOBEE_USER_AGENT || deviceUserAgent,
54
59
  });
55
60
  register(context);
56
61
  processPageParams.stopAll = async () => {
57
62
  try {
58
63
  await context.close().catch(() => { });
59
- await browser.close().catch(() => { });
60
- }
61
- catch {
62
64
  }
65
+ catch { }
63
66
  };
64
67
  // For handling closing playwright browser and continue generate artifacts etc
65
68
  registerSoftClose(processPageParams.stopAll);
66
69
  addUrlGuardScript(context, { fallbackUrl: url });
70
+ const page = context.pages().find(existingPage => !existingPage.isClosed()) || (await context.newPage());
71
+ await initNewPage(page, pageClosePromises, processPageParams, pagesDict);
67
72
  // Detection of new page
68
73
  context.on('page', async (newPage) => {
69
- await initNewPage(newPage, pageClosePromises, processPageParams, pagesDict);
74
+ try {
75
+ await initNewPage(newPage, pageClosePromises, processPageParams, pagesDict);
76
+ }
77
+ catch (e) {
78
+ log(`Error initializing new page: ${e}`);
79
+ }
70
80
  });
71
- const page = await context.newPage();
72
81
  await page.goto(url, { timeout: 0 });
73
82
  // to execute and wait for all pages to close
74
83
  // idea is for promise to be pending until page.on('close') detected