forum.module 46 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\node\Plugin\Core\Entity\Node;
10
use Drupal\entity\Plugin\Core\Entity\EntityDisplay;
11
use Drupal\taxonomy\Plugin\Core\Entity\Term;
12

13
/**
14
 * Implements hook_help().
15
 */
16 17
function forum_help($path, $arg) {
  switch ($path) {
18
    case 'admin/help#forum':
19 20 21 22 23 24 25 26 27 28 29 30
      $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>';
31
      $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>';
32 33
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
34 35 36 37
      $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>';
38 39
      $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>';
40 41 42
      $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>';
43
      $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>';
44
      $output .= '</dl>';
45
      return $output;
46
    case 'admin/structure/forum':
47
      $output = '<p>' . t('Forums contain forum topics. Use containers to group related forums.') . '</p>';
48
      $output .= theme('more_help_link', array('url' => 'admin/help/forum'));
49
      return $output;
50
    case 'admin/structure/forum/add/container':
51
      return '<p>' . t('Use containers to group related forums.') . '</p>';
52
    case 'admin/structure/forum/add/forum':
53
      return '<p>' . t('A forum holds related forum topics.') . '</p>';
54
    case 'admin/structure/forum/settings':
55
      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>';
56 57 58
  }
}

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

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

167
/**
168
 * Implements hook_menu_local_tasks().
169
 */
170
function forum_menu_local_tasks(&$data, $router_item, $root_path) {
171 172
  global $user;

173 174 175
  // 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);
176 177
    $forum_term = forum_forum_load($tid);
    if ($forum_term) {
178 179
      $links = array();
      // Loop through all bundles for forum taxonomy vocabulary field.
180
      $field = field_info_field('taxonomy_forums');
181 182 183 184 185
      foreach ($field['bundles']['node'] as $type) {
        if (node_access('create', $type)) {
          $links[$type] = array(
            '#theme' => 'menu_local_action',
            '#link' => array(
186
              'title' => t('Add new @node_type', array('@node_type' => node_type_get_label($type))),
187
              'href' => 'node/add/' . $type . '/' . $forum_term->tid,
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
            ),
          );
        }
      }
      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(
207
              'title' => t('<a href="@login">Log in</a> to post new content in the forum.', array(
208 209 210 211 212 213 214
                '@login' => url('user/login', array('query' => drupal_get_destination())),
              )),
              'localized_options' => array('html' => TRUE),
            ),
          );
        }
      }
215
      $data['actions'] += $links;
216 217 218
    }
  }
}
219

220
/**
221
 * Implements hook_entity_bundle_info_alter().
222
 */
223
function forum_entity_bundle_info_alter(&$bundles) {
224
  // Take over URI construction for taxonomy terms that are forums.
225
  if ($vid = config('forum.settings')->get('vocabulary')) {
226 227
    if (isset($bundles['taxonomy_term'][$vid])) {
      $bundles['taxonomy_term'][$vid]['uri_callback'] = 'forum_uri';
228 229 230 231 232
    }
  }
}

/**
233
 * Entity URI callback used in forum_entity_info_alter().
234 235 236 237 238 239 240
 */
function forum_uri($forum) {
  return array(
    'path' => 'forum/' . $forum->tid,
  );
}

241
/**
242
 * Checks whether a node can be used in a forum, based on its content type.
243
 *
244 245
 * @param Drupal\node\Node $node
 *   A node entity.
246
 *
247 248
 * @return
 *   Boolean indicating if the node can be assigned to a forum.
249
 */
250
function _forum_node_check_node_type(Node $node) {
251
  // Fetch information about the forum field.
252 253
  $instance = field_info_instance('node', 'taxonomy_forums', $node->type);
  return !empty($instance);
254
}
255

256
/**
257
 * Implements hook_node_view().
258
 */
259
function forum_node_view(Node $node, EntityDisplay $display, $view_mode) {
260
  $vid = config('forum.settings')->get('vocabulary');
261
  $vocabulary = taxonomy_vocabulary_load($vid);
262
  if (_forum_node_check_node_type($node)) {
263
    if ($view_mode == 'full' && node_is_page($node)) {
264 265 266
      // Breadcrumb navigation
      $breadcrumb[] = l(t('Home'), NULL);
      $breadcrumb[] = l($vocabulary->name, 'forum');
267
      if ($parents = taxonomy_term_load_parents_all($node->forum_tid)) {
268
        $parents = array_reverse($parents);
269
        foreach ($parents as $parent) {
270
          $breadcrumb[] = l($parent->label(), 'forum/' . $parent->tid);
271 272 273
        }
      }
      drupal_set_breadcrumb($breadcrumb);
274

275 276 277
    }
  }
}
278

279
/**
280
 * Implements hook_node_validate().
281
 *
282 283
 * Checks in particular that the node is assigned only a "leaf" term in the
 * forum taxonomy.
284
 */
285
function forum_node_validate(Node $node, $form) {
286 287
  if (_forum_node_check_node_type($node)) {
    $langcode = $form['taxonomy_forums']['#language'];
288
    // vocabulary is selected, not a "container" term.
289
    if (!empty($node->taxonomy_forums[$langcode])) {
290
      // Extract the node's proper topic ID.
291
      $containers = config('forum.settings')->get('containers');
292 293 294 295 296 297 298
      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;
        }
299
        $term = taxonomy_term_load($item['tid']);
300 301 302 303
        if (!$term) {
          form_set_error('taxonomy_forums', t('Select a forum.'));
          continue;
        }
304
        $used = db_query_range('SELECT 1 FROM {taxonomy_term_data} WHERE tid = :tid AND vid = :vid', 0, 1, array(
305
          ':tid' => $term->tid,
306
          ':vid' => $term->bundle(),
307
        ))->fetchField();
308
        if ($used && in_array($term->tid, $containers)) {
309
          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())));
310 311
        }
      }
312 313 314
    }
  }
}
315

316
/**
317
 * Implements hook_node_presave().
318
 *
319
 * Assigns the forum taxonomy when adding a topic from within a forum.
320
 */
321
function forum_node_presave(Node $node) {
322
  if (_forum_node_check_node_type($node)) {
323 324
    // Make sure all fields are set properly:
    $node->icon = !empty($node->icon) ? $node->icon : '';
325 326
    reset($node->taxonomy_forums);
    $langcode = key($node->taxonomy_forums);
327
    if (!empty($node->taxonomy_forums[$langcode])) {
328
      $node->forum_tid = $node->taxonomy_forums[$langcode][0]['tid'];
329
      $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();
330
      if ($old_tid && isset($node->forum_tid) && ($node->forum_tid != $old_tid) && !empty($node->shadow)) {
331
        // A shadow copy needs to be created. Retain new term and add old term.
332
        $node->taxonomy_forums[$langcode][] = array('tid' => $old_tid);
333 334 335 336
      }
    }
  }
}
337

338
/**
339
 * Implements hook_node_update().
340
 */
341
function forum_node_update(Node $node) {
342
  if (_forum_node_check_node_type($node)) {
343 344 345
    // 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()) {
346
      if (!empty($node->forum_tid)) {
347
        db_update('forum')
348
          ->fields(array('tid' => $node->forum_tid))
349 350
          ->condition('vid', $node->vid)
          ->execute();
351 352 353
      }
      // The node is removed from the forum.
      else {
354 355 356
        db_delete('forum')
          ->condition('nid', $node->nid)
          ->execute();
357 358 359
      }
    }
    else {
360
      if (!empty($node->forum_tid)) {
361 362
        db_insert('forum')
          ->fields(array(
363
            'tid' => $node->forum_tid,
364 365 366 367
            'vid' => $node->vid,
            'nid' => $node->nid,
          ))
          ->execute();
368
      }
369
    }
370 371
    // If the node has a shadow forum topic, update the record for this
    // revision.
372
    if (!empty($node->shadow)) {
373 374 375 376 377 378 379 380 381 382 383 384
      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();
     }
385 386
  }
}
387

388
/**
389
 * Implements hook_node_insert().
390
 */
391
function forum_node_insert(Node $node) {
392 393
  if (_forum_node_check_node_type($node)) {
    if (!empty($node->forum_tid)) {
394 395
      $nid = db_insert('forum')
        ->fields(array(
396
          'tid' => $node->forum_tid,
397 398 399 400
          'vid' => $node->vid,
          'nid' => $node->nid,
        ))
        ->execute();
401 402 403
    }
  }
}
404

405
/**
406
 * Implements hook_node_predelete().
407
 */
408
function forum_node_predelete(Node $node) {
409
  if (_forum_node_check_node_type($node)) {
410 411 412
    db_delete('forum')
      ->condition('nid', $node->nid)
      ->execute();
413 414 415
    db_delete('forum_index')
      ->condition('nid', $node->nid)
      ->execute();
416
  }
417
}
418

419
/**
420
 * Implements hook_node_load().
421
 */
422
function forum_node_load($nodes) {
423 424
  $node_vids = array();
  foreach ($nodes as $node) {
425
    if (_forum_node_check_node_type($node)) {
426 427 428 429
      $node_vids[] = $node->vid;
    }
  }
  if (!empty($node_vids)) {
430 431 432 433 434
    $query = db_select('forum', 'f');
    $query
      ->fields('f', array('nid', 'tid'))
      ->condition('f.vid', $node_vids);
    $result = $query->execute();
435 436 437
    foreach ($result as $record) {
      $nodes[$record->nid]->forum_tid = $record->tid;
    }
438
  }
439 440
}

441
/**
442
 * Implements hook_node_info().
443
 */
444
function forum_node_info() {
445 446
  return array(
    'forum' => array(
447
      'name' => t('Forum topic'),
448
      'base' => 'forum',
449
      'description' => t('A <em>forum topic</em> starts a new discussion thread within a forum.'),
450 451 452
      'title_label' => t('Subject'),
    )
  );
Dries's avatar
Dries committed
453 454
}

455
/**
456
 * Implements hook_permission().
457
 */
458
function forum_permission() {
459
  $perms = array(
460 461 462
    'administer forums' => array(
      'title' => t('Administer forums'),
    ),
463 464
  );
  return $perms;
465
}
Dries's avatar
Dries committed
466

467
/**
468
 * Implements hook_taxonomy_term_delete().
469
 */
470
function forum_taxonomy_term_delete(Term $term) {
471
  // For containers, remove the tid from the forum_containers variable.
472 473
  $config = config('forum.settings');
  $containers = $config->get('containers');
474
  $key = array_search($term->tid, $containers);
475 476 477
  if ($key !== FALSE) {
    unset($containers[$key]);
  }
478
  $config->set('containers', $containers)->save();
479
}
480

481
/**
482
 * Implements hook_comment_publish().
483
 *
484
 * This actually handles the insertion and update of published nodes since
485 486 487
 * comment_save() calls hook_comment_publish() for all published comments.
 */
function forum_comment_publish($comment) {
488
  _forum_update_forum_index($comment->nid->target_id);
489 490 491
}

/**
492
 * Implements hook_comment_update().
493
 *
494 495
 * The Comment module doesn't call hook_comment_unpublish() when saving
 * individual comments, so we need to check for those here.
496 497
 */
function forum_comment_update($comment) {
498 499
  // comment_save() calls hook_comment_publish() for all published comments,
  // so we need to handle all other values here.
500
  if (!$comment->status->value) {
501
    _forum_update_forum_index($comment->nid->target_id);
502 503 504 505
  }
}

/**
506
 * Implements hook_comment_unpublish().
507 508
 */
function forum_comment_unpublish($comment) {
509
  _forum_update_forum_index($comment->nid->target_id);
510 511 512
}

/**
513
 * Implements hook_comment_delete().
514 515
 */
function forum_comment_delete($comment) {
516
  _forum_update_forum_index($comment->nid->target_id);
517 518 519
}

/**
520
 * Implements hook_field_storage_pre_insert().
521
 */
522 523
function forum_field_storage_pre_insert(EntityInterface $entity, &$skip_fields) {
  if ($entity->entityType() == 'node' && $entity->status && _forum_node_check_node_type($entity)) {
524
    $query = db_insert('forum_index')->fields(array('nid', 'title', 'tid', 'sticky', 'created', 'comment_count', 'last_comment_timestamp'));
525
    foreach ($entity->taxonomy_forums as $language) {
526
      foreach ($language as $item) {
527
        $query->values(array(
528 529
          'nid' => $entity->nid,
          'title' => $entity->title,
530
          'tid' => $item['tid'],
531 532
          'sticky' => $entity->sticky,
          'created' => $entity->created,
533
          'comment_count' => 0,
534
          'last_comment_timestamp' => $entity->created,
535 536 537 538 539 540 541 542
        ));
      }
    }
    $query->execute();
  }
}

/**
543
 * Implements hook_field_storage_pre_update().
544
 */
545
function forum_field_storage_pre_update(EntityInterface $entity, &$skip_fields) {
546 547
  $first_call = &drupal_static(__FUNCTION__, array());

548
  if ($entity->entityType() == 'node' && _forum_node_check_node_type($entity)) {
549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572

    // 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,
          ));
        }
573
      }
574 575 576 577
      $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);
578
    }
579 580 581 582 583 584

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

585 586
  }
}
587

588
/**
589
 * Implements hook_form_BASE_FORM_ID_alter().
590
 */
591
function forum_form_taxonomy_vocabulary_form_alter(&$form, &$form_state, $form_id) {
592
  $vid = config('forum.settings')->get('vocabulary');
593 594
  $vocabulary = $form_state['controller']->getEntity($form_state);
  if ($vid == $vocabulary->id()) {
595 596 597 598 599 600 601 602 603 604
    $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;
605 606
    // Do not allow to change a vid of forum's vocabulary.
    $form['vid']['#disabled'] = TRUE;
607 608 609 610
  }
}

/**
611
 * Implements hook_form_FORM_ID_alter() for taxonomy_term_form().
612
 */
613
function forum_form_taxonomy_term_form_alter(&$form, &$form_state, $form_id) {
614
  $vid = config('forum.settings')->get('vocabulary');
615
  if (isset($form['vid']['#value']) && $form['vid']['#value'] == $vid) {
616
    // Hide multiple parents select from forum terms.
617
    $form['relations']['parent']['#access'] = FALSE;
618
  }
619 620 621
}

/**
622
 * Implements hook_form_BASE_FORM_ID_alter() for node_form().
623 624 625
 */
function forum_form_node_form_alter(&$form, &$form_state, $form_id) {
  if (isset($form['taxonomy_forums'])) {
626
    $langcode = $form['taxonomy_forums']['#language'];
627
    // Make the vocabulary required for 'real' forum-nodes.
628 629
    $form['taxonomy_forums'][$langcode]['#required'] = TRUE;
    $form['taxonomy_forums'][$langcode]['#multiple'] = FALSE;
630 631
    if (empty($form['taxonomy_forums'][$langcode]['#default_value'])) {
      // If there is no default forum already selected, try to get the forum
632 633
      // 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).
634
      $requested_forum_id = arg(3);
635
      $form['taxonomy_forums'][$langcode]['#default_value'] = is_numeric($requested_forum_id) ? $requested_forum_id : '';
636
    }
637
  }
638 639
}

640
/**
641 642 643 644 645 646
 * 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()
 */
647 648 649
function forum_block_view_pre_render($elements) {
  $result = $elements['#query']->execute();
  if ($node_title_list = node_title_list($result)) {
650
    $elements['forum_list'] = $node_title_list;
651
    $elements['forum_more'] = array('#theme' => 'more_link', '#url' => 'forum', '#title' => t('Read the latest forum topics.'));
652
  }
653
  return $elements;
654 655
}

656
/**
657
 * Implements hook_form().
658
 */
659 660
function forum_form(Node $node, &$form_state) {
  $type = node_type_load($node->type);
661 662 663 664 665 666
  $form['title'] = array(
    '#type' => 'textfield',
    '#title' => check_plain($type->title_label),
    '#default_value' => !empty($node->title) ? $node->title : '',
    '#required' => TRUE, '#weight' => -5
  );
667

668
  if (!empty($node->nid)) {
669
    $forum_terms = $node->taxonomy_forums;
670
    // If editing, give option to leave shadows.
671
    $shadow = (count($forum_terms) > 1);
672
    $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.'));
673
    $form['forum_tid'] = array('#type' => 'value', '#value' => $node->forum_tid);
Dries's avatar
Dries committed
674
  }
675

676
  return $form;
Dries's avatar
Dries committed
677 678
}

679
/**
680
 * Returns a tree of all forums for a given taxonomy term ID.
681 682
 *
 * @param $tid
683 684 685
 *   (optional) Taxonomy term ID of the forum. If not given all forums will be
 *   returned.
 *
686
 * @return
687
 *   A tree of taxonomy objects, with the following additional properties:
688 689 690 691
 *   - 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.
692
 */
693
function forum_forum_load($tid = NULL) {
694 695
  $cache = &drupal_static(__FUNCTION__, array());

696 697 698 699
  // Return a cached forum tree if available.
  if (!isset($tid)) {
    $tid = 0;
  }
700 701 702
  if (isset($cache[$tid])) {
    return $cache[$tid];
  }
703

704 705
  $config = config('forum.settings');
  $vid = $config->get('vocabulary');
706 707 708 709

  // Load and validate the parent term.
  if ($tid) {
    $forum_term = taxonomy_term_load($tid);
710
    if (!$forum_term || ($forum_term->bundle() != $vid)) {
711 712 713
      return $cache[$tid] = FALSE;
    }
  }
714
  // If $tid is 0, create an empty entity to hold the child terms.
715
  elseif ($tid === 0) {
716
    $forum_term = entity_create('taxonomy_term', array(
717
      'tid' => 0,
718
    ));
719 720 721
  }

  // Determine if the requested term is a container.
722
  if (!$forum_term->tid || in_array($forum_term->tid, $config->get('containers'))) {
723 724 725 726
    $forum_term->container = 1;
  }

  // Load parent terms.
727
  $forum_term->parents = taxonomy_term_load_parents_all($forum_term->tid);
728 729 730

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

733
  if (count($_forums)) {
734 735
    $query = db_select('node', 'n');
    $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
736
    $query->join('forum', 'f', 'n.vid = f.vid');
737 738 739
    $query->addExpression('COUNT(n.nid)', 'topic_count');
    $query->addExpression('SUM(ncs.comment_count)', 'comment_count');
    $counts = $query
740
      ->fields('f', array('tid'))
741
      ->condition('n.status', 1)
742 743 744 745
      ->groupBy('tid')
      ->addTag('node_access')
      ->execute()
      ->fetchAllAssoc('tid');
746
  }
747

748
  foreach ($_forums as $forum) {
749
    // Determine if the child term is a container.
750
    if (in_array($forum->tid, $config->get('containers'))) {
751 752
      $forum->container = 1;
    }
753

754
    // Merge in the topic and post counters.
755
    if (!empty($counts[$forum->tid])) {
756 757 758 759 760 761 762 763
      $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;
    }

764
    // Query "Last Post" information for this forum.
765
    $query = db_select('node', 'n');
766
    $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', array(':tid' => $forum->tid));
767
    $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
768 769
    $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');
770

771 772 773 774 775 776 777 778
    $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();
779

780
    // Merge in the "Last Post" information.
781
    $last_post = new stdClass();
782
    if (!empty($topic->last_comment_timestamp)) {
783
      $last_post->created = $topic->last_comment_timestamp;
784 785 786
      $last_post->name = $topic->last_comment_name;
      $last_post->uid = $topic->last_comment_uid;
    }
787
    $forum->last_post = $last_post;
788

789 790 791
    $forums[$forum->tid] = $forum;
  }

792 793 794 795
  // Cache the result, and return the tree.
  $forum_term->forums = $forums;
  $cache[$tid] = $forum_term;
  return $forum_term;
796 797
}

798
/**
799 800
 * Calculates the number of new posts in a forum that the user has not yet read.
 *
801
 * Nodes are new if they are newer than HISTORY_READ_LIMIT.
802 803 804 805 806 807 808 809
 *
 * @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.
810 811
 */
function _forum_topics_unread($term, $uid) {
812
  $query = db_select('node', 'n');
813
  $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', array(':tid' => $term));
814
  $query->leftJoin('history', 'h', 'n.nid = h.nid AND h.uid = :uid', array(':uid' => $uid));
815 816 817
  $query->addExpression('COUNT(n.nid)', 'count');
  return $query
    ->condition('status', 1)
818
    ->condition('n.created', HISTORY_READ_LIMIT, '>')
819 820 821 822
    ->isNull('h.nid')
    ->addTag('node_access')
    ->execute()
    ->fetchField();
823 824
}

825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841
/**
 * 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
842
function forum_get_topics($tid, $sortby, $forum_per_page) {
843
  global $user, $forum_topic_list_header;
844

845
  $forum_topic_list_header = array(
846 847 848
    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'),
849
  );
850

Dries's avatar
Dries committed
851
  $order = _forum_get_topic_order($sortby);
852
  for ($i = 0; $i < count($forum_topic_list_header); $i++) {
Dries's avatar
Dries committed
853 854
    if ($forum_topic_list_header[$i]['field'] == $order['field']) {
      $forum_topic_list_header[$i]['sort'] = $order[