moderation_note-8.x-1.0-beta3/moderation_note.module
moderation_note.module
<?php
/**
* @file
* Contains hook implementations for moderation_note.
*/
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\RevisionLogInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\moderation_note\ModerationNoteInterface;
use Drupal\user\EntityOwnerInterface;
use Drupal\user\UserInterface;
/**
* Implements hook_entity_delete().
*/
function moderation_note_entity_delete(EntityInterface $entity) {
if ($entity instanceof ModerationNoteInterface) {
// Load and delete all child notes for this parent.
$ids = \Drupal::entityQuery('moderation_note')
->accessCheck(FALSE)
->condition('parent', $entity->id())
->execute();
$notes = \Drupal::entityTypeManager()->getStorage('moderation_note')->loadMultiple($ids);
}
else {
// Load and delete all associated notes for this entity.
$ids = \Drupal::entityQuery('moderation_note')
->accessCheck(FALSE)
->condition('entity_type', $entity->getEntityTypeId())
->condition('entity_id', $entity->id())
->execute();
$notes = \Drupal::entityTypeManager()->getStorage('moderation_note')->loadMultiple($ids);
}
\Drupal::entityTypeManager()->getStorage('moderation_note')->delete($notes);
}
/**
* Implements hook_menu_local_tasks_alter().
*/
function moderation_note_menu_local_tasks_alter(&$data, $route_name, RefinableCacheableDependencyInterface &$cacheability) {
/** @var \Drupal\content_moderation\ModerationInformation $information */
$information = \Drupal::service('content_moderation.moderation_information');
// Get the current Entity.
foreach (\Drupal::routeMatch()->getParameters() as $parameter) {
if ($parameter instanceof EntityInterface && $information->isModeratedEntity($parameter)) {
$entity = $parameter;
break;
}
}
if (!isset($entity) || $entity->isNew()) {
return;
}
$entity_type = $entity->getEntityTypeId();
$entity_id = $entity->id();
$link = \Drupal::service('moderation_note.menu_count')
->contentLink($entity_type, $entity_id);
$link['#access'] = AccessResult::allowedIfHasPermissions(\Drupal::currentUser(), [
'access moderation notes',
]);
$link['#attached'] = [
'library' => ['moderation_note/main'],
];
$data['tabs'][0]['moderation_note.list'] = $link;
$cacheability->addCacheContexts(['user.permissions']);
$cacheability->addCacheTags(["moderation_note:$entity_type:$entity_id"]);
}
/**
* Implements hook_toolbar_alter().
*/
function moderation_note_toolbar_alter(&$items) {
$user = \Drupal::currentUser();
$uid = $user->id();
if (isset($items['user']) && $user->hasPermission('access moderation notes')) {
$link = \Drupal::service('moderation_note.menu_count')
->assignedTo($uid);
$link['#cache'] = [
'contexts' => ['user'],
'tags' => ['moderation_note:user:' . $uid],
];
$items['user']['tray']['moderation_note'] = $link;
}
return $items;
}
/**
* Implements hook_preprocess_HOOK() for field templates.
*/
function moderation_note_preprocess_field(&$variables) {
$variables['#cache']['contexts'][] = 'user.permissions';
$element = $variables['element'];
/** @var \Drupal\Core\Entity\EntityInterface $entity */
$entity = $element['#object'];
/** @var \Drupal\Core\Field\FieldItemList $field_list */
$field_list = $element['#items'];
$field_definition = $field_list->getFieldDefinition();
if (!_moderation_note_access($entity)) {
return;
}
// Check the field type - we only support text and entity reference revision
// fields at this time.
$supported_types = [
'boolean',
'datetime',
'entity_reference_revisions',
'entity_reference',
'list_string',
'string_long',
'string',
'text_long',
'text_with_summary',
'text',
];
if (!in_array($field_definition->getType(), $supported_types, TRUE)) {
return;
}
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$definition = $entity->getFieldDefinition($element['#field_name']);
if (!$definition->isComputed()) {
$variables['#attached']['library'][] = 'moderation_note/main';
if (_moderation_note_on_entity($entity)->isAllowed()) {
$variables['attributes']['data-moderation-note-can-create'] = TRUE;
}
$variables['attributes']['data-moderation-note-field-id'] = $entity->getEntityTypeId() . '/' . $entity->id() . '/' . $element['#field_name'] . '/' . $element['#language'] . '/' . $element['#view_mode'];
$variables['#cache']['tags'][] = implode(':', [
'moderation_note',
$entity->getEntityTypeId(),
$entity->id(),
$element['#field_name'],
$element['#language'],
]);
_moderation_note_attach_field_notes($variables);
}
}
/**
* Check is the account can create a moderation note on the entity.
*/
function _moderation_note_on_entity(EntityInterface $entity, ?AccountInterface $account = NULL) {
if (!$account) {
$account = \Drupal::currentUser();
}
if ($account->hasPermission('create moderation notes')) {
return AccessResult::allowed();
}
if ($account->hasPermission('create moderation notes on uneditable entities')
&& $entity->access('update', $account, TRUE)->isForbidden()) {
return AccessResult::allowed();
}
return AccessResult::neutral();
}
/**
* Attaches drupal settings that represent moderation notes to a field.
*
* @param array $variables
* The render array for a field as passed to hook_preprocess_field().
*/
function _moderation_note_attach_field_notes(array &$variables) {
$element = $variables['element'];
/** @var \Drupal\Core\Entity\EntityInterface $entity */
$entity = $element['#object'];
if (!_moderation_note_access($entity)) {
return;
}
// Load notes for this entity field.
$ids = \Drupal::entityQuery('moderation_note')
->accessCheck(FALSE)
->condition('entity_type', $entity->getEntityTypeId())
->condition('entity_id', $entity->id())
->condition('entity_field_name', $element['#field_name'])
->condition('entity_langcode', $element['#language'])
->condition('entity_view_mode_id', $element['#view_mode'])
->condition('published', 1)
->notExists('parent')
->execute();
/** @var \Drupal\moderation_note\ModerationNoteInterface[] $notes */
$notes = \Drupal::entityTypeManager()->getStorage('moderation_note')->loadMultiple($ids);
foreach ($notes as $note) {
$setting = [
'field_id' => _moderation_note_generate_field_id($note),
'id' => $note->id(),
'quote' => $note->getQuote(),
'quote_offset' => $note->getQuoteOffset(),
];
$variables['#attached']['drupalSettings']['moderation_notes'][$note->id()] = $setting;
}
}
/**
* Access callback to determine if an Entity can be annotated.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The Entity to check.
*
* @return bool
* TRUE if the current user can access the Entity, FALSE otherwise.
*/
function _moderation_note_access(EntityInterface $entity) {
// If this entity is being referenced with entity reference revisions, it
// should not be notated individually.
if (isset($entity->_referringItem) && is_a($entity->_referringItem, '\Drupal\entity_reference_revisions\Plugin\Field\FieldType\EntityReferenceRevisionsItem')) {
return FALSE;
}
if (!($entity instanceof ContentEntityInterface)) {
return FALSE;
}
$has_permission = \Drupal::currentUser()->hasPermission('access moderation notes');
if (!$has_permission) {
return FALSE;
}
/** @var \Drupal\content_moderation\ModerationInformation $moderation_information */
$moderation_information = \Drupal::service('content_moderation.moderation_information');
$is_moderated_entity = $moderation_information->isModeratedEntity($entity);
// Check if this is the latest moderated revision and if the user has access.
// @todo When Quick Edit is rendering an entity after an edit, the revision
// ID is null. Investigate and/or file a core issue.
$is_latest_revision = ($entity->getRevisionId() === NULL || $entity->isLatestRevision());
return $is_moderated_entity && $is_latest_revision && !$entity->isNew();
}
/**
* Implements hook_theme().
*/
function moderation_note_theme() {
return [
'moderation_note' => [
'render element' => 'elements',
],
'moderation_note__preview' => [
'base hook' => 'moderation_note',
],
'mail_moderation_note' => [
'variables' => [
'note' => '',
'quote' => '',
'author' => '',
'header' => '',
'view_link' => NULL,
'previous_note' => '',
'previous_note_quote' => '',
'previous_note_author' => '',
],
],
];
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function moderation_note_theme_suggestions_moderation_note(array $variables) {
return [
'moderation_note__' . $variables['elements']['#view_mode'],
];
}
/**
* Prepares variables for moderation_note templates.
*
* Default template: moderation-note.html.twig.
*
* @param array $variables
* An associative array containing:
* - elements: An array of elements to display in view mode.
*/
function template_preprocess_moderation_note(array &$variables) {
/** @var \Drupal\content_moderation\ModerationInformation $information */
$information = \Drupal::service('content_moderation.moderation_information');
$userViewBuilder = \Drupal::entityTypeManager()->getViewBuilder('user');
$variables['moderation_note'] = $variables['elements']['#moderation_note'];
/** @var \Drupal\moderation_note\ModerationNoteInterface $note */
$note = $variables['moderation_note'];
$variables['text'] = $note->getText();
$variables['quote'] = $note->getQuote();
$variables['created'] = $note->getCreatedTime();
$variables['created_pretty'] = _moderation_note_pretty_time($note->getCreatedTime());
$variables['updated'] = $note->getChangedTime();
$variables['updated_pretty'] = _moderation_note_pretty_time($note->getChangedTime());
if ($owner = $note->getOwner()) {
$variables['author_name'] = $owner->getDisplayName();
$variables['author_link'] = $owner->toLink()->toRenderable();
$variables['author_picture'] = $userViewBuilder->view($owner, 'compact');
}
$variables['parent'] = $note->getParent();
$variables['published'] = $note->isPublished();
if ($information->hasPendingRevision($note->getModeratedEntity())) {
$rel = 'latest-version';
}
else {
$rel = 'canonical';
}
$variables['moderated_entity_link'] = $note->getModeratedEntity()->toLink(NULL, $rel);
if ($assignee = $note->getAssignee()) {
$variables['assignee_name'] = $assignee->getDisplayName();
$variables['assignee_link'] = $assignee->toLink()->toRenderable();
$variables['assignee_picture'] = $userViewBuilder->view($assignee, 'compact');
}
// Attributes.
$variables['attributes']['class'][] = 'moderation-note';
$variables['attributes']['data-moderation-note-id'] = $note->id();
if ($note->getParent()) {
$variables['attributes']['class'][] = 'moderation-note-reply';
}
if (!$note->isPublished()) {
$variables['attributes']['class'][] = 'moderation-note-resolved';
}
$params = ['moderation_note' => $note->id()];
// We show note actions inline with the note, if the user has access.
$actions = [];
if ($note->access('update')) {
$url = Url::fromRoute('moderation_note.edit', $params);
$actions['edit'] = [
'#type' => 'link',
'#title' => t('Edit'),
'#url' => $url,
'#attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas',
],
];
}
if ($note->isPublished() && $note->access('resolve')) {
$url = Url::fromRoute('moderation_note.resolve', $params);
$actions['resolve'] = [
'#type' => 'link',
'#title' => t('Resolve'),
'#url' => $url,
'#attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas',
],
];
}
if (!$note->isPublished() && $note->access('resolve')) {
$url = Url::fromRoute('moderation_note.resolve', $params);
$actions['re_open'] = [
'#type' => 'link',
'#title' => t('Re-open'),
'#url' => $url,
'#attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas',
],
];
}
if ($note->access('delete')) {
$url = Url::fromRoute('moderation_note.delete', $params);
$actions['delete'] = [
'#type' => 'link',
'#title' => t('Delete'),
'#url' => $url,
'#attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas',
],
];
}
// If a user is already viewing a note in the sidebar, it's assumed that the
// note is already being viewed in the context of the moderated entity.
if (\Drupal::request()->query->get('_wrapper_format') === 'drupal_dialog.off_canvas') {
$url = Url::fromRoute('moderation_note.view', $params);
$url->setOption('query', ['from-preview' => '1']);
$actions['view'] = [
'#type' => 'link',
'#title' => t('View full note'),
'#url' => $url,
'#attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas',
],
];
}
else {
$actions['view'] = [
'#type' => 'link',
'#title' => t('View note at "%label"', [
'%label' => $note->getModeratedEntity()->label(),
]),
'#url' => $note->toUrl(),
];
}
$variables['elements']['#cache']['contexts'][] = 'url.query_args';
$variables['actions'] = $actions;
}
/**
* Displays a timestamp in a human-readable fashion.
*
* @param int $time
* A timestamp.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* Markup representing a formatted time.
*/
function _moderation_note_pretty_time($time) {
$time = (int) $time;
$too_old = strtotime('-1 month');
// Show formatted time differences for edits younger than a month.
if ($time > $too_old) {
$diff = \Drupal::service('date.formatter')->formatTimeDiffSince($time, ['granularity' => 1]);
$time_pretty = t('@diff ago', ['@diff' => $diff]);
}
else {
$date = date('m/d/Y - h:i A', $time);
$time_pretty = t('on @date', ['@date' => $date]);
}
return $time_pretty;
}
/**
* Generates a field ID for a given note.
*
* @param \Drupal\moderation_note\ModerationNoteInterface $note
* The note to generate the field ID from.
*
* @return string
* A string representing a note's field ID.
*/
function _moderation_note_generate_field_id(ModerationNoteInterface $note) {
return $note->getModeratedEntityTypeId() . '/' . $note->getModeratedEntityId() . '/' . $note->getEntityFieldName() . '/' . $note->getEntityLanguage() . '/' . $note->getEntityViewModeId();
}
/**
* Implements hook_entity_insert().
*/
function moderation_note_moderation_note_insert(ModerationNoteInterface $note) {
/** @var \Drupal\Core\Mail\MailManagerInterface $mail_manager */
$mail_manager = \Drupal::service('plugin.manager.mail');
$to = _moderation_note_collect_recipients($note);
$mail_manager->mail('moderation_note', 'insert', $to, $note->getEntityLanguage(), [
'note' => $note,
]);
if ($note->getAssignee()) {
$to = _moderation_note_get_user_email($note->getAssignee());
$mail_manager->mail('moderation_note', 'assign', $to, $note->getEntityLanguage(), [
'note' => $note,
]);
}
// Moderation notes provide cache tags that invalidate their related field.
\Drupal::service('cache_tags.invalidator')->invalidateTags($note->getCacheTagsToInvalidate());
}
/**
* Implements hook_entity_insert().
*/
function moderation_note_moderation_note_update(ModerationNoteInterface $note) {
/** @var \Drupal\moderation_note\ModerationNoteInterface $original */
$original = $note->original;
/** @var \Drupal\Core\Mail\MailManagerInterface $mail_manager */
$mail_manager = \Drupal::service('plugin.manager.mail');
if ($original->getAssignee() !== $note->getAssignee() && $note->getAssignee()) {
$to = _moderation_note_get_user_email($note->getAssignee());
$mail_manager->mail('moderation_note', 'assign', $to, $note->getEntityLanguage(), [
'note' => $note,
]);
}
if (!$note->hasParent() && $original->isPublished() !== $note->isPublished()) {
$to = _moderation_note_collect_recipients($note);
$key = $note->isPublished() ? 're-open' : 'resolve';
$mail_manager->mail('moderation_note', $key, $to, $note->getEntityLanguage(), [
'note' => $note,
]);
}
}
/**
* Implements hook_entity_view_mode_info_alter().
*/
function moderation_note_entity_view_mode_info_alter(&$view_modes) {
$view_modes['moderation_note']['preview'] = [
'id' => 'moderation_note.preview',
'label' => 'Preview',
'targetEntityType' => 'moderation_note',
'cache' => TRUE,
];
}
/**
* Implements hook_mail().
*/
function moderation_note_mail($key, &$message, $params) {
if (!\Drupal::config('moderation_note.settings')->get('send_email')) {
$message['send'] = FALSE;
return;
}
/** @var \Drupal\moderation_note\ModerationNoteInterface $note */
$note = $params['note'];
/** @var \Drupal\content_moderation\ModerationInformation $information */
$information = \Drupal::service('content_moderation.moderation_information');
if ($information->hasPendingRevision($note->getModeratedEntity())) {
$rel = 'latest-version';
}
else {
$rel = 'canonical';
}
$view_url = $note->getModeratedEntity()->toUrl($rel)->setAbsolute();
if ($key === 'resolve') {
$view_link = [
'#type' => 'link',
'#title' => t('Click here to view the related content'),
'#url' => $view_url,
];
}
else {
$view_link = [
'#type' => 'link',
'#title' => t('Click here to view this note in context'),
'#url' => $note->toUrl(),
];
}
$options = [
'langcode' => $message['langcode'],
];
$header = 'note';
switch ($key) {
case 'insert':
if ($note->hasParent()) {
$message['subject'] = t('Reply on "@label"', [
'@label' => $note->getModeratedEntity()->label(),
], $options);
$header = t('A new reply has been created on "@label" by @author:', [
'@label' => $note->getModeratedEntity()->label(),
'@author' => $note->getOwner()->getDisplayName(),
], $options);
}
else {
$message['subject'] = t('Note on "@label"', [
'@label' => $note->getModeratedEntity()->label(),
], $options);
$header = t('A new note has been created on "@label" by @author:', [
'@label' => $note->getModeratedEntity()->label(),
'@author' => $note->getOwner()->getDisplayName(),
], $options);
}
break;
case 'resolve':
$message['subject'] = t('Note resolved on "@label"', [
'@label' => $note->getModeratedEntity()->label(),
], $options);
$header = t('A note has been resolved on "@label":', [
'@label' => $note->getModeratedEntity()->label(),
], $options);
break;
case 're-open':
$message['subject'] = t('Note re-opened on "@label"', [
'@label' => $note->getModeratedEntity()->label(),
], $options);
$header = t('A note has been re-opened on "@label":', [
'@label' => $note->getModeratedEntity()->label(),
], $options);
break;
case 'assign':
$message['subject'] = t('Note assigned to you on "@label"', [
'@label' => $note->getModeratedEntity()->label(),
], $options);
$header = t('A note has been assigned to you on "@label":', [
'@label' => $note->getModeratedEntity()->label(),
], $options);
break;
}
$body = [
'#theme' => 'mail_moderation_note',
'#header' => $header,
'#note' => $note->getText(),
'#author' => $note->getOwner()->getDisplayName(),
'#quote' => !$note->hasParent() ? $note->getQuote() : '',
'#view_link' => $view_link,
];
if ($note->hasParent()) {
$parent = $note->getParent();
$children = $parent->getChildren();
array_pop($children);
$previous_note = empty($children) ? $parent : end($children);
$body['#previous_note'] = $previous_note->getText();
$body['#previous_note_author'] = $previous_note->getOwner()->getDisplayName();
$body['#previous_note_quote'] = !$previous_note->hasParent() ? $previous_note->getQuote() : '';
}
$message['body'][] = \Drupal::service('renderer')->renderRoot($body);
}
/**
* Collects email addresses for users related to this note.
*
* @param \Drupal\moderation_note\ModerationNoteInterface $note
* A moderation note.
*
* @return string
* Email addresses for users related to this note.
*/
function _moderation_note_collect_recipients(ModerationNoteInterface $note) {
$users = [];
$users[] = $note->getOwner();
$users[] = $note->getAssignee();
$entity = $note->getModeratedEntity();
if ($entity instanceof EntityOwnerInterface) {
$users[] = $entity->getOwner();
}
if ($entity instanceof RevisionLogInterface) {
$users[] = $entity->getRevisionUser();
}
$parent = $note->hasParent() ? $note->getParent() : $note;
$users[] = $parent->getOwner();
foreach ($parent->getChildren() as $child) {
$users[] = $child->getOwner();
$users[] = $child->getAssignee();
}
$emails = [];
foreach ($users as $user) {
if ($user instanceof UserInterface && $user->isActive() && $note->access('view', $user)) {
$emails[] = _moderation_note_get_user_email($user);
}
}
$emails = array_unique($emails);
return implode(', ', $emails);
}
/**
* Fetches a user's email in the format "<Name>" email@example.com.
*
* @param \Drupal\user\UserInterface $user
* A user you want to get an email for.
*
* @return string
* An email suitable for PHP's mail function.
*/
function _moderation_note_get_user_email(UserInterface $user) {
$name = htmlspecialchars($user->getDisplayName(), ENT_QUOTES);
$email = filter_var($user->getEmail(), FILTER_SANITIZE_EMAIL);
return "\"$name\" <$email>";
}
/**
* Assembles a URL to view a Moderation Note in context.
*
* @param \Drupal\moderation_note\ModerationNoteInterface $note
* The moderation note.
*
* @return \Drupal\Core\Url
* The URL object.
*/
function moderation_note_uri(ModerationNoteInterface $note) {
/** @var \Drupal\content_moderation\ModerationInformation $information */
$information = \Drupal::service('content_moderation.moderation_information');
if ($information->hasPendingRevision($note->getModeratedEntity())) {
$rel = 'latest-version';
}
else {
$rel = 'canonical';
}
$query = [
'open-moderation-note' => $note->hasParent() ? $note->getParent()->id() : $note->id(),
];
return $note->getModeratedEntity()->toUrl($rel)
->setOption('query', $query);
}
