comment_notify.module 19.1 KB
Newer Older
1 2 3 4
<?php

/**
 * @file
5 6
 *
 * This module provides comment follow-up e-mail notification for anonymous and registered users.
7 8
 */

9 10 11 12 13 14
use Drupal\comment\CommentInterface;
use Drupal\Component\Render\PlainTextOutput;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
15
use Drupal\Core\Url;
16 17 18
use Drupal\field\Entity\FieldConfig;
use Drupal\node\Entity\NodeType;

19 20 21 22
define('COMMENT_NOTIFY_DISABLED', 0);
define('COMMENT_NOTIFY_NODE', 1);
define('COMMENT_NOTIFY_COMMENT', 2);

23
/**
24
 * Provide an array of available options for notification on a comment.
25
 */
26 27 28 29 30 31
function _comment_notify_options() {
  $total_options = array(
    COMMENT_NOTIFY_NODE     => t('All comments'),
    COMMENT_NOTIFY_COMMENT  => t('Replies to my comment')
  );

32 33
  $selected_options = array_filter(\Drupal::config('comment_notify.settings')->get('available_alerts'));
  $available_options = array_intersect_key($total_options, $selected_options);
34 35

  return $available_options;
36 37
}

38

39 40 41
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'))) {
42 43
    return;
  }
44

45 46 47
  /** @var \Drupal\Core\Entity\EntityInterface $commented_entity */
  $commented_entity = $form_state->getFormObject()->getEntity()->getCommentedEntity();

48
  // Only add the checkbox if this is an enabled content type
49 50
  $enabled_types = \Drupal::config('comment_notify.settings')->get('node_types');
  if (!in_array($commented_entity->bundle(), $enabled_types)) {
51 52
    return;
  }
53

54
  $available_options = _comment_notify_options();
55
  // Add the checkbox for anonymous users.
56
  if ($user->isAnonymous()) {
57
    // If anonymous users can't enter their e-mail don't tempt them with the checkbox.
58
    if (empty($form['author']['mail'])) {
59
      return;
60
    }
61
    $form['#validate'][] = 'comment_notify_comment_validate';
62
  }
63
  module_load_include('inc', 'comment_notify', 'comment_notify');
64
  $preference = comment_notify_get_user_comment_notify_preference($user->id());
65

66
  // If you want to hide this on your site see http://drupal.org/node/322482
67 68 69 70
  $form['comment_notify_settings'] = array(
    '#attached' => ['library' => ['comment_notify/comment_notify']],
  );
  $form['comment_notify_settings']['notify'] = array(
71 72 73
    '#type' => 'checkbox',
    '#title' => t('Notify me when new comments are posted'),
    '#default_value' => (bool) $preference,
74
  );
75

76
  $form['comment_notify_settings']['notify_type'] = array(
77 78
    '#type' => 'radios',
    '#options' => $available_options,
79
    '#default_value' => $preference != "none" ? $preference : 1,
80 81
  );
  if (count($available_options) == 1) {
82 83
    $form['comment_notify_settings']['notify_type']['#type'] = 'hidden';
    $form['comment_notify_settings']['notify_type']['#value'] = key($available_options);
84
  }
85

86
  // If this is an existing comment we set the default value based on their selection last time.
87 88 89 90 91
  /** @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;
92
    if (count($available_options) > 1) {
93
      $form['comment_notify_settings']['notify_type']['#default_value'] = empty($notify) ? COMMENT_NOTIFY_NODE : $notify;
94
    }
95
    else {
96
      $form['comment_notify_settings']['notify_type']['#default_value'] = key($available_options);
97
    }
98
  }
99

100
  $form['actions']['submit']['#submit'][] = '_comment_notify_submit_comment_form';
101 102
}

103 104
function comment_notify_comment_validate(&$form, FormStateInterface $form_state) {
  $user = \Drupal::currentUser();
105 106
  // 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.
107 108
  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 e-mail address.'));
109 110
  }
}
111

112 113 114 115
function comment_notify_comment_publish($comment) {
  // And send notifications - the real purpose of the module.
  _comment_notify_mailalert($comment);
}
116

117
/**
118
 * Additional submit handler for the comment form.
119
 */
120
function _comment_notify_submit_comment_form(array &$form, FormStateInterface $form_state) {
121
  module_load_include('inc', 'comment_notify', 'comment_notify');
122

123 124 125 126 127 128 129 130 131 132 133 134
  /** @var \Drupal\comment\CommentInterface $comment */
  $comment = $form_state->getFormObject()->getEntity();
  $user = \Drupal::currentUser();

  // 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.
    if (!$user->isAnonymous()) {
      comment_notify_set_user_notification_setting($user->id(), NULL, $status);
    }
135 136
  }
  else {
137
    $status = comment_notify_get_user_comment_notify_preference($user->id());
138
  }
139 140 141 142 143

  // Save notification settings.
  if (comment_notify_get_notification_type($comment->id())) {
    // Update existing record.
    comment_notify_update_notification($comment->id(), $status);
144
  }
145 146 147 148 149 150 151 152 153
  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().
    $hostname = !$comment->getHostname() ? $comment->getHostname() : (isset($user->hostname) ? $user->hostname : '');
    $notify_hash = \Drupal::csrfToken()->get($hostname . $comment->id());
154
    comment_notify_add_notification($comment->id(), $status, $notify_hash, $comment->notified);
155 156 157 158 159 160 161
  }
}

/**
 * Implements hook_ENTITY_TYPE_update() for comment.
 */
function comment_notify_comment_update(CommentInterface $comment) {
162
  // And send notifications - the real purpose of the module.
163
  if ($comment->isPublished()) {
164 165 166 167
    _comment_notify_mailalert($comment);
  }
}

168 169 170
/**
 * Implements hook_comment_insert().
 */
171
function comment_notify_comment_insert(CommentInterface $comment) {
172
  module_load_include('inc', 'comment_notify', 'comment_notify');
173

174
  // And send notifications - the real purpose of the module.
175
  if ($comment->isPublished()) {
176
    _comment_notify_mailalert($comment);
177 178 179
  }
}

180
function comment_notify_comment_delete(CommentInterface $comment) {
181
  module_load_include('inc', 'comment_notify', 'comment_notify');
182
  comment_notify_remove_all_notifications($comment->id());
183 184 185
}


186
/**
187
 * Implement hook_form_alter().
188
 */
189
function comment_notify_form_user_form_alter(&$form, FormStateInterface &$form_state, $form_id) {
190
  module_load_include('inc', 'comment_notify', 'comment_notify');
191

192 193
  /** @var \Drupal\user\UserInterface $user */
  $user = $form_state->getFormObject()->getEntity();
194
  $notify_settings = $user->id() && comment_notify_get_user_notification_setting($user->id()) ? comment_notify_get_user_notification_setting($user->id()) : comment_notify_get_default_notification_setting();
195 196 197

  // Only show the node followup UI if the user has permission to create nodes.
  $nodes = FALSE;
198
  foreach (node_type_get_names() as $type => $name) {
199
    if (\Drupal::entityManager()->getAccessControlHandler('node')->createAccess($type)) {
200 201 202 203 204
      $nodes = TRUE;
      break;
    }
  }

205 206 207 208 209 210 211 212 213 214 215 216 217
  // 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') && $nodes === FALSE) && ($user->hasPermission( 'subscribe to comments') === FALSE)) {
    return;
  }

  $form['comment_notify_settings'] = array(
    '#type' => 'details',
    '#title' => t('Comment follow-up notification settings'),
    '#weight' => 4,
    '#open' => TRUE,
  );

218
  if (\Drupal::currentUser()->hasPermission('administer nodes') || $nodes) {
219
    $form['comment_notify_settings']['node_notify'] = array(
220
      '#type' => 'checkbox',
221
      '#title' => t('Receive content follow-up notification e-mails'),
222
      '#default_value' => isset($notify_settings->node_notify) ? $notify_settings->node_notify : NULL,
223
      '#description' => t('Check this box to receive an e-mail notification for follow-ups on your content. You can not disable notifications for individual threads.')
224 225 226 227
    );
  }
  else {
    $form['comment_notify_settings']['node_notify'] = array(
228 229
      '#type' => 'hidden',
      '#value' => COMMENT_NOTIFY_DISABLED,
230 231
    );
  }
232

233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
  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'] = array(
      '#type' => 'select',
      '#title' => t('Receive comment follow-up notification e-mails'),
      '#default_value' => isset ($notify_settings->comment_notify) ? $notify_settings->comment_notify : NULL,
      '#options' => $available_options,
      '#description' => t("Check this box to receive e-mail notification for follow-up comments to comments you posted. You can later disable this on a post-by-post basis... so if you leave this to YES, you can still disable follow-up notifications for comments you don't want follow-up mails anymore - i.e. for very popular posts.")
    );
  }
  else {
    $form['comment_notify_settings']['comment_notify'] = array(
      '#type' => 'hidden',
      '#value' => COMMENT_NOTIFY_DISABLED,
    );
  }
250
  $form['actions']['submit']['#submit'][] = '_comment_notify_submit_user_form';
251
}
252

253 254 255 256
/**
 * Additional submit handler for the user form.
 */
function _comment_notify_submit_user_form(array &$form, FormStateInterface $form_state) {
257
  module_load_include('inc', 'comment_notify', 'comment_notify');
258

259 260
  /** @var \Drupal\user\UserInterface $user */
  $user = $form_state->getFormObject()->getEntity();
261

262
  if (!$user->isAnonymous() && is_numeric($form_state->getValue('node_notify')) && is_numeric($form_state->getValue('comment_notify'))) {
263 264 265
    // Save the values to {comment_notify_user_settings}.
    comment_notify_set_user_notification_setting($user->id(), $form_state->getValue('node_notify'), $form_state->getValue('comment_notify'));
  }
266 267
}

268 269 270 271 272 273 274 275 276 277
/**
 * Implements hook_user_delete().
 */
function comment_notify_user_predelete(\Drupal\Core\Entity\EntityInterface $entity) {
  // This hook is invoked when the account is deleted.
  comment_notify_remove_user_settings($entity->id());
}
/**
 * Implements hook_user_cancel().
 */
278
function comment_notify_user_cancel($edit, $account, $method) {
279 280 281 282 283 284 285 286 287 288 289
  // 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) {
290
  module_load_include('inc', 'comment_notify', 'comment_notify');
291
  comment_notify_delete_user_notification_setting($uid);
292 293
}

294 295 296 297 298 299
/**
 * Implements hook_comment_load().
 */
function comment_notify_comment_load($comments) {
  // Load some comment_notify specific information into the comment object.
  $query = db_select('comment_notify', 'cn');
300 301 302
  $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');
303 304 305 306 307 308 309 310
  $query->fields('cn', array('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;
311
    $comments[$cid]->notify_type = $record->notify;
312 313 314 315 316 317 318 319
    $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;
  }
}

320 321
/**
 * Private function to send the notifications.
greggles's avatar
greggles committed
322
 *
323 324
 * @param \Drupal\comment\CommentInterface $comment
 *   The comment entity.
325
 */
326
function _comment_notify_mailalert(CommentInterface $comment) {
327

328
  module_load_include('inc', 'comment_notify', 'comment_notify');
329

330 331 332
  $config = \Drupal::config('comment_notify.settings');
  $user = \Drupal::currentUser();
  $nid = $comment->getCommentedEntityId();
333

334 335
  $comment_type = $comment->get('comment_type')->getString();

336 337 338
  // 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.
339
  if (!empty($comment->notified)) {
340 341 342
    return;
  }

343 344
  /** @var \Drupal\node\NodeInterface $node */
  $node = \Drupal::entityManager()->getStorage('node')->load($nid);
345

346 347 348
  // No mails if this is not an enabled content type
  $enabled_types = $config->get('node_types');
  if (!in_array($node->bundle(), $enabled_types) && !empty($enabled_types)) {
349 350 351
    return;
  }

352 353 354 355 356
  if (empty($comment->getAuthorEmail())) {
    /** @var \Drupal\user\UserInterface $comment_account */
    if ($comment_account = user_load_by_name($comment->getAuthorName())) {
      $comment_mail = $comment_account->getEmail() ?: '';
    }
357 358
  }
  else {
359
    $comment_mail = $comment->getAuthorEmail();
360
  }
361 362
  $sent_to = array();

363
  // Send to a subscribed author if they are not the current commenter.
364 365
  $author = $node->getOwner();
  $author_notify_settings = comment_notify_get_user_notification_setting($author->id()) ?: comment_notify_get_default_notification_setting();
366

367 368
  // Do they explicitly want this? Or is it default to send to users?
  // Is the comment author not the node author? Do they have access? Do they have an email (e.g. anonymous)?
369 370 371 372 373 374 375 376 377 378
  if (
    (
      (!empty($author_notify_settings->node_notify) && $author_notify_settings->node_notify == 1)
      || ($config->get('enable_default.entity_author') == 1 && !isset($author_notify_settings->node_notify))
    )
    && $user->id() != $author->id()
    && $node->access('view', $author)
    && !empty($author->getEmail())
  ) {
    $raw_values = $config->get('mail_templates.entity_author');
379
    $token_data = ['comment' => $comment, 'node' => $node];
380 381 382 383 384 385
    $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());
386
  }
387

388
  // For "reply to my comments" notifications, figure out what thread this is.
389
  $thread = $comment->getThread() ?: '';
390

391
  // Get the list of commenters to notify.
392
  $watchers = comment_notify_get_watchers($nid, $comment_type);
393

394
  foreach ($watchers as $alert) {
395 396
    // If the user is not anonymous, always load the current e-mail address
    // from his or her user account instead of trusting $comment->mail.
397 398
    $recipient_user = $alert->getOwner();
    $mail = !empty($recipient_user->getEmail()) ? $recipient_user->getEmail() : $alert->getAuthorEmail();
399 400
    // Trim the trailing / off the thread, if present.
    $alert_thread = rtrim((string) $alert->getThread(), '/');
401

402 403
    $relevant_thread = Unicode::substr($thread, 0, Unicode::strlen($alert_thread));
    if ($alert->notify == COMMENT_NOTIFY_COMMENT && strcmp($relevant_thread, $alert_thread) != 0) {
404
      continue;
405
    }
406

407
    if ($mail != $comment_mail && !in_array(strtolower($mail), $sent_to) && ($alert->getOwnerId() != $comment->getOwnerId() || $alert->getOwnerId() == 0)) {
408
      $message = array();
409

410
      // Make sure they have access to this node before showing a bunch of node information.
411
      if (!$node->access('view', $recipient_user)) {
412
        continue;
413
      }
414

415
      $raw_values = $config->get('mail_templates.watcher');
416
      $token_data = ['comment' => $comment, 'node' => $node, 'comment-subscribed' => $alert];
417 418
      $message['subject'] = PlainTextOutput::renderFromHtml(\Drupal::token()->replace($raw_values['subject'], $token_data));
      $message['body'] = \Drupal::token()->replace($raw_values['body'], $token_data);
419

420 421
      $language = !empty($alert->uid) ? $recipient_user->getPreferredLangcode() : LanguageInterface::LANGCODE_DEFAULT;
      \Drupal::service('plugin.manager.mail')->mail('comment_notify', 'comment_notify_mail', $mail, $language, $message);
422
      $sent_to[] = strtolower($mail);
423

424
      // Make the mail link to user's /edit, unless it's an anonymous user.
425 426
      if ($alert->getOwnerId() != 0) {
        $user_mail = $alert->link($mail, 'edit-form');
427 428
      }
      else {
429
        $user_mail = Html::escape($mail);
430
      }
431

432
      // Add an entry to the watchdog log.
433
      \Drupal::logger('comment_notify')->notice('Notified: @user_mail', ['@user_mail' => $user_mail, 'link' => $alert->link(t('source comment'))]);
434 435
    }
  }
436 437
  // Record that a notification was sent for this comment so that
  // notifications aren't sent again if the comment is later edited.
438
  comment_notify_mark_comment_as_notified($comment);
439 440
}

441
/**
442
 * Implements hook_mail().
443
 */
444 445
function comment_notify_mail($key, &$message, $params) {
  $message['subject'] = $params['subject'];
446
  $message['body'][] = $params['body'];
447 448
}

449 450 451
/**
 * Get the unsubscribe link for a comment subscriber.
 *
452
 * @param \Drupal\comment\CommentInterface $comment
453 454
 *   The subscribed comment object.
 *
455 456 457
 * @return string|null
 *   An absolute URL string for the unsubscribe page, or NULL if the comment is
 *   missing a notification hash.
458 459
 *
 * @todo In what case would a comment be missing its notification hash?
460
 */
461
function comment_notify_get_unsubscribe_url(CommentInterface $comment) {
462
  if (!empty($comment->notify_hash)) {
463 464 465 466
    return Url::fromRoute('comment_notify.disable', [
      'hash' => $comment->notify_hash,
    ])->setAbsolute()
      ->toString();
467
  }
468
  return NULL;
469
}
470 471 472
/**
 * Implements hook_field_extra_fields().
 */
473
function comment_notify_entity_extra_field_info() {
474 475 476
  module_load_include('inc', 'comment_notify', 'comment_notify');
  $extras = array();

477 478 479 480 481
  foreach (\Drupal::config('comment_notify.settings')->get('node_types') as $node_type) {
    $comment_field = FieldConfig::loadByName('node', $node_type, 'comment');
    if ($comment_field) {
      $comment_type = $comment_field->getSetting('comment_type');
      $extras['comment'][$comment_type]['form']['comment_notify_settings'] = array(
482
        'label' => t('Comment Notify settings'),
483
        'description' => t('@node_type settings for Comment Notify', array('@node_type' => NodeType::load($node_type)->label())),
484 485 486 487 488
        'weight' => 1,
      );
    }
  }

489
  $extras['user']['user']['form']['comment_notify_settings'] = array(
490 491
    'label' => t('Comment Notify settings'),
    'description' => t('User settings for Comment Notify'),
492 493 494
    'weight' => 4,
  );
  return $extras;
495
}