@govtechsg/oobee 0.10.85 → 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 (38) 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/combine.js +1 -1
  5. package/dist/constants/common.js +15 -4
  6. package/dist/constants/constants.js +604 -1
  7. package/dist/crawlers/commonCrawlerFunc.js +3 -2
  8. package/dist/crawlers/crawlSitemap.js +98 -80
  9. package/dist/crawlers/custom/utils.js +137 -31
  10. package/dist/crawlers/guards/urlGuard.js +8 -15
  11. package/dist/crawlers/runCustom.js +18 -11
  12. package/dist/generateOobeeClientScanner.js +570 -0
  13. package/dist/mergeAxeResults.js +5 -4
  14. package/dist/npmIndex.js +10 -2
  15. package/dist/proxyService.js +18 -3
  16. package/dist/services/s3Uploader.js +21 -10
  17. package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +2 -2
  18. package/dist/static/ejs/partials/scripts/ruleModal/constants.ejs +1 -761
  19. package/dist/static/ejs/summary.ejs +10 -5
  20. package/oobee-client-scanner.js +34992 -0
  21. package/package.json +2 -2
  22. package/src/combine.ts +3 -1
  23. package/src/constants/common.ts +22 -10
  24. package/src/constants/constants.ts +602 -1
  25. package/src/crawlers/commonCrawlerFunc.ts +4 -3
  26. package/src/crawlers/crawlSitemap.ts +116 -98
  27. package/src/crawlers/custom/utils.ts +143 -38
  28. package/src/crawlers/guards/urlGuard.ts +24 -31
  29. package/src/crawlers/runCustom.ts +29 -11
  30. package/src/generateOobeeClientScanner.ts +591 -0
  31. package/src/mergeAxeResults.ts +5 -3
  32. package/src/npmIndex.ts +12 -2
  33. package/src/proxyService.ts +25 -4
  34. package/src/services/s3Uploader.ts +23 -11
  35. package/src/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +2 -2
  36. package/src/static/ejs/partials/scripts/ruleModal/constants.ejs +1 -761
  37. package/src/static/ejs/summary.ejs +10 -5
  38. package/testStaticJSScanner.html +534 -0
@@ -196,7 +196,7 @@ export const filterAxeResults = (
196
196
  const conformance = tags.filter(tag => tag.startsWith('wcag') || tag === 'best-practice');
197
197
 
198
198
  nodes.forEach(node => {
199
- const { html } = node;
199
+ const { html, target } = node;
200
200
  if (!(rule in passed.rules)) {
201
201
  passed.rules[rule] = {
202
202
  description,
@@ -207,9 +207,10 @@ export const filterAxeResults = (
207
207
  items: [],
208
208
  };
209
209
  }
210
-
210
+
211
211
  const finalHtml = truncateHtml(html);
212
- passed.rules[rule].items.push({ html: finalHtml, screenshotPath: '', message: '', xpath: '' });
212
+ const xpath = target.length === 1 && typeof target[0] === 'string' ? target[0] : undefined;
213
+ passed.rules[rule].items.push({ html: finalHtml, screenshotPath: '', message: '', xpath: xpath || '' });
213
214
 
214
215
  passed.totalItems += 1;
215
216
  passed.rules[rule].totalItems += 1;
@@ -76,6 +76,7 @@ const crawlSitemap = async ({
76
76
  let dataset: crawlee.Dataset;
77
77
  let urlsCrawled: UrlsCrawled;
78
78
  let durationExceeded = false;
79
+ let isAbortingScan = false;
79
80
 
80
81
  if (fromCrawlIntelligentSitemap) {
81
82
  dataset = datasetFromIntelligent;
@@ -244,135 +245,152 @@ const crawlSitemap = async ({
244
245
  return;
245
246
  }
246
247
 
247
- await waitForPageLoaded(page, 10000);
248
+ try {
249
+ await waitForPageLoaded(page, 10000);
248
250
 
249
- const actualUrl = page.url() || request.loadedUrl || request.url;
251
+ const actualUrl = page.url() || request.loadedUrl || request.url;
250
252
 
251
- const hasExceededDuration =
252
- scanDuration > 0 && Date.now() - crawlStartTime > scanDuration * 1000;
253
+ const hasExceededDuration =
254
+ scanDuration > 0 && Date.now() - crawlStartTime > scanDuration * 1000;
253
255
 
254
- if (urlsCrawled.scanned.length >= maxRequestsPerCrawl || hasExceededDuration) {
255
- if (hasExceededDuration) {
256
- console.log(`Crawl duration of ${scanDuration}s exceeded. Aborting sitemap crawl.`);
257
- durationExceeded = true;
256
+ if (urlsCrawled.scanned.length >= maxRequestsPerCrawl || hasExceededDuration) {
257
+ isAbortingScan = true;
258
+ if (hasExceededDuration) {
259
+ console.log(`Crawl duration of ${scanDuration}s exceeded. Aborting sitemap crawl.`);
260
+ durationExceeded = true;
261
+ }
262
+ crawler.autoscaledPool.abort(); // stops new requests
263
+ return;
258
264
  }
259
- crawler.autoscaledPool.abort(); // stops new requests
260
- return;
261
- }
262
265
 
263
- if (request.skipNavigation && actualUrl === 'about:blank') {
264
- if (isScanPdfs) {
265
- // pushes download promise into pdfDownloads
266
- const { pdfFileName, url } = handlePdfDownload(
267
- randomToken,
268
- pdfDownloads,
269
- request,
270
- sendRequest,
271
- urlsCrawled,
272
- );
266
+ if (request.skipNavigation && actualUrl === 'about:blank') {
267
+ if (isScanPdfs) {
268
+ // pushes download promise into pdfDownloads
269
+ const { pdfFileName, url } = handlePdfDownload(
270
+ randomToken,
271
+ pdfDownloads,
272
+ request,
273
+ sendRequest,
274
+ urlsCrawled,
275
+ );
276
+
277
+ uuidToPdfMapping[pdfFileName] = url;
278
+ return;
279
+ }
280
+
281
+ guiInfoLog(guiInfoStatusTypes.SKIPPED, {
282
+ numScanned: urlsCrawled.scanned.length,
283
+ urlScanned: request.url,
284
+ });
285
+ urlsCrawled.userExcluded.push({
286
+ url: request.url,
287
+ pageTitle: request.url,
288
+ actualUrl: request.url, // because about:blank is not useful
289
+ metadata: STATUS_CODE_METADATA[1],
290
+ httpStatusCode: 1,
291
+ });
273
292
 
274
- uuidToPdfMapping[pdfFileName] = url;
275
293
  return;
276
294
  }
277
295
 
278
- guiInfoLog(guiInfoStatusTypes.SKIPPED, {
279
- numScanned: urlsCrawled.scanned.length,
280
- urlScanned: request.url,
281
- });
282
- urlsCrawled.userExcluded.push({
283
- url: request.url,
284
- pageTitle: request.url,
285
- actualUrl: request.url, // because about:blank is not useful
286
- metadata: STATUS_CODE_METADATA[1],
287
- httpStatusCode: 1,
288
- });
296
+ const contentType = response?.headers?.()['content-type'] || '';
297
+ const status = response ? response.status() : 0;
289
298
 
290
- return;
291
- }
299
+ if (isScanHtml && status < 300 && isWhitelistedContentType(contentType)) {
300
+ const isRedirected = !areLinksEqual(page.url(), request.url);
301
+ const isLoadedUrlInCrawledUrls = urlsCrawled.scanned.some(
302
+ item => (item.actualUrl || item.url) === page.url(),
303
+ );
292
304
 
293
- const contentType = response?.headers?.()['content-type'] || '';
294
- const status = response ? response.status() : 0;
305
+ if (isRedirected && isLoadedUrlInCrawledUrls) {
306
+ urlsCrawled.notScannedRedirects.push({
307
+ fromUrl: request.url,
308
+ toUrl: actualUrl, // i.e. actualUrl
309
+ });
310
+ return;
311
+ }
295
312
 
296
- if (isScanHtml && status < 300 && isWhitelistedContentType(contentType)) {
297
- const isRedirected = !areLinksEqual(page.url(), request.url);
298
- const isLoadedUrlInCrawledUrls = urlsCrawled.scanned.some(
299
- item => (item.actualUrl || item.url) === page.url(),
300
- );
313
+ // This logic is different from crawlDomain, as it also checks if the pae is redirected before checking if it is excluded using exclusions.txt
314
+ if (isRedirected && blacklistedPatterns && isSkippedUrl(actualUrl, blacklistedPatterns)) {
315
+ urlsCrawled.userExcluded.push({
316
+ url: request.url,
317
+ pageTitle: request.url,
318
+ actualUrl,
319
+ metadata: STATUS_CODE_METADATA[0],
320
+ httpStatusCode: 0,
321
+ });
301
322
 
302
- if (isRedirected && isLoadedUrlInCrawledUrls) {
303
- urlsCrawled.notScannedRedirects.push({
304
- fromUrl: request.url,
305
- toUrl: actualUrl, // i.e. actualUrl
306
- });
307
- return;
308
- }
323
+ guiInfoLog(guiInfoStatusTypes.SKIPPED, {
324
+ numScanned: urlsCrawled.scanned.length,
325
+ urlScanned: request.url,
326
+ });
327
+ return;
328
+ }
309
329
 
310
- // This logic is different from crawlDomain, as it also checks if the pae is redirected before checking if it is excluded using exclusions.txt
311
- if (isRedirected && blacklistedPatterns && isSkippedUrl(actualUrl, blacklistedPatterns)) {
312
- urlsCrawled.userExcluded.push({
313
- url: request.url,
314
- pageTitle: request.url,
315
- actualUrl,
316
- metadata: STATUS_CODE_METADATA[0],
317
- httpStatusCode: 0,
318
- });
330
+ const results = await runAxeScript({ includeScreenshots, page, randomToken });
319
331
 
320
- guiInfoLog(guiInfoStatusTypes.SKIPPED, {
332
+ guiInfoLog(guiInfoStatusTypes.SCANNED, {
321
333
  numScanned: urlsCrawled.scanned.length,
322
334
  urlScanned: request.url,
323
335
  });
324
- return;
325
- }
326
336
 
327
- const results = await runAxeScript({ includeScreenshots, page, randomToken });
328
-
329
- guiInfoLog(guiInfoStatusTypes.SCANNED, {
330
- numScanned: urlsCrawled.scanned.length,
331
- urlScanned: request.url,
332
- });
333
-
334
- urlsCrawled.scanned.push({
335
- url: request.url,
336
- pageTitle: results.pageTitle,
337
- actualUrl, // i.e. actualUrl
338
- });
337
+ urlsCrawled.scanned.push({
338
+ url: request.url,
339
+ pageTitle: results.pageTitle,
340
+ actualUrl, // i.e. actualUrl
341
+ });
339
342
 
340
- urlsCrawled.scannedRedirects.push({
341
- fromUrl: request.url,
342
- toUrl: actualUrl,
343
- });
343
+ urlsCrawled.scannedRedirects.push({
344
+ fromUrl: request.url,
345
+ toUrl: actualUrl,
346
+ });
344
347
 
345
- results.url = request.url;
346
- results.actualUrl = actualUrl;
348
+ results.url = request.url;
349
+ results.actualUrl = actualUrl;
347
350
 
348
- await dataset.pushData(results);
349
- } else {
350
- guiInfoLog(guiInfoStatusTypes.SKIPPED, {
351
- numScanned: urlsCrawled.scanned.length,
352
- urlScanned: request.url,
353
- });
351
+ await dataset.pushData(results);
352
+ } else {
353
+ guiInfoLog(guiInfoStatusTypes.SKIPPED, {
354
+ numScanned: urlsCrawled.scanned.length,
355
+ urlScanned: request.url,
356
+ });
354
357
 
355
- if (isScanHtml) {
356
- // carry through the HTTP status metadata
357
- const status = response?.status();
358
- const metadata =
359
- typeof status === 'number'
360
- ? STATUS_CODE_METADATA[status] || STATUS_CODE_METADATA[599]
361
- : STATUS_CODE_METADATA[2];
358
+ if (isScanHtml) {
359
+ // carry through the HTTP status metadata
360
+ const status = response?.status();
361
+ const metadata =
362
+ typeof status === 'number'
363
+ ? STATUS_CODE_METADATA[status] || STATUS_CODE_METADATA[599]
364
+ : STATUS_CODE_METADATA[2];
365
+
366
+ urlsCrawled.invalid.push({
367
+ actualUrl,
368
+ url: request.url,
369
+ pageTitle: request.url,
370
+ metadata,
371
+ httpStatusCode: typeof status === 'number' ? status : 0,
372
+ });
373
+ }
374
+ }
375
+ } catch (e) {
376
+ if (!isAbortingScan) {
377
+ guiInfoLog(guiInfoStatusTypes.ERROR, {
378
+ numScanned: urlsCrawled.scanned.length,
379
+ urlScanned: request.url,
380
+ });
362
381
 
363
- urlsCrawled.invalid.push({
364
- actualUrl,
382
+ urlsCrawled.error.push({
365
383
  url: request.url,
366
384
  pageTitle: request.url,
367
- metadata,
368
- httpStatusCode: typeof status === 'number' ? status : 0,
385
+ actualUrl: request.url,
386
+ metadata: STATUS_CODE_METADATA[2],
387
+ httpStatusCode: 0,
369
388
  });
370
389
  }
371
390
  }
372
391
  },
373
392
  failedRequestHandler: async ({ request, response, error }) => {
374
- // check if scanned pages have reached limit due to multi-instances of handler running
375
- if (urlsCrawled.scanned.length >= maxRequestsPerCrawl) {
393
+ if (isAbortingScan) {
376
394
  return;
377
395
  }
378
396
 
@@ -100,17 +100,17 @@ export const screenshotFullPage = async (page, screenshotsDir: string, screensho
100
100
  window.scrollTo(0, document.body.scrollHeight);
101
101
  });
102
102
 
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
- });
103
+ const isLoadMoreContent = async () => {
104
+ await new Promise(resolve => setTimeout(resolve, 2500));
105
+ if (page.isClosed()) return false;
106
+ try {
107
+ await page.waitForLoadState('domcontentloaded');
108
+ const newHeight = await page.evaluate(() => document.body.scrollHeight);
109
+ return newHeight > prevHeight;
110
+ } catch {
111
+ return false;
112
+ }
113
+ };
114
114
 
115
115
  const result = await isLoadMoreContent();
116
116
  return result;
@@ -409,25 +409,72 @@ export const addOverlayMenu = async (
409
409
  const h2 = document.createElement('h2');
410
410
  h2.id = 'oobeeHPagesScanned';
411
411
  h2.className = 'oobee-section-title';
412
- h2.textContent = 'Pages Scanned';
413
-
412
+ h2.textContent = `Pages Scanned (${vars.urlsCrawled.scanned.length || 0})`;
413
+
414
+ const scanIcon = document.createElement('span');
415
+ scanIcon.className = 'oobee-btn-icon';
416
+
417
+ const SCAN_SVG = `
418
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
419
+ <g clip-path="url(#clip0_1421_431)">
420
+ <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"/>
421
+ <path d="M18.5 0H19.5C19.7761 0 20 0.223858 20 0.5V5H18.5V0Z" fill="white"/>
422
+ <path d="M19.5 2.18552e-08L19.5 1.5L15 1.5L15 -2.18556e-07L19.5 2.18552e-08Z" fill="white"/>
423
+ <path d="M1.5 0H0.5C0.223858 0 0 0.223858 0 0.5V5H1.5V0Z" fill="white"/>
424
+ <path d="M0.5 2.18552e-08L0.5 1.5L5 1.5L5 -2.18556e-07L0.5 2.18552e-08Z" fill="white"/>
425
+ <path d="M1.5 20H0.5C0.223858 20 0 19.7761 0 19.5V15H1.5V20Z" fill="white"/>
426
+ <path d="M0.5 20L0.5 18.5L5 18.5L5 20L0.5 20Z" fill="white"/>
427
+ <path d="M18.5 20H19.5C19.7761 20 20 19.7761 20 19.5V15H18.5V20Z" fill="white"/>
428
+ <path d="M19.5 20L19.5 18.5L15 18.5L15 20L19.5 20Z" fill="white"/>
429
+ </g>
430
+ <defs>
431
+ <clipPath id="clip0_1421_431">
432
+ <rect width="20" height="20" fill="white"/>
433
+ </clipPath>
434
+ </defs>
435
+ </svg>
436
+ `;
437
+
438
+ scanIcon.innerHTML = SCAN_SVG;
414
439
  const scanBtn = document.createElement('button');
415
440
  scanBtn.id = 'oobeeBtnScan';
416
441
  scanBtn.className = 'oobee-btn oobee-btn-primary';
417
- scanBtn.innerText = 'Scan this page';
418
442
  scanBtn.disabled = inProgress;
443
+ scanBtn.appendChild(scanIcon);
444
+
445
+ const scanText = document.createElement('span');
446
+ scanText.className = 'oobee-btn-text';
447
+ scanText.innerText = 'Scan page';
448
+ scanBtn.appendChild(scanText);
449
+
419
450
  scanBtn.addEventListener('click', async () => customWindow.handleOnScanClick?.());
420
451
 
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?.());
452
+ const endScanIcon = document.createElement('span');
453
+ endScanIcon.className = 'oobee-btn-icon';
454
+
455
+ const ENDSCAN_SVG =
456
+ `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
457
+ <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"/>
458
+ </svg>
459
+ `;
460
+
461
+ endScanIcon.innerHTML = ENDSCAN_SVG;
462
+ const endScanBtn = document.createElement('button');
463
+ endScanBtn.id = 'oobeeBtnEndScan';
464
+ endScanBtn.className = 'oobee-btn oobee-btn-secondary';
465
+ endScanBtn.appendChild(endScanIcon);
466
+
467
+ const endScanText = document.createElement('span');
468
+ endScanText.className = 'oobee-btn-text';
469
+ endScanText.innerText = 'End scan';
470
+ endScanBtn.appendChild(endScanText);
471
+
472
+ endScanBtn.addEventListener('click', async () => customWindow.handleOnStopClick?.());
426
473
 
427
474
  const btnGroup = document.createElement('div');
428
475
  btnGroup.className = 'oobee-actions';
429
476
  btnGroup.appendChild(scanBtn);
430
- btnGroup.appendChild(stopBtn);
477
+ btnGroup.appendChild(endScanBtn);
431
478
 
432
479
  const listWrap = document.createElement('div');
433
480
  listWrap.id = 'oobeeList';
@@ -503,7 +550,7 @@ export const addOverlayMenu = async (
503
550
  border-right: 1px solid rgba(0,0,0,.08)
504
551
  }
505
552
  .oobee-panel.collapsed {
506
- width: 56px;
553
+ width: 58px;
507
554
  overflow: hidden
508
555
  }
509
556
 
@@ -580,6 +627,12 @@ export const addOverlayMenu = async (
580
627
  padding: 1rem;
581
628
  }
582
629
 
630
+ .oobee-panel.collapsed .oobee-actions {
631
+ display: flex;
632
+ justify-content: center;
633
+ padding: 1rem 0.7rem;
634
+ }
635
+
583
636
  /* Base button */
584
637
  .oobee-btn {
585
638
  width: 100%;
@@ -590,6 +643,10 @@ export const addOverlayMenu = async (
590
643
  line-height: 1.2;
591
644
  font-weight: 400;
592
645
  cursor: pointer;
646
+ display: flex;
647
+ align-items: center;
648
+ justify-content: center;
649
+ gap: 10px;
593
650
  transition: {
594
651
  box-shadow .12s ease,
595
652
  transform .02s ease,
@@ -603,6 +660,19 @@ export const addOverlayMenu = async (
603
660
  cursor:not-allowed
604
661
  }
605
662
 
663
+ .oobee-panel.collapsed .oobee-btn {
664
+ width: 44px !important;
665
+ height: 44px !important;
666
+ min-width: 44px !important;
667
+ min-height: 44px !important;
668
+ max-width: 44px !important;
669
+ max-height: 44px !important;
670
+ border-radius: 50% !important;
671
+ padding: 0 !important;
672
+ justify-content: center;
673
+ gap: 0;
674
+ }
675
+
606
676
  /* Primary (filled) */
607
677
  .oobee-btn-primary {
608
678
  background: #9021a6;
@@ -658,6 +728,25 @@ export const addOverlayMenu = async (
658
728
  display: none;
659
729
  }
660
730
 
731
+ .oobee-btn-icon {
732
+ display: inline-flex;
733
+ align-items: center;
734
+ justify-content: center;
735
+ width: 20px;
736
+ height: 20px;
737
+ vertical-align: middle;
738
+ }
739
+
740
+ .oobee-btn-text {
741
+ display: inline;
742
+ white-space: nowrap;
743
+ vertical-align: middle;
744
+ }
745
+
746
+ .oobee-panel.collapsed .oobee-btn-text {
747
+ display: none;
748
+ }
749
+
661
750
  #oobeeStopOverlay[hidden] {
662
751
  display:none !important;
663
752
  }
@@ -675,7 +764,10 @@ export const addOverlayMenu = async (
675
764
  }
676
765
 
677
766
  .oobee-panel.collapsed .oobee-section-title {
678
- display: none;
767
+ font-size: 14px;
768
+ display: flex;
769
+ justify-content: center;
770
+ text-align: center;
679
771
  }
680
772
 
681
773
  .oobee-ol {
@@ -1077,7 +1169,13 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
1077
1169
  // eslint-disable-next-line no-underscore-dangle
1078
1170
  const pageId = page._guid;
1079
1171
 
1080
- page.on('dialog', () => {});
1172
+ page.on('dialog', async dialog => {
1173
+ try {
1174
+ await dialog.dismiss();
1175
+ } catch {
1176
+ // dialog may already be closed
1177
+ }
1178
+ });
1081
1179
 
1082
1180
  const pageClosePromise = new Promise(resolve => {
1083
1181
  page.on('close', () => {
@@ -1109,6 +1207,7 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
1109
1207
  log('Scan: success');
1110
1208
  pagesDict[pageId].isScanning = false;
1111
1209
 
1210
+ if (page.isClosed()) return;
1112
1211
  const allowed = isOverlayAllowed(page.url(), processPageParams.entryUrl);
1113
1212
 
1114
1213
  if (allowed) {
@@ -1150,10 +1249,10 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
1150
1249
 
1151
1250
  if (!inputValue?.confirmed) {
1152
1251
  await page.evaluate(() => {
1153
- const stopBtn = document.getElementById('oobeeBtnStop') as HTMLButtonElement | null;
1154
- if (stopBtn) {
1155
- stopBtn.disabled = false;
1156
- stopBtn.textContent = 'Stop';
1252
+ const endScanBtn = document.getElementById('oobeeBtnEndScan') as HTMLButtonElement | null;
1253
+ if (endScanBtn) {
1254
+ endScanBtn.disabled = false;
1255
+ endScanBtn.textContent = 'Stop';
1157
1256
  }
1158
1257
  });
1159
1258
  return;
@@ -1182,6 +1281,7 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
1182
1281
  };
1183
1282
 
1184
1283
  page.on('domcontentloaded', async () => {
1284
+ if (page.isClosed()) return;
1185
1285
  try {
1186
1286
  const allowed = isOverlayAllowed(page.url(), processPageParams.entryUrl);
1187
1287
 
@@ -1218,19 +1318,24 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
1218
1318
  }
1219
1319
  });
1220
1320
 
1221
- await page.exposeFunction('handleOnScanClick', handleOnScanClick);
1222
- await page.exposeFunction('handleOnStopClick', handleOnStopClick);
1321
+ try {
1322
+ if (page.isClosed()) return page;
1323
+ await page.exposeFunction('handleOnScanClick', handleOnScanClick);
1324
+ await page.exposeFunction('handleOnStopClick', handleOnStopClick);
1223
1325
 
1224
- type UpdateMenuPosFunction = (newPos: any) => void;
1326
+ type UpdateMenuPosFunction = (newPos: any) => void;
1225
1327
 
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);
1328
+ // Define the updateMenuPos function
1329
+ const updateMenuPos: UpdateMenuPosFunction = newPos => {
1330
+ const prevPos = menuPos;
1331
+ if (prevPos !== newPos) {
1332
+ menuPos = newPos;
1333
+ }
1334
+ };
1335
+ await page.exposeFunction('updateMenuPos', updateMenuPos);
1336
+ } catch (e) {
1337
+ log(`Error exposing functions on page: ${e}`);
1338
+ }
1234
1339
 
1235
1340
  return page;
1236
1341
  };
@@ -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)) {