comment_notify-8.x-1.x-dev/comment_notify.module
comment_notify.module
<?php
/**
* @file
* This module provides comment follow-up email notifications.
*
* It works for anonymous and registered users.
*/
use Drupal\Core\Entity\EntityInterface;
use Drupal\comment\CommentInterface;
use Drupal\comment_notify\Form\CommentNotifySettings;
use Drupal\Component\Render\PlainTextOutput;
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
use Drupal\user\Entity\User;
use Drupal\Core\Routing\RouteMatchInterface;
define('COMMENT_NOTIFY_DISABLED', 0);
define('COMMENT_NOTIFY_ENTITY', 1);
define('COMMENT_NOTIFY_COMMENT', 2);
/**
* Provide an array of available options for notification on a comment.
*/
function _comment_notify_options() {
$total_options = [
COMMENT_NOTIFY_ENTITY => t('All comments'),
COMMENT_NOTIFY_COMMENT => t('Replies to my comment'),
];
$selected_options = array_filter(\Drupal::config('comment_notify.settings')->get('available_alerts'));
$available_options = array_intersect_key($total_options, $selected_options);
return $available_options;
}
/**
* Add the comment_notify fields in the comment form.
*/
function comment_notify_form_comment_form_alter(&$form, FormStateInterface $form_state, $form_id) {
$user = \Drupal::currentUser();
if (!($user->hasPermission('subscribe to comments') || $user->hasPermission('administer comments'))) {
return;
}
/** @var \Drupal\Core\Entity\EntityInterface $comment_entity */
$comment_entity = $form_state->getFormObject()->getEntity();
$field_identifier = CommentNotifySettings::getCommentFieldIdentifier($comment_entity);
// Only add the checkbox if this is an enabled content type.
$enabled_types = \Drupal::config('comment_notify.settings')->get('bundle_types');
if (!in_array($field_identifier, $enabled_types)) {
return;
}
$available_options = _comment_notify_options();
// Add the checkbox for anonymous users.
if ($user->isAnonymous()) {
// If anonymous users can't enter their email don't tempt them with the
// checkbox.
if (empty($form['author']['mail'])) {
return;
}
$form['#validate'][] = 'comment_notify_comment_validate';
}
\Drupal::moduleHandler()->loadInclude('comment_notify', 'inc');
/** @var \Drupal\comment_notify\UserNotificationSettings $user_settings */
$user_settings = \Drupal::service('comment_notify.user_settings');
$preference = $user_settings->getSetting($user->id(), 'comment_notify');
// If you want to hide this on your site see http://drupal.org/node/322482
$form['comment_notify_settings'] = [
'#attached' => ['library' => ['comment_notify/comment_notify']],
];
$form['#attributes']['class'][] = 'comment-notify-form';
$form['comment_notify_settings']['notify'] = [
'#type' => 'checkbox',
'#title' => t('Notify me when new comments are posted'),
'#default_value' => (bool) $preference,
'#attributes' => ['class' => ['comment-notify']],
];
$form['comment_notify_settings']['notify_type'] = [
'#type' => 'radios',
'#options' => $available_options,
'#default_value' => $preference != "none" ? $preference : 1,
'#attributes' => ['class' => ['comment-notify-type']],
];
if (count($available_options) == 1) {
$form['comment_notify_settings']['notify_type']['#type'] = 'hidden';
$form['comment_notify_settings']['notify_type']['#value'] = key($available_options);
}
// If this is an existing comment we set the default value based on their
// selection last time.
/** @var \Drupal\comment\CommentInterface $comment */
$comment = $form_state->getFormObject()->getEntity();
if (!$comment->isNew()) {
$notify = comment_notify_get_notification_type($comment->id());
$form['comment_notify_settings']['notify']['#default_value'] = (bool) $notify;
if (count($available_options) > 1) {
$form['comment_notify_settings']['notify_type']['#default_value'] = empty($notify) ? COMMENT_NOTIFY_ENTITY : $notify;
}
else {
$form['comment_notify_settings']['notify_type']['#default_value'] = key($available_options);
}
}
$form['actions']['submit']['#submit'][] = '_comment_notify_submit_comment_form';
}
/**
* Checks if the values used in the comment_notify fields are valid.
*/
function comment_notify_comment_validate(&$form, FormStateInterface $form_state) {
$user = \Drupal::currentUser();
// We assume that if they are non-anonymous then they have a valid mail.
// For anonymous users, though, we verify that they entered a mail and let
// comment.module validate it is real.
if ($user->isAnonymous() && $form['comment_notify_settings']['notify']['#value'] && empty($form['author']['mail']['#value'])) {
$form_state->setErrorByName('mail', t('If you want to subscribe to comments you must supply a valid email address.'));
}
}
/**
* Sends the mail alerts if necessary.
*/
function comment_notify_comment_publish($comment) {
// And send notifications - the real purpose of the module.
_comment_notify_mailalert($comment);
}
/**
* Additional submit handler for the comment form.
*/
function _comment_notify_submit_comment_form(array &$form, FormStateInterface $form_state) {
\Drupal::moduleHandler()->loadInclude('comment_notify', 'inc');
/** @var \Drupal\comment\CommentInterface $comment */
$comment = $form_state->getFormObject()->getEntity();
$user = \Drupal::currentUser();
/** @var \Drupal\comment_notify\UserNotificationSettings $user_settings */
$user_settings = \Drupal::service('comment_notify.user_settings');
// If the form component is visible, these values should be in the form state.
// Otherwise, use the user's preferences.
if ($form_state->hasValue('notify') && $form_state->hasValue('notify_type')) {
$status = $form_state->getValue('notify') ? $form_state->getValue('notify_type') : COMMENT_NOTIFY_DISABLED;
// Update user's preference.
// @todo are the user notifications allowed to be edited here?
if (!$user->isAnonymous()) {
/** @var \Drupal\comment_notify\UserNotificationSettings $user_settings */
$user_settings->saveSettings($user->id(), NULL, $status);
}
}
else {
$status = $user_settings->getSetting($user->id(), 'comment_notify');
}
// Save notification settings.
if (comment_notify_get_notification_type($comment->id())) {
// Update existing record.
comment_notify_update_notification($comment->id(), $status);
}
else {
// For new comments, we first build up a string to be used as the identifier
// for the alert. This identifier is used to later unsubscribe the user or
// allow them to potentially edit their comment / preferences if they are
// anonymous. The string is built with token and their host and comment
// identifier. It is stored and referenced, we really just need something
// unique/unguessable. See comment_notify_unsubscribe_by_hash().
// @todo make the comment_notify_add_notification() generate the hash.
$hostname = !$comment->getHostname() ? $comment->getHostname() : (isset($user->hostname) ? $user->hostname : '');
$notify_hash = \Drupal::csrfToken()->get($hostname . $comment->id());
comment_notify_add_notification($comment->id(), $status, $notify_hash, $comment->notified);
}
}
/**
* Implements hook_ENTITY_TYPE_update() for comment.
*/
function comment_notify_comment_update(CommentInterface $comment) {
// And send notifications - the real purpose of the module.
if ($comment->isPublished()) {
_comment_notify_mailalert($comment);
}
}
/**
* Implements hook_comment_insert().
*/
function comment_notify_comment_insert(CommentInterface $comment) {
\Drupal::moduleHandler()->loadInclude('comment_notify', 'inc');
// And send notifications - the real purpose of the module.
if ($comment->isPublished()) {
_comment_notify_mailalert($comment);
}
}
/**
* Deletes all the notifications when a comment is deleted.
*/
function comment_notify_comment_delete(CommentInterface $comment) {
\Drupal::moduleHandler()->loadInclude('comment_notify', 'inc');
comment_notify_remove_all_notifications($comment->id());
}
/**
* Returns array of comment notify enabled entity type & bundles.
*/
function _comment_notify_get_comment_enabled_bundles() {
$field_manager = \Drupal::service('entity_field.manager');
$bundles_with_comment_fields = [];
$comment_field_map = $field_manager->getFieldMapByFieldType('comment');
foreach ($comment_field_map as $entity_type => $comment_fields) {
foreach ($comment_fields as $field_name => $field_info) {
foreach ($field_info['bundles'] as $field_bundle) {
$bundles_with_comment_fields[$entity_type] = $field_bundle;
}
}
}
return $bundles_with_comment_fields;
}
/**
* Implements hook_form_alter().
*/
function comment_notify_form_user_form_alter(&$form, FormStateInterface &$form_state, $form_id) {
\Drupal::moduleHandler()->loadInclude('comment_notify', 'inc');
/** @var \Drupal\user\UserInterface $user */
$user = $form_state->getFormObject()->getEntity();
/** @var \Drupal\comment_notify\UserNotificationSettings $user_settings */
$user_settings = \Drupal::service('comment_notify.user_settings');
$notify_settings = $user->id() && $user_settings->getSettings($user->id()) ? $user_settings->getSettings($user->id()) : $user_settings->getDefaultSettings();
// Only show the entity followup UI if the user has permission to create
// entities.
$bundles = FALSE;
foreach (_comment_notify_get_comment_enabled_bundles() as $entity_type => $bundle) {
if (\Drupal::entityTypeManager()->getAccessControlHandler($entity_type)->createAccess($bundle)) {
$bundles = TRUE;
break;
}
}
// If the user cannot create nodes nor has the 'subscribe to comments'
// permission then there is no need to alter the user_form.
if ((!\Drupal::currentUser()->hasPermission('administer nodes') && $bundles === FALSE) && ($user->hasPermission('subscribe to comments') === FALSE)) {
return;
}
$form['comment_notify_settings'] = [
'#type' => 'details',
'#title' => t('Comment follow-up notification settings'),
'#weight' => 4,
'#open' => TRUE,
];
if (\Drupal::currentUser()->hasPermission('administer nodes') || $bundles) {
$form['comment_notify_settings']['entity_notify'] = [
'#type' => 'checkbox',
'#title' => t('Receive content follow-up notification emails'),
'#default_value' => isset($notify_settings['entity_notify']) ? $notify_settings['entity_notify'] : NULL,
'#description' => t('Check this box to receive email notifications for comments on your <strong>content</strong> (e.g. an article authored by you). You cannot disable notifications for individual threads.'),
];
}
else {
$form['comment_notify_settings']['entity_notify'] = [
'#type' => 'hidden',
'#value' => COMMENT_NOTIFY_DISABLED,
];
}
if ($user->hasPermission('subscribe to comments')) {
$available_options[COMMENT_NOTIFY_DISABLED] = t('No notifications');
$available_options += _comment_notify_options();
$form['comment_notify_settings']['comment_notify'] = [
'#type' => 'select',
'#title' => t('Receive comment follow-up notification emails'),
'#default_value' => isset($notify_settings['comment_notify']) ? $notify_settings['comment_notify'] : NULL,
'#options' => $available_options,
'#description' => t("Choose whether, by default, to subscribe to email notifications for replies to your own <strong>comments</strong>. Your site administrator may have customised the options available. You can change this setting on a per-comment basis, and later unsubscribe from individual posts."),
];
}
else {
$form['comment_notify_settings']['comment_notify'] = [
'#type' => 'hidden',
'#value' => COMMENT_NOTIFY_DISABLED,
];
}
$form['actions']['submit']['#submit'][] = '_comment_notify_submit_user_form';
}
/**
* Additional submit handler for the user form.
*/
function _comment_notify_submit_user_form(array &$form, FormStateInterface $form_state) {
\Drupal::moduleHandler()->loadInclude('comment_notify', 'inc');
/** @var \Drupal\user\UserInterface $user */
$user = $form_state->getFormObject()->getEntity();
if (!$user->isAnonymous() && is_numeric($form_state->getValue('entity_notify')) && is_numeric($form_state->getValue('comment_notify'))) {
/** @var \Drupal\comment_notify\UserNotificationSettings $user_settings */
$user_settings = \Drupal::service('comment_notify.user_settings');
$user_settings->saveSettings($user->id(), $form_state->getValue('entity_notify'), $form_state->getValue('comment_notify'));
}
}
/**
* Implements hook_user_delete().
*/
function comment_notify_user_predelete(EntityInterface $entity) {
// This hook is invoked when the account is deleted.
comment_notify_remove_user_settings($entity->id());
}
/**
* Implements hook_user_cancel().
*/
function comment_notify_user_cancel($edit, $account, $method) {
// This hook is invoked when the account is disabled.
comment_notify_remove_user_settings($account->id());
}
/**
* Remove the user settings of of the user with the given $uid.
*
* @param int $uid
* The user id.
*/
function comment_notify_remove_user_settings($uid) {
/** @var \Drupal\comment_notify\UserNotificationSettings $user_settings */
$user_settings = \Drupal::service('comment_notify.user_settings');
$user_settings->deleteSettings($uid);
}
/**
* Implements hook_comment_load().
*/
function comment_notify_comment_load($comments) {
// Load some comment_notify specific information into the comment object.
$query = \Drupal::database()->select('comment_notify', 'cn');
$query->join('comment_field_data', 'c', 'c.cid = cn.cid');
$query->leftJoin('users_field_data', 'u', 'c.uid = u.uid');
$query->condition('c.cid', array_keys($comments), 'IN');
$query->fields('cn', ['cid', 'notify', 'notify_hash', 'notified']);
$query->addField('c', 'mail', 'cmail');
$query->addField('u', 'init', 'uinit');
$query->addField('u', 'mail', 'umail');
$records = $query->execute()->fetchAllAssoc('cid');
foreach ($records as $cid => $record) {
$comments[$cid]->notify = $record->notify;
$comments[$cid]->notify_type = $record->notify;
$comments[$cid]->notify_hash = $record->notify_hash;
$comments[$cid]->notified = $record->notified;
$comments[$cid]->cmail = $record->cmail;
$comments[$cid]->uinit = $record->uinit;
$comments[$cid]->umail = $record->umail;
}
}
/**
* Private function to send the notifications.
*
* @param \Drupal\comment\CommentInterface $comment
* The comment entity.
*/
function _comment_notify_mailalert(CommentInterface $comment) {
\Drupal::moduleHandler()->loadInclude('comment_notify', 'inc');
$config = \Drupal::config('comment_notify.settings');
$user = \Drupal::currentUser();
$entity_id = $comment->getCommentedEntityId();
$entity_type = $comment->getCommentedEntityTypeId();
$field_identifier = CommentNotifySettings::getCommentFieldIdentifier($comment);
$comment_type = $comment->get('comment_type')->getString();
// Check to see if a notification has already been sent for this
// comment so that edits to a comment don't trigger an additional
// notification.
if (!empty($comment->notified)) {
return;
}
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($entity_id);
// No mails if this is not an enabled content type.
$enabled_types = $config->get('bundle_types');
if (!empty($enabled_types) && !in_array($field_identifier, $enabled_types)) {
return;
}
if (empty($comment->getAuthorEmail())) {
/** @var \Drupal\user\UserInterface $comment_account */
if ($comment_account = user_load_by_name($comment->getAuthorName())) {
$comment_mail = $comment_account->getEmail() ?: '';
}
else {
$comment_mail = '';
}
}
else {
$comment_mail = $comment->getAuthorEmail();
}
$sent_to = [];
// @todo Add hook to allow entity types to specify their type's author field?
$author_fields = ['uid', 'user_id'];
foreach ($author_fields as $author_field) {
// Send to a subscribed author if they are not the current commenter.
if ($entity->hasField($author_field)) {
// If the entity is a user object, then the user object is the "author'.
if ($entity instanceof User) {
$author = $entity;
}
// For now assume every other entity has an "owner".
else {
$author = $entity->getOwner();
}
/** @var \Drupal\comment_notify\UserNotificationSettings $user_settings */
$user_settings = \Drupal::service('comment_notify.user_settings');
$author_notify_settings = $user_settings->getSettings($author->id()) ?: $user_settings->getDefaultSettings();
$author_email = $author->getEmail();
// Do they explicitly want this? Or is it default to send to users? Is
// the comment author not the entity author? Do they have access? Do they
// have an email (e.g. anonymous)?
$entity_notify_a = !empty($author_notify_settings['entity_notify']) && $author_notify_settings['entity_notify'] == 1;
$entity_notify_b = $config->get('enable_default.entity_author') == 1 && !isset($author_notify_settings['entity_notify']);
if (!empty($author_email)
&& ($entity_notify_a || $entity_notify_b)
&& $user->id() != $author->id()
&& $entity->access('view', $author)
) {
if (in_array($author_email, $sent_to)) {
continue;
}
$raw_values = $config->get('mail_templates.entity_author.' . $entity->getEntityTypeId());
$token_data = [
'comment' => $comment,
'user' => $author,
];
if ($entity_type == 'node') {
$token_data['node'] = $entity;
}
$message['subject'] = PlainTextOutput::renderFromHtml(\Drupal::token()->replace($raw_values['subject'], $token_data));
$message['body'] = \Drupal::token()->replace($raw_values['body'], $token_data);
$language = $author->getPreferredLangcode();
\Drupal::service('plugin.manager.mail')->mail('comment_notify', 'comment_notify_mail', $author->getEmail(), $language, $message);
$sent_to[] = strtolower($author->getEmail());
}
// This field existed, so there's no need to try again.
break;
}
}
// For "reply to my comments" notifications, figure out what thread this is.
$thread = $comment->getThread() ?: '';
// Get the list of commenters to notify.
$watchers = comment_notify_get_watchers($entity_id, $comment_type);
foreach ($watchers as $alert) {
// If the user is not anonymous, always load the current email address
// from his or her user account instead of trusting $comment->mail.
$recipient_user = $alert->getOwner();
$mail = !empty($recipient_user->getEmail()) ? $recipient_user->getEmail() : $alert->getAuthorEmail();
// Trim the trailing / off the thread, if present.
$alert_thread = rtrim((string) $alert->getThread(), '/');
$relevant_thread = mb_substr($thread, 0, mb_strlen($alert_thread));
if ($alert->notify == COMMENT_NOTIFY_COMMENT && strcmp($relevant_thread, $alert_thread) != 0) {
continue;
}
if ($mail != $comment_mail && !in_array(strtolower($mail), $sent_to) && ($alert->getOwnerId() != $comment->getOwnerId() || $alert->getOwnerId() == 0)) {
$message = [];
// Make sure they have access to this entity before showing a bunch of
// entity information.
if (!$entity->access('view', $recipient_user)) {
continue;
}
$raw_values = $config->get('mail_templates.watcher.' . $entity->getEntityTypeId());
$token_data = [
'comment' => $comment,
'comment-subscribed' => $alert,
];
if ($entity_type == 'node') {
$token_data['node'] = $entity;
}
$message['subject'] = PlainTextOutput::renderFromHtml(\Drupal::token()->replace($raw_values['subject'], $token_data));
$message['body'] = \Drupal::token()->replace($raw_values['body'], $token_data);
$language = !empty($alert->uid) ? $recipient_user->getPreferredLangcode() : LanguageInterface::LANGCODE_DEFAULT;
\Drupal::service('plugin.manager.mail')
->mail('comment_notify', 'comment_notify_mail', $mail, $language, $message);
$sent_to[] = strtolower($mail);
// Make the mail link to user's /edit, unless it's an anonymous user.
if ($alert->getOwnerId() != 0) {
$user_mail = $alert->toLink($mail, 'edit-form')->toString();
}
else {
$user_mail = Html::escape($mail);
}
// Add an entry to the watchdog log.
$args = [
'@user_mail' => $user_mail,
'link' => $alert->toLink(t('source comment'))->toString(),
];
\Drupal::logger('comment_notify')
->notice('Notified: @user_mail', $args);
}
}
// Record that a notification was sent for this comment so that
// notifications aren't sent again if the comment is later edited.
comment_notify_mark_comment_as_notified($comment);
}
/**
* Implements hook_mail().
*/
function comment_notify_mail($key, &$message, $params) {
$message['subject'] = $params['subject'];
$message['body'][] = $params['body'];
}
/**
* Get the unsubscribe link for a comment subscriber.
*
* @param \Drupal\comment\CommentInterface $comment
* The subscribed comment object.
*
* @return string|null
* An absolute URL string for the unsubscribe page, or NULL if the comment is
* missing a notification hash.
*
* @todo In what case would a comment be missing its notification hash?
*/
function comment_notify_get_unsubscribe_url(CommentInterface $comment) {
if (!empty($comment->notify_hash)) {
return Url::fromRoute('comment_notify.disable', [
'hash' => $comment->notify_hash,
])->setAbsolute()
->toString();
}
return NULL;
}
/**
* Implements hook_field_extra_fields().
*/
function comment_notify_entity_extra_field_info() {
\Drupal::moduleHandler()->loadInclude('comment_notify', 'inc');
$extras = [];
$config = \Drupal::config('comment_notify.settings');
foreach ($config->get('bundle_types') as $bundle_type) {
$field_identifier = explode('--', $bundle_type);
$comment_field = FieldConfig::loadByName($field_identifier[0], $field_identifier[1], $field_identifier[2]);
if ($comment_field) {
$bundle_info = \Drupal::service("entity_type.bundle.info")->getBundleInfo($field_identifier[0]);
$bundle_label = $bundle_info[$field_identifier[1]]['label'];
$comment_type = $comment_field->getSetting('comment_type');
$extras['comment'][$comment_type]['form']['comment_notify_settings'] = [
'label' => t('Comment Notify settings'),
'description' => t('@bundle_type settings for Comment Notify', ['@bundle_type' => $bundle_label]),
'weight' => 1,
];
}
}
$extras['user']['user']['form']['comment_notify_settings'] = [
'label' => t('Comment Notify settings'),
'description' => t('User settings for Comment Notify'),
'weight' => 4,
];
return $extras;
}
/**
* Implements hook_help().
*/
function comment_notify_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.comment_notify':
$text = file_get_contents(dirname(__FILE__) . '/README.md');
if (!\Drupal::moduleHandler()->moduleExists('markdown')) {
return '<pre>' . $text . '</pre>';
}
else {
// Use the Markdown filter to render the README.
$filter_manager = \Drupal::service('plugin.manager.filter');
$settings = \Drupal::configFactory()->get('markdown.settings')->getRawData();
$config = ['settings' => $settings];
$filter = $filter_manager->createInstance('markdown', $config);
return $filter->process($text, 'en');
}
}
return NULL;
}
