comment_notify.module 17.5 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 15 16 17
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;
use Drupal\field\Entity\FieldConfig;
use Drupal\node\Entity\NodeType;

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

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

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

  return $available_options;
35 36
}

37

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

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

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

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

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

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

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

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

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

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

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

122 123 124 125 126 127 128 129 130 131 132 133
  /** @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);
    }
134 135
  }
  else {
136
    $status = comment_notify_get_user_comment_notify_preference($user->id());
137
  }
138 139 140 141 142

  // Save notification settings.
  if (comment_notify_get_notification_type($comment->id())) {
    // Update existing record.
    comment_notify_update_notification($comment->id(), $status);
143
  }
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
  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());
    comment_notify_add_notification($comment->id(), $status, $notify_hash);
  }
}

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

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

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

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


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

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

  $form['comment_notify_settings'] = array(
196
    '#type' => 'details',
197 198
    '#title' => t('Comment follow-up notification settings'),
    '#weight' => 4,
199
    '#open' => TRUE,
200 201 202 203
  );

  // Only show the node followup UI if the user has permission to create nodes.
  $nodes = FALSE;
204
  foreach (node_type_get_names() as $type => $name) {
205
    if (\Drupal::entityManager()->getAccessControlHandler('node')->createAccess($type)) {
206 207 208 209 210
      $nodes = TRUE;
      break;
    }
  }

211
  if (\Drupal::currentUser()->hasPermission('administer nodes') || $nodes) {
212
    $form['comment_notify_settings']['node_notify'] = array(
213
      '#type' => 'checkbox',
214
      '#title' => t('Receive content follow-up notification e-mails'),
215
      '#default_value' => $notify_settings->node_notify,
216
      '#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.')
217 218 219 220
    );
  }
  else {
    $form['comment_notify_settings']['node_notify'] = array(
221 222
      '#type' => 'hidden',
      '#value' => COMMENT_NOTIFY_DISABLED,
223 224
    );
  }
225

226 227 228
  $available_options[COMMENT_NOTIFY_DISABLED] = t('No notifications');
  $available_options += _comment_notify_options();
  $form['comment_notify_settings']['comment_notify'] = array(
229 230
    '#type' => 'select',
    '#title' => t('Receive comment follow-up notification e-mails'),
231
    '#default_value' => $notify_settings->comment_notify,
232 233
    '#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.")
234
  );
235

236
  $form['actions']['submit']['#submit'][] = '_comment_notify_submit_user_form';
237
}
238

239 240 241 242
/**
 * Additional submit handler for the user form.
 */
function _comment_notify_submit_user_form(array &$form, FormStateInterface $form_state) {
243
  module_load_include('inc', 'comment_notify', 'comment_notify');
244

245 246
  /** @var \Drupal\user\UserInterface $user */
  $user = $form_state->getFormObject()->getEntity();
247

248 249 250 251
  if (!$user->isAnonymous()) {
    // 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'));
  }
252 253
}

254
function comment_notify_user_cancel($edit, $account, $method) {
255
  module_load_include('inc', 'comment_notify', 'comment_notify');
256 257 258
  comment_notify_delete_user_notification_setting($account->uid);
}

259 260 261 262 263 264
/**
 * 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');
265 266 267
  $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');
268 269 270 271 272 273 274 275
  $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;
276
    $comments[$cid]->notify_type = $record->notify;
277 278 279 280 281 282 283 284
    $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;
  }
}

285 286
/**
 * Private function to send the notifications.
greggles's avatar
greggles committed
287
 *
288 289
 * @param \Drupal\comment\CommentInterface $comment
 *   The comment entity.
290
 */
291
function _comment_notify_mailalert(CommentInterface $comment) {
292

293
  module_load_include('inc', 'comment_notify', 'comment_notify');
294

295 296 297
  $config = \Drupal::config('comment_notify.settings');
  $user = \Drupal::currentUser();
  $nid = $comment->getCommentedEntityId();
298 299 300 301

  // 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.
302
  if (!empty($comment->notified)) {
303 304 305
    return;
  }

306 307
  /** @var \Drupal\node\NodeInterface $node */
  $node = \Drupal::entityManager()->getStorage('node')->load($nid);
308

309 310 311
  // 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)) {
312 313 314
    return;
  }

315 316 317 318 319
  if (empty($comment->getAuthorEmail())) {
    /** @var \Drupal\user\UserInterface $comment_account */
    if ($comment_account = user_load_by_name($comment->getAuthorName())) {
      $comment_mail = $comment_account->getEmail() ?: '';
    }
320 321
  }
  else {
322
    $comment_mail = $comment->getAuthorEmail();
323
  }
324 325
  $sent_to = array();

326
  // Send to a subscribed author if they are not the current commenter.
327 328
  $author = $node->getOwner();
  $author_notify_settings = comment_notify_get_user_notification_setting($author->id()) ?: comment_notify_get_default_notification_setting();
329

330 331
  // 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)?
332 333 334 335 336 337 338 339 340 341
  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');
342
    $token_data = ['comment' => $comment, 'node' => $node];
343 344 345 346 347 348
    $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());
349
  }
350

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

354 355
  // Get the list of commenters to notify.
  $watchers = comment_notify_get_watchers($nid);
356

357
  foreach ($watchers as $alert) {
358 359
    // If the user is not anonymous, always load the current e-mail address
    // from his or her user account instead of trusting $comment->mail.
360 361
    $recipient_user = $alert->getOwner();
    $mail = !empty($recipient_user->getEmail()) ? $recipient_user->getEmail() : $alert->getAuthorEmail();
362

363 364
    $relevant_thread = Unicode::substr($thread, 0, Unicode::strlen($alert->getThread()) -1);
    if ($alert->notify == COMMENT_NOTIFY_COMMENT && strcmp($relevant_thread . '/', $alert->getThread()) != 0) {
365
      continue;
366
    }
367

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

371
      // Make sure they have access to this node before showing a bunch of node information.
372
      if (!$node->access('view', $recipient_user)) {
373
        continue;
374
      }
375

376
      $raw_values = $config->get('mail_templates.watcher');
377
      $token_data = ['comment' => $comment, 'node' => $node, 'comment-subscribed' => $alert];
378 379
      $message['subject'] = PlainTextOutput::renderFromHtml(\Drupal::token()->replace($raw_values['subject'], $token_data));
      $message['body'] = \Drupal::token()->replace($raw_values['body'], $token_data);
380

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

385
      // Make the mail link to user's /edit, unless it's an anonymous user.
386 387
      if ($alert->getOwnerId() != 0) {
        $user_mail = $alert->link($mail, 'edit-form');
388 389
      }
      else {
390
        $user_mail = Html::escape($mail);
391
      }
392

393
      // Add an entry to the watchdog log.
394
      \Drupal::logger('comment_notify')->notice('Notified: @user_mail', ['@user_mail' => $user_mail, 'link' => $alert->link(t('source comment'))]);
395 396
    }
  }
397 398
  // Record that a notification was sent for this comment so that
  // notifications aren't sent again if the comment is later edited.
399
  comment_notify_mark_comment_as_notified($comment);
400 401
}

402
/**
403
 * Implements hook_mail().
404
 */
405 406
function comment_notify_mail($key, &$message, $params) {
  $message['subject'] = $params['subject'];
407
  $message['body'][] = $params['body'];
408 409
}

410 411 412
/**
 * Get the unsubscribe link for a comment subscriber.
 *
413
 * @param \Drupal\comment\CommentInterface $comment
414 415
 *   The subscribed comment object.
 *
416 417 418 419 420
 * @return \Drupal\Core\Url|null
 *   A Url object 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?
421
 */
422
function comment_notify_get_unsubscribe_url(CommentInterface $comment) {
423
  if (!empty($comment->notify_hash)) {
424
    return \Drupal::url('comment_notify.disable', ['hash' => $comment->notify_hash]);
425
  }
426
  return NULL;
427
}
428 429 430
/**
 * Implements hook_field_extra_fields().
 */
431
function comment_notify_entity_extra_field_info() {
432 433 434
  module_load_include('inc', 'comment_notify', 'comment_notify');
  $extras = array();

435 436 437 438 439
  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(
440
        'label' => t('Comment Notify settings'),
441
        'description' => t('@node_type settings for Comment Notify', array('@node_type' => NodeType::load($node_type)->label())),
442 443 444 445 446
        'weight' => 1,
      );
    }
  }

447
  $extras['user']['user']['form']['comment_notify_settings'] = array(
448 449
    'label' => t('Comment Notify settings'),
    'description' => t('User settings for Comment Notify'),
450 451 452
    'weight' => 4,
  );
  return $extras;
453
}
454