@govtechsg/oobee 0.10.85 → 0.10.87

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 (62) hide show
  1. package/.github/workflows/publish.yml +10 -0
  2. package/DETAILS.md +29 -0
  3. package/dist/cli.js +18 -5
  4. package/dist/combine.js +3 -1
  5. package/dist/constants/cliFunctions.js +2 -2
  6. package/dist/constants/common.js +70 -17
  7. package/dist/constants/constants.js +604 -1
  8. package/dist/crawlers/commonCrawlerFunc.js +3 -2
  9. package/dist/crawlers/crawlDomain.js +38 -13
  10. package/dist/crawlers/crawlIntelligentSitemap.js +62 -30
  11. package/dist/crawlers/crawlSitemap.js +141 -84
  12. package/dist/crawlers/custom/utils.js +218 -71
  13. package/dist/crawlers/guards/urlGuard.js +8 -15
  14. package/dist/crawlers/runCustom.js +18 -11
  15. package/dist/generateHtmlReport.js +18 -11
  16. package/dist/generateOobeeClientScanner.js +570 -0
  17. package/dist/mergeAxeResults/itemReferences.js +60 -25
  18. package/dist/mergeAxeResults/sentryTelemetry.js +4 -1
  19. package/dist/mergeAxeResults.js +23 -13
  20. package/dist/npmIndex.js +10 -2
  21. package/dist/proxyService.js +18 -3
  22. package/dist/services/s3Uploader.js +21 -10
  23. package/dist/static/ejs/partials/scripts/decodeUnzipParse.ejs +6 -3
  24. package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +2 -2
  25. package/dist/static/ejs/partials/scripts/ruleModal/constants.ejs +1 -761
  26. package/dist/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +38 -2
  27. package/dist/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +1 -1
  28. package/dist/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +4 -4
  29. package/dist/static/ejs/summary.ejs +19 -8
  30. package/dist/utils.js +4 -3
  31. package/fix-summary-html-oom-pr.md +62 -0
  32. package/oobee-client-scanner.js +34992 -0
  33. package/package.json +5 -5
  34. package/src/cli.ts +19 -5
  35. package/src/combine.ts +5 -1
  36. package/src/constants/cliFunctions.ts +2 -2
  37. package/src/constants/common.ts +87 -22
  38. package/src/constants/constants.ts +602 -1
  39. package/src/crawlers/commonCrawlerFunc.ts +4 -3
  40. package/src/crawlers/crawlDomain.ts +39 -13
  41. package/src/crawlers/crawlIntelligentSitemap.ts +63 -30
  42. package/src/crawlers/crawlSitemap.ts +165 -100
  43. package/src/crawlers/custom/utils.ts +241 -80
  44. package/src/crawlers/guards/urlGuard.ts +24 -31
  45. package/src/crawlers/runCustom.ts +29 -11
  46. package/src/generateHtmlReport.ts +21 -11
  47. package/src/generateOobeeClientScanner.ts +591 -0
  48. package/src/mergeAxeResults/itemReferences.ts +70 -26
  49. package/src/mergeAxeResults/sentryTelemetry.ts +4 -1
  50. package/src/mergeAxeResults.ts +26 -14
  51. package/src/npmIndex.ts +12 -2
  52. package/src/proxyService.ts +25 -4
  53. package/src/services/s3Uploader.ts +23 -11
  54. package/src/static/ejs/partials/scripts/decodeUnzipParse.ejs +6 -3
  55. package/src/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +2 -2
  56. package/src/static/ejs/partials/scripts/ruleModal/constants.ejs +1 -761
  57. package/src/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +38 -2
  58. package/src/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +1 -1
  59. package/src/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +4 -4
  60. package/src/static/ejs/summary.ejs +19 -8
  61. package/src/utils.ts +4 -3
  62. package/testStaticJSScanner.html +534 -0
@@ -40,6 +40,7 @@ const RESTRICT_OVERLAY_TO_ENTRY_DOMAIN = parseBoolEnv(
40
40
  process.env.RESTRICT_OVERLAY_TO_ENTRY_DOMAIN,
41
41
  false,
42
42
  );
43
+ const OVERLAY_OPERATION_TIMEOUT_MS = 5000;
43
44
 
44
45
  const isOverlayAllowed = (currentUrl: string, entryUrl: string) => {
45
46
  try {
@@ -100,17 +101,17 @@ export const screenshotFullPage = async (page, screenshotsDir: string, screensho
100
101
  window.scrollTo(0, document.body.scrollHeight);
101
102
  });
102
103
 
103
- const isLoadMoreContent = async () =>
104
- new Promise(resolve => {
105
- setTimeout(async () => {
106
- await page.waitForLoadState('domcontentloaded');
107
-
108
- const newHeight = await page.evaluate(() => document.body.scrollHeight);
109
- const result = newHeight > prevHeight;
110
-
111
- resolve(result);
112
- }, 2500);
113
- });
104
+ const isLoadMoreContent = async () => {
105
+ await new Promise(resolve => setTimeout(resolve, 2500));
106
+ if (page.isClosed()) return false;
107
+ try {
108
+ await page.waitForLoadState('domcontentloaded');
109
+ const newHeight = await page.evaluate(() => document.body.scrollHeight);
110
+ return newHeight > prevHeight;
111
+ } catch {
112
+ return false;
113
+ }
114
+ };
114
115
 
115
116
  const result = await isLoadMoreContent();
116
117
  return result;
@@ -306,7 +307,7 @@ export const addOverlayMenu = async (
306
307
  collapsed: false,
307
308
  },
308
309
  ) => {
309
- await page.waitForLoadState('domcontentloaded');
310
+ await page.waitForLoadState('domcontentloaded', { timeout: OVERLAY_OPERATION_TIMEOUT_MS });
310
311
  consoleLogger.info(`Overlay menu: adding to ${menuPos}...`);
311
312
 
312
313
  // Add the overlay menu with initial styling
@@ -409,25 +410,72 @@ export const addOverlayMenu = async (
409
410
  const h2 = document.createElement('h2');
410
411
  h2.id = 'oobeeHPagesScanned';
411
412
  h2.className = 'oobee-section-title';
412
- h2.textContent = 'Pages Scanned';
413
-
413
+ h2.textContent = `Pages Scanned (${vars.urlsCrawled.scanned.length || 0})`;
414
+
415
+ const scanIcon = document.createElement('span');
416
+ scanIcon.className = 'oobee-btn-icon';
417
+
418
+ const SCAN_SVG = `
419
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
420
+ <g clip-path="url(#clip0_1421_431)">
421
+ <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"/>
422
+ <path d="M18.5 0H19.5C19.7761 0 20 0.223858 20 0.5V5H18.5V0Z" fill="white"/>
423
+ <path d="M19.5 2.18552e-08L19.5 1.5L15 1.5L15 -2.18556e-07L19.5 2.18552e-08Z" fill="white"/>
424
+ <path d="M1.5 0H0.5C0.223858 0 0 0.223858 0 0.5V5H1.5V0Z" fill="white"/>
425
+ <path d="M0.5 2.18552e-08L0.5 1.5L5 1.5L5 -2.18556e-07L0.5 2.18552e-08Z" fill="white"/>
426
+ <path d="M1.5 20H0.5C0.223858 20 0 19.7761 0 19.5V15H1.5V20Z" fill="white"/>
427
+ <path d="M0.5 20L0.5 18.5L5 18.5L5 20L0.5 20Z" fill="white"/>
428
+ <path d="M18.5 20H19.5C19.7761 20 20 19.7761 20 19.5V15H18.5V20Z" fill="white"/>
429
+ <path d="M19.5 20L19.5 18.5L15 18.5L15 20L19.5 20Z" fill="white"/>
430
+ </g>
431
+ <defs>
432
+ <clipPath id="clip0_1421_431">
433
+ <rect width="20" height="20" fill="white"/>
434
+ </clipPath>
435
+ </defs>
436
+ </svg>
437
+ `;
438
+
439
+ scanIcon.innerHTML = SCAN_SVG;
414
440
  const scanBtn = document.createElement('button');
415
441
  scanBtn.id = 'oobeeBtnScan';
416
442
  scanBtn.className = 'oobee-btn oobee-btn-primary';
417
- scanBtn.innerText = 'Scan this page';
418
443
  scanBtn.disabled = inProgress;
444
+ scanBtn.appendChild(scanIcon);
445
+
446
+ const scanText = document.createElement('span');
447
+ scanText.className = 'oobee-btn-text';
448
+ scanText.innerText = 'Scan page';
449
+ scanBtn.appendChild(scanText);
450
+
419
451
  scanBtn.addEventListener('click', async () => customWindow.handleOnScanClick?.());
420
452
 
421
- const stopBtn = document.createElement('button');
422
- stopBtn.id = 'oobeeBtnStop';
423
- stopBtn.className = 'oobee-btn oobee-btn-secondary';
424
- stopBtn.innerText = 'Stop scan';
425
- stopBtn.addEventListener('click', async () => customWindow.handleOnStopClick?.());
453
+ const endScanIcon = document.createElement('span');
454
+ endScanIcon.className = 'oobee-btn-icon';
455
+
456
+ const ENDSCAN_SVG =
457
+ `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
458
+ <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"/>
459
+ </svg>
460
+ `;
461
+
462
+ endScanIcon.innerHTML = ENDSCAN_SVG;
463
+ const endScanBtn = document.createElement('button');
464
+ endScanBtn.id = 'oobeeBtnEndScan';
465
+ endScanBtn.className = 'oobee-btn oobee-btn-secondary';
466
+ endScanBtn.appendChild(endScanIcon);
467
+
468
+ const endScanText = document.createElement('span');
469
+ endScanText.className = 'oobee-btn-text';
470
+ endScanText.innerText = 'End scan';
471
+ endScanBtn.appendChild(endScanText);
472
+
473
+ endScanBtn.addEventListener('click', async () => customWindow.handleOnStopClick?.());
426
474
 
427
475
  const btnGroup = document.createElement('div');
428
476
  btnGroup.className = 'oobee-actions';
429
477
  btnGroup.appendChild(scanBtn);
430
- btnGroup.appendChild(stopBtn);
478
+ btnGroup.appendChild(endScanBtn);
431
479
 
432
480
  const listWrap = document.createElement('div');
433
481
  listWrap.id = 'oobeeList';
@@ -503,7 +551,7 @@ export const addOverlayMenu = async (
503
551
  border-right: 1px solid rgba(0,0,0,.08)
504
552
  }
505
553
  .oobee-panel.collapsed {
506
- width: 56px;
554
+ width: 58px;
507
555
  overflow: hidden
508
556
  }
509
557
 
@@ -580,6 +628,12 @@ export const addOverlayMenu = async (
580
628
  padding: 1rem;
581
629
  }
582
630
 
631
+ .oobee-panel.collapsed .oobee-actions {
632
+ display: flex;
633
+ justify-content: center;
634
+ padding: 1rem 0.7rem;
635
+ }
636
+
583
637
  /* Base button */
584
638
  .oobee-btn {
585
639
  width: 100%;
@@ -590,6 +644,10 @@ export const addOverlayMenu = async (
590
644
  line-height: 1.2;
591
645
  font-weight: 400;
592
646
  cursor: pointer;
647
+ display: flex;
648
+ align-items: center;
649
+ justify-content: center;
650
+ gap: 10px;
593
651
  transition: {
594
652
  box-shadow .12s ease,
595
653
  transform .02s ease,
@@ -603,6 +661,19 @@ export const addOverlayMenu = async (
603
661
  cursor:not-allowed
604
662
  }
605
663
 
664
+ .oobee-panel.collapsed .oobee-btn {
665
+ width: 44px !important;
666
+ height: 44px !important;
667
+ min-width: 44px !important;
668
+ min-height: 44px !important;
669
+ max-width: 44px !important;
670
+ max-height: 44px !important;
671
+ border-radius: 50% !important;
672
+ padding: 0 !important;
673
+ justify-content: center;
674
+ gap: 0;
675
+ }
676
+
606
677
  /* Primary (filled) */
607
678
  .oobee-btn-primary {
608
679
  background: #9021a6;
@@ -658,6 +729,25 @@ export const addOverlayMenu = async (
658
729
  display: none;
659
730
  }
660
731
 
732
+ .oobee-btn-icon {
733
+ display: inline-flex;
734
+ align-items: center;
735
+ justify-content: center;
736
+ width: 20px;
737
+ height: 20px;
738
+ vertical-align: middle;
739
+ }
740
+
741
+ .oobee-btn-text {
742
+ display: inline;
743
+ white-space: nowrap;
744
+ vertical-align: middle;
745
+ }
746
+
747
+ .oobee-panel.collapsed .oobee-btn-text {
748
+ display: none;
749
+ }
750
+
661
751
  #oobeeStopOverlay[hidden] {
662
752
  display:none !important;
663
753
  }
@@ -675,7 +765,10 @@ export const addOverlayMenu = async (
675
765
  }
676
766
 
677
767
  .oobee-panel.collapsed .oobee-section-title {
678
- display: none;
768
+ font-size: 14px;
769
+ display: flex;
770
+ justify-content: center;
771
+ text-align: center;
679
772
  }
680
773
 
681
774
  .oobee-ol {
@@ -1051,6 +1144,7 @@ export const addOverlayMenu = async (
1051
1144
  })
1052
1145
  .catch(error => {
1053
1146
  consoleLogger.error('Overlay menu: failed to add', error);
1147
+ throw error;
1054
1148
  });
1055
1149
  };
1056
1150
 
@@ -1073,11 +1167,19 @@ export const removeOverlayMenu = async page => {
1073
1167
 
1074
1168
  export const initNewPage = async (page, pageClosePromises, processPageParams, pagesDict) => {
1075
1169
  let menuPos = MENU_POSITION.right;
1170
+ let overlayRefreshSeq = 0;
1171
+ let overlayRefreshChain = Promise.resolve();
1076
1172
 
1077
1173
  // eslint-disable-next-line no-underscore-dangle
1078
1174
  const pageId = page._guid;
1079
1175
 
1080
- page.on('dialog', () => {});
1176
+ page.on('dialog', async dialog => {
1177
+ try {
1178
+ await dialog.dismiss();
1179
+ } catch {
1180
+ // dialog may already be closed
1181
+ }
1182
+ });
1081
1183
 
1082
1184
  const pageClosePromise = new Promise(resolve => {
1083
1185
  page.on('close', () => {
@@ -1096,6 +1198,83 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
1096
1198
  };
1097
1199
  }
1098
1200
 
1201
+ const reconcileOverlayMenu = async (trigger: string) => {
1202
+ // Mark this as the latest refresh so older ones can stop.
1203
+ const refreshSeq = ++overlayRefreshSeq;
1204
+
1205
+ // Serialize overlay updates so multiple navigation events do not add/remove concurrently.
1206
+ overlayRefreshChain = overlayRefreshChain
1207
+ .catch(() => {})
1208
+ .then(async () => {
1209
+ if (refreshSeq !== overlayRefreshSeq || page.isClosed()) return;
1210
+
1211
+ try {
1212
+ // `framenavigated` can fire before the new document is ready for DOM inspection/injection.
1213
+ await page.waitForLoadState('domcontentloaded', { timeout: 5000 });
1214
+ } catch {
1215
+ // Best effort only. The page may still be mid-navigation.
1216
+ }
1217
+
1218
+ try {
1219
+ // Give fast redirect chains a brief chance to advance before we inject/remove the overlay.
1220
+ await page.waitForTimeout(300);
1221
+ } catch {
1222
+ // Best effort only. The page may already be closing.
1223
+ }
1224
+
1225
+ // Re-check staleness after waiting because a newer navigation may have happened meanwhile.
1226
+ if (refreshSeq !== overlayRefreshSeq || page.isClosed()) return;
1227
+
1228
+ const allowed = isOverlayAllowed(page.url(), processPageParams.entryUrl);
1229
+
1230
+ if (!allowed) {
1231
+ await Promise.race([
1232
+ removeOverlayMenu(page),
1233
+ new Promise((_, reject) => {
1234
+ setTimeout(() => {
1235
+ reject(
1236
+ new Error(
1237
+ `removeOverlayMenu timed out after ${OVERLAY_OPERATION_TIMEOUT_MS}ms`,
1238
+ ),
1239
+ );
1240
+ }, OVERLAY_OPERATION_TIMEOUT_MS);
1241
+ }),
1242
+ ]);
1243
+ return;
1244
+ }
1245
+
1246
+ const hasOverlay = await page.evaluate(() =>
1247
+ Boolean(document.querySelector('#oobeeShadowHost')),
1248
+ );
1249
+
1250
+ consoleLogger.info(`Overlay state (${trigger}): ${hasOverlay}`);
1251
+
1252
+ if (!hasOverlay) {
1253
+ // Recreate the overlay after allowed redirects while preserving current UI state.
1254
+ consoleLogger.info(`Adding overlay menu to page (${trigger}): ${page.url()}`);
1255
+ await Promise.race([
1256
+ addOverlayMenu(page, processPageParams.urlsCrawled, menuPos, {
1257
+ inProgress: !!pagesDict[pageId]?.isScanning,
1258
+ collapsed: !!pagesDict[pageId]?.collapsed,
1259
+ hideStopInput: !!processPageParams.customFlowLabel,
1260
+ }),
1261
+ new Promise((_, reject) => {
1262
+ setTimeout(() => {
1263
+ reject(
1264
+ new Error(`addOverlayMenu timed out after ${OVERLAY_OPERATION_TIMEOUT_MS}ms`),
1265
+ );
1266
+ }, OVERLAY_OPERATION_TIMEOUT_MS);
1267
+ }),
1268
+ ]);
1269
+ }
1270
+ })
1271
+ .catch(() => {
1272
+ consoleLogger.info('Error in adding overlay menu to page');
1273
+ });
1274
+
1275
+ await overlayRefreshChain;
1276
+ };
1277
+
1099
1278
  type handleOnScanClickFunction = () => void;
1100
1279
 
1101
1280
  // Window functions exposed in browser
@@ -1109,17 +1288,8 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
1109
1288
  log('Scan: success');
1110
1289
  pagesDict[pageId].isScanning = false;
1111
1290
 
1112
- const allowed = isOverlayAllowed(page.url(), processPageParams.entryUrl);
1113
-
1114
- if (allowed) {
1115
- await addOverlayMenu(page, processPageParams.urlsCrawled, menuPos, {
1116
- inProgress: false,
1117
- collapsed: !!pagesDict[pageId]?.collapsed,
1118
- hideStopInput: !!processPageParams.customFlowLabel,
1119
- });
1120
- } else {
1121
- await removeOverlayMenu(page);
1122
- }
1291
+ if (page.isClosed()) return;
1292
+ await reconcileOverlayMenu('scan-click');
1123
1293
  } catch (error) {
1124
1294
  log(`Scan failed ${error}`);
1125
1295
  }
@@ -1150,10 +1320,10 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
1150
1320
 
1151
1321
  if (!inputValue?.confirmed) {
1152
1322
  await page.evaluate(() => {
1153
- const stopBtn = document.getElementById('oobeeBtnStop') as HTMLButtonElement | null;
1154
- if (stopBtn) {
1155
- stopBtn.disabled = false;
1156
- stopBtn.textContent = 'Stop';
1323
+ const endScanBtn = document.getElementById('oobeeBtnEndScan') as HTMLButtonElement | null;
1324
+ if (endScanBtn) {
1325
+ endScanBtn.disabled = false;
1326
+ endScanBtn.textContent = 'Stop';
1157
1327
  }
1158
1328
  });
1159
1329
  return;
@@ -1182,55 +1352,46 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
1182
1352
  };
1183
1353
 
1184
1354
  page.on('domcontentloaded', async () => {
1185
- try {
1186
- const allowed = isOverlayAllowed(page.url(), processPageParams.entryUrl);
1355
+ if (page.isClosed()) return;
1356
+ await reconcileOverlayMenu('domcontentloaded');
1187
1357
 
1188
- if (!allowed) {
1189
- await removeOverlayMenu(page);
1190
- return;
1358
+ if (isCypressTest) {
1359
+ try {
1360
+ await handleOnScanClick();
1361
+ page.close();
1362
+ } catch {
1363
+ consoleLogger.info(
1364
+ `Error in calling handleOnScanClick, isCypressTest: ${isCypressTest}`,
1365
+ );
1191
1366
  }
1367
+ }
1368
+ });
1192
1369
 
1193
- const existingOverlay = await page.evaluate(() => {
1194
- return document.querySelector('#oobeeShadowHost');
1195
- });
1370
+ page.on('framenavigated', async (frame: any) => {
1371
+ if (frame !== page.mainFrame() || page.isClosed()) return;
1372
+ await reconcileOverlayMenu('framenavigated');
1373
+ });
1196
1374
 
1197
- consoleLogger.info(`Overlay state: ${existingOverlay}`);
1375
+ try {
1376
+ if (page.isClosed()) return page;
1377
+ await page.exposeFunction('handleOnScanClick', handleOnScanClick);
1378
+ await page.exposeFunction('handleOnStopClick', handleOnStopClick);
1198
1379
 
1199
- if (!existingOverlay) {
1200
- consoleLogger.info(`Adding overlay menu to page: ${page.url()}`);
1201
- await addOverlayMenu(page, processPageParams.urlsCrawled, menuPos, {
1202
- inProgress: !!pagesDict[pageId]?.isScanning,
1203
- collapsed: !!pagesDict[pageId]?.collapsed,
1204
- hideStopInput: !!processPageParams.customFlowLabel,
1205
- });
1206
- }
1380
+ type UpdateMenuPosFunction = (newPos: any) => void;
1207
1381
 
1208
- if (isCypressTest) {
1209
- try {
1210
- await handleOnScanClick();
1211
- page.close();
1212
- } catch {
1213
- consoleLogger.info(`Error in calling handleOnScanClick, isCypressTest: ${isCypressTest}`);
1214
- }
1382
+ // Define the updateMenuPos function
1383
+ const updateMenuPos: UpdateMenuPosFunction = newPos => {
1384
+ const prevPos = menuPos;
1385
+ if (prevPos !== newPos) {
1386
+ menuPos = newPos;
1215
1387
  }
1216
- } catch {
1217
- consoleLogger.info('Error in adding overlay menu to page');
1218
- }
1219
- });
1220
-
1221
- await page.exposeFunction('handleOnScanClick', handleOnScanClick);
1222
- await page.exposeFunction('handleOnStopClick', handleOnStopClick);
1223
-
1224
- type UpdateMenuPosFunction = (newPos: any) => void;
1388
+ };
1389
+ await page.exposeFunction('updateMenuPos', updateMenuPos);
1390
+ } catch (e) {
1391
+ log(`Error exposing functions on page: ${e}`);
1392
+ }
1225
1393
 
1226
- // Define the updateMenuPos function
1227
- const updateMenuPos: UpdateMenuPosFunction = newPos => {
1228
- const prevPos = menuPos;
1229
- if (prevPos !== newPos) {
1230
- menuPos = newPos;
1231
- }
1232
- };
1233
- await page.exposeFunction('updateMenuPos', updateMenuPos);
1394
+ await reconcileOverlayMenu('init');
1234
1395
 
1235
1396
  return page;
1236
1397
  };
@@ -5,41 +5,34 @@ export function addUrlGuardScript(context, opts = {}) {
5
5
 
6
6
  const lastAllowedUrlByPage = new WeakMap();
7
7
 
8
- const attachGuardsToPage = (page) => {
8
+ const attachGuardsToPage = page => {
9
9
  if (!lastAllowedUrlByPage.has(page) && fallbackUrl) {
10
10
  lastAllowedUrlByPage.set(page, String(fallbackUrl));
11
11
  }
12
12
 
13
- page.addInitScript(() => {
14
- const isAllowedProtocol = (value) => {
15
- try {
16
- const s = value instanceof URL ? value.toString() : String(value);
17
- const protocol = new URL(s, window.location.href).protocol;
18
- return protocol === 'http:' || protocol === 'https:';
19
- } catch {
20
- return false;
21
- }
22
- };
13
+ page
14
+ .addInitScript(() => {
15
+ const isAllowedProtocol = value => {
16
+ try {
17
+ const s = value instanceof URL ? value.toString() : String(value);
18
+ const { protocol } = new URL(s, window.location.href);
19
+ return protocol === 'http:' || protocol === 'https:';
20
+ } catch {
21
+ return false;
22
+ }
23
+ };
23
24
 
24
- const win = window;
25
+ const win = window;
25
26
 
26
- const openOriginal = win.open;
27
- win.open = function (targetUrl, ...args) {
28
- if (!isAllowedProtocol(targetUrl)) return null;
29
- return openOriginal.call(this, targetUrl, ...args);
30
- };
31
-
32
- const assignOriginal = win.location.assign.bind(win.location);
33
- const replaceOriginal = win.location.replace.bind(win.location);
34
-
35
- win.location.assign = (nextUrl) => { if (isAllowedProtocol(nextUrl)) assignOriginal(nextUrl); };
36
- win.location.replace = (nextUrl) => { if (isAllowedProtocol(nextUrl)) replaceOriginal(nextUrl); };
37
-
38
- Object.defineProperty(win.location, 'href', {
39
- get() { return String(win.location.toString()); },
40
- set(nextUrl) { if (isAllowedProtocol(nextUrl)) assignOriginal(nextUrl); },
27
+ const openOriginal = win.open;
28
+ win.open = function (targetUrl, ...args) {
29
+ if (!isAllowedProtocol(targetUrl)) return null;
30
+ return openOriginal.call(this, targetUrl, ...args);
31
+ };
32
+ })
33
+ .catch(() => {
34
+ // page may have closed before addInitScript completed; safe to ignore
41
35
  });
42
- });
43
36
 
44
37
  const restoreToSafeUrl = async (page, attemptedUrl) => {
45
38
  try {
@@ -50,15 +43,15 @@ export function addUrlGuardScript(context, opts = {}) {
50
43
  }
51
44
  };
52
45
 
53
- page.on('framenavigated', async (frame) => {
46
+ page.on('framenavigated', async frame => {
54
47
  if (frame !== page.mainFrame()) return;
55
48
 
56
49
  const urlStr = frame.url();
57
50
  let urlObj;
58
51
  try {
59
- urlObj = new URL(urlStr);
52
+ urlObj = new URL(urlStr);
60
53
  } catch {
61
- return restoreToSafeUrl(page, urlStr);
54
+ return restoreToSafeUrl(page, urlStr);
62
55
  }
63
56
 
64
57
  if (ALLOWED_PROTOCOLS.has(urlObj.protocol)) {
@@ -1,5 +1,4 @@
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, {
@@ -11,7 +10,12 @@ import { DEBUG, initNewPage, log } from './custom/utils.js';
11
10
  import { guiInfoLog } from '../logs.js';
12
11
  import { ViewportSettingsClass } from '../combine.js';
13
12
  import { addUrlGuardScript } from './guards/urlGuard.js';
14
- import { getPlaywrightLaunchOptions } from '../constants/common.js';
13
+ import {
14
+ getBrowserToRun,
15
+ getPlaywrightLaunchOptions,
16
+ initModifiedUserAgent,
17
+ } from '../constants/common.js';
18
+ import { BrowserTypes } from '../constants/constants.js';
15
19
 
16
20
  // Export of classes
17
21
 
@@ -50,6 +54,8 @@ export class ProcessPageParams {
50
54
  const runCustom = async (
51
55
  url: string,
52
56
  randomToken: string,
57
+ browserToRun: string,
58
+ userDataDirectory: string,
53
59
  viewportSettings: ViewportSettingsClass,
54
60
  blacklistedPatterns: string[] | null,
55
61
  includeScreenshots: boolean,
@@ -81,10 +87,19 @@ const runCustom = async (
81
87
  const pageClosePromises = [];
82
88
 
83
89
  try {
90
+ const { browserToRun: resolvedBrowserToRun } = getBrowserToRun(
91
+ randomToken,
92
+ browserToRun as BrowserTypes,
93
+ false,
94
+ );
84
95
  const deviceConfig = viewportSettings.playwrightDeviceDetailsObject;
85
96
  const hasCustomViewport = !!deviceConfig;
97
+ const rawDevice = (deviceConfig || {}) as Record<string, unknown>;
98
+ const { userAgent: deviceUserAgent, ...contextDeviceOptions } = rawDevice;
86
99
 
87
- const baseLaunchOptions = getPlaywrightLaunchOptions();
100
+ await initModifiedUserAgent(resolvedBrowserToRun, viewportSettings.playwrightDeviceDetailsObject);
101
+
102
+ const baseLaunchOptions = getPlaywrightLaunchOptions(resolvedBrowserToRun);
88
103
 
89
104
  // Merge base args with custom flow specific args
90
105
  const baseArgs = baseLaunchOptions.args || [];
@@ -94,17 +109,15 @@ const runCustom = async (
94
109
  ...customArgs,
95
110
  ];
96
111
 
97
- const browser = await chromium.launch({
112
+ const context = await constants.launcher.launchPersistentContext(userDataDirectory, {
98
113
  ...baseLaunchOptions,
99
114
  args: mergedArgs,
100
115
  headless: false,
101
- });
102
-
103
- const context = await browser.newContext({
104
116
  ignoreHTTPSErrors: true,
105
117
  serviceWorkers: 'block',
106
118
  viewport: null,
107
- ...(hasCustomViewport ? deviceConfig : {}),
119
+ ...(hasCustomViewport ? contextDeviceOptions : {}),
120
+ userAgent: process.env.OOBEE_USER_AGENT || (deviceUserAgent as string | undefined),
108
121
  });
109
122
 
110
123
  register(context);
@@ -112,7 +125,6 @@ const runCustom = async (
112
125
  processPageParams.stopAll = async () => {
113
126
  try {
114
127
  await context.close().catch(() => {});
115
- await browser.close().catch(() => {});
116
128
  } catch {}
117
129
  };
118
130
 
@@ -121,12 +133,18 @@ const runCustom = async (
121
133
 
122
134
  addUrlGuardScript(context, { fallbackUrl: url });
123
135
 
136
+ const page = context.pages().find(existingPage => !existingPage.isClosed()) || (await context.newPage());
137
+ await initNewPage(page, pageClosePromises, processPageParams, pagesDict);
138
+
124
139
  // Detection of new page
125
140
  context.on('page', async newPage => {
126
- await initNewPage(newPage, pageClosePromises, processPageParams, pagesDict);
141
+ try {
142
+ await initNewPage(newPage, pageClosePromises, processPageParams, pagesDict);
143
+ } catch (e) {
144
+ log(`Error initializing new page: ${e}`);
145
+ }
127
146
  });
128
147
 
129
- const page = await context.newPage();
130
148
  await page.goto(url, { timeout: 0 });
131
149
 
132
150
  // to execute and wait for all pages to close