specfile
[plewww.git] / modules / forum.module
1 <?php
2 // $Id: forum.module 144 2007-03-28 07:52:20Z thierry $
3
4 /**
5  * @file
6  * Enable threaded discussions about general topics.
7  */
8
9 /**
10  * Implementation of hook_help().
11  */
12 function forum_help($section) {
13   switch ($section) {
14     case 'admin/help#forum':
15       $output = '<p>'. t('The forum module lets you create threaded discussion forums for a particular topic on your site. This is similar to a message board system such as phpBB. Forums are very useful because they allow community members to discuss topics with one another, and they are archived for future reference.') .'</p>';
16       $output .= '<p>'. t('Forums can be organized under what are called <em>containers</em>. Containers hold forums and, in turn, forums hold threaded discussions. Both containers and forums can be placed inside other containers and forums. By planning the structure of your containers and forums well, you make it easier for users to find a topic area of interest to them.  Forum topics can be moved by selecting a different forum and can be left in the existing forum by selecting <em>leave a shadow copy</em>.  Forum topics can also have their own URL.') .'</p>';
17       $output .= '<p>'. t('Forums module <strong>requires Taxonomy and Comments module</strong> be enabled.') .'</p>';
18       $output .= t('<p>You can</p>
19 <ul>
20 <li>administer forums at <a href="%admin-forum">administer &gt;&gt; forums</a>.</li>
21 <li>enable the required comment and taxonomy modules at <a href="%admin-modules">administer &gt;&gt; modules</a>.</li>
22 <li>read about the comment module at <a href="%admin-help-comment">administer &gt;&gt; help &gt;&gt; comment</a>.</li>
23 <li>read about the taxonomy module at <a href="%admin-help-taxonomy">administer &gt;&gt; help &gt;&gt; taxonomy</a>.</li>
24 </ul>
25 ', array('%admin-forum' => url('admin/forum'), '%admin-modules' => url('admin/modules'), '%admin-help-comment' => url('admin/help/comment'), '%admin-help-taxonomy' => url('admin/help/taxonomy')));
26       $output .= '<p>'. t('For more information please read the configuration and customization handbook <a href="%forum">Forum page</a>.', array('%forum' => 'http://drupal.org/handbook/modules/forum/')) .'</p>';
27       return $output;
28     case 'admin/modules#description':
29       return t('Enables threaded discussions about general topics.');
30     case 'admin/forum':
31       return t('<p>This is a list of existing containers and forums that you can edit. Containers hold forums and, in turn, forums hold threaded discussions. Both containers and forums can be placed inside other containers and forums. By planning the structure of your containers and forums well, you make it easier for users to find a topic area of interest to them.</p>');
32     case 'admin/forum/add/container':
33       return t('<p>Containers help you organize your forums. The job of a container is to hold, or contain, other forums that are related. For example, a container named "Food" might hold two forums named "Fruit" and "Vegetables".</p>');
34     case 'admin/forum/add/forum':
35       return t('<p>A forum holds discussion topics that are related. For example, a forum named "Fruit" might contain topics titled "Apples" and "Bananas".</p>');
36     case 'admin/forum/configure':
37       return t('This is where you can configure system-wide options for how your forums act and display.');
38     case 'node/add#forum':
39       return t('Create a new topic for discussion in the forums.');
40   }
41 }
42
43 /**
44  * Implementation of hook_menu().
45  */
46 function forum_menu($may_cache) {
47   $items = array();
48
49   if ($may_cache) {
50     $items[] = array('path' => 'node/add/forum',
51       'title' => t('forum topic'),
52       'access' => user_access('create forum topics'));
53     $items[] = array('path' => 'forum',
54       'title' => t('forums'),
55       'callback' => 'forum_page',
56       'access' => user_access('access content'),
57       'type' => MENU_SUGGESTED_ITEM);
58     $items[] = array('path' => 'admin/forum',
59       'title' => t('forums'),
60       'callback' => 'forum_overview',
61       'access' => user_access('administer forums'),
62       'type' => MENU_NORMAL_ITEM);
63     $items[] = array('path' => 'admin/forum/list',
64       'title' => t('list'),
65       'access' => user_access('administer forums'),
66       'type' => MENU_DEFAULT_LOCAL_TASK,
67       'weight' => -10);
68     $items[] = array('path' => 'admin/forum/add/container',
69       'title' => t('add container'),
70       'callback' => 'forum_form_container',
71       'access' => user_access('administer forums'),
72       'type' => MENU_LOCAL_TASK);
73     $items[] = array('path' => 'admin/forum/add/forum',
74       'title' => t('add forum'),
75       'callback' => 'forum_form_forum',
76       'access' => user_access('administer forums'),
77       'type' => MENU_LOCAL_TASK);
78     $items[] = array('path' => 'admin/forum/configure',
79       'title' => t('configure'),
80       'callback' => 'forum_admin_configure',
81       'access' => user_access('administer forums'),
82       'type' => MENU_LOCAL_TASK);
83   }
84   elseif (is_numeric(arg(4))) {
85     $term = taxonomy_get_term(arg(4));
86     // Check if this is a valid term.
87     if ($term) {
88       $items[] = array('path' => 'admin/forum/edit/container',
89         'title' => t('edit container'),
90         'callback' => 'forum_form_container',
91         'callback arguments' => array((array)$term),
92         'access' => user_access('administer forums'),
93         'type' => MENU_CALLBACK);
94       $items[] = array('path' => 'admin/forum/edit/forum',
95         'title' => t('edit forum'),
96         'callback' => 'forum_form_forum',
97         'callback arguments' => array((array)$term),
98         'access' => user_access('administer forums'),
99         'type' => MENU_CALLBACK);
100     }
101   }
102
103   return $items;
104 }
105
106 /**
107  * Implementation of hook_node_info().
108  */
109 function forum_node_info() {
110   return array('forum' => array('name' => t('forum topic'), 'base' => 'forum'));
111 }
112
113 /**
114  * Implementation of hook_access().
115  */
116 function forum_access($op, $node) {
117   global $user;
118
119   if ($op == 'create') {
120     return user_access('create forum topics');
121   }
122
123   if ($op == 'update' || $op == 'delete') {
124     if (user_access('edit own forum topics') && ($user->uid == $node->uid)) {
125       return TRUE;
126     }
127   }
128 }
129
130 /**
131  * Implementation of hook_perm().
132  */
133 function forum_perm() {
134   return array('create forum topics', 'edit own forum topics', 'administer forums');
135 }
136
137 /**
138  * Implementation of hook_nodeapi().
139  */
140 function forum_nodeapi(&$node, $op, $teaser, $page) {
141   switch ($op) {
142     case 'delete revision':
143       db_query('DELETE FROM {forum} WHERE vid = %d', $node->vid);
144       break;
145   }
146 }
147
148 /**
149  * Implementation of hook_taxonomy().
150  */
151 function forum_taxonomy($op, $type, $term = NULL) {
152   if ($op == 'delete' && $term['vid'] == _forum_get_vid()) {
153     switch ($type) {
154       case 'term':
155         $results = db_query('SELECT f.nid FROM {forum} f WHERE f.tid = %d', $term['tid']);
156         while ($node = db_fetch_object($results)) {
157           // node_delete will also remove any association with non-forum vocabularies.
158           node_delete($node->nid);
159         }
160
161         // For containers, remove the tid from the forum_containers variable.
162         $containers = variable_get('forum_containers', array());
163         $key = array_search($term['tid'], $containers);
164         if ($key !== FALSE) {
165           unset($containers[$key]);
166         }
167         variable_set('forum_containers', $containers);
168         break;
169       case 'vocabulary':
170         variable_del('forum_nav_vocabulary');
171     }
172   }
173 }
174
175 /**
176  * Implementation of hook_settings
177  */
178 function forum_admin_configure() {
179
180   $form = array();
181   $number = drupal_map_assoc(array(5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 80, 100, 150, 200, 250, 300, 350, 400, 500));
182   $form['forum_hot_topic'] = array('#type' => 'select',
183     '#title' => t('Hot topic threshold'),
184     '#default_value' => variable_get('forum_hot_topic', 15),
185     '#options' => $number,
186     '#description' => t('The number of posts a topic must have to be considered hot.'),
187   );
188   $number = drupal_map_assoc(array(10, 25, 50, 75, 100));
189   $form['forum_per_page'] = array('#type' => 'select',
190     '#title' => t('Topics per page'),
191     '#default_value' => variable_get('forum_per_page', 25),
192     '#options' => $number,
193     '#description' => t('The default number of topics displayed per page; links to browse older messages are automatically being displayed.'),
194   );
195   $forder = array(1 => t('Date - newest first'), 2 => t('Date - oldest first'), 3 => t('Posts - most active first'), 4=> t('Posts - least active first'));
196   $form['forum_order'] = array('#type' => 'radios',
197     '#title' => t('Default order'),
198     '#default_value' => variable_get('forum_order', '1'),
199     '#options' => $forder,
200     '#description' => t('The default display order for topics.'),
201   );
202
203   return system_settings_form('forum_admin_configure', $form);
204 }
205
206 /**
207  * Implementation of hook_form_alter().
208  */
209 function forum_form_alter($form_id, &$form) {
210   // hide critical options from forum vocabulary
211   if ($form_id == 'taxonomy_form_vocabulary') {
212     if ($form['vid']['#value'] == _forum_get_vid()) {
213       $form['help_forum_vocab'] = array(
214         '#value' => t('This is the designated forum vocabulary. Some of the normal vocabulary options have been removed.'),
215         '#weight' => -1,
216       );
217       $form['nodes']['forum'] = array('#type' => 'checkbox', '#value' => 1, '#title' => t('forum topic'), '#attributes' => array('disabled' => '' ), '#description' => t('forum topic is affixed to the forum vocabulary.'));
218       $form['hierarchy'] = array('#type' => 'value', '#value' => 1);
219       unset($form['relations']);
220       unset($form['tags']);
221       unset($form['multiple']);
222       $form['required'] = array('#type' => 'value', '#value' => 1);
223     }
224     else {
225       unset($form['nodes']['forum']);
226     }
227   }
228 }
229
230 /**
231  * Implementation of hook_load().
232  */
233 function forum_load($node) {
234   $forum = db_fetch_object(db_query('SELECT * FROM {forum} WHERE vid = %d', $node->vid));
235
236   return $forum;
237 }
238
239 /**
240  * Implementation of hook_block().
241  *
242  * Generates a block containing the currently active forum topics and the
243  * most recently added forum topics.
244  */
245 function forum_block($op = 'list', $delta = 0, $edit = array()) {
246   switch ($op) {
247     case 'list':
248       $blocks[0]['info'] = t('Active forum topics');
249       $blocks[1]['info'] = t('New forum topics');
250       return $blocks;
251
252     case 'configure':
253       $form['forum_block_num_'. $delta] = array('#type' => 'select', '#title' => t('Number of topics'), '#default_value' => variable_get('forum_block_num_'. $delta, '5'), '#options' => drupal_map_assoc(array(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)));
254       return $form;
255
256     case 'save':
257       variable_set('forum_block_num_'. $delta, $edit['forum_block_num_'. $delta]);
258       break;
259
260     case 'view':
261       if (user_access('access content')) {
262         switch ($delta) {
263           case 0:
264             $title = t('Active forum topics');
265             $sql = db_rewrite_sql("SELECT n.nid, n.title, l.comment_count FROM {node} n INNER JOIN {node_comment_statistics} l ON n.nid = l.nid WHERE n.status = 1 AND n.type = 'forum' ORDER BY l.last_comment_timestamp DESC");
266             $result = db_query_range($sql, 0, variable_get('forum_block_num_0', '5'));
267             if (db_num_rows($result)) {
268               $content = node_title_list($result);
269             }
270             break;
271
272           case 1:
273             $title = t('New forum topics');
274             $sql = db_rewrite_sql("SELECT n.nid, n.title, l.comment_count FROM {node} n INNER JOIN {node_comment_statistics} l ON n.nid = l.nid WHERE n.type = 'forum' AND n.status = 1 ORDER BY n.nid DESC");
275             $result = db_query_range($sql, 0, variable_get('forum_block_num_1', '5'));
276             if (db_num_rows($result)) {
277               $content = node_title_list($result);
278             }
279             break;
280         }
281
282         if ($content) {
283           $content .= '<div class="more-link">'. l(t('more'), 'forum', array('title' => t('Read the latest forum topics.'))) .'</div>';
284         }
285
286         $block['subject'] = $title;
287         $block['content'] = $content;
288
289         return $block;
290       }
291   }
292 }
293
294 /**
295  * Implementation of hook_view().
296  */
297 function forum_view(&$node, $teaser = FALSE, $page = FALSE) {
298   if ($page) {
299     $vocabulary = taxonomy_get_vocabulary(variable_get('forum_nav_vocabulary', ''));
300     // Breadcrumb navigation
301     $breadcrumb = array();
302     $breadcrumb[] = array('path' => 'forum', 'title' => $vocabulary->name);
303     if ($parents = taxonomy_get_parents_all($node->tid)) {
304       $parents = array_reverse($parents);
305       foreach ($parents as $p) {
306         $breadcrumb[] = array('path' => 'forum/'. $p->tid, 'title' => $p->name);
307       }
308     }
309     $breadcrumb[] = array('path' => 'node/'. $node->nid);
310     menu_set_location($breadcrumb);
311   }
312
313   $node = node_prepare($node, $teaser);
314
315   $node->body .= theme('forum_topic_navigation', $node);
316 }
317
318 /**
319  * Implementation of hook_submit().
320  *
321  * Check in particular that only a "leaf" term in the associated taxonomy
322  * vocabulary is selected, not a "container" term.
323  */
324 function forum_submit(&$node) {
325   // Make sure all fields are set properly:
326   $node->icon = $node->icon ? $node->icon : '';
327
328   if ($node->taxonomy) {
329     // Extract the node's proper topic ID.
330     $vocabulary = variable_get('forum_nav_vocabulary', '');
331     foreach ($node->taxonomy as $term) {
332       if (db_result(db_query('SELECT COUNT(*) FROM {term_data} WHERE tid = %d AND vid = %d', $term, $vocabulary))) {
333         $node->tid = $term;
334       }
335     }
336     if ($node->tid && $node->shadow) {
337       // A shadow copy needs to be created. Retain existing term and add new term.
338       $terms = array_keys(taxonomy_node_get_terms($node->nid));
339       if (!in_array($node->tid, $terms)) {
340         $terms[] = $node->tid;
341       }
342       $node->taxonomy = $terms;
343     }
344   }
345 }
346
347 /**
348  * Implementation of hook_validate().
349  *
350  * Check in particular that only a "leaf" term in the associated taxonomy
351  * vocabulary is selected, not a "container" term.
352  */
353 function forum_validate($node) {
354   if ($node->taxonomy) {
355     // Extract the node's proper topic ID.
356     $vocabulary = variable_get('forum_nav_vocabulary', '');
357     $containers = variable_get('forum_containers', array());
358     foreach ($node->taxonomy as $term) {
359       if (db_result(db_query('SELECT COUNT(*) FROM {term_data} WHERE tid = %d AND vid = %d', $term, $vocabulary))) {
360         if (in_array($term, $containers)) {
361           $term = taxonomy_get_term($term);
362           form_set_error('taxonomy', t('The item %forum is only a container for forums. Please select one of the forums below it.', array('%forum' => theme('placeholder', $term->name))));
363         }
364       }
365     }
366   }
367 }
368
369 /**
370  * Implementation of hook_update().
371  */
372 function forum_update($node) {
373   if ($node->revision) {
374     db_query("INSERT INTO {forum} (nid, vid, tid) VALUES (%d, %d, %d)", $node->nid, $node->vid, $node->tid);
375   }
376   else {
377     db_query('UPDATE {forum} SET tid = %d WHERE vid = %d', $node->tid, $node->vid);
378   }
379 }
380
381 /**
382  * Implementation of hook_form().
383  */
384 function forum_form(&$node) {
385   $form['title'] = array('#type' => 'textfield', '#title' => t('Subject'), '#default_value' => $node->title, '#required' => TRUE, '#weight' => -5);
386
387   if ($node->nid) {
388     $forum_terms = taxonomy_node_get_terms_by_vocabulary(_forum_get_vid(), $node->nid);
389     // if editing, give option to leave shadows
390     $shadow = (count($forum_terms) > 1);
391     $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.'));
392   }
393
394   $form['body_filter']['body'] = array('#type' => 'textarea', '#title' => t('Body'), '#default_value' => $node->body, '#rows' => 20, '#required' => TRUE);
395   $form['body_filter']['format'] = filter_form($node->format);
396
397   return $form;
398 }
399
400 /**
401  * Implementation of hook_prepare; assign forum taxonomy when adding a topic from within a forum.
402  */
403 function forum_prepare(&$node) {
404   if (!$node->nid) {
405     // new topic
406     $node->taxonomy[arg(3)]->vid = _forum_get_vid();
407     $node->taxonomy[arg(3)]->tid = arg(3);
408   }
409 }
410
411 /**
412  * Implementation of hook_insert().
413  */
414 function forum_insert($node) {
415   db_query('INSERT INTO {forum} (nid, vid, tid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $node->tid);
416 }
417
418 /**
419  * Implementation of hook_delete().
420  */
421 function forum_delete(&$node) {
422   db_query('DELETE FROM {forum} WHERE nid = %d', $node->nid);
423 }
424
425 /**
426  * Returns a form for adding a container to the forum vocabulary
427  *
428  * @param $edit Associative array containing a container term to be added or edited.
429  */
430 function forum_form_container($edit = array()) {
431   // Handle a delete operation.
432   if ($_POST['op'] == t('Delete') || $_POST['edit']['confirm']) {
433     return _forum_confirm_delete($edit['tid']);
434   }
435
436   $form['name'] = array(
437     '#title' => t('Container name'),
438     '#type' => 'textfield',
439     '#default_value' => $edit['name'],
440     '#maxlength' =>  64,
441     '#description' => t('The container name is used to identify related forums.'),
442     '#required' => TRUE
443   );
444
445   $form['description'] = array(
446     '#type' => 'textarea',
447     '#title' => t('Description'),
448     '#default_value' => $edit['description'],
449     '#description' => t('The container description can give users more information about the forums it contains.')
450   );
451   $form['parent']['#tree'] = TRUE;
452   $form['parent'][0] = _forum_parent_select($edit['tid'], t('Parent'), 'container');
453   $form['weight'] = array('#type' => 'weight',
454     '#title' => t('Weight'),
455     '#default_value' => $edit['weight'],
456     '#description' => t('When listing containers, those with with light (small) weights get listed before containers with heavier (larger) weights. Containers with equal weights are sorted alphabetically.')
457   );
458
459   $form['vid'] = array('#type' => 'hidden',
460     '#value' => _forum_get_vid());
461   $form['submit'] = array(
462     '#type' => 'submit',
463     '#value' => t('Submit')
464   );
465   if ($edit['tid']) {
466     $form['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
467     $form['tid'] = array('#type' => 'value', '#value' => $edit['tid']);
468   }
469
470   return drupal_get_form('forum_form_container', $form, 'forum_form');
471 }
472
473 /**
474  * Returns a form for adding a forum to the forum vocabulary
475  *
476  * @param $edit Associative array containing a forum term to be added or edited.
477  */
478 function forum_form_forum($edit = array()) {
479   // Handle a delete operation.
480   if ($_POST['op'] == t('Delete') || $_POST['edit']['confirm']) {
481     return _forum_confirm_delete($edit['tid']);
482   }
483
484   $form['name'] = array('#type' => 'textfield',
485     '#title' => t('Forum name'),
486     '#default_value' => $edit['name'],
487     '#maxlength' =>  64,
488     '#description' => t('The forum name is used to identify related discussions.'),
489     '#required' => TRUE,
490   );
491   $form['description'] = array('#type' => 'textarea',
492     '#title' => t('Description'),
493     '#default_value' => $edit['description'],
494     '#description' => t('The forum description can give users more information about the discussion topics it contains.'),
495   );
496   $form['parent']['#tree'] = TRUE;
497   $form['parent'][0] = _forum_parent_select($edit['tid'], t('Parent'), 'forum');
498   $form['weight'] = array('#type' => 'weight',
499     '#title' => t('Weight'),
500     '#default_value' => $edit['weight'],
501     '#description' => t('When listing forums, those with lighter (smaller) weights get listed before containers with heavier (larger) weights. Forums with equal weights are sorted alphabetically.'),
502   );
503
504   $form['vid'] = array('#type' => 'hidden', '#value' => _forum_get_vid());
505   $form['submit' ] = array('#type' => 'submit', '#value' => t('Submit'));
506   if ($edit['tid']) {
507     $form['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
508     $form['tid'] = array('#type' => 'hidden', '#value' => $edit['tid']);
509   }
510
511   return drupal_get_form('forum_form_forum', $form, 'forum_form');
512 }
513
514 /**
515  * Process forum form and container form submissions.
516  */
517 function forum_form_submit($form_id, $form_values) {
518   if ($form_id == 'forum_form_container') {
519     $container = TRUE;
520     $type = t('forum container');
521   }
522   else {
523     $container = false;
524     $type = t('forum');
525   }
526
527   $status = taxonomy_save_term($form_values);
528   switch ($status) {
529     case SAVED_NEW:
530       if ($container) {
531         $containers = variable_get('forum_containers', array());
532         $containers[] = $form_values['tid'];
533         variable_set('forum_containers', $containers);
534       }
535       drupal_set_message(t('Created new %type %term.', array('%term' => theme('placeholder', $form_values['name']), '%type' => $type)));
536       break;
537     case SAVED_UPDATED:
538       drupal_set_message(t('The %type %term has been updated.', array('%term' => theme('placeholder', $form_values['name']), '%type' => $type)));
539       break;
540   }
541   return 'admin/forum';
542 }
543
544 /**
545  * Returns a confirmation page for deleting a forum taxonomy term.
546  *
547  * @param $tid ID of the term to be deleted
548  */
549 function _forum_confirm_delete($tid) {
550   $term = taxonomy_get_term($tid);
551
552   $form['tid'] = array('#type' => 'value', '#value' => $tid);
553   $form['name'] = array('#type' => 'value', '#value' => $term->name);
554
555   return confirm_form('forum_confirm_delete', $form, t('Are you sure you want to delete the forum %name?', array('%name' => theme('placeholder', $term->name))), 'admin/forums', t('Deleting a forum or container will delete all sub-forums and associated posts as well. This action cannot be undone.'), t('Delete'), t('Cancel'));
556 }
557
558 /**
559  * Implementation of forms api _submit call. Deletes a forum after confirmation.
560  */
561 function forum_confirm_delete_submit($form_id, $form_values) {
562   taxonomy_del_term($form_values['tid']);
563   drupal_set_message(t('The forum %term and all sub-forums and associated posts have been deleted.', array('%term' => theme('placeholder', $form_values['name']))));
564   watchdog('content', t('forum: deleted %term and all its sub-forums and associated posts.', array('%term' => theme('placeholder', $form_values['name']))));
565
566   return 'admin/forum';
567 }
568
569 /**
570  * Returns an overview list of existing forums and containers
571  */
572 function forum_overview() {
573   $header = array(t('Name'), t('Operations'));
574
575   $tree = taxonomy_get_tree(_forum_get_vid());
576   if ($tree) {
577     foreach ($tree as $term) {
578       if (in_array($term->tid, variable_get('forum_containers', array()))) {
579         $rows[] = array(_taxonomy_depth($term->depth) .' '. check_plain($term->name), l(t('edit container'), "admin/forum/edit/container/$term->tid"));
580       }
581       else {
582         $rows[] = array(_taxonomy_depth($term->depth) .' '. check_plain($term->name), l(t('edit forum'), "admin/forum/edit/forum/$term->tid"));
583        }
584
585     }
586   }
587   else {
588     $rows[] = array(array('data' => '<em>' . t('There are no existing containers or forums. You may add some on the <a href="%container">add container</a> or <a href="%forum">add forum</a> pages.', array('%container' => url('admin/forum/add/container'), '%forum' => url('admin/forum/add/forum'))) . '</em>', 'colspan' => 2));
589   }
590   return theme('table', $header, $rows);
591 }
592
593 /**
594  * Returns a select box for available parent terms
595  *
596  * @param $tid ID of the term which is being added or edited
597  * @param $title Title to display the select box with
598  * @param $child_type Whether the child is forum or container
599  */
600 function _forum_parent_select($tid, $title, $child_type) {
601
602   $parents = taxonomy_get_parents($tid);
603   if ($parents) {
604     $parent = array_shift($parents);
605     $parent = $parent->tid;
606   }
607   else {
608     $parent = 0;
609   }
610
611   $children = taxonomy_get_tree(_forum_get_vid(), $tid);
612
613   // A term can't be the child of itself, nor of its children.
614   foreach ($children as $child) {
615     $exclude[] = $child->tid;
616   }
617   $exclude[] = $tid;
618
619   $tree = taxonomy_get_tree(_forum_get_vid());
620   $options[0] = '<'. t('root') .'>';
621   if ($tree) {
622     foreach ($tree as $term) {
623       if (!in_array($term->tid, $exclude)) {
624         $options[$term->tid] = _taxonomy_depth($term->depth) . $term->name;
625       }
626     }
627   }
628   if ($child_type == 'container') {
629     $description = t('Containers are usually placed at the top (root) level of your forum but you can also place a container inside a parent container or forum.');
630   }
631   else if ($child_type == 'forum') {
632     $description = t('You may place your forum inside a parent container or forum, or at the top (root) level of your forum.');
633   }
634
635   return array('#type' => 'select', '#title' => $title, '#default_value' => $parent, '#options' => $options, '#description' => $description, '#required' => TRUE);
636 }
637
638 function forum_term_path($term) {
639   return 'forum/'. $term->tid;
640 }
641
642 /**
643  * Returns the vocabulary id for forum navigation.
644  */
645 function _forum_get_vid() {
646   $vid = variable_get('forum_nav_vocabulary', '');
647   if (empty($vid)) {
648     // Check to see if a forum vocabulary exists
649     $vid = db_result(db_query("SELECT vid FROM {vocabulary} WHERE module = '%s'", 'forum'));
650     if (!$vid) {
651       $edit = array('name' => 'Forums', 'multiple' => 0, 'required' => 1, 'hierarchy' => 1, 'relations' => 0, 'module' => 'forum', 'nodes' => array('forum' => 1));
652       taxonomy_save_vocabulary($edit);
653       $vid = $edit['vid'];
654     }
655     variable_set('forum_nav_vocabulary', $vid);
656   }
657
658   return $vid;
659 }
660
661 /**
662  * Formats a topic for display
663  *
664  * @TODO Give a better description. Not sure where this function is used yet.
665  */
666 function _forum_format($topic) {
667   if ($topic && $topic->timestamp) {
668     return t('%time ago<br />by %author', array('%time' => format_interval(time() - $topic->timestamp), '%author' => theme('username', $topic)));
669   }
670   else {
671     return message_na();
672   }
673 }
674
675 /**
676  * Returns a list of all forums for a given taxonomy id
677  *
678  * Forum objects contain the following fields
679  * -num_topics Number of topics in the forum
680  * -num_posts Total number of posts in all topics
681  * -last_post Most recent post for the forum
682  *
683  * @param $tid
684  *   Taxonomy ID of the vocabulary that holds the forum list.
685  * @return
686  *   Array of object containing the forum information.
687  */
688 function forum_get_forums($tid = 0) {
689
690   $forums = array();
691   $_forums = taxonomy_get_tree(variable_get('forum_nav_vocabulary', ''), $tid);
692
693   if (count($_forums)) {
694
695     $counts = array();
696
697     $sql = "SELECT r.tid, COUNT(n.nid) AS topic_count, SUM(l.comment_count) AS comment_count FROM {node} n INNER JOIN {node_comment_statistics} l ON n.nid = l.nid INNER JOIN {term_node} r ON n.nid = r.nid WHERE n.status = 1 AND n.type = 'forum' GROUP BY r.tid";
698     $sql = db_rewrite_sql($sql);
699     $_counts = db_query($sql, $forum->tid);
700     while ($count = db_fetch_object($_counts)) {
701       $counts[$count->tid] = $count;
702     }
703   }
704
705   foreach ($_forums as $forum) {
706     if (in_array($forum->tid, variable_get('forum_containers', array()))) {
707       $forum->container = 1;
708     }
709
710     if ($counts[$forum->tid]) {
711       $forum->num_topics = $counts[$forum->tid]->topic_count;
712       $forum->num_posts = $counts[$forum->tid]->topic_count + $counts[$forum->tid]->comment_count;
713     }
714     else {
715       $forum->num_topics = 0;
716       $forum->num_posts = 0;
717     }
718
719     // This query does not use full ANSI syntax since MySQL 3.x does not support
720     // table1 INNER JOIN table2 INNER JOIN table3 ON table2_criteria ON table3_criteria
721     // used to join node_comment_statistics to users.
722     $sql = "SELECT ncs.last_comment_timestamp, IF (ncs.last_comment_uid != 0, u2.name, ncs.last_comment_name) AS last_comment_name, ncs.last_comment_uid FROM {node} n INNER JOIN {users} u1 ON n.uid = u1.uid INNER JOIN {term_node} tn ON n.nid = tn.nid INNER JOIN {node_comment_statistics} ncs ON n.nid = ncs.nid INNER JOIN {users} u2 ON ncs.last_comment_uid=u2.uid WHERE n.status = 1 AND tn.tid = %d ORDER BY ncs.last_comment_timestamp DESC";
723     $sql = db_rewrite_sql($sql);
724     $topic = db_fetch_object(db_query_range($sql, $forum->tid, 0, 1));
725
726     $last_post = new StdClass();
727     $last_post->timestamp = $topic->last_comment_timestamp;
728     $last_post->name = $topic->last_comment_name;
729     $last_post->uid = $topic->last_comment_uid;
730     $forum->last_post = $last_post;
731
732     $forums[$forum->tid] = $forum;
733   }
734
735   return $forums;
736 }
737
738 /**
739  * Calculate the number of nodes the user has not yet read and are newer
740  * than NODE_NEW_LIMIT.
741  */
742 function _forum_topics_unread($term, $uid) {
743   $sql = "SELECT COUNT(n.nid) FROM {node} n INNER JOIN {term_node} tn ON n.nid = tn.nid AND tn.tid = %d LEFT JOIN {history} h ON n.nid = h.nid AND h.uid = %d WHERE n.status = 1 AND n.type = 'forum' AND n.created > %d AND h.nid IS NULL";
744   $sql = db_rewrite_sql($sql);
745   return db_result(db_query($sql, $term, $uid, NODE_NEW_LIMIT));
746 }
747
748 function forum_get_topics($tid, $sortby, $forum_per_page) {
749   global $user, $forum_topic_list_header;
750
751   $forum_topic_list_header = array(
752     array('data' => '&nbsp;'),
753     array('data' => t('Topic'), 'field' => 'n.title'),
754     array('data' => t('Replies'), 'field' => 'l.comment_count'),
755     array('data' => t('Created'), 'field' => 'n.created'),
756     array('data' => t('Last reply'), 'field' => 'l.last_comment_timestamp'),
757   );
758
759   $order = _forum_get_topic_order($sortby);
760   for ($i = 0; $i < count($forum_topic_list_header); $i++) {
761     if ($forum_topic_list_header[$i]['field'] == $order['field']) {
762       $forum_topic_list_header[$i]['sort'] = $order['sort'];
763     }
764   }
765
766   $term = taxonomy_get_term($tid);
767
768   $sql = db_rewrite_sql("SELECT n.nid, f.tid, n.title, n.sticky, u.name, u.uid, n.created AS timestamp, n.comment AS comment_mode, l.last_comment_timestamp, IF(l.last_comment_uid != 0, cu.name, l.last_comment_name) AS last_comment_name, l.last_comment_uid, l.comment_count AS num_comments FROM {node_comment_statistics} l, {users} cu, {term_node} r, {users} u, {forum} f, {node} n WHERE n.status = 1 AND l.last_comment_uid = cu.uid AND n.nid = l.nid AND n.nid = r.nid AND r.tid = %d AND n.uid = u.uid AND n.vid = f.vid");
769   $sql .= tablesort_sql($forum_topic_list_header, 'n.sticky DESC,');
770   $sql .= ', n.created DESC';  // Always add a secondary sort order so that the news forum topics are on top.
771
772   $sql_count = db_rewrite_sql("SELECT COUNT(n.nid) FROM {node} n INNER JOIN {term_node} r ON n.nid = r.nid AND r.tid = %d WHERE n.status = 1 AND n.type = 'forum'");
773
774   $result = pager_query($sql, $forum_per_page, 0, $sql_count, $tid);
775
776   while ($topic = db_fetch_object($result)) {
777     if ($user->uid) {
778       // folder is new if topic is new or there are new comments since last visit
779       if ($topic->tid != $tid) {
780         $topic->new = 0;
781       }
782       else {
783         $history = _forum_user_last_visit($topic->nid);
784         $topic->new_replies = comment_num_new($topic->nid, $history);
785         $topic->new = $topic->new_replies || ($topic->timestamp > $history);
786       }
787     }
788     else {
789       // Do not track "new replies" status for topics if the user is anonymous.
790       $topic->new_replies = 0;
791       $topic->new = 0;
792     }
793
794     if ($topic->num_comments > 0) {
795       $last_reply = new StdClass();
796       $last_reply->timestamp = $topic->last_comment_timestamp;
797       $last_reply->name = $topic->last_comment_name;
798       $last_reply->uid = $topic->last_comment_uid;
799       $topic->last_reply = $last_reply;
800     }
801     $topics[] = $topic;
802   }
803
804   return $topics;
805 }
806
807 /**
808  * Finds the first unread node for a given forum.
809  */
810 function _forum_new($tid) {
811   global $user;
812
813   $sql = "SELECT n.nid FROM {node} n LEFT JOIN {history} h ON n.nid = h.nid AND h.uid = %d INNER JOIN {term_node} r ON n.nid = r.nid AND r.tid = %d WHERE n.status = 1 AND n.type = 'forum' AND h.nid IS NULL AND n.created > %d ORDER BY created";
814   $sql = db_rewrite_sql($sql);
815   $nid = db_result(db_query_range($sql, $user->uid, $tid, NODE_NEW_LIMIT, 0, 1));
816
817   return $nid ? $nid : 0;
818 }
819
820 /**
821  * Menu callback; prints a forum listing.
822  */
823 function forum_page($tid = 0) {
824   if (module_exist('taxonomy') && module_exist('comment')) {
825     $forum_per_page = variable_get('forum_per_page', 25);
826     $sortby = variable_get('forum_order', 1);
827
828     $forums = forum_get_forums($tid);
829     $parents = taxonomy_get_parents_all($tid);
830     if ($tid && !in_array($tid, variable_get('forum_containers', array()))) {
831       $topics = forum_get_topics($tid, $sortby, $forum_per_page);
832     }
833
834     return theme('forum_display', $forums, $topics, $parents, $tid, $sortby, $forum_per_page);
835   }
836   else {
837     drupal_set_message(t('The forum module requires both the taxonomy module and the comment module to be enabled and configured.'), 'error');
838     return ' ';
839   }
840 }
841
842 /**
843  * Format the forum body.
844  *
845  * @ingroup themeable
846  */
847 function theme_forum_display($forums, $topics, $parents, $tid, $sortby, $forum_per_page) {
848   global $user;
849   // forum list, topics list, topic browser and 'add new topic' link
850
851   $vocabulary = taxonomy_get_vocabulary(variable_get('forum_nav_vocabulary', ''));
852   $title = $vocabulary->name;
853
854   // Breadcrumb navigation:
855   $breadcrumb = array();
856   if ($tid) {
857     $breadcrumb[] = array('path' => 'forum', 'title' => $title);
858   }
859
860   if ($parents) {
861     $parents = array_reverse($parents);
862     foreach ($parents as $p) {
863       if ($p->tid == $tid) {
864         $title = $p->name;
865       }
866       else {
867         $breadcrumb[] = array('path' => 'forum/'. $p->tid, 'title' => $p->name);
868       }
869     }
870   }
871
872   drupal_set_title($title);
873
874   $breadcrumb[] = array('path' => $_GET['q']);
875   menu_set_location($breadcrumb);
876
877   if (count($forums) || count($parents)) {
878     $output  = '<div id="forum">';
879     $output .= '<ul>';
880
881     if (module_exist('tracker')) {
882       if ($user->uid) {
883         $output .= ' <li>'. l(t('My discussions.'), "tracker/$user->uid") .'</li>';
884       }
885
886       $output .= ' <li>'. l(t('Active discussions.'), 'tracker') .'</li>';
887     }
888
889     if (user_access('create forum topics')) {
890       $output .= '<li>'. l(t('Post new forum topic.'), "node/add/forum/$tid") .'</li>';
891     }
892     else if ($user->uid) {
893       $output .= '<li>'. t('You are not allowed to post a new forum topic.') .'</li>';
894     }
895     else {
896       $output .= '<li>'. t('<a href="%login">Login</a> to post a new forum topic.', array('%login' => url('user/login'))) .'</li>';
897     }
898     $output .= '</ul>';
899
900     $output .= theme('forum_list', $forums, $parents, $tid);
901
902     if ($tid && !in_array($tid, variable_get('forum_containers', array()))) {
903       drupal_add_link(array('rel' => 'alternate',
904                             'type' => 'application/rss+xml',
905                             'title' => 'RSS - '. $title,
906                             'href' => url('taxonomy/term/'. $tid .'/0/feed')));
907
908       $output .= theme('forum_topic_list', $tid, $topics, $sortby, $forum_per_page);
909       $output .= theme('feed_icon', url("taxonomy/term/$tid/0/feed"));
910     }
911     $output .= '</div>';
912   }
913   else {
914     drupal_set_title(t('No forums defined'));
915     $output = '';
916   }
917
918   return $output;
919 }
920
921 /**
922  * Format the forum listing.
923  *
924  * @ingroup themeable
925  */
926 function theme_forum_list($forums, $parents, $tid) {
927   global $user;
928
929   if ($forums) {
930
931     $header = array(t('Forum'), t('Topics'), t('Posts'), t('Last post'));
932
933     foreach ($forums as $forum) {
934       if ($forum->container) {
935         $description  = '<div style="margin-left: '. ($forum->depth * 30) ."px;\">\n";
936         $description .= ' <div class="name">'. l($forum->name, "forum/$forum->tid") ."</div>\n";
937
938         if ($forum->description) {
939           $description .= ' <div class="description">'. filter_xss_admin($forum->description) ."</div>\n";
940         }
941         $description .= "</div>\n";
942
943         $rows[] = array(array('data' => $description, 'class' => 'container', 'colspan' => '4'));
944       }
945       else {
946         $new_topics = _forum_topics_unread($forum->tid, $user->uid);
947         $forum->old_topics = $forum->num_topics - $new_topics;
948         if (!$user->uid) {
949           $new_topics = 0;
950         }
951
952         $description  = '<div style="margin-left: '. ($forum->depth * 30) ."px;\">\n";
953         $description .= ' <div class="name">'. l($forum->name, "forum/$forum->tid") ."</div>\n";
954
955         if ($forum->description) {
956           $description .= ' <div class="description">'. filter_xss_admin($forum->description) ."</div>\n";
957         }
958         $description .= "</div>\n";
959
960         $rows[] = array(
961           array('data' => $description, 'class' => 'forum'),
962           array('data' => $forum->num_topics . ($new_topics ? '<br />'. l(format_plural($new_topics, '1 new', '%count new'), "forum/$forum->tid", NULL, NULL, 'new') : ''), 'class' => 'topics'),
963           array('data' => $forum->num_posts, 'class' => 'posts'),
964           array('data' => _forum_format($forum->last_post), 'class' => 'last-reply'));
965       }
966     }
967
968     return theme('table', $header, $rows);
969
970   }
971
972 }
973
974 /**
975  * Format the topic listing.
976  *
977  * @ingroup themeable
978  */
979 function theme_forum_topic_list($tid, $topics, $sortby, $forum_per_page) {
980   global $forum_topic_list_header;
981
982   if ($topics) {
983
984     foreach ($topics as $topic) {
985       // folder is new if topic is new or there are new comments since last visit
986       if ($topic->tid != $tid) {
987         $rows[] = array(
988           array('data' => theme('forum_icon', $topic->new, $topic->num_comments, $topic->comment_mode, $topic->sticky), 'class' => 'icon'),
989           array('data' => check_plain($topic->title), 'class' => 'title'),
990           array('data' => l(t('This topic has been moved'), "forum/$topic->tid"), 'colspan' => '3')
991         );
992       }
993       else {
994         $rows[] = array(
995           array('data' => theme('forum_icon', $topic->new, $topic->num_comments, $topic->comment_mode, $topic->sticky), 'class' => 'icon'),
996           array('data' => l($topic->title, "node/$topic->nid"), 'class' => 'topic'),
997           array('data' => $topic->num_comments . ($topic->new_replies ? '<br />'. l(format_plural($topic->new_replies, '1 new', '%count new'), "node/$topic->nid", NULL, NULL, 'new') : ''), 'class' => 'replies'),
998           array('data' => _forum_format($topic), 'class' => 'created'),
999           array('data' => _forum_format($topic->last_reply), 'class' => 'last-reply')
1000         );
1001       }
1002     }
1003   }
1004
1005   $output .= theme('table', $forum_topic_list_header, $rows);
1006   $output .= theme('pager', NULL, $forum_per_page, 0);
1007
1008   return $output;
1009 }
1010
1011 /**
1012  * Format the icon for each individual topic.
1013  *
1014  * @ingroup themeable
1015  */
1016 function theme_forum_icon($new_posts, $num_posts = 0, $comment_mode = 0, $sticky = 0) {
1017
1018   if ($num_posts > variable_get('forum_hot_topic', 15)) {
1019     $icon = $new_posts ? 'hot-new' : 'hot';
1020   }
1021   else {
1022     $icon = $new_posts ? 'new' : 'default';
1023   }
1024
1025   if ($comment_mode == COMMENT_NODE_READ_ONLY || $comment_mode == COMMENT_NODE_DISABLED) {
1026     $icon = 'closed';
1027   }
1028
1029   if ($sticky == 1) {
1030     $icon = 'sticky';
1031   }
1032
1033   $output = theme('image', "misc/forum-$icon.png");
1034
1035   if ($new_posts) {
1036     $output = "<a name=\"new\">$output</a>";
1037   }
1038
1039   return $output;
1040 }
1041
1042 /**
1043  * Format the next/previous forum topic navigation links.
1044  *
1045  * @ingroup themeable
1046  */
1047 function theme_forum_topic_navigation($node) {
1048   $output = '';
1049
1050   // get previous and next topic
1051   $sql = "SELECT n.nid, n.title, n.sticky, l.comment_count, l.last_comment_timestamp FROM {node} n INNER JOIN {node_comment_statistics} l ON n.nid = l.nid INNER JOIN {term_node} r ON n.nid = r.nid AND r.tid = %d WHERE n.status = 1 AND n.type = 'forum' ORDER BY n.sticky DESC, ". _forum_get_topic_order_sql(variable_get('forum_order', 1));
1052   $result = db_query(db_rewrite_sql($sql), $node->tid);
1053
1054   while ($topic = db_fetch_object($result)) {
1055     if ($stop == 1) {
1056       $next = new StdClass();
1057       $next->nid = $topic->nid;
1058       $next->title = $topic->title;
1059       break;
1060     }
1061     if ($topic->nid == $node->nid) {
1062       $stop = 1;
1063     }
1064     else {
1065       $prev = new StdClass();
1066       $prev->nid = $topic->nid;
1067       $prev->title = $topic->title;
1068     }
1069   }
1070
1071   if ($prev || $next) {
1072     $output .= '<div class="forum-topic-navigation">';
1073
1074     if ($prev) {
1075       $output .= l(t('‹ ') . $prev->title, 'node/'. $prev->nid, array('class' => 'topic-previous', 'title' => t('Go to previous forum topic')));
1076     }
1077     if ($prev && $next) {
1078       // Word break (a is an inline element)
1079       $output .= ' ';
1080     }
1081     if ($next) {
1082       $output .= l($next->title . t(' ›'), 'node/'. $next->nid, array('class' => 'topic-next', 'title' => t('Go to next forum topic')));
1083     }
1084
1085     $output .= '</div>';
1086   }
1087
1088   return $output;
1089 }
1090
1091 function _forum_user_last_visit($nid) {
1092   global $user;
1093   static $history = array();
1094
1095   if (empty($history)) {
1096     $result = db_query('SELECT nid, timestamp FROM {history} WHERE uid = %d', $user->uid);
1097     while ($t = db_fetch_object($result)) {
1098       $history[$t->nid] = $t->timestamp > NODE_NEW_LIMIT ? $t->timestamp : NODE_NEW_LIMIT;
1099     }
1100   }
1101   return $history[$nid] ? $history[$nid] : NODE_NEW_LIMIT;
1102 }
1103
1104 function _forum_get_topic_order($sortby) {
1105   switch ($sortby) {
1106     case 1:
1107       return array('field' => 'l.last_comment_timestamp', 'sort' => 'desc');
1108       break;
1109     case 2:
1110       return array('field' => 'l.last_comment_timestamp', 'sort' => 'asc');
1111       break;
1112     case 3:
1113       return array('field' => 'l.comment_count', 'sort' => 'desc');
1114       break;
1115     case 4:
1116       return array('field' => 'l.comment_count', 'sort' => 'asc');
1117       break;
1118   }
1119 }
1120
1121 function _forum_get_topic_order_sql($sortby) {
1122   $order = _forum_get_topic_order($sortby);
1123   return $order['field'] .' '. $order['sort'];
1124 }
1125
1126