=== ACCESSIBILITY API ROUTES === 1500:app.get('/api/audit', requireSameOriginGet, ipRateLimit(30), ...requireTool('site_audit'), handleAudit); 1501:app.post('/api/audit', ipRateLimit(30), ...requireTool('site_audit'), handleAudit); 1504:app.post('/api/audit-free', requireSameOriginGet, freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), function(req, res) { 1540: var accessibilityChecker= require('./lib/accessibility-checker'); 1659: runSection('accessibility', function() { 1671: var a11yStill = function() { if (!ended) send({ type: 'progress', section: 'accessibility', status: 'running' }); }; 1678: try { p = require('./lib/a11y-rendered').runRenderedAccessibilityCheck(html, finalUrl || url, a11yFetchCss, a11yStill); } 1685: return accessibilityChecker.runAccessibilityCheck(html); 2086:app.get('/api/audit-full', requireSameOriginGet, ipRateLimit(15), sapAuditCacheCheck('site_audit_full'), ...requireTool('site_audit_full'), handleAuditFull); 2087:app.post('/api/audit-full', ipRateLimit(15), sapAuditCacheCheck('site_audit_full'), ...requireTool('site_audit_full'), handleAuditFull); 2124:app.get('/api/audit-full-free', requireSameOriginGet, freeProOnce, freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), _auditFullFree); 2125:app.post('/api/audit-full-free', freeProOnce, freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), _auditFullFree); 2164:app.get('/api/audit-results', requireAuth, async (req, res) => { 2183:app.get('/api/audit-results/:id', requireAuth, async (req, res) => { 2211:app.get('/api/audit-files', requireAuth, async (req, res) => { 2404:app.post('/api/audit-export', requireSameOriginGet, express.json({ limit: '4mb' }), requireAuth, async (req, res) => { // S71-CSRF-POST 2522:app.get('/api/audit-files/:id/download', requireAuth, async (req, res) => { 2562:app.get('/api/audit-history', requireAuth, async (req, res) => { 2805:app.post('/api/audit-full-brand', express.json({ limit: '256kb' }), ipRateLimit(15), sapAuditCacheCheck('site_audit_full_brand'), ...requireTool('site_audit_full_brand'), handleAuditFullBrand); 2806:app.get('/api/audit-full-brand', requireSameOriginGet, ipRateLimit(15), sapAuditCacheCheck('site_audit_full_brand'), ...requireTool('site_audit_full_brand'), handleAuditFullBrand); 2915:app.post('/api/audit-full-compare', ipRateLimit(15), sapAuditCacheCheck('site_audit_full_compare'), ...requireTool('site_audit_full_compare'), handleAuditFullCompare); 2916:app.get('/api/audit-full-compare', requireSameOriginGet, ipRateLimit(15), sapAuditCacheCheck('site_audit_full_compare'), ...requireTool('site_audit_full_compare'), handleAuditFullCompare); 2917:app.post('/api/audit-full-brand-compare', express.json({ limit: '256kb' }), ipRateLimit(15), sapAuditCacheCheck('site_audit_full_brand_compare'), ...requireTool('site_audit_full_brand_compare'), handleAuditFullBrandCompare); 2918:app.get('/api/audit-full-brand-compare', requireSameOriginGet, ipRateLimit(15), sapAuditCacheCheck('site_audit_full_brand_compare'), ...requireTool('site_audit_full_brand_compare'), handleAuditFullBrandCompare); 2988:app.post('/api/pagespeed-free', requireSameOriginGet, freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), function(req, res) { 3000:app.post('/api/core-web-vitals-free', freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), handlePagespeed); 3128:// Loads the page in the sandboxed Chrome of lib/a11y-rendered.js with its own scripts running and returns a 3136: require('./lib/a11y-rendered').renderWithScripts(url) 3143:app.get('/api/renderedhtml', requireAuth, ipRateLimit(20, 60000, 'renderedhtml'), handleRenderedHtml); 3144:app.post('/api/renderedhtml', requireAuth, ipRateLimit(20, 60000, 'renderedhtml'), handleRenderedHtml); 3153: require('./lib/a11y-rendered').keyboardCheck(url) 3157:app.get('/api/a11ykeyboard', requireAuth, ipRateLimit(10, 60000, 'a11ykeyboard'), handleA11yKeyboard); 3158:app.post('/api/a11ykeyboard', requireAuth, ipRateLimit(10, 60000, 'a11ykeyboard'), handleA11yKeyboard); 3175: require('./lib/a11y-rendered').innerReports(url, inner, fetchCss) 3179:app.post('/api/a11ypages', requireAuth, ipRateLimit(10, 60000, 'a11ypages'), handleA11yPages); 3183:app.post('/api/a11yqueue', requireAuth, a11yQueue.handler); 3209: app.post('/api/audit-pdf/request', ipRateLimit(10, 60000, 'audit-pdf'), async function(req, res) { 3238: app.get('/api/audit-pdf/verify/:token', async function(req, res) { 3249: app.get('/api/audit-pdf/download/:token', async function(req, res) { 3266:app.get('/api/rawhtml-free', requireSameOriginGet, freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), handleRawHtml); 3267:app.post('/api/rawhtml-free', freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), handleRawHtml); 3683:app.get('/api/robots-sitemap-free', requireSameOriginGet, freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), handleRobotsSitemap); 3684:app.post('/api/robots-sitemap-free', freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), handleRobotsSitemap); 3862:app.get('/api/redirect-check-free', requireSameOriginGet, freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), handleRedirectCheck); 3863:app.post('/api/redirect-check-free', freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), handleRedirectCheck); 3916:app.get('/api/hreflang-free', requireSameOriginGet, freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), handleHreflang); 3917:app.post('/api/hreflang-free', freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), handleHreflang); 4352:app.post('/api/aeo-free', requireSameOriginGet, freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), function(req, res) { 4694:app.post('/api/eeat-free', requireSameOriginGet, freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), function(req, res) { 4889:app.post('/api/readability-free', requireSameOriginGet, freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), function(req, res) { 5296:app.post('/api/keywords-free', requireSameOriginGet, freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), function(req, res) { 8732:app.post('/api/schema-generate-free', requireSameOriginGet, freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), (req, res) => { 11579:app.get('/api/gdpr-audit', requireSameOriginGet, ipRateLimit(20), ...requireTool('gdpr_audit_free'), gdprAuditHandler); 11582:app.get('/api/gdpr-audit-free', requireSameOriginGet, freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), function(req, res) { 12003: // Skip-to-content links: accessibility anchors (href="#...") that aren't page 12014: // the exact standard accessibility phrases only — these are never content. 12589:app.post('/api/markdown-generator-free', requireSameOriginGet, freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), function(req, res) { 13051:app.get('/api/llms-audit', requireSameOriginGet, ipRateLimit(20), ...requireTool('llms_audit'), llmsAuditHandler); 13056:app.get('/api/llms-audit-free', requireSameOriginGet, freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), function(req, res) { 14265:app.get('/api/trust-score-free', requireSameOriginGet, freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), handleTrustScore); 14266:app.post('/api/trust-score-free', freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), handleTrustScore); 14735:app.get('/api/spam-audit-free-left', function (req, res) { 14742:app.post('/api/spam-audit-free', spamAuditPoolLimit, spamAuditLimit, freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), handleSpamAudit); 14744:app.post('/api/spam-audit', ipRateLimit(30), ...requireSpamAudit(), (req, res) => handleSpamAudit(req, res, true)); 14748:app.post('/api/spam-audit-expired', ipRateLimit(20, 3600000, 'spam_expired'), async function (req, res) { 14764:app.post('/api/spam-audit-email-check', ipRateLimit(60, 3600000, 'spam_mailchk'), async function (req, res) { 14769:app.post('/api/spam-audit-email', ipRateLimit(10, 3600000, 'spam_mail'), async function (req, res) { 14820:app.post('/api/maps-audit', attachUser, requireAuth, proxyToSubscriptions); 14831:app.post('/api/maps-audit-free', freeRunLimit, proxyToSubscriptions); 14834:app.post('/api/http-error-audit', attachUser, requireAuth, proxyToSubscriptions); 14835:app.post('/api/http-error-audit-free', freeRunLimit, proxyToSubscriptions); 14839:app.post('/api/gbp', ...requireTool('gbp_audit'), async function(req, res) { 14896:app.post('/api/maps-audit', ...requireTool('maps_audit'), async function(req, res) { 15112:app.post('/api/crawl-free', freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), proxyToSubscriptions); 15113:app.post('/api/security-audit-free', freeRunLimit, ipRateLimit(100, 86400000, 'free_audits'), proxyToSubscriptions); 15482: app.post(`/api/app-audit/buy/${bundleKey}`, ...requireTool(bundleKey), async (req, res) => { 15542: app.get('/api/app-audit/list', requireAuth, async (req, res) => { 15561: app.get('/api/app-audit/:id/runs', requireAuth, async (req, res) => { 15587: app.get('/api/app-audit/:id/diff', requireAuth, async (req, res) => { 15612: app.post('/api/app-audit/:id/cancel', requireAuth, async (req, res) => { 16895: app.post(`/api/migration-audit/buy/${bundleKey}`, ...requireTool(bundleKey), async (req, res) => { 16937: app.get('/api/migration-audit/list', requireAuth, async (req, res) => { 16956: app.get('/api/migration-audit/:id', requireAuth, async (req, res) => { === RENDERER EXPORTS === // axe-core and its runner, from this server's own copy, for the tool page's extra WCAG checks (see a11yAxe). let axeSrc = null; try { axeSrc = { axe: await fs.promises.readFile(AXE_FILES['axe.min.js'], 'utf8'), run: await fs.promises.readFile(AXE_FILES['run.js'], 'utf8'), spacing: await fs.promises.readFile(path.join(__dirname, '../public/a11y-axe/text-spacing.js'), 'utf8') }; } catch (e) { axeSrc = null; } const run = page.evaluate(async (h, base, byScripts, axeSrc, kb, inner) => { if (axeSrc) window.__a11yAxeSrc = axeSrc; window.a11yKeyboard = kb || null; window.a11yKeyboardP = null; window.a11yInner = inner || null; window.a11yInnerFixed = true; window.fetchRawHtml = (u, r) => window.__a11yFetch(u, r || null); window.a11yBuiltByScripts = byScripts; window.a11yLastReport = null; document.getElementById('a11y-input').value = h; checkA11y(base); const until = Date.now() + 58000; while (!window.a11yLastReport && Date.now() < until) await new Promise(r => setTimeout(r, 100)); return window.a11yLastReport; }, html, url || '', builtByScripts, axeSrc, keyboard, inner || null); const report = await Promise.race([run, new Promise((_, rej) => { timer = setTimeout(() => rej(new Error('rendered accessibility check timed out')), TIMEOUT_MS); })]); if (!report || !Array.isArray(report.issues)) throw new Error('rendered accessibility check returned no report'); report.rendered = true; return report; } finally { if (timer) clearTimeout(timer); if (page) await page.close().catch(() => {}); release(); } } // The two inner pages, each loaded as visitors see it and given the full report on its own. A page that cannot be // loaded that way (error status, bot-check, almost empty, a different site after redirects) is left out. // onProgress (optional) is called while they run, so a caller streaming results can show the audit is still working. async function innerReports(homeUrl, urls, fetchCss, onProgress) { const pages = []; let homeHost = ''; try { homeHost = new URL(homeUrl).hostname.replace(/^www\./, ''); } catch (e) { return pages; } const tick = onProgress ? setInterval(() => { try { onProgress(); } catch (e) {} }, 20000) : null; try { // up to 4 menu links are tried, until 2 pages are checked; a link that ends up on a login page after its redirects // (HSBC's "log on" link lands on /auth/?returnUrl=) is not a page to audit const SKIPRE = /\/(cart|basket|checkout|login|log-in|logon|signin|sign-in|logout|auth|oauth|sso|account|my-account|register|signup|sign-up|search|wp-admin|wp-login\.php|admin)(\/|\?|$)|[?&](returnurl|redirect|redirect_uri)=/i; for (const u of (urls || []).slice(0, 4)) { if (pages.length >= 2) break; try { if (new URL(u).hostname.replace(/^www\./, '') !== homeHost) continue; const r = await renderWithScripts(u, { keyboard: true }); if (!r.ok || (r.status && (r.status < 200 || r.status >= 300)) || nearlyEmpty(r.html) || botCheck(r.html)) continue; if (new URL(r.finalUrl || u).hostname.replace(/^www\./, '') !== homeHost || SKIPRE.test(r.finalUrl || '')) continue; const rep = await toolReport(r.html, r.finalUrl || u, true, r.keyboard || null, fetchCss, null); pages.push({ url: r.finalUrl || u, issues: rep.issues, errorCount: rep.errorCount, warnCount: rep.warnCount, site: r.site || null }); } catch (e) {} } } finally { if (tick) clearInterval(tick); } return pages; } async function runRenderedAccessibilityCheck(html, url, fetchCss, onProgress) { if (!html || typeof html !== 'string') throw new Error('html required'); // Standalone SVG is not an HTML page. Do not run HTML-only checks. if (/^\s*(?:<\?xml[^>]*>\s*)?(?:]*>\s*)?= 200 && r.status < 300)) && !nearlyEmpty(r.html) && !botCheck(r.html)) { html = r.html; url = r.finalUrl || url; builtByScripts = true; keyboard = r.keyboard || null; site = r.site || null; } } // two inner pages of the site, checked first so the home page's report can add what they found let inner = null; if (site && site.inner && site.inner.length) { const pages = await innerReports(url, site.inner, fetchCss, onProgress).catch(() => []); inner = { home: site, pages: pages, tried: Math.min(2, site.inner.length) }; } return toolReport(html, url, builtByScripts, keyboard, fetchCss, inner); } // The keyboard check alone, for the Accessibility page tool (/api/a11ykeyboard). null when the page could not be // loaded as visitors see it (error status, bot-check, almost empty): a check of the wrong page is not given. async function keyboardCheck(url) { const r = await renderWithScripts(url, { keyboard: true }); if (!r.ok || (r.status && (r.status < 200 || r.status >= 300)) || nearlyEmpty(r.html) || botCheck(r.html)) return null; if (r.keyboard) r.keyboard.site = r.site || null; return r.keyboard || null; } module.exports = { runRenderedAccessibilityCheck, renderWithScripts, keyboardCheck, innerReports, nearlyEmpty, botCheck }; === ACCESSIBILITY REQUEST HANDLING === 3:// lib/a11y-rendered.js loads accessibility-audit.html in headless Chrome and calls checkA11y() from this file 19:async function fetchRawHtml(url, referer) { 21: const r = await fetch(`${BACKEND_URL}/api/rawhtml`, { 38: const r = await fetch(`${BACKEND_URL}/api/renderedhtml`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ url }) }); 44:// Keyboard focus check of the live page (server: lib/a11y-rendered.js keyboardCheck). null when it could not run. 47: const r = await fetch(`${BACKEND_URL}/api/a11ypages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ url, inner }) }); 53:// Accessibility audit queue (server: lib/a11y-queue.js through /api/a11yqueue). The audit waits for its turn and 69: const r = await fetch(`${BACKEND_URL}/api/a11yqueue`, { method: 'POST', credentials: 'include', signal: ctl.signal }); 91: const r = await fetch(`${BACKEND_URL}/api/a11ykeyboard`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ url }) }); 122: var r = await fetch('/api/geo-grid/check-url', { 188: let d = await fetchRawHtml(url); 258: checkA11y(d.finalUrl || url); 458:// see lib/a11y-rendered.js); the drawn frame turns it back into shadow roots, the parsed page gets it written in place 1050:// once), or from window.__a11yAxeSrc when the server's check (lib/a11y-rendered.js) provides it from disk. 1058: const get = f => fetch(A11Y_AXE_BASE + f, { credentials: 'same-origin' }).then(r => r.ok && /javascript/i.test(r.headers.get('content-type') || '') ? r.text() : Promise.reject(new Error('HTTP ' + r.status))); 1175: // IKEA 72) and stays under the server's 100-a-minute limit on /api/rawhtml. 1180: if (typeof fetchRawHtml !== 'function') { failed.push(url); return Promise.resolve(''); } 1196: const ask = () => fetchRawHtml(url, ref || base).catch(() => null); 1264:function checkA11y(baseUrl) { 1940: // The finished report as data, for the server's full audit (lib/a11y-rendered.js), which runs this 2019: const r = await fetch(BACKEND_URL + '/api/plans', { credentials: 'include' }); 2043: const r = await fetch(BACKEND_URL + '/api/auth/me', { credentials: 'include' }); 2058: const r = await fetch(BACKEND_URL + '/api/tools/check', { 2067: const dr = await fetch(BACKEND_URL + '/api/tools/deduct', { 2075: fetch(BACKEND_URL + '/api/tools/log', { 2139: await fetch(BACKEND_URL + '/api/auth/logout', { method: 'POST', credentials: 'include' }); 2148: const dr = await fetch(BACKEND_URL + '/api/tools/deduct', { 2234: on('#a11y-paste-btn', 'click', function () { SEOGate.check('accessibility', function () { window.a11yBuiltByScripts = false; window.a11yKeyboard = null; window.a11yKeyboardP = null; checkA11y(); }, true); }); === LOCAL LISTENING PORTS === LISTEN 0 511 127.0.0.1:3003 0.0.0.0:* users:(("node /var/www/v",pid=2482903,fd=23)) LISTEN 0 511 127.0.0.1:3002 0.0.0.0:* users:(("node /var/www/v",pid=2660453,fd=23)) LISTEN 0 511 *:3001 *:* users:(("node /var/www/v",pid=2635408,fd=26)) LISTEN 0 511 *:3000 *:* users:(("node /var/www/v",pid=2635414,fd=26)) === PRODUCTION STATUS === Production files changed: NO PM2 restarted: NO