forum.module 46.3 KB
Newer Older
Dries's avatar
Dries committed
1 2
<?php

3 4
/**
 * @file
5
 * Provides discussion forums.
6 7
 */

8
use Drupal\Core\Entity\EntityInterface;
9
use Drupal\entity\Plugin\Core\Entity\EntityDisplay;
10
use Drupal\taxonomy\Plugin\Core\Entity\Term;
11

12
/**
13
 * Implements hook_help().
14
 */
15 16
function forum_help($path, $arg) {
  switch ($path) {
17
    case 'admin/help#forum':
18 19 20 21 22 23 24 25 26 27 28 29
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('The Forum module lets you create threaded discussion forums with functionality similar to other message board systems. Forums are useful because they allow community members to discuss topics with one another while ensuring those conversations are archived for later reference. In a forum, users post topics and threads in nested hierarchies, allowing discussions to be categorized and grouped. The forum hierarchy consists of:') . '</p>';
      $output .= '<ul>';
      $output .= '<li>' . t('Optional containers (for example, <em>Support</em>), which can hold:') . '</li>';
      $output .= '<ul><li>' . t('Forums (for example, <em>Installing Drupal</em>), which can hold:') . '</li>';
      $output .= '<ul><li>' . t('Forum topics submitted by users (for example, <em>How to start a Drupal 6 Multisite</em>), which start discussions and are starting points for:') . '</li>';
      $output .= '<ul><li>' . t('Threaded comments submitted by users (for example, <em>You have these options...</em>).') . '</li>';
      $output .= '</ul>';
      $output .= '</ul>';
      $output .= '</ul>';
      $output .= '</ul>';
30
      $output .= '<p>' . t('For more information, see the online handbook entry for <a href="@forum">Forum module</a>.', array('@forum' => 'http://drupal.org/documentation/modules/forum')) . '</p>';
31 32
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
33 34 35 36
      $output .= '<dt>' . t('Setting up forum structure') . '</dt>';
      $output .= '<dd>' . t('Visit the <a href="@forums">Forums page</a> to set up containers and forums to hold your discussion topics.', array('@forums' => url('admin/structure/forum'))) . '</dd>';
      $output .= '<dt>' . t('Starting a discussion') . '</dt>';
      $output .= '<dd>' . t('The <a href="@create-topic">Forum topic</a> link on the <a href="@content-add">Add new content</a> page creates the first post of a new threaded discussion, or thread.', array('@create-topic' => url('node/add/forum'), '@content-add' => url('node/add'))) . '</dd>';
37 38
      $output .= '<dt>' . t('Forum navigation') . '</dt>';
      $output .= '<dd>' . t('Enabling the Forum module provides a default <em>Forums</em> menu item in the Tools menu that links to the <a href="@forums">Forums page</a>.', array('@forums' => url('forum'))) . '</dd>';
39 40 41
      $output .= '<dt>' . t('Moving forum topics') . '</dt>';
      $output .= '<dd>' . t('A forum topic (and all of its comments) may be moved between forums by selecting a different forum while editing a forum topic. When moving a forum topic between forums, the <em>Leave shadow copy</em> option creates a link in the original forum pointing to the new location.') . '</dd>';
      $output .= '<dt>' . t('Locking and disabling comments') . '</dt>';
42
      $output .= '<dd>' . t('Selecting <em>Closed</em> under <em>Comment settings</em> while editing a forum topic will lock (prevent new comments on) the thread. Selecting <em>Hidden</em> under <em>Comment settings</em> while editing a forum topic will hide all existing comments on the thread, and prevent new ones.') . '</dd>';
43
      $output .= '</dl>';
44
      return $output;
45
    case 'admin/structure/forum':
46
      $output = '<p>' . t('Forums contain forum topics. Use containers to group related forums.') . '</p>';
47
      $output .= theme('more_help_link', array('url' => 'admin/help/forum'));
48
      return $output;
49
    case 'admin/structure/forum/add/container':
50
      return '<p>' . t('Use containers to group related forums.') . '</p>';
51
    case 'admin/structure/forum/add/forum':
52
      return '<p>' . t('A forum holds related forum topics.') . '</p>';
53
    case 'admin/structure/forum/settings':
54
      return '<p>' . t('Adjust the display of your forum topics. Organize the forums on the <a href="@forum-structure">forum structure page</a>.', array('@forum-structure' => url('admin/structure/forum'))) . '</p>';
55 56 57
  }
}

58
/**
59
 * Implements hook_theme().
60 61 62
 */
function forum_theme() {
  return array(
63
    'forums' => array(
64
      'template' => 'forums',
65
      'variables' => array('forums' => NULL, 'topics' => NULL, 'parents' => NULL, 'tid' => NULL, 'sortby' => NULL, 'forum_per_page' => NULL),
66 67
    ),
    'forum_list' => array(
68
      'template' => 'forum-list',
69
      'variables' => array('forums' => NULL, 'parents' => NULL, 'tid' => NULL),
70 71
    ),
    'forum_topic_list' => array(
72
      'template' => 'forum-topic-list',
73
      'variables' => array('tid' => NULL, 'topics' => NULL, 'sortby' => NULL, 'forum_per_page' => NULL),
74 75
    ),
    'forum_icon' => array(
76
      'template' => 'forum-icon',
77
      'variables' => array('new_posts' => NULL, 'num_posts' => 0, 'comment_mode' => 0, 'sticky' => 0, 'first_new' => FALSE),
78
    ),
79
    'forum_submitted' => array(
80
      'template' => 'forum-submitted',
81
      'variables' => array('topic' => NULL),
82
    ),
83
    'forum_form' => array(
84
      'render element' => 'form',
85 86
      'file' => 'forum.admin.inc',
    ),
87 88 89
  );
}

90
/**
91
 * Implements hook_menu().
92
 */
93 94
function forum_menu() {
  $items['forum'] = array(
95
    'title' => 'Forums',
96 97
    'page callback' => 'forum_page',
    'access arguments' => array('access content'),
98
    'file' => 'forum.pages.inc',
99
  );
100 101
  $items['forum/%forum_forum'] = array(
    'title' => 'Forums',
102 103
    'title callback' => 'entity_page_label',
    'title arguments' => array(1),
104 105 106 107 108
    'page callback' => 'forum_page',
    'page arguments' => array(1),
    'access arguments' => array('access content'),
    'file' => 'forum.pages.inc',
  );
109
  $items['admin/structure/forum'] = array(
110
    'title' => 'Forums',
111
    'description' => 'Control forum hierarchy settings.',
112 113
    'page callback' => 'drupal_get_form',
    'page arguments' => array('forum_overview'),
114
    'access arguments' => array('administer forums'),
115
    'file' => 'forum.admin.inc',
116
  );
117
  $items['admin/structure/forum/list'] = array(
118
    'title' => 'List',
119 120
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
121
  $items['admin/structure/forum/add/container'] = array(
122
    'title' => 'Add container',
123 124
    'page callback' => 'forum_form_main',
    'page arguments' => array('container'),
125
    'access arguments' => array('administer forums'),
126
    'type' => MENU_LOCAL_ACTION,
127
    'parent' => 'admin/structure/forum',
128
    'file' => 'forum.admin.inc',
129
  );
130
  $items['admin/structure/forum/add/forum'] = array(
131
    'title' => 'Add forum',
132 133
    'page callback' => 'forum_form_main',
    'page arguments' => array('forum'),
134
    'access arguments' => array('administer forums'),
135
    'type' => MENU_LOCAL_ACTION,
136
    'parent' => 'admin/structure/forum',
137
    'file' => 'forum.admin.inc',
138
  );
139
  $items['admin/structure/forum/settings'] = array(
140
    'title' => 'Settings',
141
    'weight' => 100,
142
    'type' => MENU_LOCAL_TASK,
143
    'parent' => 'admin/structure/forum',
144
    'route_name' => 'forum_settings',
145
  );
146
  $items['admin/structure/forum/edit/container/%taxonomy_term'] = array(
147
    'title' => 'Edit container',
148
    'page callback' => 'forum_form_main',
149
    'page arguments' => array('container', 5),
150
    'access arguments' => array('administer forums'),
151
    'file' => 'forum.admin.inc',
152
  );
153
  $items['admin/structure/forum/edit/forum/%taxonomy_term'] = array(
154
    'title' => 'Edit forum',
155 156
    'page callback' => 'forum_form_main',
    'page arguments' => array('forum', 5),
157
    'access arguments' => array('administer forums'),
158
    'file' => 'forum.admin.inc',
159 160 161
  );
  return $items;
}
162

163
/**
164
 * Implements hook_menu_local_tasks().
165
 */
166
function forum_menu_local_tasks(&$data, $router_item, $root_path) {
167 168
  global $user;

169 170 171
  // Add action link to 'node/add/forum' on 'forum' sub-pages.
  if ($root_path == 'forum' || $root_path == 'forum/%') {
    $tid = (isset($router_item['page_arguments'][0]) ? $router_item['page_arguments'][0]->tid : 0);
172 173
    $forum_term = forum_forum_load($tid);
    if ($forum_term) {
174 175
      $links = array();
      // Loop through all bundles for forum taxonomy vocabulary field.
176
      $field = field_info_field('taxonomy_forums');
177 178 179 180 181
      foreach ($field['bundles']['node'] as $type) {
        if (node_access('create', $type)) {
          $links[$type] = array(
            '#theme' => 'menu_local_action',
            '#link' => array(
182
              'title' => t('Add new @node_type', array('@node_type' => node_type_get_label($type))),
183
              'href' => 'node/add/' . $type . '/' . $forum_term->tid,
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
            ),
          );
        }
      }
      if (empty($links)) {
        // Authenticated user does not have access to create new topics.
        if ($user->uid) {
          $links['disallowed'] = array(
            '#theme' => 'menu_local_action',
            '#link' => array(
              'title' => t('You are not allowed to post new content in the forum.'),
            ),
          );
        }
        // Anonymous user does not have access to create new topics.
        else {
          $links['login'] = array(
            '#theme' => 'menu_local_action',
            '#link' => array(
203
              'title' => t('<a href="@login">Log in</a> to post new content in the forum.', array(
204 205 206 207 208 209 210
                '@login' => url('user/login', array('query' => drupal_get_destination())),
              )),
              'localized_options' => array('html' => TRUE),
            ),
          );
        }
      }
211
      $data['actions'] += $links;
212 213 214
    }
  }
}
215

216 217 218 219 220 221 222 223 224 225 226 227
/**
 * Implements hook_menu_local_tasks_alter().
 *
 * Remove the 'Add Forum' and 'Add container' local tasks on the delete form.
 */
function forum_menu_local_tasks_alter(&$data, $router_item, $root_path) {
  if ($root_path == 'admin/structure/forum' && !empty($router_item['map'][3]) &&
      $router_item['map'][3] == 'delete') {
    $data = array();
  }
}

228
/**
229
 * Implements hook_entity_bundle_info_alter().
230
 */
231
function forum_entity_bundle_info_alter(&$bundles) {
232
  // Take over URI construction for taxonomy terms that are forums.
233
  if ($vid = config('forum.settings')->get('vocabulary')) {
234 235
    if (isset($bundles['taxonomy_term'][$vid])) {
      $bundles['taxonomy_term'][$vid]['uri_callback'] = 'forum_uri';
236 237 238 239 240
    }
  }
}

/**
241
 * Entity URI callback used in forum_entity_info_alter().
242 243 244 245 246 247 248
 */
function forum_uri($forum) {
  return array(
    'path' => 'forum/' . $forum->tid,
  );
}

249
/**
250
 * Checks whether a node can be used in a forum, based on its content type.
251
 *
252
 * @param \Drupal\Core\Entity\EntityInterface $node
253
 *   A node entity.
254
 *
255 256
 * @return
 *   Boolean indicating if the node can be assigned to a forum.
257
 */
258
function _forum_node_check_node_type(EntityInterface $node) {
259
  // Fetch information about the forum field.
260 261
  $instance = field_info_instance('node', 'taxonomy_forums', $node->type);
  return !empty($instance);
262
}
263

264
/**
265
 * Implements hook_node_view().
266
 */
267
function forum_node_view(EntityInterface $node, EntityDisplay $display, $view_mode) {
268
  $vid = config('forum.settings')->get('vocabulary');
269
  $vocabulary = taxonomy_vocabulary_load($vid);
270
  if (_forum_node_check_node_type($node)) {
271
    if ($view_mode == 'full' && node_is_page($node)) {
272 273 274
      // Breadcrumb navigation
      $breadcrumb[] = l(t('Home'), NULL);
      $breadcrumb[] = l($vocabulary->name, 'forum');
275
      if ($parents = taxonomy_term_load_parents_all($node->forum_tid)) {
276
        $parents = array_reverse($parents);
277
        foreach ($parents as $parent) {
278
          $breadcrumb[] = l($parent->label(), 'forum/' . $parent->tid);
279 280 281
        }
      }
      drupal_set_breadcrumb($breadcrumb);
282

283 284 285
    }
  }
}
286

287
/**
288
 * Implements hook_node_validate().
289
 *
290 291
 * Checks in particular that the node is assigned only a "leaf" term in the
 * forum taxonomy.
292
 */
293
function forum_node_validate(EntityInterface $node, $form) {
294 295
  if (_forum_node_check_node_type($node)) {
    $langcode = $form['taxonomy_forums']['#language'];
296
    // vocabulary is selected, not a "container" term.
297
    if (!empty($node->taxonomy_forums[$langcode])) {
298
      // Extract the node's proper topic ID.
299
      $containers = config('forum.settings')->get('containers');
300 301 302 303 304 305 306
      foreach ($node->taxonomy_forums[$langcode] as $delta => $item) {
        // If no term was selected (e.g. when no terms exist yet), remove the
        // item.
        if (empty($item['tid'])) {
          unset($node->taxonomy_forums[$langcode][$delta]);
          continue;
        }
307
        $term = taxonomy_term_load($item['tid']);
308 309 310 311
        if (!$term) {
          form_set_error('taxonomy_forums', t('Select a forum.'));
          continue;
        }
312
        $used = db_query_range('SELECT 1 FROM {taxonomy_term_data} WHERE tid = :tid AND vid = :vid', 0, 1, array(
313
          ':tid' => $term->tid,
314
          ':vid' => $term->bundle(),
315
        ))->fetchField();
316
        if ($used && in_array($term->tid, $containers)) {
317
          form_set_error('taxonomy_forums', t('The item %forum is a forum container, not a forum. Select one of the forums below instead.', array('%forum' => $term->label())));
318 319
        }
      }
320 321 322
    }
  }
}
323

324
/**
325
 * Implements hook_node_presave().
326
 *
327
 * Assigns the forum taxonomy when adding a topic from within a forum.
328
 */
329
function forum_node_presave(EntityInterface $node) {
330
  if (_forum_node_check_node_type($node)) {
331 332
    // Make sure all fields are set properly:
    $node->icon = !empty($node->icon) ? $node->icon : '';
333 334
    reset($node->taxonomy_forums);
    $langcode = key($node->taxonomy_forums);
335
    if (!empty($node->taxonomy_forums[$langcode])) {
336
      $node->forum_tid = $node->taxonomy_forums[$langcode][0]['tid'];
337
      $old_tid = db_query_range("SELECT f.tid FROM {forum} f INNER JOIN {node} n ON f.vid = n.vid WHERE n.nid = :nid ORDER BY f.vid DESC", 0, 1, array(':nid' => $node->nid))->fetchField();
338
      if ($old_tid && isset($node->forum_tid) && ($node->forum_tid != $old_tid) && !empty($node->shadow)) {
339
        // A shadow copy needs to be created. Retain new term and add old term.
340
        $node->taxonomy_forums[$langcode][] = array('tid' => $old_tid);
341 342 343 344
      }
    }
  }
}
345

346
/**
347
 * Implements hook_node_update().
348
 */
349
function forum_node_update(EntityInterface $node) {
350
  if (_forum_node_check_node_type($node)) {
351 352 353
    // If this is not a new revision and does exist, update the forum record,
    // otherwise insert a new one.
    if ($node->getRevisionId() == $node->original->getRevisionId() && db_query('SELECT tid FROM {forum} WHERE nid=:nid', array(':nid' => $node->nid))->fetchField()) {
354
      if (!empty($node->forum_tid)) {
355
        db_update('forum')
356
          ->fields(array('tid' => $node->forum_tid))
357 358
          ->condition('vid', $node->vid)
          ->execute();
359 360 361
      }
      // The node is removed from the forum.
      else {
362 363 364
        db_delete('forum')
          ->condition('nid', $node->nid)
          ->execute();
365 366 367
      }
    }
    else {
368
      if (!empty($node->forum_tid)) {
369 370
        db_insert('forum')
          ->fields(array(
371
            'tid' => $node->forum_tid,
372 373 374 375
            'vid' => $node->vid,
            'nid' => $node->nid,
          ))
          ->execute();
376
      }
377
    }
378 379
    // If the node has a shadow forum topic, update the record for this
    // revision.
380
    if (!empty($node->shadow)) {
381 382 383 384 385 386 387 388 389 390 391 392
      db_delete('forum')
        ->condition('nid', $node->nid)
        ->condition('vid', $node->vid)
        ->execute();
      db_insert('forum')
        ->fields(array(
          'nid' => $node->nid,
          'vid' => $node->vid,
          'tid' => $node->forum_tid,
        ))
        ->execute();
     }
393 394
  }
}
395

396
/**
397
 * Implements hook_node_insert().
398
 */
399
function forum_node_insert(EntityInterface $node) {
400 401
  if (_forum_node_check_node_type($node)) {
    if (!empty($node->forum_tid)) {
402 403
      $nid = db_insert('forum')
        ->fields(array(
404
          'tid' => $node->forum_tid,
405 406 407 408
          'vid' => $node->vid,
          'nid' => $node->nid,
        ))
        ->execute();
409 410 411
    }
  }
}
412

413
/**
414
 * Implements hook_node_predelete().
415
 */
416
function forum_node_predelete(EntityInterface $node) {
417
  if (_forum_node_check_node_type($node)) {
418 419 420
    db_delete('forum')
      ->condition('nid', $node->nid)
      ->execute();
421 422 423
    db_delete('forum_index')
      ->condition('nid', $node->nid)
      ->execute();
424
  }
425
}
426

427
/**
428
 * Implements hook_node_load().
429
 */
430
function forum_node_load($nodes) {
431 432
  $node_vids = array();
  foreach ($nodes as $node) {
433
    if (_forum_node_check_node_type($node)) {
434 435 436 437
      $node_vids[] = $node->vid;
    }
  }
  if (!empty($node_vids)) {
438 439 440 441 442
    $query = db_select('forum', 'f');
    $query
      ->fields('f', array('nid', 'tid'))
      ->condition('f.vid', $node_vids);
    $result = $query->execute();
443 444 445
    foreach ($result as $record) {
      $nodes[$record->nid]->forum_tid = $record->tid;
    }
446
  }
447 448
}

449
/**
450
 * Implements hook_node_info().
451
 */
452
function forum_node_info() {
453 454
  return array(
    'forum' => array(
455
      'name' => t('Forum topic'),
456
      'base' => 'forum',
457
      'description' => t('A <em>forum topic</em> starts a new discussion thread within a forum.'),
458 459 460
      'title_label' => t('Subject'),
    )
  );
Dries's avatar
Dries committed
461 462
}

463
/**
464
 * Implements hook_permission().
465
 */
466
function forum_permission() {
467
  $perms = array(
468 469 470
    'administer forums' => array(
      'title' => t('Administer forums'),
    ),
471 472
  );
  return $perms;
473
}
Dries's avatar
Dries committed
474

475
/**
476
 * Implements hook_taxonomy_term_delete().
477
 */
478
function forum_taxonomy_term_delete(Term $term) {
479
  // For containers, remove the tid from the forum_containers variable.
480 481
  $config = config('forum.settings');
  $containers = $config->get('containers');
482
  $key = array_search($term->tid, $containers);
483 484 485
  if ($key !== FALSE) {
    unset($containers[$key]);
  }
486
  $config->set('containers', $containers)->save();
487
}
488

489
/**
490
 * Implements hook_comment_publish().
491
 *
492
 * This actually handles the insertion and update of published nodes since
493 494 495
 * comment_save() calls hook_comment_publish() for all published comments.
 */
function forum_comment_publish($comment) {
496
  _forum_update_forum_index($comment->nid->target_id);
497 498 499
}

/**
500
 * Implements hook_comment_update().
501
 *
502 503
 * The Comment module doesn't call hook_comment_unpublish() when saving
 * individual comments, so we need to check for those here.
504 505
 */
function forum_comment_update($comment) {
506 507
  // comment_save() calls hook_comment_publish() for all published comments,
  // so we need to handle all other values here.
508
  if (!$comment->status->value) {
509
    _forum_update_forum_index($comment->nid->target_id);
510 511 512 513
  }
}

/**
514
 * Implements hook_comment_unpublish().
515 516
 */
function forum_comment_unpublish($comment) {
517
  _forum_update_forum_index($comment->nid->target_id);
518 519 520
}

/**
521
 * Implements hook_comment_delete().
522 523
 */
function forum_comment_delete($comment) {
524
  _forum_update_forum_index($comment->nid->target_id);
525 526 527
}

/**
528
 * Implements hook_field_storage_pre_insert().
529
 */
530 531
function forum_field_storage_pre_insert(EntityInterface $entity, &$skip_fields) {
  if ($entity->entityType() == 'node' && $entity->status && _forum_node_check_node_type($entity)) {
532
    $query = db_insert('forum_index')->fields(array('nid', 'title', 'tid', 'sticky', 'created', 'comment_count', 'last_comment_timestamp'));
533 534 535 536 537 538 539 540 541 542 543
    foreach ($entity->getTranslationLanguages() as $langcode => $language) {
      $translation = $entity->getTranslation($langcode, FALSE);
      $query->values(array(
        'nid' => $entity->id(),
        'title' => $translation->title->value,
        'tid' => $translation->taxonomy_forums->tid,
        'sticky' => $entity->sticky,
        'created' => $entity->created,
        'comment_count' => 0,
        'last_comment_timestamp' => $entity->created,
      ));
544 545 546 547 548 549
    }
    $query->execute();
  }
}

/**
550
 * Implements hook_field_storage_pre_update().
551
 */
552
function forum_field_storage_pre_update(EntityInterface $entity, &$skip_fields) {
553 554
  $first_call = &drupal_static(__FUNCTION__, array());

555
  if ($entity->entityType() == 'node' && _forum_node_check_node_type($entity)) {
556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579

    // If the node is published, update the forum index.
    if ($entity->status) {

      // We don't maintain data for old revisions, so clear all previous values
      // from the table. Since this hook runs once per field, per object, make
      // sure we only wipe values once.
      if (!isset($first_call[$entity->nid])) {
        $first_call[$entity->nid] = FALSE;
        db_delete('forum_index')->condition('nid', $entity->nid)->execute();
      }
      $query = db_insert('forum_index')->fields(array('nid', 'title', 'tid', 'sticky', 'created', 'comment_count', 'last_comment_timestamp'));
      foreach ($entity->taxonomy_forums as $language) {
        foreach ($language as $item) {
          $query->values(array(
            'nid' => $entity->nid,
            'title' => $entity->title,
            'tid' => $item['tid'],
            'sticky' => $entity->sticky,
            'created' => $entity->created,
            'comment_count' => 0,
            'last_comment_timestamp' => $entity->created,
          ));
        }
580
      }
581 582 583 584
      $query->execute();
      // The logic for determining last_comment_count is fairly complex, so
      // call _forum_update_forum_index() too.
      _forum_update_forum_index($entity->nid);
585
    }
586 587 588 589 590 591

    // When a forum node is unpublished, remove it from the forum_index table.
    else {
      db_delete('forum_index')->condition('nid', $entity->nid)->execute();
    }

592 593
  }
}
594

595
/**
596
 * Implements hook_form_BASE_FORM_ID_alter().
597
 */
598
function forum_form_taxonomy_vocabulary_form_alter(&$form, &$form_state, $form_id) {
599
  $vid = config('forum.settings')->get('vocabulary');
600 601
  $vocabulary = $form_state['controller']->getEntity($form_state);
  if ($vid == $vocabulary->id()) {
602 603 604 605 606 607 608 609 610 611
    $form['help_forum_vocab'] = array(
      '#markup' => t('This is the designated forum vocabulary. Some of the normal vocabulary options have been removed.'),
      '#weight' => -1,
    );
    // Forum's vocabulary always has single hierarchy. Forums and containers
    // have only one parent or no parent for root items. By default this value
    // is 0.
    $form['hierarchy']['#value'] = TAXONOMY_HIERARCHY_SINGLE;
    // Do not allow to delete forum's vocabulary.
    $form['actions']['delete']['#access'] = FALSE;
612 613
    // Do not allow to change a vid of forum's vocabulary.
    $form['vid']['#disabled'] = TRUE;
614 615 616 617
  }
}

/**
618
 * Implements hook_form_FORM_ID_alter() for taxonomy_term_form().
619
 */
620
function forum_form_taxonomy_term_form_alter(&$form, &$form_state, $form_id) {
621
  $vid = config('forum.settings')->get('vocabulary');
622
  if (isset($form['vid']['#value']) && $form['vid']['#value'] == $vid) {
623
    // Hide multiple parents select from forum terms.
624
    $form['relations']['parent']['#access'] = FALSE;
625
  }
626 627 628
}

/**
629
 * Implements hook_form_BASE_FORM_ID_alter() for node_form().
630 631 632
 */
function forum_form_node_form_alter(&$form, &$form_state, $form_id) {
  if (isset($form['taxonomy_forums'])) {
633
    $langcode = $form['taxonomy_forums']['#language'];
634
    // Make the vocabulary required for 'real' forum-nodes.
635 636
    $form['taxonomy_forums'][$langcode]['#required'] = TRUE;
    $form['taxonomy_forums'][$langcode]['#multiple'] = FALSE;
637 638
    if (empty($form['taxonomy_forums'][$langcode]['#default_value'])) {
      // If there is no default forum already selected, try to get the forum
639 640
      // ID from the URL (e.g., if we are on a page like node/add/forum/2, we
      // expect "2" to be the ID of the forum that was requested).
641
      $requested_forum_id = arg(3);
642
      $form['taxonomy_forums'][$langcode]['#default_value'] = is_numeric($requested_forum_id) ? $requested_forum_id : '';
643
    }
644
  }
645 646
}

647
/**
648 649 650 651 652 653
 * Render API callback: Lists nodes based on the element's #query property.
 *
 * This function can be used as a #pre_render callback.
 *
 * @see forum_block_view()
 */
654 655 656
function forum_block_view_pre_render($elements) {
  $result = $elements['#query']->execute();
  if ($node_title_list = node_title_list($result)) {
657
    $elements['forum_list'] = $node_title_list;
658
    $elements['forum_more'] = array('#theme' => 'more_link', '#url' => 'forum', '#title' => t('Read the latest forum topics.'));
659
  }
660
  return $elements;
661 662
}

663
/**
664
 * Implements hook_form().
665
 */
666
function forum_form(EntityInterface $node, &$form_state) {
667
  $type = node_type_load($node->type);
668 669 670 671 672 673
  $form['title'] = array(
    '#type' => 'textfield',
    '#title' => check_plain($type->title_label),
    '#default_value' => !empty($node->title) ? $node->title : '',
    '#required' => TRUE, '#weight' => -5
  );
674

675
  if (!empty($node->nid)) {
676
    $forum_terms = $node->taxonomy_forums;
677
    // If editing, give option to leave shadows.
678
    $shadow = (count($forum_terms) > 1);
679
    $form['shadow'] = array('#type' => 'checkbox', '#title' => t('Leave shadow copy'), '#default_value' => $shadow, '#description' => t('If you move this topic, you can leave a link in the old forum to the new forum.'));
680
    $form['forum_tid'] = array('#type' => 'value', '#value' => $node->forum_tid);
Dries's avatar
Dries committed
681
  }
682

683
  return $form;
Dries's avatar
Dries committed
684 685
}

686
/**
687
 * Returns a tree of all forums for a given taxonomy term ID.
688 689
 *
 * @param $tid
690 691 692
 *   (optional) Taxonomy term ID of the forum. If not given all forums will be
 *   returned.
 *
693
 * @return
694
 *   A tree of taxonomy objects, with the following additional properties:
695 696 697 698
 *   - num_topics: Number of topics in the forum.
 *   - num_posts: Total number of posts in all topics.
 *   - last_post: Most recent post for the forum.
 *   - forums: An array of child forums.
699
 */
700
function forum_forum_load($tid = NULL) {
701 702
  $cache = &drupal_static(__FUNCTION__, array());

703 704 705 706
  // Return a cached forum tree if available.
  if (!isset($tid)) {
    $tid = 0;
  }
707 708 709
  if (isset($cache[$tid])) {
    return $cache[$tid];
  }
710

711 712
  $config = config('forum.settings');
  $vid = $config->get('vocabulary');
713 714 715 716

  // Load and validate the parent term.
  if ($tid) {
    $forum_term = taxonomy_term_load($tid);
717
    if (!$forum_term || ($forum_term->bundle() != $vid)) {
718 719 720
      return $cache[$tid] = FALSE;
    }
  }
721
  // If $tid is 0, create an empty entity to hold the child terms.
722
  elseif ($tid === 0) {
723
    $forum_term = entity_create('taxonomy_term', array(
724
      'tid' => 0,
725
    ));
726 727 728
  }

  // Determine if the requested term is a container.
729
  if (!$forum_term->tid || in_array($forum_term->tid, $config->get('containers'))) {
730 731 732 733
    $forum_term->container = 1;
  }

  // Load parent terms.
734
  $forum_term->parents = taxonomy_term_load_parents_all($forum_term->tid);
735 736 737

  // Load the tree below.
  $forums = array();
738
  $_forums = taxonomy_get_tree($vid, $tid, NULL, TRUE);
739

740
  if (count($_forums)) {
741 742
    $query = db_select('node', 'n');
    $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
743
    $query->join('forum', 'f', 'n.vid = f.vid');
744 745 746
    $query->addExpression('COUNT(n.nid)', 'topic_count');
    $query->addExpression('SUM(ncs.comment_count)', 'comment_count');
    $counts = $query
747
      ->fields('f', array('tid'))
748
      ->condition('n.status', 1)
749 750 751 752
      ->groupBy('tid')
      ->addTag('node_access')
      ->execute()
      ->fetchAllAssoc('tid');
753
  }
754

755
  foreach ($_forums as $forum) {
756
    // Determine if the child term is a container.
757
    if (in_array($forum->tid, $config->get('containers'))) {
758 759
      $forum->container = 1;
    }
760

761
    // Merge in the topic and post counters.
762
    if (!empty($counts[$forum->tid])) {
763 764 765 766 767 768 769 770
      $forum->num_topics = $counts[$forum->tid]->topic_count;
      $forum->num_posts = $counts[$forum->tid]->topic_count + $counts[$forum->tid]->comment_count;
    }
    else {
      $forum->num_topics = 0;
      $forum->num_posts = 0;
    }

771
    // Query "Last Post" information for this forum.
772
    $query = db_select('node', 'n');
773
    $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', array(':tid' => $forum->tid));
774
    $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
775 776
    $query->join('users', 'u', 'ncs.last_comment_uid = u.uid');
    $query->addExpression('CASE ncs.last_comment_uid WHEN 0 THEN ncs.last_comment_name ELSE u.name END', 'last_comment_name');
777

778 779 780 781 782 783 784 785
    $topic = $query
      ->fields('ncs', array('last_comment_timestamp', 'last_comment_uid'))
      ->condition('n.status', 1)
      ->orderBy('last_comment_timestamp', 'DESC')
      ->range(0, 1)
      ->addTag('node_access')
      ->execute()
      ->fetchObject();
786

787
    // Merge in the "Last Post" information.
788
    $last_post = new stdClass();
789
    if (!empty($topic->last_comment_timestamp)) {
790
      $last_post->created = $topic->last_comment_timestamp;
791 792 793
      $last_post->name = $topic->last_comment_name;
      $last_post->uid = $topic->last_comment_uid;
    }
794
    $forum->last_post = $last_post;
795

796 797 798
    $forums[$forum->tid] = $forum;
  }

799 800 801 802
  // Cache the result, and return the tree.
  $forum_term->forums = $forums;
  $cache[$tid] = $forum_term;
  return $forum_term;
803 804
}

805
/**
806 807
 * Calculates the number of new posts in a forum that the user has not yet read.
 *
808
 * Nodes are new if they are newer than HISTORY_READ_LIMIT.
809 810 811 812 813 814 815 816
 *
 * @param $term
 *   The term ID of the forum.
 * @param $uid
 *   The user ID.
 *
 * @return
 *   The number of new posts in the forum that have not been read by the user.
817 818
 */
function _forum_topics_unread($term, $uid) {
819
  $query = db_select('node', 'n');
820
  $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', array(':tid' => $term));
821
  $query->leftJoin('history', 'h', 'n.nid = h.nid AND h.uid = :uid', array(':uid' => $uid));
822 823 824
  $query->addExpression('COUNT(n.nid)', 'count');
  return $query
    ->condition('status', 1)
825
    ->condition('n.created', HISTORY_READ_LIMIT, '>')
826 827 828 829
    ->isNull('h.nid')
    ->addTag('node_access')
    ->execute()
    ->fetchField();
830 831
}

832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848
/**
 * Gets all the topics in a forum.
 *
 * @param $tid
 *   The term ID of the forum.
 * @param $sortby
 *   One of the following integers indicating the sort criteria:
 *   - 1: Date - newest first.
 *   - 2: Date - oldest first.
 *   - 3: Posts with the most comments first.
 *   - 4: Posts with the least comments first.
 * @param $forum_per_page
 *   The maximum number of topics to display per page.
 *
 * @return
 *   A list of all the topics in a forum.
 */
Dries's avatar
Dries committed
849
function forum_get_topics($tid, $sortby, $forum_per_page) {
850
  global $user, $forum_topic_list_header;
851

852
  $forum_topic_list_header = array(
853 854 855
    array('data' => t('Topic'), 'field' => 'f.title'),
    array('data' => t('Replies'), 'field' => 'f.comment_count'),
    array('data' => t('Last reply'), 'field' => 'f.last_comment_timestamp'),
856
  );
857

Dries's avatar
Dries committed
858
  $order = _forum_get_topic_order($sortby);
859
  for ($i = 0; $i < count($forum_topic_list_header); $i++) {
Dries's avatar
Dries committed
860 861
    if ($forum_topic_list_header[$i]['field'] == $order['field']) {
      $forum_topic_list_header[$i]['sort'] = $order['sort'];
862 863 864
    }
  }

865 866
  $query = db_select('forum_index', 'f')
    ->extend('Drupal\Core\Database\Query\PagerSelectExtender')
867
    ->extend('Drupal\Core\Database\Query\TableSortExtender');
868
  $query->fields('f');
869
  $query
870
    ->condition('f.tid', $tid)
871
    ->addTag('node_access')
872
    ->addMetaData('base_table', 'forum_index')
873
    ->orderBy('f.sticky', 'DESC')
874 875
    ->orderByHeader($forum_topic_list_header)
    ->limit($forum_per_page);
876

877 878
  $count_query = db_select('forum_index', 'f');
  $count_query->condition('f.tid', $tid);
879
  $count_query->addExpression('COUNT(*)');
880
  $count_query->addTag('node_access');
881
  $count_query->addMetaData('base_table', 'forum_index');
882

883 884
  $query->setCountQuery($count_query);
  $result = $query->execute();
885 886 887 888 889
  $nids = array();
  foreach ($result as $record) {
    $nids[] = $record->nid;
  }
  if ($nids) {
890 891
    $nodes = node_load_multiple($nids);

892 893
    $query = db_select('node', 'n')
      ->extend('Drupal\Core\Database\Query\TableSortExtender');
894
    $query->fields('n', array('nid'));
895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913

    $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
    $query->fields('ncs', array('cid', 'last_comment_uid', 'last_comment_timestamp', 'comment_count'));

    $query->join('forum_index', 'f', 'f.nid = ncs.nid');
    $query->addField('f', 'tid', 'forum_tid');

    $query->join('users', 'u', 'n.uid = u.uid');
    $query->addField('u', 'name');

    $query->join('users', 'u2', 'ncs.last_comment_uid = u2.uid');

    $query->addExpression('CASE ncs.last_comment_uid WHEN 0 THEN ncs.last_comment_name ELSE u2.name END', 'last_comment_name');

    $query
      ->orderBy('f.sticky', 'DESC')
      ->orderByHeader($forum_topic_list_header)
      ->condition('n.nid', $nids);

914 915 916 917 918 919 920 921 922 923
    $result = array();
    foreach ($query->execute() as $row) {
      $topic = $nodes[$row->nid];
      $topic->comment_mode = $topic->comment;

      foreach ($row as $key => $value) {
        $topic->{$key} = $value;
      }
      $result[] = $topic;
    }
924 925 926 927 928
  }
  else {
    $result = array();
  }

929
  $topics = array();
930
  $first_new_found = FALSE;
931
  foreach ($result as $topic) {
Dries's avatar
Dries committed
932
    if ($user->uid) {
933 934
      // A forum is new if the topic is new, or if there are new comments since
      // the user's last visit.
935
      if ($topic->forum_tid != $tid) {
Dries's avatar
Dries committed
936
        $topic->new = 0;
937 938
      }
      else {
Dries's avatar
Dries committed
939
        $history = _forum_user_last_visit($topic->nid);
940
        $topic->new_replies = comment_num_new($topic->nid, $history);
941
        $topic->new = $topic->new_replies || ($topic->last_comment_timestamp > $history);
942
      }
943 944
    }
    else {
945
      // Do not track "new replies" status for topics if the user is anonymous.
Dries's avatar
Dries committed
946 947
      $topic->new_replies = 0;
      $topic->new = 0;
948
    }
949

950 951 952 953 954 955 956
    // Make sure only one topic is indicated as the first new topic.
    $topic->first_new = FALSE;
    if ($topic->new != 0 && !$first_new_found) {
      $topic->first_new = TRUE;
      $first_new_found = TRUE;
    }

957
    if ($topic->comment_count > 0) {
958
      $last_reply = new stdClass();
959
      $last_reply->created = $topic->last_comment_timestamp;
960 961 962 963
      $last_reply->name = $topic->last_comment_name;
      $last_reply->uid = $topic->last_comment_uid;
      $topic->last_reply = $last_reply;
    }
964
    $topics[$topic->nid] = $topic;
965 966
  }

Dries's avatar
Dries committed
967
  return $topics;
968 969
}

970
/**
971
 * Implements hook_preprocess_HOOK() for block.tpl.php.
972 973 974
 */
function forum_preprocess_block(&$variables) {
  if ($variables['block']->module == 'forum') {
975
    $variables['attributes']['role'] = 'navigation';
976 977 978
  }
}

979
/**
980
 * Preprocesses variables for forums.tpl.php.
981
 *
982 983 984 985 986 987 988 989 990 991 992 993 994 995
 * @param $variables
 *   An array containing the following elements:
 *   - forums: An array of all forum objects to display for the given taxonomy
 *     term ID. If tid = 0 then all the top-level forums are displayed.
 *   - topics: An array of all the topics in the current forum.
 *   - parents: An array of taxonomy term objects that are ancestors of the
 *     current term ID.
 *   - tid: Taxonomy term ID of the current forum.
 *   - sortby: One of the following integers indicating the sort criteria:
 *     - 1: Date - newest first.
 *     - 2: Date - oldest first.
 *     - 3: Posts with the most comments first.
 *     - 4: Posts with the least comments first.
 *   - forum_per_page: The maximum number of topics to display per page.
996
 *
997
 * @see forums.tpl.php
998
 */
999
function template_preprocess_forums(&$variables) {
1000
  if ($variables['forums_defined'] = count($variables['forums']) || count($variables['parents'])) {
1001
    if (!empty($variables['forums'])) {
1002
      $variables['forums'] = theme('forum_list', $variables);
1003 1004 1005 1006
    }
    else {
      $variables['forums'] = '';
    }
1007

1008
    if ($variables['tid'] && array_search($variables['tid'], config('forum.settings')->get('containers')) === FALSE) {
1009
      $variables['topics'] = theme('forum_topic_list', $variables);
1010
    }
1011 1012 1013 1014 1015
    else {
      $variables['topics'] = '';
    }

    // Provide separate template suggestions based on what's being output. Topic id is also accounted for.
1016
    // Check both variables to be safe then the inverse. Forums with topic ID's take precedence.
1017
    if ($variables['forums'] && !$variables['topics']) {
1018 1019 1020
      $variables['theme_hook_suggestions'][] = 'forums__containers';
      $variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid'];
      $variables['theme_hook_suggestions'][] = 'forums__containers__' . $variables['tid'];
1021
    }
1022
    elseif (!$variables['forums'] && $variables['topics']) {
1023 1024 1025
      $variables['theme_hook_suggestions'][] = 'forums__topics';
      $variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid'];
      $variables['theme_hook_suggestions'][] = 'forums__topics__' . $variables['tid'];
1026
    }
1027
    else {
1028
      $variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid'];
1029
    }
1030

1031 1032
  }
  else {
1033 1034
    $variables['forums'] = '';
    $variables['topics'] = '';
1035 1036 1037
  }
}

1038
/**
jhodgdon's avatar