=== AXE RUNNER === 1 /* Runs axe-core (Deque Systems, MPL-2.0, loaded from axe.min.js beside this file) on the page this frame holds, 2 for the WCAG A/AA rules the aiwebpageseo Accessibility check does not cover itself, and posts the result to the 3 tool page. The page in this frame is a copy of the audited page with all of its own scripts taken out. */ 4 (function () { 5 try { parent.postMessage({ a11yAxe: 2 }, '*'); } catch (e) {} // started: the tool page stops waiting if this never arrives 6 var RULES = ['input-image-alt', 'nested-interactive', 'list', 'listitem', 'definition-list', 'dlitem', 'meta-viewport', 7 'aria-required-parent', 'aria-required-children', 'aria-valid-attr', 'aria-valid-attr-value', 'aria-allowed-attr', 8 'aria-prohibited-attr', 'aria-required-attr', 'aria-roles', 'aria-hidden-body', 'aria-input-field-name', 9 'aria-toggle-field-name', 'aria-command-name', 'aria-meter-name', 'aria-progressbar-name', 'aria-tooltip-name', 10 'aria-braille-equivalent', 'aria-conditional-attr', 'aria-deprecated-role', 'scrollable-region-focusable', 11 'link-in-text-block', 'svg-img-alt', 'role-img-alt', 'object-alt', 'area-alt', 'input-button-name', 'summary-name', 12 'document-title', 'avoid-inline-spacing', 'server-side-image-map', 'blink', 'marquee', 'td-headers-attr', 13 'th-has-data-cells', 'valid-lang', 'html-xml-lang-mismatch', 'autocomplete-valid', 'target-size']; 14 // Best-practice rules (axe-core's own "best-practice" tag): not WCAG requirements, so the tool page reports any that 15 // fail as unscored notes. Left out: heading-order, page-has-heading-one and skip-link (the tool's own checks cover 16 // them) and frame-tested (it only reports frames axe could not enter). 17 var BEST = ['region', 'landmark-one-main', 'landmark-unique', 'landmark-no-duplicate-banner', 18 'landmark-no-duplicate-contentinfo', 'landmark-no-duplicate-main', 'landmark-banner-is-top-level', 19 'landmark-contentinfo-is-top-level', 'landmark-main-is-top-level', 'landmark-complementary-is-top-level', 20 'empty-heading', 'empty-table-header', 'tabindex', 'accesskeys', 'image-redundant-alt', 'label-title-only', 21 'aria-allowed-role', 'aria-dialog-name', 'aria-treeitem-name', 'aria-text', 'presentation-role-conflict', 22 'scope-attr-valid', 'table-duplicate-name', 'meta-viewport-large']; 23 var sent = false, started = false; 24 function send(m) { if (sent) return; sent = true; m.a11yAxe = 1; try { parent.postMessage(m, '*'); } catch (e) {} } 25 function go() { 26 if (sent || started) return; 27 started = true; 28 if (!window.axe) return send({ ok: false, error: 'the checker did not load' }); 29 var known = axe.getRules().map(function (r) { return r.ruleId; }); 30 var rules = RULES.concat(BEST).filter(function (r) { return known.indexOf(r) >= 0; }); 31 axe.run(document, { runOnly: { type: 'rule', values: rules }, resultTypes: ['violations'] }).then(function (r) { 32 send({ ok: true, version: axe.version, violations: r.violations.map(function (v) { 33 return { id: v.id, impact: v.impact, help: v.help, description: v.description, tags: v.tags, n: v.nodes.length, bp: BEST.indexOf(v.id) >= 0, 34 ex: v.nodes.slice(0, 3).map(function (n) { return { html: String(n.html || '').slice(0, 200), fix: String(n.failureSummary || '').slice(0, 300) }; }) }; 35 }) }); 36 }, function (e) { send({ ok: false, error: String((e && e.message) || e).slice(0, 200) }); }); 37 } 38 if (document.readyState === 'complete') go(); else { window.addEventListener('load', go); setTimeout(go, 8000); } 39 })(); === RENDERED AUDIT: ENTRY POINTS === 33:const TIMEOUT_MS = 60000; // whole check (contrast + the axe-core checks), then the caller falls back to the raw-HTML check 34:// axe-core and its runner, asked for by the tool page's check frame at this address, are served from this 35:// server's own copy (public/a11y-axe/), so the full audit does not depend on fetching them over the network. 36:const AXE_URL = 'https://aiwebpageseo.com/a11y-axe/'; 37:const AXE_FILES = { 'axe.min.js': path.join(__dirname, '..', 'public', 'a11y-axe', 'axe.min.js'), 'run.js': path.join(__dirname, '..', 'public', 'a11y-axe', 'run.js') }; 41:function getBrowser() { 47: }).then(b => { b.on('disconnected', () => { browserP = null; }); return b; }) 50: return browserP; 57:function kbAcquire() { if (kbActive < KB_MAX) { kbActive++; return Promise.resolve(); } return new Promise(r => kbWaiting.push(r)); } 58:function kbRelease() { const next = kbWaiting.shift(); if (next) next(); else kbActive--; } 59:function ownBrowser() { return puppeteer.launch({ headless: 'new', executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined, args: ['--disable-dev-shm-usage'] }); } 64:function acquire() { if (active < MAX_PAGES) { active++; return Promise.resolve(); } return new Promise(r => waiting.push(r)); } 65:function release() { const next = waiting.shift(); if (next) next(); else active--; } 68:function privateIp(ip) { 71: return a === 0 || a === 10 || a === 127 || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31) || 75: if (v === '::' || v === '::1') return true; 76: const m = v.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/); if (m) return privateIp(m[1]); 77: return /^(fe[89ab]|f[cd]|ff)/.test(v); 80:async function publicHost(host) { 82: if (host === 'localhost' || host.endsWith('.local') || host.endsWith('.internal')) return false; 83: if (net.isIP(host)) return !privateIp(host); 84: if (hostCache.has(host)) return hostCache.get(host); 88: return ok; 96:function pageSize(html) { 100: return { words: words, links: links }; 102:function nearlyEmpty(html) { const z = pageSize(html); return z.words < 40 && z.links <= 1; } 103:function botCheck(html) { const z = pageSize(html); return BOT_SURE.test(html) || (BOT_MAYBE.test(html) && z.words < 300 && z.links <= 5); } 105:// Load a page with its own scripts running (sandboxed) and return a copy of the finished page: 121:function withKbLock(fn) { const run = kbChain.then(fn, fn); kbChain = run.catch(() => {}); return run; } 133:async function hoverCheck(page) { 134: const ids = await kbWithin(page.evaluate(() => { 135: const vis = e => { const r = e.getBoundingClientRect(), st = getComputedStyle(e); return r.width > 0 && r.height > 0 && st.visibility === 'visible' && st.display !== 'none' && parseFloat(st.opacity) > 0.1; }; 136: const hidden = e => { for (let a = e; a; a = a.parentElement) { const st = getComputedStyle(a); if (st.display === 'none' || st.visibility !== 'visible' || parseFloat(st.opacity) < 0.1) return true; if (a.tagName === 'LI') break; } const r = e.getBoundingClientRect(); return r.width === 0 || r.height === 0; }; 149: return out; 152: const markVisible = () => page.evaluate(() => { window.__kseen = new WeakSet(); for (const e of document.querySelectorAll('body *')) { const r = e.getBoundingClientRect(), st = getComputedStyle(e); if (r.width > 0 && r.height > 0 && st.visibility === 'visible' && parseFloat(st.opacity) > 0.1) window.__kseen.add(e); } }); 153: const findNew = i => page.evaluate(i => { const trig = document.querySelector('[data-khov="' + i + '"]'); const out = []; for (const e of document.querySelectorAll('body *')) { if (window.__kseen.has(e) || (trig && (trig === e || trig.contains(e)))) continue; const r = e.getBoundingClientRect(), st = getComputedStyle(e); if (r.width < 30 || r.height < 10 || st.visibility !== 'visible' || parseFloat(st.opacity) <= 0.1 || !(e.innerText || '').trim()) continue; e.dataset.knew = i; let ov = false; for (let a = e, k = 0; a && k < 5; a = a.parentElement, k++) if (/absolute|fixed/.test(getComputedStyle(a).position)) { ov = true; break; } out.push({ l: r.left, r: r.right, t: r.top, h: r.height, ov }); } return { n: out.length, at: out[0] || null, overlay: out.some(o => o.ov), name: trig ? (trig.innerText || trig.getAttribute('aria-label') || '').replace(/\s+/g, ' ').trim().slice(0, 40) : '' }; }, i); 154: const stillShown = i => page.evaluate(i => [...document.querySelectorAll('[data-knew="' + i + '"]')].some(e => { const r = e.getBoundingClientRect(), st = getComputedStyle(e); return r.width > 0 && r.height > 0 && st.visibility === 'visible' && parseFloat(st.opacity) > 0.1 && st.display !== 'none'; }), i); 178: return { tried: ids.length, noEscape: { count: noEsc.length, examples: noEsc.slice(0, 5) }, noHover: { count: noHover.length, examples: noHover.slice(0, 5) } }; 180:async function flashCheck(page, frames) { 182: if (fr.length < 2) return { checked: true, count: 0, examples: [], fps: 0 }; 188: const res = await kbWithin(blank.evaluate(async fr => { 189: const lin = c => { c /= 255; return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); }; 213: return { bad, W }; 215: if (!res) return null; 219: for (const b of res.bad.slice(0, 6)) { const nm = await page.evaluate((x, y) => { const e = document.elementFromPoint(x, y); if (!e) return ''; const a = e.closest('video, canvas, img, svg, iframe, section, div') || e; return (a.getAttribute('aria-label') || a.getAttribute('alt') || (a.innerText || '').replace(/\s+/g, ' ').trim().slice(0, 50) || a.tagName.toLowerCase() + (typeof a.className === 'string' && a.className.trim() ? '.' + a.className.trim().split(/\s+/)[0].slice(0, 30) : '')); }, b.x * scale, b.y * scale).catch(() => ''); if (nm && !names.includes(nm)) names.push(nm); } 220: return { checked: true, count: res.bad.length ? Math.max(1, names.length) : 0, examples: names.map(n => ({ name: n })), fps: Math.round(fps) }; 224:async function layoutChecks(page) { 245: out.motion = await kbWithin(page.evaluate(async () => { 250: const st = getComputedStyle(e); if (st.visibility !== 'visible') continue; m.set(e, [r.left, r.top, st.transform, parseFloat(st.opacity), e.scrollLeft, e.scrollTop]); } return m; }; 252: if (Math.abs(x[0] - y[0]) > 8 || Math.abs(x[1] - y[1]) > 8 || x[2] !== y[2] || Math.abs(x[3] - y[3]) > 0.2 || Math.abs(x[4] - y[4]) > 8 || Math.abs(x[5] - y[5]) > 8) out.add(e); } return out; }; 266: const ancOp = e => { let o = 1; for (let a = e.parentElement; a && a.nodeType === 1; a = a.parentElement) o = Math.min(o, parseFloat(getComputedStyle(a).opacity)); return o; }; 268: moving = [...new Set(moving)].filter(e => { const r = e.getBoundingClientRect(); return r.right > 0 && r.left < innerWidth && ancOp(e) >= 0.1 && ownOp(e) >= 0.1; }); 270: const vids = [...document.querySelectorAll('video')].filter(v => { const r = v.getBoundingClientRect(); return !v.paused && (v.loop || v.duration > 5) && r.width >= 150 && r.height >= 80 && r.bottom > 0 && r.top < H && getComputedStyle(v).visibility === 'visible'; }); 276: if ([...a.querySelectorAll('button, [role="button"], a, input[type="button"]')].some(c => PAUSE.test(nameOf(c)) || c.hasAttribute('aria-pressed'))) return true; } return false; }; 277: const label = e => { const t = (e.innerText || e.getAttribute('aria-label') || e.getAttribute('alt') || '').replace(/\s+/g, ' ').trim().slice(0, 50); return t || (e.tagName.toLowerCase() + (typeof e.className === 'string' && e.className.trim() ? '.' + e.className.trim().split(/\s+/)[0].slice(0, 30) : '')); }; 288: const seen = new Set(); const uniq = bad.filter(b => { if (seen.has(b.name)) return false; seen.add(b.name); return true; }); 289: return { count: uniq.length, examples: uniq.slice(0, 5) }; 298: out.spacing = await kbWithin(page.evaluate(async () => { 300: const faded = e => { for (let a = e; a && a.nodeType === 1; a = a.parentElement) if (parseFloat(getComputedStyle(a).opacity) < 0.1) return true; return false; }; 302: const boxes = [...document.querySelectorAll('body *')].filter(e => { const s = getComputedStyle(e); if (!/hidden|clip/.test(s.overflowY + ' ' + s.overflowX) || s.visibility !== 'visible' || e.clientHeight < 10 || e.clientWidth < 10 || (e.innerText || '').trim().length < 2) return false; 303: const r = e.getBoundingClientRect(); if (r.right <= 0 || r.left >= docW || r.bottom + scrollY <= 0) return false; return !faded(e); }).slice(0, 3000); 323: return { count: hit.length, examples: hit.slice(0, 6).map(e => ({ name: (e.innerText || '').replace(/\s+/g, ' ').trim().slice(0, 50) })) }; 332: const clipState = () => kbWithin(page.evaluate(() => { 333: const faded = e => { for (let a = e; a && a.nodeType === 1; a = a.parentElement) if (parseFloat(getComputedStyle(a).opacity) < 0.1) return true; return false; }; 339: const hasNav = e => { for (let a = e, i = 0; a && i < 4 && a !== document.body; a = a.parentElement, i++) for (const c of a.querySelectorAll('button, [role="button"], a')) { if (NAV.test((c.getAttribute('aria-label') || '') + ' ' + (c.textContent || '').slice(0, 40) + ' ' + (typeof c.className === 'string' ? c.className.replace(/[-_]/g, ' ') : ''))) return true; } return false; }; 340: const linkedElsewhere = (el, box) => { const a = el.closest('a[href]'); if (!a || !a.href || /^javascript:|#$/.test(a.getAttribute('href'))) return false; for (const o of document.querySelectorAll('a[href]')) if (o !== a && !box.contains(o) && o.href === a.href) return true; return false; }; 341: const cardLink = box => { for (let a = box, i = 0; a && i < 5 && a !== document.body; a = a.parentElement, i++) if (a.matches('a[href]') || a.querySelector('a[href]')) return true; return false; }; 343: const hx = /hidden|clip/.test(st.overflowX), hy = /hidden|clip/.test(st.overflowY); if (!hx && !hy) return 0; 344: if (!(hy && e.scrollHeight > e.clientHeight + 2) && !(hx && e.scrollWidth > e.clientWidth + 2)) return 0; 359: return n; }; 365: return out; }), 15000); 372: for (const x of hit.slice(0, 20)) { try { const ok = await kbWithin(page.evaluate(id => { const e = document.querySelector('[data-kz="' + id + '"]'); if (!e) return false; e.scrollIntoView({ block: 'center' }); const r = e.getBoundingClientRect(); const t = document.elementFromPoint(Math.min(Math.max(r.left + r.width / 2, 1), innerWidth - 2), Math.min(Math.max(r.top + Math.min(r.height, 40) / 2, 1), innerHeight - 2)); return !!t && (t === e || e.contains(t)); }, x[0]), 3000); if (ok) seenHit.push(x); } catch (e) {} } 374: try { const keep = await kbWithin(page.evaluate(ids => ids.filter(id => { const e = document.querySelector('[data-kz="' + id + '"]'); return !ids.some(o => o !== id && e && e.contains(document.querySelector('[data-kz="' + o + '"]'))); }), seenHit.map(x => x[0])), 3000); inner = seenHit.filter(x => keep.includes(x[0])); } catch (e) {} 376: await page.evaluate(() => window.scrollTo(0, 0)).catch(() => {}); 381: out.reflow = await kbWithin(page.evaluate(() => { 391: const insideScroller = e => { for (let a = e.parentElement; a && a !== document.body && a !== de; a = a.parentElement) { if (!/visible/.test(getComputedStyle(a).overflowX)) return true; } return false; }; 397: const nm = e => { const t = (e.innerText || e.getAttribute('aria-label') || e.getAttribute('alt') || '').replace(/\s+/g, ' ').trim().slice(0, 40); return t || (e.tagName.toLowerCase() + (typeof e.className === 'string' && e.className.trim() ? '.' + e.className.trim().split(/\s+/)[0].slice(0, 30) : '')); }; 398: return { width: W, pageWidth: sw, scrolls: scrolls && offenders.length > 0, exemptOnly: scrolls && !offenders.length, examples: offenders.slice(0, 5).map(e => ({ name: nm(e), w: Math.round(e.getBoundingClientRect().right) })) }; 405: const read = () => kbWithin(page.evaluate(() => { const t = (document.body.innerText || '').replace(/\s+/g, ' '); return { len: t.length, rotate: /\b(rotate|turn) (your )?(device|phone|screen|tablet)\b|\b(portrait|landscape) (mode|orientation) (only|is required|required)\b|\bplease use (portrait|landscape)\b/i.test(t) }; }), 5000); 412: try { await page.setViewport({ width: 1280, height: 900 }); await new Promise(r => setTimeout(r, 900)); await page.evaluate(() => window.scrollTo(0, 0)); } catch (e) {} 414: return out; 421:async function siteInfo(page) { 422: return kbWithin(page.evaluate(() => { 423: const norm = h => { try { const u = new URL(h, location.href); if (!/^https?:$/.test(u.protocol)) return ''; u.hash = ''; return u.origin.toLowerCase() + (u.pathname.replace(/\/+$/, '') || '') + u.search; } catch (e) { return ''; } }; 425: const same = u => { try { return new URL(u).hostname.replace(/^www\./, '') === host; } catch (e) { return false; } }; 429: const away = a => { const raw = (a.getAttribute('href') || '').trim(); if (!raw || raw.startsWith('#') || /^javascript:/i.test(raw)) return ''; const h = norm(raw); return h && !(raw.includes('#') && h === here) ? h : ''; }; 439: const region = e => { if (e.closest('footer, [role="contentinfo"]')) return 'footer'; if (e.closest('header, [role="banner"]')) return 'header'; 440: for (let a = e; a && a.nodeType === 1; a = a.parentElement) { const p = getComputedStyle(a).position; if (p === 'fixed' || p === 'sticky') return 'fixed'; } return e.closest('nav') ? 'menu' : 'main'; }; 457: return { url: here, nav: nav.map(n => ({ h: n.h, t: n.t })), blocks, help, inner }; 461:async function answerConsent(page) { 465: const hit = await fr.evaluate((CONSENT, src) => { 468: const vis = e => { const r = e.getBoundingClientRect(), s = getComputedStyle(e); return r.width > 2 && r.height > 2 && s.visibility === 'visible' && s.display !== 'none'; }; 469: const all = sel => { const out = [...document.querySelectorAll(sel)]; const walk = n => { for (const e of n.querySelectorAll('*')) if (e.shadowRoot) { out.push(...e.shadowRoot.querySelectorAll(sel)); walk(e.shadowRoot); } }; walk(document); return out; }; 470: const pinned = e => { for (let a = e; a && a.nodeType === 1; a = a.parentElement || (a.getRootNode && a.getRootNode().host)) { const p = getComputedStyle(a).position; if (p === 'fixed' || p === 'sticky') return true; } return false; }; 475: if (!b) return false; 476: b.click(); return true; 483: return answered; 485:async function keyboardWalk(page, answered) { 493: const total = await page.evaluate(() => { 496: return [...document.querySelectorAll('a[href], button, input:not([type=hidden]), select, textarea, [tabindex]:not([tabindex="-1"]), summary')].filter(e => e.getClientRects().length && getComputedStyle(e).visibility === 'visible' && !e.disabled).length; 507: await page.evaluate(() => document.addEventListener('focusin', e => { try { const el = e.target; if (window.__kfocused) window.__kfocused((el.getAttribute('aria-label') || el.textContent || el.value || '').replace(/\s+/g, ' ').trim()); } catch (x) {} }, true)).catch(() => {}); 514: const s = await kbWithin(page.evaluate(() => { 516: if (!el || el === document.body || el === document.documentElement || el.id === '__kstart') return { none: true }; 531: const paints = t => { for (let e = t; e && e !== document.body && !e.contains(el); e = e.parentElement || (e.getRootNode && e.getRootNode().host)) { const st = getComputedStyle(e); if (/^(IMG|VIDEO|CANVAS|SVG|IFRAME|PICTURE)$/i.test(e.tagName) || st.backgroundImage !== 'none') return true; const m = (st.backgroundColor.match(/[\d.]+/g) || []).map(Number); if (m.length >= 3 && (m.length < 4 || m[3] >= 0.9)) return true; } return false; }; 532: const deep = (x, y) => { let t = document.elementFromPoint(x, y); while (t && t.shadowRoot) { const u = t.shadowRoot.elementFromPoint(x, y); if (!u || u === t) break; t = u; } return t; }; 536: return { kid: el.__kid, name, x: r.left, y: r.top, w: r.width, h: r.height, inDialog, clipBy, frame: el.tagName === 'IFRAME' || (/^(VIDEO|AUDIO)$/.test(el.tagName) && el.controls), typing: /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) || el.isContentEditable, pts: pts.length, covered, pinnedCover, by }; 546: const r2 = await kbWithin(page.evaluate(() => { let el = document.activeElement; while (el && el.shadowRoot && el.shadowRoot.activeElement) el = el.shadowRoot.activeElement; if (!el) return null; const r = el.getBoundingClientRect(); return { x: r.left, y: r.top, w: r.width, h: r.height }; }), 6000).catch(() => null); 557: await page.evaluate(() => { let el = document.activeElement; while (el && el.shadowRoot && el.shadowRoot.activeElement) el = el.shadowRoot.activeElement; window.__kf = el; if (el && el.blur) el.blur(); }); 560: await page.evaluate(() => { if (window.__kf) window.__kf.focus({ preventScroll: true }); }); 562: const n = await kbWithin(blank.evaluate(async (a, b, cut, box) => { 563: const load = async src => { const im = new Image(); im.src = 'data:image/png;base64,' + src; await im.decode(); const c = document.createElement('canvas'); c.width = Math.max(1, cut.w); c.height = Math.max(1, cut.h); const x = c.getContext('2d'); x.drawImage(im, -cut.x, -cut.y); return x.getImageData(0, 0, c.width, c.height).data; }; 565: const lin = c => { c /= 255; return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); }; 574: return { k, strong, inside }; 587: const once = kind => { const m = new Map(); for (const s of stops) if (s.kind === kind && !m.has(s.kid)) m.set(s.kid, s); return [...m.values()]; }; 592: return { checked: true, stops: stops.length, distinct: seen.size, total, ended, answered, 600:async function renderWithScripts(url, opts) { 601: let target; try { target = new URL(url); } catch (e) { return { ok: false, html: '', error: 'bad url' }; } 602: if (!/^https?:$/.test(target.protocol) || !(await publicHost(target.hostname))) return { ok: false, html: '', error: 'blocked' }; 615: if (/^(data|about|blob):/i.test(u)) return req.continue().catch(() => {}); 616: let host; try { const p = new URL(u); if (!/^https?:$/.test(p.protocol)) return req.abort().catch(() => {}); host = p.hostname; } catch (e) { return req.abort().catch(() => {}); } 617: if (BLOCK_HOSTS.test(host)) return req.abort().catch(() => {}); 642: await page.evaluate(async () => { 654: try { const shown = await page.evaluate(() => { const e = performance.getEntriesByType('navigation')[0]; return (e && e.responseStatus) || 0; }); if (shown) status = shown; } catch (e) {} 657: const html = await page.evaluate(cssMap => { 664: const abs = (u, base) => { try { return new URL(u, base).href; } catch (e) { return u; } }; 668: if (depth < 3 && cssMap[full] != null) { const inner = expand(cssMap[full], full, depth + 1); return media.trim() ? '@media ' + media.trim() + '{' + inner + '}' : inner; } 669: return '@import url("' + full + '")' + (media.trim() ? ' ' + media.trim() : '') + ';'; 711: return '' + shell.slice(0, cut) + de.getHTML({ serializableShadowRoots: true, shadowRoots: roots }) + shell.slice(cut); } 714: return '' + de.outerHTML; 727: return k; 731: return { ok: true, status: status, html: html, finalUrl: shownUrl, keyboard: keyboard, site: site }; 733: return { ok: false, html: '', error: String(e && e.message || e) }; 745:async function toolReport(html, url, builtByScripts, keyboard, fetchCss, inner) { 757: if (!f) return req.abort().catch(() => {}); 758: return fs.promises.readFile(f).then(body => req.respond({ status: 200, contentType: 'application/javascript', body })).catch(() => req.abort().catch(() => {})); 762: if (/^file:/i.test(u) && u.split('#')[0].split('?')[0] !== TOOL) return req.abort().catch(() => {}); 763: return req.continue().catch(() => {}); 766: try { const p = new URL(u); if (!/^https?:$/.test(p.protocol)) return req.abort().catch(() => {}); host = p.hostname; } catch (e) { return req.abort().catch(() => {}); } 767: if (BLOCK_HOSTS.test(host)) return req.abort().catch(() => {}); 773: if (!/^https?:$/.test(p.protocol) || !(await publicHost(p.hostname))) return { ok: false, html: '', error: 'blocked' }; 774: return await fetchCss(u, ref || null); 775: } catch (e) { return { ok: false, html: '', error: String(e && e.message || e) }; } 780: // axe-core and its runner, from this server's own copy, for the tool page's extra WCAG checks (see a11yAxe). 781: let axeSrc = null; 782: try { axeSrc = { axe: await fs.promises.readFile(AXE_FILES['axe.min.js'], 'utf8'), run: await fs.promises.readFile(AXE_FILES['run.js'], 'utf8') }; } catch (e) { axeSrc = null; } 783: const run = page.evaluate(async (h, base, byScripts, axeSrc, kb, inner) => { 784: if (axeSrc) window.__a11yAxeSrc = axeSrc; 793: return window.a11yLastReport; 794: }, html, url || '', builtByScripts, axeSrc, keyboard, inner || null); 796: if (!report || !Array.isArray(report.issues)) throw new Error('rendered accessibility check returned no report'); 798: return report; 809:async function innerReports(homeUrl, urls, fetchCss, onProgress) { 811: let homeHost = ''; try { homeHost = new URL(homeUrl).hostname.replace(/^www\./, ''); } catch (e) { return pages; } 825: pages.push({ url: r.finalUrl || u, issues: rep.issues, errorCount: rep.errorCount, warnCount: rep.warnCount, site: r.site || null }); 829: return pages; 832:async function runRenderedAccessibilityCheck(html, url, fetchCss, onProgress) { 836: return { 837: issues: [], 857: return toolReport(html, url, builtByScripts, keyboard, fetchCss, inner); 862:async function keyboardCheck(url) { 864: if (!r.ok || (r.status && (r.status < 200 || r.status >= 300)) || nearlyEmpty(r.html) || botCheck(r.html)) return null; 866: return r.keyboard || null; 869:module.exports = { runRenderedAccessibilityCheck, renderWithScripts, keyboardCheck, innerReports, nearlyEmpty, botCheck }; === RENDERED AUDIT: FIRST 240 LINES === 1 // Accessibility check for the full audit, drawn the way a browser draws it. 2 // 3 // The Accessibility page tool (public/accessibility-audit.html) runs its checks on the page as Chrome 4 // draws it with the site's own CSS: it measures colour contrast and control edges, and it knows which 5 // text the CSS hides. lib/accessibility-checker.js reads the raw HTML only. This module runs the page 6 // tool's own check (checkA11y) in one shared headless Chrome on the HTML the full audit already fetched, 7 // so /api/audit-full gives customers the same answers as the page tool. Any failure (Chrome missing, 8 // time-out, crash) rejects, and the caller falls back to lib/accessibility-checker.js. 9 // 10 // Every page is first loaded with its own scripts running, in Chrome's sandbox, and a copy of the finished 11 // page is audited (renderWithScripts), so the audit sees what visitors see after the page loads (content, 12 // colours and banners added by scripts, and pages built entirely by JavaScript). A bot-check page is never 13 // run with scripts: a site that blocks checkers is respected, not worked around. If the page cannot be loaded 14 // that way (error status, bot-check, or it comes back almost empty), the fetched HTML is audited instead. 15 // 16 // Safety: Chrome runs with its sandbox ON (it runs other sites' scripts). Every request Chrome makes (stylesheets go through the app's own guarded fetch; images and 17 // fonts the audited page asks for) is checked first and blocked if it points at a private, loopback or 18 // link-local address, so an audited page cannot make the server call itself or its network. The tool 19 // page's own analytics are blocked so server-side runs are not counted as visits. 20 'use strict'; 21 const path = require('path'); 22 const fs = require('fs'); 23 const dns = require('dns').promises; 24 const net = require('net'); 25 const puppeteer = require('puppeteer'); 26 27 const TOOL = 'file://' + path.join(__dirname, '..', 'public', 'accessibility-audit.html'); 28 // The tool page keeps its checker and styles in its own files; they are added to the page from this server's copy 29 // after it opens (the page's own /accessibility-audit.js and .css links do not resolve from a file:// address). 30 const TOOL_JS = path.join(__dirname, '..', 'public', 'accessibility-audit.js'); 31 const TOOL_CSS = path.join(__dirname, '..', 'public', 'accessibility-audit.css'); 32 const MAX_PAGES = 3; // pages drawn at once; more wait their turn 33 const TIMEOUT_MS = 60000; // whole check (contrast + the axe-core checks), then the caller falls back to the raw-HTML check 34 // axe-core and its runner, asked for by the tool page's check frame at this address, are served from this 35 // server's own copy (public/a11y-axe/), so the full audit does not depend on fetching them over the network. 36 const AXE_URL = 'https://aiwebpageseo.com/a11y-axe/'; 37 const AXE_FILES = { 'axe.min.js': path.join(__dirname, '..', 'public', 'a11y-axe', 'axe.min.js'), 'run.js': path.join(__dirname, '..', 'public', 'a11y-axe', 'run.js') }; 38 const BLOCK_HOSTS = /(^|\.)(googletagmanager\.com|google-analytics\.com|analytics\.google\.com|doubleclick\.net|clarity\.ms|facebook\.net|hotjar\.com)$/i; 39 40 let browserP = null; 41 function getBrowser() { 42 if (!browserP) { 43 browserP = puppeteer.launch({ 44 headless: 'new', 45 executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined, 46 args: ['--disable-dev-shm-usage'] // sandbox on: other sites' scripts may run in it 47 }).then(b => { b.on('disconnected', () => { browserP = null; }); return b; }) 48 .catch(e => { browserP = null; throw e; }); 49 } 50 return browserP; 51 } 52 53 // A keyboard check photographs the page as focus moves, and Chrome only draws the front tab, so each keyboard check 54 // gets a Chrome of its own (closed when it ends) and up to KB_MAX run at once; more wait their turn. 55 const KB_MAX = 5; 56 let kbActive = 0; const kbWaiting = []; 57 function kbAcquire() { if (kbActive < KB_MAX) { kbActive++; return Promise.resolve(); } return new Promise(r => kbWaiting.push(r)); } 58 function kbRelease() { const next = kbWaiting.shift(); if (next) next(); else kbActive--; } 59 function ownBrowser() { return puppeteer.launch({ headless: 'new', executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined, args: ['--disable-dev-shm-usage'] }); } 60 61 // pages drawn at once 62 let active = 0; 63 const waiting = []; 64 function acquire() { if (active < MAX_PAGES) { active++; return Promise.resolve(); } return new Promise(r => waiting.push(r)); } 65 function release() { const next = waiting.shift(); if (next) next(); else active--; } 66 67 // private / loopback / link-local / shared / multicast / unspecified addresses 68 function privateIp(ip) { 69 if (net.isIPv4(ip)) { 70 const [a, b] = ip.split('.').map(Number); 71 return a === 0 || a === 10 || a === 127 || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31) || 72 (a === 192 && b === 168) || (a === 100 && b >= 64 && b <= 127) || a >= 224; 73 } 74 const v = ip.toLowerCase(); 75 if (v === '::' || v === '::1') return true; 76 const m = v.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/); if (m) return privateIp(m[1]); 77 return /^(fe[89ab]|f[cd]|ff)/.test(v); 78 } 79 const hostCache = new Map(); 80 async function publicHost(host) { 81 host = host.replace(/^\[|\]$/g, '').toLowerCase(); 82 if (host === 'localhost' || host.endsWith('.local') || host.endsWith('.internal')) return false; 83 if (net.isIP(host)) return !privateIp(host); 84 if (hostCache.has(host)) return hostCache.get(host); 85 let ok = false; 86 try { const addrs = await dns.lookup(host, { all: true }); ok = addrs.length > 0 && addrs.every(a => !privateIp(a.address)); } catch (e) { ok = false; } 87 hostCache.set(host, ok); if (hostCache.size > 2000) hostCache.clear(); 88 return ok; 89 } 90 91 // Same test as the page tool: almost no words and at most one link = the HTML is not the page visitors see. 92 // Bot-check pages (same rule as the page tool). Clear signs always mean a bot-check. Protection and form-captcha 93 // script names also appear on ordinary full pages, so alone they mean a bot-check only on a tiny page. 94 const BOT_SURE = /cf-chl-(?!widget)|cf_chl_opt|px-captcha|Pardon Our Interruption|Attention Required!? \| Cloudflare|