accessibility_scanner-8.x-1.0-alpha8/src/Plugin/CaptureUtility/AxeCoreCliCaptureUtility.php
src/Plugin/CaptureUtility/AxeCoreCliCaptureUtility.php
<?php
namespace Drupal\accessibility_scanner\Plugin\CaptureUtility;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\accessibility_scanner\Plugin\CaptureResponse\AxeCoreCliCaptureResponse;
use Drupal\web_page_archive\Plugin\ConfigurableCaptureUtilityBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Process\Process;
use Psr\Log\LoggerInterface;
/**
* The @axe-core/cli accessibility scanner capture utility.
*
* @CaptureUtility(
* id = "wpa_axecore_cli_capture",
* label = @Translation("@axe-core/cli - Accessibility Scanner", context = "Web Page Archive"),
* description = @Translation("Scans URLs for accessibility issues using @axe-core/cli.", context = "Web Page Archive")
* )
*/
class AxeCoreCliCaptureUtility extends ConfigurableCaptureUtilityBase {
use DependencySerializationTrait;
use MessengerTrait;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* Constructs a new AxeCoreCliCaptureUtility.
*
* @param array $configuration
* The plugin configuration, i.e. an array with configuration values keyed
* by configuration option name. The special key 'context' may be used to
* initialize the defined contexts by setting it to an array of context
* values keyed by context names.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param string $plugin_definition
* The plugin implementation definition.
* @param \Psr\Log\LoggerInterface $logger
* The logger service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, LoggerInterface $logger, ConfigFactoryInterface $config_factory, FileSystemInterface $file_system) {
$this->configFactory = $config_factory;
$this->fileSystem = $file_system;
parent::__construct($configuration, $plugin_id, $plugin_definition, $logger);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('logger.factory')->get('accessibility_scanner'),
$container->get('config.factory'),
$container->get('file_system')
);
}
/**
* Most recent response.
*
* @var string|null
*/
private $response = NULL;
/**
* {@inheritdoc}
*/
public function capture(array $data = []) {
// Attempt to validate the axe binary.
$this->checkValidAxeBinary();
// Retrieve the capture utility settings.
$global_settings = $this->configFactory->get('web_page_archive.settings')->get('system');
$capture_utility_settings = $this->configFactory->get('web_page_archive.wpa_axecore_cli_capture.settings')->get('system');
// Handle missing URLs.
if (!isset($data['url'])) {
throw new \Exception('Capture URL is required');
}
// Get relative path.
$filename = $this->getFileName($data, 'json');
$real_path = $this->fileSystem->realpath($filename);
$file_parts = pathinfo($real_path);
$command = [
$global_settings['node_path'],
$capture_utility_settings['axecore_cli_binary'],
'--tags',
implode(',', array_filter($this->configuration['guidelines'])),
$data['url'],
'--save',
$file_parts['basename'],
'--dir',
$file_parts['dirname'],
];
if (!empty($this->configuration['selectors_exclude'])) {
$command[] = '--exclude';
$command[] = $this->configuration['selectors_exclude'];
}
if (!empty($this->configuration['selectors_include'])) {
$command[] = '--include';
$command[] = $this->configuration['selectors_include'];
}
$process = new Process($command);
if (!empty($capture_utility_settings['node_modules_parent_path'])) {
$process->setWorkingDirectory($capture_utility_settings['node_modules_parent_path']);
}
$process->run();
if (!$process->isSuccessful()) {
throw new \Exception("Failed to execute @axe-core/cli binary." . $process->getErrorOutput());
}
$this->response = new AxeCoreCliCaptureResponse($filename, $data['url']);
return $this;
}
/**
* {@inheritdoc}
*/
public function getResponse() {
return $this->response;
}
/**
* Encodes selectors list.
*/
public function encodeSelectors($string) {
$eol = PHP_EOL;
$selectors = preg_split("/(${eol}|,)/", trim($string));
$list = [];
foreach ($selectors as $selector) {
$full_selector = trim($selector);
if (!empty($full_selector)) {
$list[] = $full_selector;
}
}
return implode(',', $list);
}
/**
* Decodes selectors list.
*/
public function decodeSelectors($string) {
$eol = PHP_EOL;
$selectors = preg_split("/(${eol}|,)/", trim($string));
$list = [];
foreach ($selectors as $selector) {
$full_selector = trim($selector);
if (!empty($full_selector)) {
$list[] = $full_selector;
}
}
return implode(PHP_EOL, $list);
}
/**
* Returns a list of valid guidelines.
*
* @var array[]
*/
public function getGuidelines() {
return [
'wcag2a' => $this->t('WCAG 2.0 Level A'),
'wcag2aa' => $this->t('WCAG 2.0 Level AA'),
'wcag21a' => $this->t('WCAG 2.1 Level A'),
'wcag21aa' => $this->t('WCAG 2.1 Level AA'),
'best-practice' => $this->t('Common accessibility best practices'),
'ACT' => $this->t('W3C approved Accessibility Conformance Testing rules'),
'section508' => $this->t('Old Section 508 rules'),
'experimental' => $this->t('Cutting-edge rules'),
];
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'guidelines' => $this->configFactory->get('web_page_archive.wpa_axecore_cli_capture.settings')->get('defaults.guidelines'),
'selectors_exclude' => '',
'selectors_include' => '',
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
try {
$this->checkValidAxeBinary();
}
catch (\Exception $e) {
$this->messenger()->addError($e->getMessage());
}
// Use the Form API to create fields. Each field should have corresponding
// entry in your config/module.schema.yml file.
$url = 'https://www.deque.com/axe/core-documentation/api-documentation/#axe-core-tags';
$label = $this->t('Axe-core tags documentation');
$axe_cli_documentation_link = $this->getFormDescriptionLinkFromUrl($url, $label);
$form['guidelines'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Guidelines'),
'#description' => $this->t('For details about guidelines read the @documentation_link.', ['@documentation_link' => $axe_cli_documentation_link]),
'#options' => $this->getGuidelines(),
'#default_value' => $this->configuration['guidelines'],
];
$form['selectors_exclude'] = [
'#type' => 'textarea',
'#title' => $this->t('CSS Selectors to Exclude'),
'#description' => $this->t('You can specify any CSS selectors you would like excluded from the scan. One per line. Leave blank if you do not wish to exclude any selectors.'),
'#default_value' => $this->decodeSelectors($this->configuration['selectors_exclude']),
];
$form['selectors_include'] = [
'#type' => 'textarea',
'#title' => $this->t('CSS Selectors to Include'),
'#description' => $this->t('You can specify CSS selectors if you only want to test specific areas of a page. One per line. Leave blank if you want to test the entire page.'),
'#default_value' => $this->decodeSelectors($this->configuration['selectors_include']),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::submitConfigurationForm($form, $form_state);
$this->configuration['guidelines'] = $form_state->getValue('guidelines');
$this->configuration['selectors_exclude'] = $this->encodeSelectors($form_state->getValue('selectors_exclude'));
$this->configuration['selectors_include'] = $this->encodeSelectors($form_state->getValue('selectors_include'));
}
/**
* Helper function to determine if the axe binary is specified and working.
*/
public function checkValidAxeBinary() {
$settings = $this->configFactory->get('web_page_archive.wpa_axecore_cli_capture.settings')->get('system');
if (empty($settings['axecore_cli_binary'])) {
throw new \Exception('No @axe-core/cli binary provided.');
}
if (!file_exists($settings['axecore_cli_binary'])) {
throw new \Exception('Provided @axe-core/cli binary does not exist. Please check the specified path is correct.');
}
if (!is_executable($settings['axecore_cli_binary'])) {
throw new \Exception('Provided @axe-core/cli binary is not executable. Please check permissions.');
}
if (!empty($settings['axecore_cli_binary_verify_checksum'])) {
$expected_checksum = $settings['axecore_cli_binary_checksum'];
$actual_checksum = md5_file($settings['axecore_cli_binary']);
if ($expected_checksum != $actual_checksum) {
throw new \Exception("Provided @axe-code/cli binary does not match expected checksum: ${expected_checksum}");
}
}
return $this;
}
/**
* {@inheritdoc}
*/
public function buildSystemSettingsForm(array &$form, FormStateInterface $form_state) {
$settings = $this->configFactory->get('web_page_archive.wpa_axecore_cli_capture.settings')->get('system');
$url = 'https://github.com/dequelabs/axe-core-npm/blob/develop/packages/cli/README.md';
$label = $this->t('@axe-core/cli README');
$axe_cli_details_link = $this->getFormDescriptionLinkFromUrl($url, $label);
$form['axecore_cli_binary'] = [
'#type' => 'textfield',
'#title' => $this->t('Path to @axe-core/cli binary.'),
'#description' => $this->t('You must install @axe-core/cli on your server. See @details_link for more information about how to install. Please be sure your path is correct as you will be allowing the web server to execute a binary directly on your system.', ['@details_link' => $axe_cli_details_link]),
'#default_value' => $settings['axecore_cli_binary'],
];
if (!empty($settings['axecore_cli_binary']) && file_exists($settings['axecore_cli_binary'])) {
$checksum = md5_file($settings['axecore_cli_binary']);
}
else {
$checksum = $this->t('n/a');
}
$form['current_checksum'] = [
'#type' => 'markup',
'#markup' => $this->t('Current Checksum: @checksum', ['@checksum' => $checksum]),
];
$form['axecore_cli_binary_verify_checksum'] = [
'#type' => 'checkbox',
'#title' => $this->t('Verify binary checksum before executing?'),
'#description' => $this->t('For security purposes, it is highly recommended that you verify the checksum of the @axe-core/cli binary prior to execution. Please note that if you ever update the binary, you will need to update the checksum accordingly.'),
'#default_value' => $settings['axecore_cli_binary_verify_checksum'],
];
$form['axecore_cli_binary_checksum'] = [
'#type' => 'textfield',
'#title' => $this->t('Expected checksum'),
'#description' => $this->t('Specify the expected checksum for the @axe-core/cli binary.'),
'#maxlength' => 32,
'#default_value' => $settings['axecore_cli_binary_checksum'],
];
$form['node_modules_parent_path'] = [
'#type' => 'textfield',
'#title' => $this->t('node_modules path'),
'#description' => $this->t('Path to the directory that contains the node_modules/ directory where @axe-core/cli has been installed. Leave empty if @axe-core/cli is installed globally. This is important to help axe find other dependencies.'),
'#default_value' => $settings['node_modules_parent_path'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function cleanupRevision($revision_id) {
AxeCoreCliCaptureResponse::cleanupRevision($revision_id);
}
}
