From 5d244e3444830b0dd1f37bd9e23113f9ba7c330e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Costa=20Silva?= <1574795+joaocsilva@users.noreply.github.com> Date: Thu, 23 May 2024 18:48:41 +0200 Subject: [PATCH] DQA-9426: Create toolkit command to run AXE Scanner (#774) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: J. João Santos --- .gitignore | 3 + config/runner/toolkit-test.yml | 12 ++ src/TaskRunner/Commands/AxeCommands.php | 168 +++++++++++++++++++++++ src/TaskRunner/Commands/ToolCommands.php | 25 ++-- 4 files changed, 199 insertions(+), 9 deletions(-) create mode 100644 src/TaskRunner/Commands/AxeCommands.php diff --git a/.gitignore b/.gitignore index ed1fa6b47..0bbc3b3b5 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ docs/* !docs/guide/ docs/guide/* !docs/guide/*.rst + +# Exclude axe-scan related files. +/axe* diff --git a/config/runner/toolkit-test.yml b/config/runner/toolkit-test.yml index 72d3fbb13..bfd55fea1 100644 --- a/config/runner/toolkit-test.yml +++ b/config/runner/toolkit-test.yml @@ -102,3 +102,15 @@ toolkit: extensions: [ 'php', 'module', 'inc', 'theme', 'install' ] exclude: [ '${toolkit.build.dist.root}/', '.cache/', 'vendor/', 'web/' ] options: '' + axe-scan: + config: axe-scan.config.json + urls: + - '/' + file-path: axe-scan-urls.txt + result-types: [ 'incomplete', 'violations' ] + core-tags: [ 'wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa' ] + locale: en + result-file: axe-scan-results.csv + allow-list: axe-scan-allowlist.csv + run-summary: true + summary-result-file: axe-scan-summary.csv diff --git a/src/TaskRunner/Commands/AxeCommands.php b/src/TaskRunner/Commands/AxeCommands.php new file mode 100644 index 000000000..39adb142e --- /dev/null +++ b/src/TaskRunner/Commands/AxeCommands.php @@ -0,0 +1,168 @@ +taskExec($this->getBin('run'))->arg('toolkit:setup-axe-scan'); + + $config = $this->getConfigValue('toolkit.axe-scan'); + $exec = $this->taskExec($this->getNodeBinPath('axe-scan')) + ->arg('run') + ->option('file', $config['file-path']); + + if (!empty($config['allow-list']) && file_exists($config['allow-list'])) { + $exec->option('allowlist', $config['allow-list']); + } + if (!empty($config['result-file'])) { + $exec->rawArg('> ' . $config['result-file']); + } + $tasks[] = $exec; + + if (!empty($config['run-summary'])) { + $tasks[] = $this->taskExec($this->getBin('run'))->arg('toolkit:run-axe-scan-summary'); + } + + return $this->collectionBuilder()->addTaskList($tasks); + } + + /** + * Run the axe-scan summary. + * + * @command toolkit:run-axe-scan-summary + * + * @aliases tk-axe-sum + */ + public function toolkitRunAxeScanSummary() + { + $config = $this->getConfigValue('toolkit.axe-scan'); + + $exec = $this->taskExec($this->getNodeBinPath('axe-scan')) + ->arg('summary'); + + if (!empty($config['allow-list']) && file_exists($config['allow-list'])) { + $exec->option('allowlist', $config['allow-list']); + } + if (!empty($config['summary-result-file'])) { + $exec->rawArg('> ' . $config['summary-result-file']); + } + return $this->collectionBuilder()->addTask($exec); + } + + /** + * Make sure axe-scan is installed and properly configured. + * + * @command toolkit:setup-axe-scan + */ + public function toolkitSetupAxeScan() + { + $tasks = []; + + // Install dependencies if the bin is not present. + if (!file_exists($this->getNodeBinPath('axe-scan'))) { + $tasks[] = $this->taskExecStack() + ->exec('npm -v || npm i npm') + ->exec('[ -f package.json ] || npm init -y --scope') + ->exec('npm list axe-scan && npm update axe-scan || npm install axe-scan -y'); + } + + // Install linux dependencies. + $tasks[] = $this->taskExec($this->getBin('run')) + ->arg('toolkit:install-dependencies') + ->option('packages', implode(',', $this->dependencies)); + + $config = $this->getConfigValue('toolkit.axe-scan'); + + // Generate the URLs file. + $baseUrl = $this->getConfigValue('drupal.base_url'); + $urls = array_map(function ($url) use ($baseUrl) { + return rtrim($baseUrl, '/') . '/' . ltrim($url, '/'); + }, $config['urls']); + $tasks[] = $this->taskWriteToFile($config['file-path']) + ->text(implode(PHP_EOL, $urls)); + + // Generate the config file. + $data = [ + 'axeCoreTags' => $config['core-tags'], + 'resultTypes' => $config['result-types'], + 'filePath' => $config['file-path'], + 'locale' => $config['locale'], + ]; + $tasks[] = $this->taskWriteToFile($config['config']) + ->text(json_encode($data, JSON_PRETTY_PRINT) . PHP_EOL); + + // Apply temporary patch to axe-scan when starting puppeteer to have the + // option --no-sandbox, this avoids the error: Running as root without + // --no-sandbox is not supported. + $files = [ + 'node_modules/axe-scan/build/src/commands/run.js', + 'node_modules/axe-scan/build/src/commands/summary.js', + ]; + $from = 'const browser = await puppeteer.launch();'; + $args = '["--no-sandbox", "--disable-setuid-sandbox", "--single-process", "--disable-impl-side-painting", "--disable-gpu-sandbox", "--disable-accelerated-2d-canvas", "--disable-accelerated-jpeg-decoding", "--disable-dev-shm-usage"]'; + $to = 'const browser = await puppeteer.launch({args: ' . $args . '});'; + foreach ($files as $file) { + if (file_exists($file)) { + $tasks[] = $this->taskReplaceInFile($file)->from($from)->to($to); + } + } + + // Make sure puppeteer is installed. + if (file_exists('node_modules/puppeteer/install.mjs')) { + $tasks[] = $this->taskExec('node node_modules/puppeteer/install.mjs'); + } + + return $this->collectionBuilder()->addTaskList($tasks); + } + +} diff --git a/src/TaskRunner/Commands/ToolCommands.php b/src/TaskRunner/Commands/ToolCommands.php index 60b5316d3..554360ea4 100644 --- a/src/TaskRunner/Commands/ToolCommands.php +++ b/src/TaskRunner/Commands/ToolCommands.php @@ -594,16 +594,18 @@ public static function formatBytes($bytes, $precision = 2) } /** - * Install packages present in the opts.yml file under extra_pkgs section. + * Install packages present in the .opts.yml file under extra_pkgs section. * * @command toolkit:install-dependencies * - * @option print Shows output from apt commands. + * @option packages Specify a list of packages to install instead of read from .opts.yml. + * @option print Shows output from apt commands. * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ public function toolkitInstallDependencies(ConsoleIO $io, array $options = [ + 'packages' => InputOption::VALUE_REQUIRED, 'print' => InputOption::VALUE_NONE, ]) { @@ -611,13 +613,18 @@ public function toolkitInstallDependencies(ConsoleIO $io, array $options = [ if (!$this->getConfig()->get('toolkit.install_dependencies')) { return $return; } - if (!file_exists('.opts.yml')) { - return $return; - } - $opts = Yaml::parseFile('.opts.yml'); - $packages = $opts['extra_pkgs'] ?? []; - if (empty($packages)) { - return $return; + + if (empty($options['packages'])) { + if (!($opts = self::parseOptsYml())) { + return $return; + } + $packages = $opts['extra_pkgs'] ?? []; + if (empty($packages)) { + return $return; + } + } else { + Toolkit::ensureArray($options['packages']); + $packages = $options['packages']; } $io->title('Installing dependencies');