editoria11y-1.0.0-alpha8/editoria11y.module
editoria11y.module
<?php
/**
* @file
* Editoria11y module file.
*/
use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Link;
use Drupal\Core\Url;
/**
* Implements hook_ckeditor5_plugin_info_alter().
*/
function editoria11y_ckeditor5_plugin_info_alter(array &$plugin_definitions): void {
// Hook only called if CK5 is installed, so no PHP errors from broken use.
if (isset($plugin_definitions['ckeditor5_table'])) {
$config = Drupal::config('editoria11y.settings');
$header_config = $config->get('ck5_table_headers');
if (!$header_config || $header_config === 'none') {
return;
}
$headers = [];
if ($header_config === 'both') {
$headers['rows'] = 1;
$headers['columns'] = 1;
}
elseif ($header_config === 'row') {
$headers['rows'] = 1;
}
elseif ($header_config === 'column') {
$headers['columns'] = 1;
}
$table_plugin_definition = $plugin_definitions['ckeditor5_table']->toArray();
$table_plugin_definition['ckeditor5']['config']['table']['defaultHeadings'] = $headers;
$plugin_definitions['ckeditor5_table'] = new CKEditor5PluginDefinition($table_plugin_definition);
}
}
/**
* Implements hook_post_update().
*/
function editoria11y_hook_post_update(): void {
if (!Drupal::state()->get('editoria11y.show_help_message')) {
return;
}
$messenger = Drupal::messenger();
$config_url = Url::fromRoute('editoria11y.settings');
$config_link = Link::fromTextAndUrl(t('Editoria11y config'), $config_url)->toString();
$permissions_url = Url::fromRoute('user.admin_permissions');
$permissions_link = Link::fromTextAndUrl(t('confirm editor roles have permission'), $permissions_url)->toString();
$messenger->addStatus(
t('Please remember to set up @config for live editing, and @permissions to "View Editoria11y Checker"', [
'@config' => $config_link,
'@permissions' => $permissions_link,
]));
}
/**
* Implements template_preprocess_views_view()
*
* @param array $variables
* View settings.
*/
function editoria11y_preprocess_views_view(array &$variables): void {
$view = $variables['view'];
$views = ['editoria11y_results', 'editoria11y_pages', 'editoria11y_dismissals', 'editoria11y_export'];
if (in_array($view->id(), $views)) {
$variables['more']['#options']['attributes']['class'][] = 'button button--primary';
$apiUrl = Url::fromRoute('editoria11y.api_report')->toString();
if (Drupal::currentUser()->hasPermission('manage editoria11y results')) {
$sessionUrl = Url::fromRoute('system.csrftoken')->toString();
$variables['#attached']['drupalSettings']['editoria11y']['api_url'] = $apiUrl;
$variables['#attached']['drupalSettings']['editoria11y']['session_url'] = $sessionUrl;
$variables['#attached']['library'][] = 'editoria11y/editoria11y-admin';
$variables['#attached']['drupalSettings']['editoria11y']['admin'] = Drupal::currentUser()->hasPermission('administer editoria11y checker');
}
}
elseif ($view->id() == 'editoria11y_crawler') {
$variables['#attached']['library'][] = 'editoria11y_csa/editoria11y-csa-crawler';
}
}
/**
* Implements hook_entity_predelete().
*
* Removes relevant records from DB.
*/
function editoria11y_entity_predelete(EntityInterface $entity): void {
if (!$entity instanceof ContentEntityInterface) {
return;
}
$id = $entity->id();
$type = $entity->getEntityTypeId();
if (is_numeric($id) && $id > 0) {
// Get type from basetype.type.subtype pattern.
$connection = Drupal::database();
$connection->delete("editoria11y_dismissals")
->condition('entity_id', $id)
->condition('route_name', '%.' . $connection->escapeLike($type) . '.%', 'LIKE')
->execute();
$connection->delete("editoria11y_results")
->condition('entity_id', $id)
->condition('route_name', '%.' . $connection->escapeLike($type) . '.%', 'LIKE')
->execute();
}
}
/**
* Implements hook_page_attachments().
*
* Attaches Editoria11y library and config based on context.
*/
function editoria11y_page_attachments(array &$attachments): void {
$attach = [];
// Exit if user does not have "view" permission.
$attachments['#cache']['contexts'][] = 'user.permissions';
if (!Drupal::currentUser()->hasPermission('view editoria11y checker')) {
return;
}
// Determine which dismissals are permitted.
// "Ignore" is a global toggle, "mark OK" is per-user.
$attachments['#cache']['tags'][] = 'config:editoria11y.settings';
$config = Drupal::config('editoria11y.settings');
$db_version = $config->get('db_version');
if (empty($db_version) || $db_version !== '2') {
return;
}
// Confirm this is a "view" (not edit) route, and set last-changed time.
// @todo convert to standard Bool call once schema is updated
$sync = !$config->get('disable_sync');
$attachments['#cache']['contexts'][] = 'url';
$route_match = Drupal::routeMatch();
$route_name = $route_match->getRouteName();
if (str_starts_with($route_name, 'layout_builder.')) {
// Do not load library on layout builder routes.
return;
}
$request = Drupal::request();
// Get languages.
$attachments['#cache']['contexts'][] = 'languages';
$languageManager = \Drupal::languageManager();
$language = $languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();
/*$library = $language && $language !== 'en' ?
'editoria11y/editoria11y-localized' :
'editoria11y/editoria11y';*/
$library = 'editoria11y/editoria11y';
// @todo merge restore localization;
$attachments['#attached']['library'][] = $library;
$allow_ok = FALSE;
if ($sync) {
// Null or false unless both sync and permission are true.
$allow_ok = Drupal::currentUser()->hasPermission('mark as ok in editoria11y');
}
// Hiding can work via localStorage.
$allow_hide = Drupal::currentUser()->hasPermission('mark as hidden in editoria11y');
$view_reports = Drupal::currentUser()->hasPermission('manage editoria11y results');
$entity_type = FALSE;
$page_path = FALSE;
$entity_id = 0;
$changed = 0;
$dismissals = FALSE;
if ($route_name === 'entity.node.canonical') {
// We can expect nodes to have a $changed value for smart loading.
$changed = $route_match->getParameter('node')->changed->value;
// The NID.
$entity_id = $route_match->getParameter('node')->id();
}
elseif (str_starts_with($route_name, 'view.')) {
// Ignore PHPStorm str_starts_with suggestion; requires PHP 8
// Polite mode for views.
// @todo figure out how to get view and media nice names
$route_chunk = explode('.', $route_name);
$entity_type = ucfirst($route_chunk[0]) . ": " . ucfirst(str_replace("_", " ", $route_chunk[1]));
}
elseif (strrpos($route_name, 'preview', -1)) {
// We load previews in assertive mode AND disable reporting new issues.
$changed = Drupal::time()->getRequestTime();
$sync = $sync ? 'dismissals' : FALSE;
// $allow_ok = FALSE;
// $allow_hide = $allow_hide;
}
elseif (str_ends_with($route_name, '.add') ||
str_ends_with($route_name, '.add_form') ||
str_ends_with($route_name, '.form_add') ||
str_ends_with($route_name, '_create')
) {
// @todo media?
// Add: Node. Add_form: blocks, terms. Form_add: forms. Create: users.
// Disable reporting.
$sync = 'disable';
// Disable localStorage.
$dismissals = [];
}
elseif (!strrpos($route_name, 'canonical', -1)) {
// Disable reporting new issues on edit and revisions.
$sync = $sync ? 'dismissals' : FALSE;
// $allow_ok = FALSE;
// $allow_hide = $allow_hide;
if ($route_name === 'editoria11y.reports_dashboard') {
$attachments['#attached']['library'][] = 'editoria11y/editoria11y-admin';
}
}
if ($sync && $sync !== 'disable') {
$matched_node = FALSE;
$entity = FALSE;
// Get extra dashboard data if we plan to sync information.
if (($route = $route_match->getRouteObject()) && ($parameters = $route->getOption('parameters'))) {
// Determine if the current route represents an entity.
foreach ($parameters as $name => $options) {
if (isset($options['type']) && str_starts_with($options['type'], 'entity:')) {
$entity = $route_match->getParameter($name);
/* @noinspection PhpUndefinedFieldInspection */
if (($entity instanceof ContentEntityInterface) && $entity->hasLinkTemplate('canonical') && $entity->type && $entity->type->entity) {
/* @noinspection PhpUndefinedFieldInspection */
// This is an entity! Get its type label.
$entity_type = $entity->type->entity->label();
break;
}
}
}
// Get titles without service; it returns form name while editing.
if ($matched_node = Drupal::routeMatch()->getParameter('node')) {
$page_title = $matched_node->label();
}
elseif ($matched_term = Drupal::routeMatch()->getParameter('taxonomy_term')) {
$page_title = $matched_term->label();
// @todo used to return term label rather than bundle label.
// $entity_type = "Taxonomy: " . $matched_term->label();
$entity_type = $entity && method_exists($entity, 'bundle') ? "Taxonomy: " . $entity->bundle() : 'Taxonomy term';
// The TID.
$entity_id = $matched_term->id();
$page_path = Drupal::service('path_alias.manager')->getAliasByPath('/taxonomy/term/' . $entity_id, $language);
}
elseif ($matched_user = Drupal::routeMatch()->getParameter('user')) {
$page_title = $matched_user->label();
$entity_type = "Entity: User";
}
// Fall back to service for nonentities like admin routes.
if (empty($page_title)) {
$page_title = Drupal::service('title_resolver')->getTitle($request, $route);
}
// Render rich text titles.
if (is_array($page_title)) {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$page_title = Drupal::service('renderer')->renderInIsolation($page_title);
}
}
// No title available; use route name.
if (empty($page_title)) {
$page_title = $route_name;
}
if (!$entity_type) {
// Bundleless entities.
$route_chunk = explode('.', $route_name);
if (count($route_chunk) > 1) {
$entity_type = ucfirst($route_chunk[0]) . ": " . ucfirst(str_replace("_", " ", $route_chunk[1]));
}
else {
$entity_type = ucfirst($route_name);
}
}
// Build URI after filtering nonpreserved parameters
// $page_path = $request->getPathInfo();
if (!$page_path) {
// Attempt to get aliased path even while in edit mode.
if ($matched_node) {
$nid = $route_match->getParameter('node')->id();
if ($nid) {
$page_path = Drupal::service('path_alias.manager')->getAliasByPath('/node/' . $nid, $language);
$lang_check = Url::fromRoute('<current>')->toString();
if ($language &&
str_starts_with($lang_check, '/' . $language . '/') &&
!str_starts_with($page_path, '/' . $language . '/')) {
$page_path = '/' . $language . $page_path;
}
}
}
}
if (!$page_path) {
// If all else fails. This will not match in edit routes.
$page_path = Url::fromRoute('<current>')->toString();
}
if (!empty($config->get('preserve_params'))) {
// Based on core "request->normalizeQueryString()"
$query = $request->getQueryString();
if ('' !== $query) {
$parts = [];
$order = [];
$preserved_params = explode(',', $config->get('preserve_params') ?? '');
foreach (explode('&', $query ?? '') as $param) {
if ('' === $param || '=' === $param[0]) {
// Ignore useless delimiters, e.g. "x=y&".
// Also ignore pairs with empty key, even if there was a value, e.g.
// "=value", as such nameless values cannot be retrieved anyway.
// PHP also does not include them when building _GET.
continue;
}
$keyValuePair = explode('=', $param, 2);
if (in_array($keyValuePair[0], $preserved_params)) {
// GET parameters submitted from a HTML form, encode spaces
// as "+" by default
// (as defined in enctype application/x-www-form-urlencoded).
// PHP also converts "+" to spaces when filling the global _GET or
// when using the function parse_str. We use urldecode and
// then normalize to RFC 3986 with rawurlencode.
$parts[] = isset($keyValuePair[1]) ? rawurlencode(urldecode($keyValuePair[0])) . '=' . rawurlencode(urldecode($keyValuePair[1])) : rawurlencode(urldecode($keyValuePair[0]));
$order[] = urldecode($keyValuePair[0]);
}
}
if (!empty($parts)) {
array_multisort($order, SORT_ASC, $parts);
$page_path .= "?" . implode('&', $parts);
}
}
}
$page_path = substr($page_path, 0, 1000);
$attachments['#cache']['tags'][] = 'editoria11y:dismissals_' . preg_replace('/[^a-zA-Z0-9]/', '', substr($page_path, -80));
// Get all dismissals for this page. We want all of OK and some of ignore.
$results = Drupal::service('editoria11y.dismissals_on_page');
if ($sync === 'dismissals') {
$replace = '/\/edit$|\/preview$/';
$page_path = preg_replace($replace, '', $page_path);
}
$result = $results->getDismissals($page_path, $language);
$dismissals = [];
$pid = FALSE;
foreach ($result as $record) {
if (!$pid && $record->pid) {
$pid = $record->pid;
}
if ($record->dismissal_status === "ok") {
$dismissals[$record->result_key][$record->element_id] = "ok";
}
elseif ($record->dismissal_status === "hide" && $record->uid == Drupal::currentUser()->id()) {
$dismissals[$record->result_key][$record->element_id] = "hide";
}
}
$attach['pid'] = $pid;
$attach['page_title'] = $page_title;
$attach['allow_ok'] = $allow_ok;
$attach['view_reports'] = $view_reports;
$attach['dashboard_url'] = Url::fromRoute('editoria11y.reports_dashboard')->toString();
}
// @todo can we sync dismissals with the canonical URL? url is /node/1/edit
// and we have results for /node/1, so this should be possible.
$attach['allow_hide'] = $allow_hide;
$apiUrl = Url::fromRoute('editoria11y.api_report')->toString();
$sessionUrl = Url::fromRoute('system.csrftoken')->toString();
$attach['api_url'] = $apiUrl;
$attach['session_url'] = $sessionUrl;
$attach['lang'] = $language;
$attach['page_path'] = $page_path;
$attach['entity_type'] = substr($entity_type, 0, 32);
$attach['entity_id'] = $entity_id;
$attach['route_name'] = $route_name;
$attach['preserve_params'] = $config->get('preserve_params');
$attach['include_null_params'] = $config->get('include_null_params');
$attach['assertiveness'] = $config->get('assertiveness');
$attach['changed'] = $changed;
$attach['no_load'] = $config->get('no_load');
$attach['hide_edit_links'] = $config->get('hide_edit_links');
$attach['ignore_all_if_absent'] = $config->get('ignore_all_if_absent');
$attach['content_root'] = $config->get('content_root');
$attach['shadow_components'] = $config->get('shadow_components');
$ignore_test_config = $config->get('ignore_tests');
if (is_array($ignore_test_config)) {
$ignore_tests = [];
foreach ($ignore_test_config as $ignore_test) {
if ($ignore_test) {
$ignore_tests[] = $ignore_test;
}
}
$attach['ignore_tests'] = $ignore_tests;
}
$attach['detect_shadow'] = $config->get('detect_shadow');
$attach['ignore_elements'] = $config->get('ignore_elements');
$attach['panel_no_cover'] = $config->get('panel_no_cover');
$attach['panel_pin'] = $config->get('panel_pin');
$attach['embedded_content_warning'] = $config->get('embedded_content_warning');
$attach['hidden_handlers'] = $config->get('hidden_handlers');
$attach['live_h_inherit'] = $config->get('live_h_inherit');
$attach['live_h2'] = $config->get('live_h2');
$attach['live_h3'] = $config->get('live_h3');
$attach['live_h4'] = $config->get('live_h4');
$attach['disable_live'] = $config->get('disable_live');
$attach['download_links'] = $config->get('download_links');
$attach['link_strings_new_windows'] = $config->get('link_strings_new_windows');
$attach['ignore_link_strings'] = $config->get('ignore_link_strings');
$attach['link_ignore_selector'] = $config->get('link_ignore_selector');
$attach['sync'] = $sync;
$attach['watch_for_changes'] = $config->get('watch_for_changes');
$attach['dismissals'] = $dismissals;
$attach['theme'] = $config->get('ed11y_theme');
$attach['custom_tests'] = $config->get('custom_tests');
$attach['element_hides_overflow'] = $config->get('element_hides_overflow');
/** @var \Drupal\Core\File\FileUrlGeneratorInterface $fileUrlGenerator */
$fileUrlGenerator = Drupal::service('file_url_generator');
$attach['css_url'] = $fileUrlGenerator
->generateString(Drupal::service('extension.list.module')->getPath('editoria11y'));
// Provide hook_editoria11y_alter_config.
\Drupal::moduleHandler()->invokeAll('editoria11y_alter_config', [&$attach]);
foreach ($attach as $key => $value) {
$attachments['#attached']['drupalSettings']['editoria11y'][$key] = $value;
}
}
