accessibility-8.x-1.x-dev/modules/accessibility_testswarm/accessibility_testswarm.module
modules/accessibility_testswarm/accessibility_testswarm.module
<?php
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\Config;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Utility\ThemeRegistry;
use Drupal\Core\Theme\ThemeSettings;
/**
* Implements hook_page_build().
*/
function accessibility_testswarm_page_build(&$page) {
$library_path = libraries_get_path('quail');
drupal_add_js(array('accessibility_testswarm' => array('quail_path' => $library_path .'quail/src/', 'path' => drupal_get_path('module', 'accessibility_testswarm'))), 'setting');
return $page;
}
function accessibility_testswarm_menu() {
$items = array();
$items['admin/config/accessibility/testswarm'] = array(
'title' => 'TestSwarm',
'description' => 'Configure paths for automated accessibility testing.',
'route_name' => 'accessibility_testswarm_admin_form',
);
return $items;
}
/**
* Implements hook_testswarm_tests();
*/
function accessibility_testswarm_testswarm_tests() {
if($paths = cache()->get('accessibility_testswarm:paths')) {
$paths = $paths->data;
}
else {
$config = Drupal::config('accessibility.accessibility_testswarm');
$paths = ($config->get('check_all')) ?
module_invoke_all('accessibility_testswarm_paths') :
explode("\n", $config->get('paths'));
array_walk($paths, 'trim');
cache()->set('accessibility_testswarm:paths', $paths);
}
if(!$paths || !count($paths)) {
return;
}
$library_path = libraries_get_path('quail');
$tests = array();
foreach($paths as $path) {
$tests[str_replace('/', '_', $path)] = array(
'js' => array(
$library_path . '/src/quail.js',
drupal_get_path('module', 'accessibility_testswarm') . '/js/accessibility_testswarm.js'
),
'description' => $path,
'module' => 'accessibility_testswarm',
'path' => $path,
'dependencies' => array(
array('testswarm', 'jquery.simulate'),
),
'permissions' => array('access content')
);
}
return $tests;
}
/**
* Implements hook_menu_alter().
*/
function accessibility_testswarm_menu_alter(&$items) {
$items['testswarm-tests/detail/%/tests/%']['page callback'] = 'accessibility_testswarm_test_details_tests';
$items['testswarm-tests/detail/%/tests/%']['file'] = 'accessibility_testswarm.pages.inc';
$items['testswarm-tests/detail/%/tests/%']['file path'] = drupal_get_path('module', 'accessibility_testswarm');
}
/**
* Implements hook_accessibility_testswarm_paths().
*/
function accessibility_testswarm_accessibility_testswarm_paths() {
module_load_include('inc', 'accessibility_testswarm');
$paths = module_invoke_all('menu');
$filters = _accessibility_testswarm_path_filters();
$absolute_paths = _accessibility_testswarm_path_values();
$test_paths = array('<front>');
foreach($paths as $path => $menu) {
if((isset($menu['theme callback']) && $menu['theme callback'] == 'ajax_base_page_theme') ||
(isset($menu['type']) && $menu['type'] == MENU_DEFAULT_LOCAL_TASK)) {
continue;
}
if(isset($absolute_paths[$path])) {
if(is_array($absolute_paths[$path])) {
$test_paths = array_merge($test_paths, $absolute_paths[$path]);
}
elseif($absolute_paths[$path]) {
$test_paths[] = $absolute_paths[$path];
}
}
elseif((!isset($menu['page arguments']) || !count($menu['page arguments'])) &&
strpos($path, '%') === FALSE && strpos($path, '{') === FALSE) {
//plain path, no arguments
$test_paths[] = $path;
}
else {
$path = explode('/', $path);
foreach($path as $key => $segment) {
if(isset($filters[$segment])) {
if(is_string($filters[$segment]) && function_exists($filters[$segment])) {
$path[$key] = $filters[$segment]();
}
else {
$path[$key] = $filters[$segment];
}
}
}
foreach($path as $segment) {
if(is_array($segment)) {
foreach($segment as $argument_key => $argument) {
$string = array();
$set = true;
foreach($path as $segment) {
if(is_array($segment)) {
if(!isset($segment[$argument_key])) {
$set = false;
}
$string[] = $segment[$argument_key];
}
else {
$string[] = $segment;
}
}
if($set && count($string)) {
$test_paths[] = implode('/', $string);
}
}
}
}
}
}
return array_unique($test_paths);
}
/**
* Implements hook_testswarm_test_alter().
* See patch in issue #.
*/
function accessibility_testswarm_testswarm_test_alter(&$called_tests) {
$request = Drupal::request();
if (!$request->get('caller') || !$request->get('token')) {
return;
}
$tests = $request->get('tests') ? $request->get('tests') : array();
$logs = $request->get('log') ? $request->get('log') : array();
// @to-do this is a terribly hackish way to get access to the test run ID
$query = \Drupal::database()->select('testswarm_test_run', 'd');
$query->addExpression('MAX(id)');
$test_run_id = $query->execute()->fetchField();
$test_run_id = ($test_run_id) ? $test_run_id : 0;
foreach($tests as $test) {
foreach($logs['default'][$test['name']] as $attempt) {
if($attempt['result'] == 'false') {
$data = $attempt['accessibility_testswarm'];
foreach($data as $item) {
$hook = (is_array($item['theme']['name']))
? array_pop($item['theme']['name'])
: $item['theme']['name'];
\Drupal::database()->insert('accessibility_testswarm_test_detail')
->fields(array(
'tri' => $test_run_id,
'test' => $attempt['message'],
'hook' => $hook,
'type' => $item['theme']['type'],
'theme_item' => $item['theme']['used'],
'element' => $item['element']
))
->execute();
}
}
}
}
}
/**
* Implements hook_theme_registry_alter().
*/
function accessibility_testswarm_theme_registry_alter(&$theme_registry) {
foreach ($theme_registry as $hook => $data) {
$theme_registry[$hook] = array(
'function' => 'accessibility_testswarm_catch_function',
'theme path' => $data['theme path'],
'variables' => array(),
'original_theme' => $data,
);
}
}
/**
* Theme callback function that sets up global variables for storing theme
* information.
*/
function accessibility_testswarm_catch_function() {
$trace = debug_backtrace(FALSE);
$hook = $trace[1]['args'][0];
if (sizeof($trace[1]['args']) > 1) {
$variables = $trace[1]['args'][1];
}
else {
$variables = array();
}
$key = md5(serialize($hook));
$meta = array(
'name' => $hook,
'process functions' => array(),
'preprocess functions' => array(),
'suggestions' => array(),
'variables' => $variables,
'suggested_hook' => '',
'template_file' => '',
'extension' => '',
'type' => '',
);
$return = accessibility_testswarm_theme_twin($hook, $variables, $meta);
if (!empty($return) && !is_array($return) && !is_object($return) && user_access('access devel information')) {
if (!in_array($hook, array('html_tag', 'options_none'))) {
$return = '<span class="-a11y-testswarm" data-theme-key="' . $key .'">' . $return .'</span>';
}
if ($meta['type'] == 'function') {
global $theme;
// If the function hasn't been overwritten by the current theme, add it
// as a suggestion.
if ("{$theme}_{$meta['suggested_hook']}()" != $meta['used']) {
$meta['suggestions'][] = $meta['suggested_hook'];
}
foreach ($meta['suggestions'] as $delta => $suggestion) {
$meta['suggestions'][$delta] = "{$theme}_{$suggestion}()";
}
}
else {
// If the template hasn't been overwritten by the theme, add it as a
// suggestion.
if (FALSE === strpos($meta['template_file'], path_to_theme() . '/')) {
$meta['suggestions'][] = $meta['suggested_hook'];
}
foreach ($meta['suggestions'] as $delta => $suggestion) {
$meta['suggestions'][$delta] = strtr($suggestion, '_', '-') . $meta['extension'];
}
}
$GLOBALS['accessibility_testswarm_theme_calls'][$key] = array(
'name' => $meta['name'],
'used' => ($meta['type'] == 'function') ? $meta['used'] : $meta['template_file'],
'type' => $meta['type'],
'candidates' => $meta['suggestions'],
'preprocessors' => $meta['preprocess functions'],
'processors' => $meta['process functions']
);
}
return $return;
}
/**
* Implements hook_page_alter().
*/
function accessibility_testswarm_page_alter(&$page) {
$page['#post_render'][] = 'accessibility_testswarm_post_process_page';
}
/**
* Page callback to inject theme indexes into Drupal.settings.
*/
function accessibility_testswarm_post_process_page($page, $elements) {
if (empty($_GET['testswarm-test'])) {
return $page;
}
if (!empty($GLOBALS['accessibility_testswarm_theme_calls']) && $_SERVER['REQUEST_METHOD'] != 'POST') {
$javascript = '<script type="text/javascript">jQuery.extend(Drupal.settings, {"accessibility_testswarm_theme" : ' . drupal_json_encode($GLOBALS['accessibility_testswarm_theme_calls']) . "});</script>\n";
$page = preg_replace('#</body>#', "\n$javascript\n</body>", $page, 1);
}
return $page;
}
/**
* A mirror of the theme() function that resets a theme's callback
* to the original and also sets additional meta data.
*/
function accessibility_testswarm_theme_twin($hook, $variables, &$meta) {
static $default_attributes;
// If called before all modules are loaded, we do not necessarily have a full
// theme registry to work with, and therefore cannot process the theme
// request properly. See also _theme_load_registry().
if (!drupal_container()->get('module_handler')->isLoaded() && !defined('MAINTENANCE_MODE')) {
throw new Exception(t('theme() may not be called until all modules are loaded.'));
}
$hooks = theme_get_registry(FALSE);
// If an array of hook candidates were passed, use the first one that has an
// implementation.
if (is_array($hook)) {
foreach ($hook as $candidate) {
if (isset($hooks[$candidate])) {
break;
}
}
$hook = $candidate;
}
// Save the original theme hook, so it can be supplied to theme variable
// preprocess callbacks.
$original_hook = $hook;
// If there's no implementation, check for more generic fallbacks. If there's
// still no implementation, log an error and return an empty string.
if (!isset($hooks[$hook])) {
// Iteratively strip everything after the last '__' delimiter, until an
// implementation is found.
while ($pos = strrpos($hook, '__')) {
$hook = substr($hook, 0, $pos);
if (isset($hooks[$hook])) {
break;
}
}
if (!isset($hooks[$hook])) {
// Only log a message when not trying theme suggestions ($hook being an
// array).
if (!isset($candidate)) {
watchdog('theme', 'Theme hook %hook not found.', array('%hook' => $hook), WATCHDOG_WARNING);
}
return '';
}
}
$info = $hooks[$hook]['original_theme'];
global $theme_path;
$temp = $theme_path;
// point path_to_theme() to the currently used theme path:
$theme_path = $info['theme path'];
// Include a file if the theme function or variable processor is held
// elsewhere.
if (!empty($info['includes'])) {
foreach ($info['includes'] as $include_file) {
include_once DRUPAL_ROOT . '/' . $include_file;
}
}
// If a renderable array is passed as $variables, then set $variables to
// the arguments expected by the theme function.
if (isset($variables['#theme']) || isset($variables['#theme_wrappers'])) {
$element = $variables;
$variables = array();
if (isset($info['variables'])) {
foreach (array_keys($info['variables']) as $name) {
if (isset($element["#$name"])) {
$variables[$name] = $element["#$name"];
}
}
}
else {
$variables[$info['render element']] = $element;
// Give a hint to render engines to prevent infinite recursion.
$variables[$info['render element']]['#render_children'] = TRUE;
}
}
// Merge in argument defaults.
if (!empty($info['variables'])) {
$variables += $info['variables'];
}
elseif (!empty($info['render element'])) {
$variables += array($info['render element'] => array());
}
// Supply original caller info.
$variables += array(
'theme_hook_original' => $original_hook,
);
// Invoke the variable processors, if any. The processors may specify
// alternate suggestions for which hook's template/function to use. If the
// hook is a suggestion of a base hook, invoke the variable processors of
// the base hook, but retain the suggestion as a high priority suggestion to
// be used unless overridden by a variable processor function.
if (isset($info['base hook'])) {
$base_hook = $info['base hook'];
$base_hook_info = $hooks[$base_hook];
// Include files required by the base hook, since its variable processors
// might reside there.
if (!empty($base_hook_info['includes'])) {
foreach ($base_hook_info['includes'] as $include_file) {
include_once DRUPAL_ROOT . '/' . $include_file;
}
}
if (isset($base_hook_info['preprocess functions']) || isset($base_hook_info['process functions'])) {
$variables['theme_hook_suggestion'] = $hook;
$hook = $base_hook;
$info = $base_hook_info;
}
}
if (isset($info['preprocess functions']) || isset($info['process functions'])) {
$variables['theme_hook_suggestions'] = array();
foreach (array('preprocess functions', 'process functions') as $phase) {
if (!empty($info[$phase])) {
foreach ($info[$phase] as $processor_function) {
if (function_exists($processor_function)) {
// We don't want a poorly behaved process function changing $hook.
$hook_clone = $hook;
$processor_function($variables, $hook_clone, $info);
}
}
}
}
// If the preprocess/process functions specified hook suggestions, and the
// suggestion exists in the theme registry, use it instead of the hook that
// theme() was called with. This allows the preprocess/process step to
// route to a more specific theme hook. For example, a function may call
// theme('node', ...), but a preprocess function can add 'node__article' as
// a suggestion, enabling a theme to have an alternate template file for
// article nodes. Suggestions are checked in the following order:
// - The 'theme_hook_suggestion' variable is checked first. It overrides
// all others.
// - The 'theme_hook_suggestions' variable is checked in FILO order, so the
// last suggestion added to the array takes precedence over suggestions
// added earlier.
$suggestions = array();
if (!empty($variables['theme_hook_suggestions'])) {
$suggestions = $variables['theme_hook_suggestions'];
}
if (!empty($variables['theme_hook_suggestion'])) {
$suggestions[] = $variables['theme_hook_suggestion'];
}
foreach (array_reverse($suggestions) as $suggestion) {
if (isset($hooks[$suggestion])) {
$info = $hooks[$suggestion];
break;
}
}
}
// Generate the output using either a function or a template.
$output = '';
if (isset($info['function'])) {
$meta['type'] = 'function';
$meta['used'] = $info['function'] . '()';
if (function_exists($info['function'])) {
$output = $info['function']($variables);
}
}
else {
$meta['type'] = 'template';
// Default render function and extension.
$render_function = 'twig_render_template';
$extension = '.html.twig';
// The theme engine may use a different extension and a different renderer.
global $theme_engine;
if (isset($theme_engine)) {
if ($info['type'] != 'module') {
if (function_exists($theme_engine . '_render_template')) {
$render_function = $theme_engine . '_render_template';
}
$extension_function = $theme_engine . '_extension';
if (function_exists($extension_function)) {
$extension = $extension_function();
}
}
}
$meta['extension'] = $extension;
// In some cases, a template implementation may not have had
// template_preprocess() run (for example, if the default implementation is
// a function, but a template overrides that default implementation). In
// these cases, a template should still be able to expect to have access to
// the variables provided by template_preprocess(), so we add them here if
// they don't already exist. We don't want the overhead of running
// template_preprocess() twice, so we use the 'directory' variable to
// determine if it has already run, which while not completely intuitive,
// is reasonably safe, and allows us to save on the overhead of adding some
// new variable to track that.
if (!isset($variables['directory'])) {
$default_template_variables = array();
template_preprocess($default_template_variables, $hook);
$variables += $default_template_variables;
}
if (!isset($default_attributes)) {
$default_attributes = new Attribute();
}
foreach (array('attributes', 'title_attributes', 'content_attributes') as $key) {
if (isset($variables[$key]) && !($variables[$key] instanceof Attribute)) {
if ($variables[$key]) {
$variables[$key] = new Attribute($variables[$key]);
}
else {
// Create empty attributes.
$variables[$key] = clone $default_attributes;
}
}
}
// Render the output using the template file.
$template_file = $info['template'] . $extension;
$meta['used'] = $template_file;
if (isset($info['path'])) {
$template_file = $info['path'] . '/' . $template_file;
}
$meta['template_file'] = $template_file;
$output = $render_function($template_file, $variables);
}
// restore path_to_theme()
$theme_path = $temp;
return $output;
}