From db48160a823d0fd52553ea7226375381b27e4777 Mon Sep 17 00:00:00 2001 From: j-mendez Date: Thu, 18 May 2023 10:05:05 -0400 Subject: [PATCH] feat(extension): add pre-compiled browser extension --- .gitignore | 7 +- README.md | 16 ++++- kayle/.gitignore | 1 - kayle/build-extension.ts | 86 ++++++++++++++++++++++++ kayle/lib/config.ts | 2 + kayle/lib/kayle.ts | 58 +++++++++++++--- kayle/lib/option.ts | 12 ++-- kayle/package.json | 6 +- kayle/tests/basic-axe-playwright.spec.ts | 2 +- kayle/tests/extension.ts | 18 +---- 10 files changed, 168 insertions(+), 40 deletions(-) create mode 100644 kayle/build-extension.ts diff --git a/.gitignore b/.gitignore index 008bfa00..275d9851 100644 --- a/.gitignore +++ b/.gitignore @@ -58,5 +58,8 @@ kayle_innate/package.json kayle_innate/dll kayle/screenshot.png -# testing -chrome-extension \ No newline at end of file +# custom built +chrome-extension + +# build custom extension +build-extension.js \ No newline at end of file diff --git a/README.md b/README.md index 67f3d9df..b8cc4b4b 100644 --- a/README.md +++ b/README.md @@ -210,9 +210,21 @@ One of the main goals was to have the audit run quickly since we noticed some of complete. The performance increases we made to the project were not only done at edge cases that would scale beyond make the ability of auditing at the MS level for almost any website. Right now, the project is moving forward based on performance and accuracy for ensuring minimal to no false positives. -## Extension +## Browser Extension -We are packaging an extension that allows the script to be pre-loaded into the browser for the crawl. +If you want to compile a chrome extension for preloading scripts without needing to worry about bandwidth cost use the following to generate a custom extension to use. + +First build the extension with the command: + +1. `yarn build:extension` + +Copy the contents into your directory to load using chromes `--load-extension` and enable the flag `--extensions-on-chrome-urls`. + +View the [extension-test](kayle/tests/extension.ts) for an example on how to setup chrome with the generated extension. + +Currently we only have english support for extension. We can add different locales for the generated scripts by manually adjusting the targets. + +If you want to test the extension use `yarn test:puppeteer:extension`. ## Discord diff --git a/kayle/.gitignore b/kayle/.gitignore index 73e19ec6..8725d527 100644 --- a/kayle/.gitignore +++ b/kayle/.gitignore @@ -9,5 +9,4 @@ _tests ./test-results _data -tests/chrome-extension ./screenshot.png \ No newline at end of file diff --git a/kayle/build-extension.ts b/kayle/build-extension.ts new file mode 100644 index 00000000..7fc9f616 --- /dev/null +++ b/kayle/build-extension.ts @@ -0,0 +1,86 @@ +// compiler the runners into a valid chrome extension +import { writeFileSync, mkdirSync, existsSync } from "fs"; +import { runnersJavascript } from "./build/runner-js"; +import { cwd } from "process"; +import { join } from "path"; + +const ext = join(cwd(), "chrome-extension"); + +// if the chrome directory does not exist create +if (!existsSync(ext)) { + mkdirSync(ext); +} + +const extensionRunner = runnersJavascript["kayle"]; + +// load basic extensions - TODO: allow creating extensions from languages +const extensionAxe = `function ar() { + ${runnersJavascript["axe"]} +}`; + +const extensionHtmlcs = `function hr() { + ${runnersJavascript["htmlcs"]} +}`; + +// the parts that allow extension to run and send +const extensionRawEnd = ` +let axeLoaded = false; +let htmlcsLoaded = false; + +// receiving audit request +window.addEventListener("kayle_send", async (event) => { + for (const option of event.detail.options.runners) { + if (option === "axe" && !axeLoaded) { + ar() + axeLoaded = true; + } + if (option === "htmlcs" && !htmlcsLoaded) { + hr() + htmlcsLoaded = true; + } + } + + // send reqeust data of audit + window.dispatchEvent(new CustomEvent("kayle_receive", { + detail: { + name: 'kayle', + data: await window.__a11y.run(event.detail.options) + }, + })) +}); +`; + +writeFileSync( + `${ext}/content-script.js`, + `${extensionRunner}\n${extensionAxe}\n${extensionHtmlcs}\n${extensionRawEnd}` +); + +const extensionManifest = `{ + "name": "Kayle", + "version": "1.0.0", + "description": "A web accessibility extension that can perform full audits and fast", + "manifest_version": 2, + "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", + "permissions": [ "tabs" , "identity","http://localhost:9222/*"], + "browser_action": { + "default_title": "Kayle Accessibility", + "default_popup": "popup.html" + }, + "externally_connectable": { + "matches": ["http://*/*", "https://*/*"] + }, + "content_scripts": [ + { + "matches": [ + "http://*/*", + "https://*/*" + ], + "run_at": "document_start", + "js": [ + "content-script.js" + ] + } + ] +}`; + +writeFileSync(`${ext}/manifest.json`, extensionManifest); diff --git a/kayle/lib/config.ts b/kayle/lib/config.ts index d8cbad23..ddc424de 100644 --- a/kayle/lib/config.ts +++ b/kayle/lib/config.ts @@ -113,6 +113,8 @@ export type RunnerConfig = { language?: string; // prevent auto intercept request to prevent fetching resources. noIntercept?: boolean; + // extension only run if accesibility extensions loaded: Experimental. + _browserExtension?: boolean; }; // log singleton diff --git a/kayle/lib/kayle.ts b/kayle/lib/kayle.ts index b228517a..facf14c8 100644 --- a/kayle/lib/kayle.ts +++ b/kayle/lib/kayle.ts @@ -58,21 +58,26 @@ async function runActionsList(config: RunnerConfig) { // inject runners async function injectRunners(config: RunnerConfig) { - // load axe first to avoid conflictions axe indexed as first item in array when multiple items exist - return await Promise.all([ - config.page.evaluate(runnersJavascript["kayle"]), - config.page.evaluate(getRunner(config.language, config.runners[0])), - config.runners.length === 2 - ? config.page.evaluate(getRunner(config.language, config.runners[1])) - : undefined, - ]); + if (!config._browserExtension) { + return await Promise.all([ + config.page.evaluate(runnersJavascript["kayle"]), + config.page.evaluate(getRunner(config.language, config.runners[0])), + config.runners.length === 2 + ? config.page.evaluate(getRunner(config.language, config.runners[1])) + : undefined, + ]); + } } // perform audit async function audit(config: RunnerConfig): Promise { + // perform audit as extension + if (config._browserExtension) { + return await auditExtension(config); + } + return await config.page.evaluate( (runOptions) => { - // set top level app origin replicate if (runOptions.origin && window.origin === "null") { window.origin = runOptions.origin; } @@ -92,6 +97,41 @@ async function audit(config: RunnerConfig): Promise { ); } +// perform an audit using browser extension - only used if extension is configured on browser +export async function auditExtension(config: RunnerConfig): Promise { + return await config.page.evaluate( + (runOptions): Promise => { + return new Promise((resolve) => { + if (runOptions.origin && window.origin === "null") { + window.origin = runOptions.origin; + } + + window.addEventListener("kayle_receive", (event: CustomEvent) => + resolve(event.detail.data) + ); + + window.dispatchEvent( + new CustomEvent("kayle_send", { + detail: { + name: "kayle", + options: runOptions, + }, + }) + ); + }); + }, + { + hideElements: config.hideElements, + ignore: config.ignore || [], + rootElement: config.rootElement, + rules: config.rules || [], + runners: config.runners, + standard: config.standard, + origin: config.origin, + language: config.language, + } + ); +} /** * Run accessibility tests for page. * @param {Object} [config={}] config - Options to change the way tests run. diff --git a/kayle/lib/option.ts b/kayle/lib/option.ts index 7de52eed..1a6f2acb 100644 --- a/kayle/lib/option.ts +++ b/kayle/lib/option.ts @@ -1,9 +1,13 @@ // handle configuration for methods export function extractArgs(o) { const options = { - actions: o.actions || [], browser: o.browser, page: o.page, + timeout: o.timeout || 60000, + // private + _browserExtension: o._browserExtension, + // sent to browser + actions: o.actions || [], hideElements: o.hideElements, ignore: o.ignore || [], includeNotices: o.includeNotices, @@ -12,7 +16,6 @@ export function extractArgs(o) { rules: o.rules || [], runners: o.runners || ["htmlcs"], standard: o.standard || "WCAG2AA", - timeout: o.timeout || 60000, origin: o.origin, language: o.language || "en", }; @@ -39,11 +42,6 @@ export function extractArgs(o) { options.runners.push("htmlcs"); } - // swap axe position for conflictions on script eval order - if (options.runners.length === 2 && options.runners[1] === "axe") { - options.runners[1] = options.runners[0]; - options.runners[0] = "axe"; - } // todo: validate all options return options; } diff --git a/kayle/package.json b/kayle/package.json index 766e0382..81ce2b94 100644 --- a/kayle/package.json +++ b/kayle/package.json @@ -1,6 +1,6 @@ { "name": "kayle", - "version": "0.4.31", + "version": "0.4.32", "description": "Extremely fast accessibility evaluation for nodejs", "main": "./build/index.js", "keywords": [ @@ -18,6 +18,8 @@ "prepare": "tsc", "build": "tsc && yarn swc:dist", "compile:test": "yarn build && tsc --project tsconfig.test.json", + "compile:extension": "tsc build-extension.ts", + "build:extension": "yarn compile:extension && node build-extension.js", "lint": "eslint .", "fix": "prettier --write '**/*.{js,jsx,ts,tsx}'", "swc:dist": "npx swc --copy-files --config-file .swcrc ./build -d ./build", @@ -34,7 +36,7 @@ "test:playwright:axe": "npm run compile:test && npx playwright test ./tests/basic-axe-playwright.spec.ts", "test:puppeteer:wasm": "npm run compile:test && node _tests/tests/wasm.js", "test:puppeteer:automa": "npm run compile:test && node _tests/tests/automa.js", - "test:puppeteer:extension": "npm run compile:test && node _tests/tests/extension.js", + "test:puppeteer:extension": "npm run compile:test && yarn build:extension && node _tests/tests/extension.js", "test:lint": "node build/lint.js", "test:unit:unique-selector": "npm run compile:test && node _tests/tests/unit/unique-selector.js", "publish": "yarn prepare && yarn npm publish" diff --git a/kayle/tests/basic-axe-playwright.spec.ts b/kayle/tests/basic-axe-playwright.spec.ts index 8d7272ec..2ea26a1f 100644 --- a/kayle/tests/basic-axe-playwright.spec.ts +++ b/kayle/tests/basic-axe-playwright.spec.ts @@ -6,7 +6,7 @@ import { performance } from "perf_hooks"; import { test } from "@playwright/test"; test("fast_axecore audit drakeMock", async ({ page, browser }, testInfo) => { - page.on("console", (msg) => console.log("PAGE LOG:", msg.text())); + // page.on("console", (msg) => console.log("PAGE LOG:", msg.text())); const startTime = performance.now(); const results = await kayle({ page, diff --git a/kayle/tests/extension.ts b/kayle/tests/extension.ts index c9a1a36d..8e9ce111 100644 --- a/kayle/tests/extension.ts +++ b/kayle/tests/extension.ts @@ -29,10 +29,11 @@ import { performance } from "perf_hooks"; { page, browser, - runners: ["htmlcs"], + runners: ["htmlcs", "axe"], includeWarnings: true, html: jmendezMock, origin: "https://jeffmendez.com", // origin is the fake url in place of the raw content + _browserExtension: false, // enable the extension }, true ); @@ -46,20 +47,5 @@ import { performance } from "perf_hooks"; // valid list assert(Array.isArray(issues)); - // chrome extension adds script to execute - const data = await page.evaluate( - (runOptions) => { - // @ts-ignore injected after navigate - return window.__kayle.random(runOptions); - }, - { - origin: "https://jeffmendez.com", - } - ); - - console.log(data); - - await page.screenshot({ path: "./screenshot.png" }); - await browser.close(); })();