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); } }