editoria11y-1.0.0-alpha8/editoria11y.install
editoria11y.install
<?php
/**
* @file
* Editorially install file.
*/
use Drupal\user\Entity\Role;
use Drupal\views\Views;
use Symfony\Component\Yaml\Yaml;
/**
* Implements hook_install().
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
function editoria11y_install() {
// Assign default access to roles likely to have content editing access.
/** @var \Drupal\user\RoleInterface $role */
foreach (Role::loadMultiple() as $role) {
if ($role->hasPermission('view own unpublished content') ||
$role->hasPermission('access content overview') || $role->hasPermission('access in place editing')) {
$role->grantPermission('view editoria11y checker');
$role->grantPermission('mark as hidden in editoria11y');
$role->save();
}
}
// Display a help message once after it is installed:
\Drupal::state()->set('editoria11y.show_help_message', TRUE);
}
/**
* Pages table definition.
*/
function editoria11y_pages_table(): array {
return [
'description' => 'Pages with issues detected by Editoria11y',
'fields' => [
'pid' => [
'description' => 'Serial unique ID',
'type' => 'serial',
'size' => 'big',
'not null' => TRUE,
],
'entity_id' => [
'description' => 'The node, term or user id this record affects.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'entity_type' => [
'type' => 'varchar',
'not null' => TRUE,
'default' => 'node',
'length' => 32,
'description' => 'The entity type; "route" if no type found.',
],
'route_name' => [
'type' => 'varchar',
'not null' => TRUE,
'default' => 'unknown',
'length' => 255,
'description' => 'Route name for page.',
],
'page_path' => [
'type' => 'varchar',
'not null' => TRUE,
'default' => 'unknown',
'length' => 1024,
'description' => 'Internal, relative page path.',
],
'page_language' => [
'type' => 'varchar',
'not null' => TRUE,
'default' => 'unknown',
'length' => 64,
'description' => 'Active translation.',
],
'page_title' => [
'type' => 'varchar',
'not null' => TRUE,
'default' => 'unknown',
'length' => 1024,
'description' => 'The name of the route where this was last seen.',
],
'page_result_count' => [
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'The total number of issues on this page.',
],
'updated' => [
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'The Unix timestamp of the last update.',
],
],
'primary key' => [
'pid',
],
'unique_keys' => [
'ed11y' => [
'page_path',
'page_language',
],
],
'indexes' => [
'entity_type' => ['entity_type'],
'page_path' => ['page_path'],
'page_language' => ['page_language'],
'entity_id' => ['entity_id'],
],
];
}
/**
* Results table definition.
*/
function editoria11y_results_table(): array {
return [
'description' => 'Stores Editoria11y issue list',
'fields' => [
'id' => [
'description' => 'Test result',
'type' => 'serial',
'size' => 'big',
'not null' => TRUE,
],
'pid' => [
'description' => 'The ed11y page table record this affects',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'created' => [
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'The Unix timestamp of the first time this record was flagged.',
],
'result_name' => [
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => 'unknown',
'description' => 'The title of the test as reported by Editoria11y JS',
],
'result_key' => [
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => 'unknown',
'description' => 'The name of the test as reported by editoria11y JS',
],
'result_name_count' => [
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'The number of hits for this test type on this page',
],
],
'primary key' => [
'id',
],
'indexes' => [
'result_key' => ['result_key'],
'pid' => ['pid'],
],
'foreign keys' => [
'pid' => [
'table' => 'editoria11y_pages',
'columns' => [
'pid' => 'pid',
],
],
],
];
}
/**
* Returns dismissals array.
*/
function editoria11y_dismissals_table(): array {
return [
'description' => 'Stores Editoria11y warnings and dismissals',
'fields' => [
'id' => [
'description' => 'Element affected',
'type' => 'serial',
'size' => 'big',
'not null' => TRUE,
],
'pid' => [
'description' => 'The ed11y page table record this affects.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'uid' => [
'description' => 'The {users}.uid that dismissed this test.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'element_id' => [
'type' => 'varchar',
'length' => 2048,
'not null' => TRUE,
'default' => 'unknown',
'description' => 'Code sample to identify the flagged element.',
],
'created' => [
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'The Unix timestamp of the first time this record was flagged.',
],
'result_name' => [
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => 'unknown',
'description' => 'The title of the test as reported by editoria11y JS',
],
'result_key' => [
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => 'unknown',
'description' => 'The name of the test as reported by editoria11y JS',
],
'dismissal_status' => [
'type' => 'varchar_ascii',
'length' => 64,
'not null' => TRUE,
'default' => 'unknown',
'description' => 'The type of dismissal (e.g., OK, Ignore)',
],
'stale_date' => [
'type' => 'int',
'not null' => FALSE,
'default' => NULL,
'description' => 'The Unix timestamp when the element disappeared.',
],
],
'primary key' => [
'id',
],
'unique_keys' => [
'ed11y' => [
'pid',
'result_name',
'element_id',
'dismissal_status',
'uid',
],
],
'indexes' => [
'element_id' => ['element_id'],
'result_name' => ['result_name'],
'pid' => ['pid'],
'uid' => ['uid'],
],
'foreign keys' => [
'data_user' => [
'table' => 'users',
'columns' => [
'uid' => 'uid',
],
],
'pid' => [
'table' => 'editoria11y_pages',
'columns' => [
'pid' => 'pid',
],
],
],
];
}
/**
* Implements hook_schema().
*/
function editoria11y_schema(): array {
$schema['editoria11y_pages'] = editoria11y_pages_table();
$schema['editoria11y_results'] = editoria11y_results_table();
$schema['editoria11y_dismissals'] = editoria11y_dismissals_table();
return $schema;
}
/**
* Add new config options.
*
* Note that "Ignore containers" has become
* "Ignore elements." Make a note of your setting
* before proceeding as it will need to be rewritten.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
function editoria11y_update_9001() {
// Add ability to hide results to current viewers.
/** @var \Drupal\user\RoleInterface $role */
foreach (Role::loadMultiple() as $role) {
if ($role->hasPermission('view editoria11y checker')) {
$role->grantPermission('mark as hidden in editoria11y');
$role->save();
}
}
// Set defaults for new config items.
$config_factory = Drupal::configFactory();
$config = $config_factory->getEditable('editoria11y.settings');
$config->set('ed11y_theme', 'lightTheme');
$config->set('disable_sync', FALSE);
$config->save(TRUE);
// Create new DB tables.
/*
// ====== Removed; 9003 resets these tables.
*/
}
/**
* Store page alias in reports for future dashboard filtering.
*/
/*function editoria11y_update_9002(&$sandbox) {
// ====== Removed; 9003 resets these tables.
}*/
/**
* Creates tables for dashboard data using new schema.
*
* Note when updating between 2.x Betas that this will restore any previously
* hidden alerts.
*/
function editoria11y_update_9003() {
$config_factory = Drupal::configFactory();
$config = $config_factory->getEditable('editoria11y.settings');
$config->set('preserve_params', 'search,keys,page,language,language_content_entity');
$config->save(TRUE);
// Create new DB tables.
$database = Drupal::database();
$schema = $database->schema();
if ($schema->tableExists('editoria11y_results')) {
$schema->dropTable('editoria11y_results');
}
if ($schema->tableExists('editoria11y_dismissals')) {
$schema->dropTable('editoria11y_dismissals');
}
$table_name_results = 'editoria11y_results';
$table_schema_results = editoria11y_results_table();
$schema->createTable($table_name_results, $table_schema_results);
$table_name_dismissals = 'editoria11y_dismissals';
$table_schema_dismissals = editoria11y_dismissals_table();
$schema->createTable($table_name_dismissals, $table_schema_dismissals);
}
/**
* Adds entity IDs, node delete hooks and admin-editable Views.
*/
function editoria11y_update_9004() {
$schema = Drupal::database()->schema();
// Only needed on sites installed at 2.x versions between 9003 and 9004.
if (!$schema->fieldExists('editoria11y_results', 'entity_id')) {
// Partial schema for index calculation.
$specs = [
'fields' => [
'entity_id' => [
'description' => 'The NID or TID this record affects.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
],
'indexes' => [
'entity_id' => ['entity_id'],
],
];
$fields = ['entity_id'];
$schema->addField('editoria11y_results', 'entity_id', $specs['fields']['entity_id']);
$schema->addIndex('editoria11y_results', 'entity_id', $fields, $specs);
$schema->addField('editoria11y_dismissals', 'entity_id', $specs['fields']['entity_id']);
$schema->addIndex('editoria11y_dismissals', 'entity_id', $fields, $specs);
}
}
/**
* Updates default config for Link Purpose compatibility.
*/
function editoria11y_update_9005() {
$config_factory = Drupal::configFactory();
$config = $config_factory->getEditable('editoria11y.settings');
$config->set('link_ignore_selector', 'svg.ext, svg.mailto, .link-purpose-text');
$config->save(TRUE);
}
/**
* Issue 3422440: Fix config schema error.
*/
function editoria11y_update_9006() {
$config_factory = Drupal::configFactory();
$config = $config_factory->getEditable('editoria11y.configuration');
$config->delete();
}
/**
* Enable live checking and set default heading level for body fields.
*/
function editoria11y_update_9007() {
$config_factory = Drupal::configFactory();
$config = $config_factory->getEditable('editoria11y.settings');
$config->set('live_h2', 'form[id^="node-"] #edit-body-wrapper');
$config->set('disable_live', FALSE);
$config->save();
}
/**
* Reset value for selector for first heading level.
*/
function editoria11y_update_9008() {
$config_factory = Drupal::configFactory();
$config = $config_factory->getEditable('editoria11y.settings');
$config->set('live_h2', 'form[id^="node-"] #edit-body-wrapper .ck-content');
$config->save();
}
/**
* Set Editoria11y config to make CK5 insert tables with a header row.
*/
function editoria11y_update_9009() {
$config_factory = Drupal::configFactory();
$config = $config_factory->getEditable('editoria11y.settings');
$config->set('ck5_table_headers', 'row');
$config->save();
}
/**
* This update modifies table schemas and RESETS the dashboard Views config.
*
* Custom references to Editoria11y table data in code or Views may break.
* If you have any, STOP, remove them and make a backup before continuing.
* Details are listed in update_9010 in the editoria11y.install file.
*
* @throws \Exception
*/
function editoria11y_update_9010(&$sandbox) {
/*
* This update:
* 1. Creates a new "Pages" table with entity references and summary data.
* 2. Replaces entity data in the other tables with a Pages foreign key.
* 3. Creates new "Result Key" column to allow for more reliable translation.
* 4. Changes dismissal stale column from a boolean to a "last seen" date.
* 5. Replaces the default dashboard Views with updated versions.
*/
/*
* 1. Create new Pages table and set up foreign keys.
*/
$database = Drupal::database();
$schema = $database->schema();
$batchSize = 50;
if (!isset($sandbox['total'])) {
// First batch.
$table_name_pages = 'editoria11y_pages';
// Schema snapshot as of 3.0.0.
$table_schema_pages = [
'description' => 'Pages with issues detected by Editoria11y',
'fields' => [
'pid' => [
'description' => 'Serial unique ID',
'type' => 'serial',
'size' => 'big',
'not null' => TRUE,
],
'entity_id' => [
'description' => 'The node, term or user id this record affects.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'entity_type' => [
'type' => 'varchar',
'not null' => TRUE,
'default' => 'node',
'length' => 32,
'description' => 'The entity type; "route" if no type found.',
],
'route_name' => [
'type' => 'varchar',
'not null' => TRUE,
'default' => 'unknown',
'length' => 255,
'description' => 'Route name for page.',
],
'page_path' => [
'type' => 'varchar',
'not null' => TRUE,
'default' => 'unknown',
'length' => 1024,
'description' => 'Internal, relative page path.',
],
'page_language' => [
'type' => 'varchar',
'not null' => TRUE,
'default' => 'unknown',
'length' => 64,
'description' => 'Active translation.',
],
'page_title' => [
'type' => 'varchar',
'not null' => TRUE,
'default' => 'unknown',
'length' => 1024,
'description' => 'The name of the route where this was last seen.',
],
'page_result_count' => [
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'The total number of issues on this page.',
],
'updated' => [
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'The Unix timestamp of the last update.',
],
],
'primary key' => [
'pid',
],
'unique_keys' => [
'ed11y' => [
'page_path',
'page_language',
],
],
'indexes' => [
'entity_type' => ['entity_type'],
'page_path' => ['page_path'],
'page_language' => ['page_language'],
'entity_id' => ['entity_id'],
],
];
$schema->createTable($table_name_pages, $table_schema_pages);
// Create page reference field.
$pid_reference = [
'fields' => [
'pid' => [
'description' => 'The ed11y page table record this affects.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
],
'indexes' => [
'pid' => ['pid'],
],
];
$fields = ['pid'];
$schema->addField('editoria11y_results', 'pid', $pid_reference['fields']['pid']);
$schema->addIndex('editoria11y_results', 'pid', $fields, $pid_reference);
$schema->addField('editoria11y_dismissals', 'pid', $pid_reference['fields']['pid']);
$schema->addIndex('editoria11y_dismissals', 'pid', $fields, $pid_reference);
$ed11y_result_key = [
'fields' => [
'result_key' => [
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => 'unknown',
'description' => 'The name of the test as reported by editoria11y JS',
],
],
'indexes' => [
'result_key' => ['result_key'],
],
];
$fields = ['result_key'];
$schema->addField('editoria11y_results', 'result_key', $ed11y_result_key['fields']['result_key']);
$schema->addIndex('editoria11y_results', 'result_key', $fields, $ed11y_result_key);
// @ todo 3.0 note from 2.3: see 9004. May need to adjust indexes.
if (!$schema->fieldExists('editoria11y_dismissals', 'stale_date')) {
$schema->addField(
'editoria11y_dismissals',
'stale_date', [
'type' => 'int',
'not null' => FALSE,
'default' => NULL,
'description' => 'The Unix timestamp when the element disappeared.',
],
);
}
// Get number of rows to update.
$resultsQuery = $database->select('editoria11y_results', 'results');
$sandbox['resultCount'] = $resultsQuery->countQuery()->execute()->fetchField();
$dismissQuery = $database->select('editoria11y_dismissals', 'dismissals');
$sandbox['dismissalCount'] = $dismissQuery->countQuery()->execute()->fetchField();
$sandbox['rangePosition'] = [
'editoria11y_dismissals' => 0,
'editoria11y_results' => 0,
];
$sandbox['total'] = (int) ($sandbox['resultCount'] + $sandbox['dismissalCount'] + 2 * $batchSize);
// Add two for cleanup after the last row update batch.
$sandbox['current'] = 0;
}
// Get result rows.
$activeTable = FALSE;
if ($sandbox['current'] <= $sandbox['dismissalCount']) {
$activeTable = 'editoria11y_dismissals';
}
elseif ($sandbox['current'] <= $sandbox['dismissalCount'] + $sandbox['resultCount']) {
$activeTable = 'editoria11y_results';
}
if ($activeTable) {
$fields = [
'id',
'page_path',
'page_language',
'entity_id',
'entity_type',
'route_name',
'page_title',
'updated',
];
if ($activeTable === 'editoria11y_results') {
$fields[] = 'page_result_count';
}
// Create editoria11y_page table pid references.
$query = $database->select($activeTable);
$query->fields(
$activeTable,
$fields,
);
$query->orderBy('updated');
$query->groupBy('updated');
$query->groupBy('entity_id');
$query->groupBy('entity_type');
$query->groupBy('page_language');
$query->groupBy('page_path');
$query->groupBy('page_title');
$query->groupBy('page_language');
$query->groupBy('route_name');
$query->groupBy('id');
if ($activeTable === 'editoria11y_results') {
$query->groupBy('page_result_count');
}
$query->range($sandbox['rangePosition'][$activeTable], $sandbox['rangePosition'][$activeTable] + $batchSize);
$results = $query->execute();
$pagesUpdated = [];
foreach ($results as $record) {
if ($record->page_path) {
$upsertFields = [
'entity_id' => $record->entity_id,
'entity_type' => $record->entity_type,
'page_title' => $record->page_title,
'route_name' => $record->route_name,
'updated' => $record->updated,
'page_language' => $record->page_language,
'page_path' => $record->page_path,
];
if ($activeTable === 'editoria11y_results') {
$upsertFields['page_result_count'] = $record->page_result_count;
}
$upsert = $database->merge('editoria11y_pages');
$upsert->keys(
[
'page_language' => $record->page_language,
'page_path' => $record->page_path,
]);
$upsert->fields($upsertFields);
if (!in_array($record->page_path, $pagesUpdated)) {
$pagesUpdated[] = $record->page_path;
}
$upsert->execute();
}
}
if (count($pagesUpdated) > 0) {
// Now get back the new page ids and write them back to the origin table.
$backQuery = $database->select('editoria11y_pages', 'p')
->fields(
'p',
[
'pid',
'page_path',
'page_language',
]
)
->orderBy('pid', 'DESC')
->range(0, count($pagesUpdated));
// @ Todo 3.0 test.
$backRecords = $backQuery->execute();
foreach ($backRecords as $backRecord) {
$setPage = $database->update($activeTable);
$setPage->fields([
'pid' => $backRecord->pid,
]);
$setPage->condition('page_language', $backRecord->page_language);
$setPage->condition('page_path', $backRecord->page_path);
$setPage->execute();
}
}
if ($activeTable === 'editoria11y_dismissals') {
$getDismissals = $database->select($activeTable)
->fields($activeTable, ['stale', 'updated', 'id'])
->range($sandbox['rangePosition'][$activeTable], $sandbox['rangePosition'][$activeTable] + $batchSize);
$dismissals = $getDismissals->execute();
// Stale dismissals need the new format.
foreach ($dismissals as $dismissal) {
if ($dismissal->stale) {
$addStale = $database->update('editoria11y_dismissals')
->fields(['stale_date' => $dismissal->updated])
->condition('id', $dismissal->id);
$addStale->execute();
}
}
}
$sandbox['rangePosition'][$activeTable] = $sandbox['rangePosition'][$activeTable] + $batchSize;
}
$sandbox['current']++;
// Last passes. Bulk updates and configuration.
if ((int) $sandbox['total'] === (int) $sandbox['current'] + $batchSize) {
// Preload result keys for default result names.
$keyMap = [
'Alt text is meaningless' => 'ALT_PLACEHOLDER',
"Image's text alternative is unpronounceable" => 'ALT_UNPRONOUNCEABLE',
'Linked Image has no alt text' => 'LINK_IMAGE_NO_ALT_TEXT',
'Manual check: possibly redundant text in alt' => 'SUS_ALT',
'Manual check: very long alternative text' => 'IMAGE_ALT_TOO_LONG',
'Image has no alternative text attribute' => 'LINK_IMAGE_LONG_ALT',
'Manual check: image has no alt text' => 'IMAGE_DECORATIVE',
'Manual check: link contains both text and an image' => 'LINK_IMAGE_ALT_AND_TEXT',
"Image's text alternative is a URL" => 'ALT_FILE_EXT',
"Linked image's text alternative is a URL" => 'LINK_ALT_FILE_EXT',
'Manual check: is this a blockquote?' => 'QA_BLOCKQUOTE',
'Manual check: is an accurate transcript provided?' => 'EMBED_AUDIO',
'Manual check: is this embedded content accessible?' => 'EMBED_GENERAL',
'Manual check: is this video accurately captioned?' => 'EMBED_VIDEO',
'Manual check: is this visualization accessible?' => 'EMBED_DATA_VIZ',
'Heading tag without any text' => 'HEADING_EMPTY',
'Manual check: long heading' => 'HEADING_LONG',
'Manual check: was a heading level skipped?' => 'HEADING_SKIPPED_LEVEL',
'Manual check: is the linked document accessible?' => 'QA_PDF',
'Manual check: is opening a new window expected?' => 'LINK_NEW_TAB',
'Link with no accessible text' => 'LINK_EMPTY',
'Manual check: is this link meaningful and concise?' => 'LINK_STOPWORD',
'Manual check: is this link text a URL?' => 'LINK_URL',
'Content heading inside a table' => 'TABLES_SEMANTIC_HEADING',
'Empty table header cell' => 'TABLES_EMPTY_HEADING',
'Table has no header cells' => 'TABLES_MISSING_HEADINGS',
'Manual check: should this be a heading?' => 'QA_FAKE_HEADING',
'Manual check: should this have list formatting?' => 'QA_FAKE_LIST',
'Manual check: is this uppercase text needed?' => 'QA_UPPERCASE',
];
// @todo 3.0: test!
foreach ($keyMap as $key => $value) {
$setKey = $database->update('editoria11y_results');
$setKey->fields([
'result_key' => $value,
]);
$setKey->condition('result_name', $key);
$setKey->execute();
}
}
elseif ((int) $sandbox['total'] === $sandbox['current']) {
// Drop old columns and swap in the new Views.
$drops = [
'page_language',
'entity_id',
'route_name',
'page_url',
'page_title',
'page_result_count',
'updated',
'page_path',
'entity_type',
'stale',
];
foreach ($drops as $drop) {
if ($schema->fieldExists('editoria11y_results', $drop)) {
$schema->dropField('editoria11y_results', $drop);
}
if ($schema->fieldExists('editoria11y_dismissals', $drop)) {
$schema->dropField('editoria11y_dismissals', $drop);
}
}
$config_factory = Drupal::configFactory();
$config = $config_factory->getEditable('editoria11y.settings');
$view_ids = ['editoria11y_dismissals', 'editoria11y_results'];
foreach ($view_ids as $view_id) {
$view = Views::getView($view_id);
if ($view) {
// Delete the view configuration.
$config_factory->getEditable('views.view.' . $view_id)->delete()->save(TRUE);
}
}
$view_ids[] = 'editoria11y_pages';
foreach ($view_ids as $view_id) {
$config_path = \Drupal::service('extension.list.module')->getPath('editoria11y') . '/config/install/views.view.' . $view_id . '.yml';
$data = Yaml::parseFile($config_path);
$config_factory->getEditable('views.view.' . $view_id)->setData($data)->save(TRUE);
}
$config->set('db_version', '2');
$config->save(TRUE);
}
// Once $sandbox['#finished'] == 1, the process is complete.
$sandbox['#finished'] = (int) $sandbox['current'] / (int) $sandbox['total'];
}
