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