1bb18d5e4567be0c84e89a65adf2dcd54b7f880f
[plewww.git] / modules / taxonomy.module
1 <?php
2 // $Id: taxonomy.module 144 2007-03-28 07:52:20Z thierry $
3
4 /**
5  * @file
6  * Enables the organization of content into categories.
7  */
8
9 /**
10  * Implementation of hook_perm().
11  */
12 function taxonomy_perm() {
13   return array('administer taxonomy');
14 }
15
16 /**
17  * Implementation of hook_link().
18  *
19  * This hook is extended with $type = 'taxonomy terms' to allow themes to
20  * print lists of terms associated with a node. Themes can print taxonomy
21  * links with:
22  *
23  * if (module_exist('taxonomy')) {
24  *   $this->links(taxonomy_link('taxonomy terms', $node));
25  * }
26  */
27 function taxonomy_link($type, $node = NULL) {
28   if ($type == 'taxonomy terms' && $node != NULL) {
29     $links = array();
30     if (array_key_exists('taxonomy', $node)) {
31       foreach ($node->taxonomy as $term) {
32         $links[] = l($term->name, taxonomy_term_path($term), array('rel' => 'tag', 'title' => strip_tags($term->description)));
33       }
34     }
35     return $links;
36   }
37 }
38
39 function taxonomy_term_path($term) {
40   $vocabulary = taxonomy_get_vocabulary($term->vid);
41   if ($vocabulary->module != 'taxonomy' && $path = module_invoke($vocabulary->module, 'term_path', $term)) {
42     return $path;
43   }
44   return 'taxonomy/term/'. $term->tid;
45 }
46
47 /**
48  * Implementation of hook_menu().
49  */
50 function taxonomy_menu($may_cache) {
51   $items = array();
52
53   if ($may_cache) {
54     $items[] = array('path' => 'admin/taxonomy',
55       'title' => t('categories'),
56       'callback' => 'taxonomy_overview_vocabularies',
57       'access' => user_access('administer taxonomy'));
58
59     $items[] = array('path' => 'admin/taxonomy/list',
60       'title' => t('list'),
61       'type' => MENU_DEFAULT_LOCAL_TASK,
62       'weight' => -10);
63
64     $items[] = array('path' => 'admin/taxonomy/add/vocabulary',
65       'title' => t('add vocabulary'),
66       'callback' => 'taxonomy_admin_vocabulary_edit',
67       'access' => user_access('administer taxonomy'),
68       'type' => MENU_LOCAL_TASK);
69
70     $items[] = array('path' => 'admin/taxonomy/edit/vocabulary',
71       'title' => t('edit vocabulary'),
72       'callback' => 'taxonomy_admin_vocabulary_edit',
73       'access' => user_access('administer taxonomy'),
74       'type' => MENU_CALLBACK);
75
76     $items[] = array('path' => 'admin/taxonomy/edit/term',
77       'title' => t('edit term'),
78       'callback' => 'taxonomy_admin_term_edit',
79       'access' => user_access('administer taxonomy'),
80       'type' => MENU_CALLBACK);
81
82     $items[] = array('path' => 'taxonomy/term',
83       'title' => t('taxonomy term'),
84       'callback' => 'taxonomy_term_page',
85       'access' => user_access('access content'),
86       'type' => MENU_CALLBACK);
87
88     $items[] = array('path' => 'taxonomy/autocomplete',
89       'title' => t('autocomplete taxonomy'),
90       'callback' => 'taxonomy_autocomplete',
91       'access' => user_access('access content'),
92       'type' => MENU_CALLBACK);
93   }
94   else {
95     if (is_numeric(arg(2))) {
96       $items[] = array('path' => 'admin/taxonomy/' . arg(2),
97         'title' => t('list terms'),
98         'callback' => 'taxonomy_overview_terms',
99         'callback arguments' => array(arg(2)),
100         'access' => user_access('administer taxonomy'),
101         'type' => MENU_CALLBACK);
102
103       $items[] = array('path' => 'admin/taxonomy/' . arg(2) . '/list',
104         'title' => t('list'),
105         'type' => MENU_DEFAULT_LOCAL_TASK,
106         'weight' => -10);
107
108       $items[] = array('path' => 'admin/taxonomy/' . arg(2) . '/add/term',
109         'title' => t('add term'),
110         'callback' => 'taxonomy_form_term',
111         'callback arguments' => array(array('vid' => arg(2))),
112         'access' => user_access('administer taxonomy'),
113         'type' => MENU_LOCAL_TASK);
114     }
115   }
116
117   return $items;
118 }
119
120 /**
121  * List and manage vocabularies.
122  */
123 function taxonomy_overview_vocabularies() {
124   $vocabularies = taxonomy_get_vocabularies();
125   $rows = array();
126   foreach ($vocabularies as $vocabulary) {
127     $types = array();
128     foreach ($vocabulary->nodes as $type) {
129       $node_type = node_get_name($type);
130       $types[] = $node_type ? $node_type : $type;
131     }
132     $rows[] = array('name' => check_plain($vocabulary->name),
133       'type' => implode(', ', $types),
134       'edit' => l(t('edit vocabulary'), "admin/taxonomy/edit/vocabulary/$vocabulary->vid"),
135       'list' => l(t('list terms'), "admin/taxonomy/$vocabulary->vid"),
136       'add' => l(t('add terms'), "admin/taxonomy/$vocabulary->vid/add/term")
137     );
138   }
139   if (empty($rows)) {
140     $rows[] = array(array('data' => t('No categories available.'), 'colspan' => '5', 'class' => 'message'));
141   }
142   $header = array(t('Name'), t('Type'), array('data' => t('Operations'), 'colspan' => '3'));
143
144   return theme('table', $header, $rows, array('id' => 'taxonomy'));
145 }
146
147 /**
148  * Display a tree of all the terms in a vocabulary, with options to edit
149  * each one.
150  */
151 function taxonomy_overview_terms($vid) {
152   $destination = drupal_get_destination();
153
154   $header = array(t('Name'), t('Operations'));
155   $vocabulary = taxonomy_get_vocabulary($vid);
156
157   drupal_set_title(check_plain($vocabulary->name));
158   $start_from      = $_GET['page'] ? $_GET['page'] : 0;
159   $total_entries   = 0;  // total count for pager
160   $page_increment  = 25; // number of tids per page
161   $displayed_count = 0;  // number of tids shown
162
163   $tree = taxonomy_get_tree($vocabulary->vid);
164   foreach ($tree as $term) {
165     $total_entries++; // we're counting all-totals, not displayed
166     if (($start_from && ($start_from * $page_increment) >= $total_entries) || ($displayed_count == $page_increment)) { continue; }
167     $rows[] = array(_taxonomy_depth($term->depth) . ' ' . l($term->name, "taxonomy/term/$term->tid"), l(t('edit'), "admin/taxonomy/edit/term/$term->tid", array(), $destination));
168     $displayed_count++; // we're counting tids displayed
169   }
170
171   if (!$rows) {
172     $rows[] = array(array('data' => t('No terms available.'), 'colspan' => '2'));
173   }
174
175   $GLOBALS['pager_page_array'][] = $start_from;  // FIXME
176   $GLOBALS['pager_total'][] = intval($total_entries / $page_increment) + 1; // FIXME
177
178   if ($total_entries >= $page_increment) {
179     $rows[] = array(array('data' => theme('pager', NULL, $page_increment), 'colspan' => '2'));
180   }
181
182   return theme('table', $header, $rows, array('id' => 'taxonomy'));
183 }
184
185 /**
186  * Display form for adding and editing vocabularies.
187  */
188 function taxonomy_form_vocabulary($edit = array()) {
189   $form['name'] = array('#type' => 'textfield',
190     '#title' => t('Vocabulary name'),
191     '#default_value' => $edit['name'],
192     '#maxlength' => 64,
193     '#description' => t('The name for this vocabulary.  Example: "Topic".'),
194     '#required' => TRUE,
195   );
196   $form['description'] = array('#type' => 'textarea',
197     '#title' => t('Description'),
198     '#default_value' => $edit['description'],
199     '#description' => t('Description of the vocabulary; can be used by modules.'),
200   );
201   $form['help'] = array('#type' => 'textfield',
202     '#title' => t('Help text'),
203     '#default_value' => $edit['help'],
204     '#description' => t('Instructions to present to the user when choosing a term.'),
205   );
206   $form['nodes'] = array('#type' => 'checkboxes',
207     '#title' => t('Types'),
208     '#default_value' => $edit['nodes'],
209     '#options' => node_get_types(),
210     '#description' => t('A list of node types you want to associate with this vocabulary.'),
211     '#required' => TRUE,
212   );
213   $form['hierarchy'] = array('#type' => 'radios',
214     '#title' => t('Hierarchy'),
215     '#default_value' => $edit['hierarchy'],
216     '#options' => array(t('Disabled'), t('Single'), t('Multiple')),
217     '#description' => t('Allows <a href="%help-url">a tree-like hierarchy</a> between terms of this vocabulary.', array('%help-url' => url('admin/help/taxonomy', NULL, NULL, 'hierarchy'))),
218   );
219   $form['relations'] = array('#type' => 'checkbox',
220     '#title' => t('Related terms'),
221     '#default_value' => $edit['relations'],
222     '#description' => t('Allows <a href="%help-url">related terms</a> in this vocabulary.', array('%help-url' => url('admin/help/taxonomy', NULL, NULL, 'related-terms'))),
223   );
224   $form['tags'] = array('#type' => 'checkbox',
225     '#title' => t('Free tagging'),
226     '#default_value' => $edit['tags'],
227     '#description' => t('Content is categorized by typing terms instead of choosing from a list.'),
228   );
229   $form['multiple'] = array('#type' => 'checkbox',
230     '#title' => t('Multiple select'),
231     '#default_value' => $edit['multiple'],
232     '#description' => t('Allows nodes to have more than one term from this vocabulary (always true for free tagging).'),
233   );
234   $form['required'] = array('#type' => 'checkbox',
235     '#title' => t('Required'),
236     '#default_value' => $edit['required'],
237     '#description' => t('If enabled, every node <strong>must</strong> have at least one term in this vocabulary.'),
238   );
239   $form['weight'] = array('#type' => 'weight',
240     '#title' => t('Weight'),
241     '#default_value' => $edit['weight'],
242     '#description' => t('In listings, the heavier vocabularies will sink and the lighter vocabularies will be positioned nearer the top.'),
243   );
244
245   // Add extra vocabulary form elements.
246   $extra = module_invoke_all('taxonomy', 'form', 'vocabulary');
247   if (is_array($extra)) {
248     foreach ($extra as $key => $element) {
249       $extra[$key]['#weight'] = isset($extra[$key]['#weight']) ? $extra[$key]['#weight'] : -18;
250     }
251     $form = array_merge($form, $extra);
252   }
253
254   $form['submit'] = array('#type' => 'submit', '#value' => t('Submit'));
255   if ($edit['vid']) {
256     $form['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
257     $form['vid'] = array('#type' => 'value', '#value' => $edit['vid']);
258     $form['module'] = array('#type' => 'value', '#value' => $edit['module']);
259   }
260   return drupal_get_form('taxonomy_form_vocabulary', $form);
261 }
262
263 /**
264  * Accept the form submission for a vocabulary and save the results.
265  */
266 function taxonomy_form_vocabulary_submit($form_id, $form_values) {
267   // Fix up the nodes array to remove unchecked nodes.
268   $form_values['nodes'] = array_filter($form_values['nodes']);
269   switch (taxonomy_save_vocabulary($form_values)) {
270   case SAVED_NEW:
271     drupal_set_message(t('Created new vocabulary %name.', array('%name' => theme('placeholder', $form_values['name']))));
272     break;
273   case SAVED_UPDATED:
274     drupal_set_message(t('Updated vocabulary %name.', array('%name' => theme('placeholder', $form_values['name']))));
275     break;
276   }
277   return 'admin/taxonomy';
278 }
279
280 function taxonomy_save_vocabulary(&$edit) {
281   $edit['nodes'] = empty($edit['nodes']) ? array() : $edit['nodes'];
282
283   if ($edit['vid'] && $edit['name']) {
284     db_query("UPDATE {vocabulary} SET name = '%s', description = '%s', help = '%s', multiple = %d, required = %d, hierarchy = %d, relations = %d, tags = %d, weight = %d, module = '%s' WHERE vid = %d", $edit['name'], $edit['description'], $edit['help'], $edit['multiple'], $edit['required'], $edit['hierarchy'], $edit['relations'], $edit['tags'], $edit['weight'], isset($edit['module']) ? $edit['module'] : 'taxonomy', $edit['vid']);
285     db_query("DELETE FROM {vocabulary_node_types} WHERE vid = %d", $edit['vid']);
286     foreach ($edit['nodes'] as $type => $selected) {
287       db_query("INSERT INTO {vocabulary_node_types} (vid, type) VALUES (%d, '%s')", $edit['vid'], $type);
288     }
289     module_invoke_all('taxonomy', 'update', 'vocabulary', $edit);
290     $status = SAVED_UPDATED;
291   }
292   else if ($edit['vid']) {
293     $status = taxonomy_del_vocabulary($edit['vid']);
294   }
295   else {
296     $edit['vid'] = db_next_id('{vocabulary}_vid');
297     db_query("INSERT INTO {vocabulary} (vid, name, description, help, multiple, required, hierarchy, relations, tags, weight, module) VALUES (%d, '%s', '%s', '%s', %d, %d, %d, %d, %d, %d, '%s')", $edit['vid'], $edit['name'], $edit['description'], $edit['help'], $edit['multiple'], $edit['required'], $edit['hierarchy'], $edit['relations'], $edit['tags'], $edit['weight'], isset($edit['module']) ? $edit['module'] : 'taxonomy');
298     foreach ($edit['nodes'] as $type => $selected) {
299       db_query("INSERT INTO {vocabulary_node_types} (vid, type) VALUES (%d, '%s')", $edit['vid'], $type);
300     }
301     module_invoke_all('taxonomy', 'insert', 'vocabulary', $edit);
302     $status = SAVED_NEW;
303   }
304
305   cache_clear_all();
306
307   return $status;
308 }
309
310 function taxonomy_del_vocabulary($vid) {
311   $vocabulary = (array) taxonomy_get_vocabulary($vid);
312
313   db_query('DELETE FROM {vocabulary} WHERE vid = %d', $vid);
314   db_query('DELETE FROM {vocabulary_node_types} WHERE vid = %d', $vid);
315   $result = db_query('SELECT tid FROM {term_data} WHERE vid = %d', $vid);
316   while ($term = db_fetch_object($result)) {
317     taxonomy_del_term($term->tid);
318   }
319
320   module_invoke_all('taxonomy', 'delete', 'vocabulary', $vocabulary);
321
322   cache_clear_all();
323
324   return SAVED_DELETED;
325 }
326
327 function _taxonomy_confirm_del_vocabulary($vid) {
328   $vocabulary = taxonomy_get_vocabulary($vid);
329
330   $form['type'] = array('#type' => 'value', '#value' => 'vocabulary');
331   $form['vid'] = array('#type' => 'value', '#value' => $vid);
332   $form['name'] = array('#type' => 'value', '#value' => $vocabulary->name);
333   return confirm_form('taxonomy_vocabulary_confirm_delete', $form,
334                   t('Are you sure you want to delete the vocabulary %title?',
335                   array('%title' => theme('placeholder', $vocabulary->name))),
336                   'admin/taxonomy', t('Deleting a vocabulary will delete all the terms in it. This action cannot be undone.'),
337                   t('Delete'),
338                   t('Cancel'));
339 }
340
341 function taxonomy_vocabulary_confirm_delete_submit($form_id, $form_values) {
342   $status = taxonomy_del_vocabulary($form_values['vid']);
343   drupal_set_message(t('Deleted vocabulary %name.', array('%name' => theme('placeholder', $form_values['name']))));
344   return 'admin/taxonomy';
345 }
346
347 function taxonomy_form_term($edit = array()) {
348   $vocabulary_id = isset($edit['vid']) ? $edit['vid'] : arg(4);
349   $vocabulary = taxonomy_get_vocabulary($vocabulary_id);
350
351   $form['name'] = array('#type' => 'textfield', '#title' => t('Term name'), '#default_value' => $edit['name'], '#maxlength' => 64, '#description' => t('The name for this term.  Example: "Linux".'), '#required' => TRUE);
352
353   $form['description'] = array('#type' => 'textarea', '#title' => t('Description'), '#default_value' => $edit['description'], '#description' => t('A description of the term.'));
354
355   if ($vocabulary->hierarchy) {
356     $parent = array_keys(taxonomy_get_parents($edit['tid']));
357     $children = taxonomy_get_tree($vocabulary_id, $edit['tid']);
358
359     // A term can't be the child of itself, nor of its children.
360     foreach ($children as $child) {
361       $exclude[] = $child->tid;
362     }
363     $exclude[] = $edit['tid'];
364
365     if ($vocabulary->hierarchy == 1) {
366       $form['parent'] = _taxonomy_term_select(t('Parent'), 'parent', $parent, $vocabulary_id, l(t('Parent term'), 'admin/help/taxonomy', NULL, NULL, 'parent') .'.', 0, '<'. t('root') .'>', $exclude);
367     }
368     elseif ($vocabulary->hierarchy == 2) {
369       $form['parent'] = _taxonomy_term_select(t('Parents'), 'parent', $parent, $vocabulary_id, l(t('Parent terms'), 'admin/help/taxonomy', NULL, NULL, 'parent') .'.', 1, '<'. t('root') .'>', $exclude);
370     }
371   }
372
373   if ($vocabulary->relations) {
374     $form['relations'] = _taxonomy_term_select(t('Related terms'), 'relations', array_keys(taxonomy_get_related($edit['tid'])), $vocabulary_id, NULL, 1, '<'. t('none') .'>', array($edit['tid']));
375   }
376
377   $form['synonyms'] = array('#type' => 'textarea', '#title' => t('Synonyms'), '#default_value' => implode("\n", taxonomy_get_synonyms($edit['tid'])), '#description' => t('<a href="%help-url">Synonyms</a> of this term, one synonym per line.', array('%help-url' => url('admin/help/taxonomy', NULL, NULL, 'synonyms'))));
378   $form['weight'] = array('#type' => 'weight', '#title' => t('Weight'), '#default_value' => $edit['weight'], '#description' => t('In listings, the heavier terms will sink and the lighter terms will be positioned nearer the top.'));
379
380   // Add extra term form elements.
381   $extra = module_invoke_all('taxonomy', 'form', 'term');
382   if (is_array($extra)) {
383     foreach ($extra as $key => $element) {
384       $extra[$key]['#weight'] = isset($extra[$key]['#weight']) ? $extra[$key]['#weight'] : -18;
385     }
386     $form = array_merge($form, $extra);
387   }
388
389
390   $form['vid'] = array('#type' => 'value', '#value' => $vocabulary->vid);
391   $form['submit'] = array('#type' => 'submit', '#value' => t('Submit'));
392
393   if ($edit['tid']) {
394     $form['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
395     $form['tid'] = array('#type' => 'value', '#value' => $edit['tid']);
396   }
397   else {
398     $form['destination'] = array('#type' => 'hidden', '#value' => $_GET['q']);
399   }
400
401   return drupal_get_form('taxonomy_form_term', $form);
402 }
403
404 /**
405  * Accept the form submission for a taxonomy term and save the result.
406  */
407 function taxonomy_form_term_submit($form_id, $form_values) {
408   switch (taxonomy_save_term($form_values)) {
409     case SAVED_NEW:
410       drupal_set_message(t('Created new term %term.', array('%term' => theme('placeholder', $form_values['name']))));
411       break;
412     case SAVED_UPDATED:
413       drupal_set_message(t('The term %term has been updated.', array('%term' => theme('placeholder', $form_values['name']))));
414       break;
415   }
416   return 'admin/taxonomy';
417 }
418
419 function taxonomy_save_term(&$edit) {
420   if ($edit['tid'] && $edit['name']) {
421     db_query("UPDATE {term_data} SET name = '%s', description = '%s', weight = %d WHERE tid = %d", $edit['name'], $edit['description'], $edit['weight'], $edit['tid']);
422     module_invoke_all('taxonomy', 'update', 'term', $edit);
423     $status = SAVED_UPDATED;
424   }
425   else if ($edit['tid']) {
426     return taxonomy_del_term($edit['tid']);
427   }
428   else {
429     $edit['tid'] = db_next_id('{term_data}_tid');
430     db_query("INSERT INTO {term_data} (tid, name, description, vid, weight) VALUES (%d, '%s', '%s', %d, %d)", $edit['tid'], $edit['name'], $edit['description'], $edit['vid'], $edit['weight']);
431     module_invoke_all('taxonomy', 'insert', 'term', $edit);
432     $status = SAVED_NEW;
433   }
434
435   db_query('DELETE FROM {term_relation} WHERE tid1 = %d OR tid2 = %d', $edit['tid'], $edit['tid']);
436   if ($edit['relations']) {
437     foreach ($edit['relations'] as $related_id) {
438       if ($related_id != 0) {
439         db_query('INSERT INTO {term_relation} (tid1, tid2) VALUES (%d, %d)', $edit['tid'], $related_id);
440       }
441     }
442   }
443
444   db_query('DELETE FROM {term_hierarchy} WHERE tid = %d', $edit['tid']);
445   if (!isset($edit['parent']) || empty($edit['parent'])) {
446     $edit['parent'] = array(0);
447   }
448   if (is_array($edit['parent'])) {
449     foreach ($edit['parent'] as $parent) {
450       if (is_array($parent)) {
451         foreach ($parent as $tid) {
452           db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $edit['tid'], $tid);
453         }
454       }
455       else {
456         db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $edit['tid'], $parent);
457       }
458     }
459   }
460   else {
461     db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $edit['tid'], $edit['parent']);
462   }
463
464   db_query('DELETE FROM {term_synonym} WHERE tid = %d', $edit['tid']);
465   if ($edit['synonyms']) {
466     foreach (explode ("\n", str_replace("\r", '', $edit['synonyms'])) as $synonym) {
467       if ($synonym) {
468         db_query("INSERT INTO {term_synonym} (tid, name) VALUES (%d, '%s')", $edit['tid'], chop($synonym));
469       }
470     }
471   }
472
473   cache_clear_all();
474
475   return $status;
476 }
477
478 function taxonomy_del_term($tid) {
479   $tids = array($tid);
480   while ($tids) {
481     $children_tids = $orphans = array();
482     foreach ($tids as $tid) {
483       // See if any of the term's children are about to be become orphans:
484       if ($children = taxonomy_get_children($tid)) {
485         foreach ($children as $child) {
486           // If the term has multiple parents, we don't delete it.
487           $parents = taxonomy_get_parents($child->tid);
488           if (count($parents) == 1) {
489             $orphans[] = $child->tid;
490           }
491         }
492       }
493
494       $term = (array) taxonomy_get_term($tid);
495
496       db_query('DELETE FROM {term_data} WHERE tid = %d', $tid);
497       db_query('DELETE FROM {term_hierarchy} WHERE tid = %d', $tid);
498       db_query('DELETE FROM {term_relation} WHERE tid1 = %d OR tid2 = %d', $tid, $tid);
499       db_query('DELETE FROM {term_synonym} WHERE tid = %d', $tid);
500       db_query('DELETE FROM {term_node} WHERE tid = %d', $tid);
501
502       module_invoke_all('taxonomy', 'delete', 'term', $term);
503     }
504
505     $tids = $orphans;
506   }
507
508   cache_clear_all();
509
510   return SAVED_DELETED;
511 }
512
513 function _taxonomy_confirm_del_term($tid) {
514   $term = taxonomy_get_term($tid);
515
516   $form['type'] = array('#type' => 'value', '#value' => 'term');
517   $form['name'] = array('#type' => 'value', '#value' => $term->name);
518   $form['tid'] = array('#type' => 'value', '#value' => $tid);
519   return confirm_form('taxonomy_term_confirm_delete', $form,
520                   t('Are you sure you want to delete the term %title?',
521                   array('%title' => theme('placeholder', $term->name))),
522                   'admin/taxonomy',
523                   t('Deleting a term will delete all its children if there are any. This action cannot be undone.'),
524                   t('Delete'),
525                   t('Cancel'));
526 }
527
528 function taxonomy_term_confirm_delete_submit($form_id, $form_values) {
529   taxonomy_del_term($form_values['tid']);
530   drupal_set_message(t('Deleted term %name.', array('%name' => theme('placeholder', $form_values['name']))));
531   return 'admin/taxonomy';
532 }
533
534 /**
535  * Generate a form element for selecting terms from a vocabulary.
536  */
537 function taxonomy_form($vid, $value = 0, $help = NULL, $name = 'taxonomy') {
538   $vocabulary = taxonomy_get_vocabulary($vid);
539   $help = ($help) ? $help : $vocabulary->help;
540   if ($vocabulary->required) {
541     $blank = 0;
542   }
543   else {
544     $blank = '<'. t('none') .'>';
545   }
546
547   return _taxonomy_term_select(check_plain($vocabulary->name), $name, $value, $vid, $help, intval($vocabulary->multiple), $blank);
548 }
549
550 /**
551  * Generate a set of options for selecting a term from all vocabularies. Can be
552  * passed to form_select.
553  */
554 function taxonomy_form_all($free_tags = 0) {
555   $vocabularies = taxonomy_get_vocabularies();
556   $options = array();
557   foreach ($vocabularies as $vid => $vocabulary) {
558     if ($vocabulary->tags && !$free_tags) { continue; }
559     $tree = taxonomy_get_tree($vid);
560     $options[$vocabulary->name] = array();
561     if ($tree) {
562       foreach ($tree as $term) {
563         $options[$vocabulary->name][$term->tid] = _taxonomy_depth($term->depth, '-') . $term->name;
564       }
565     }
566   }
567   return $options;
568 }
569
570 /**
571  * Return an array of all vocabulary objects.
572  *
573  * @param $type
574  *   If set, return only those vocabularies associated with this node type.
575  */
576 function taxonomy_get_vocabularies($type = NULL) {
577   if ($type) {
578     $result = db_query(db_rewrite_sql("SELECT v.vid, v.*, n.type FROM {vocabulary} v LEFT JOIN {vocabulary_node_types} n ON v.vid = n.vid WHERE n.type = '%s' ORDER BY v.weight, v.name", 'v', 'vid'), $type);
579   }
580   else {
581     $result = db_query(db_rewrite_sql('SELECT v.*, n.type FROM {vocabulary} v LEFT JOIN {vocabulary_node_types} n ON v.vid = n.vid ORDER BY v.weight, v.name', 'v', 'vid'));
582   }
583
584   $vocabularies = array();
585   $node_types = array();
586   while ($voc = db_fetch_object($result)) {
587     $node_types[$voc->vid][] = $voc->type;
588     unset($voc->type);
589     $voc->nodes = $node_types[$voc->vid];
590     $vocabularies[$voc->vid] = $voc;
591   }
592
593   return $vocabularies;
594 }
595
596 /**
597  * Generate a form for selecting terms to associate with a node.
598  */
599 function taxonomy_form_alter($form_id, &$form) {
600   if (isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id) {
601     $node = $form['#node'];
602
603     if (!isset($node->taxonomy)) {
604       if ($node->nid) {
605         $terms = taxonomy_node_get_terms($node->nid);
606       }
607       else {
608         $terms = array();
609       }
610     }
611     else {
612       $terms = $node->taxonomy;
613     }
614
615     $c = db_query(db_rewrite_sql("SELECT v.* FROM {vocabulary} v INNER JOIN {vocabulary_node_types} n ON v.vid = n.vid WHERE n.type = '%s' ORDER BY v.weight, v.name", 'v', 'vid'), $node->type);
616
617     while ($vocabulary = db_fetch_object($c)) {
618       if ($vocabulary->tags) {
619         $typed_terms = array();
620         foreach ($terms as $term) {
621           // Extract terms belonging to the vocabulary in question.
622           if ($term->vid == $vocabulary->vid) {
623
624             // Commas and quotes in terms are special cases, so encode 'em.
625             if (preg_match('/,/', $term->name) || preg_match('/"/', $term->name)) {
626               $term->name = '"'.preg_replace('/"/', '""', $term->name).'"';
627             }
628
629             $typed_terms[] = $term->name;
630           }
631         }
632         $typed_string = implode(', ', $typed_terms) . (array_key_exists('tags', $terms) ? $terms['tags'][$vocabulary->vid] : NULL);
633
634         if ($vocabulary->help) {
635           $help = $vocabulary->help;
636         }
637         else {
638           $help = t('A comma-separated list of terms describing this content.  Example: funny, bungee jumping, "Company, Inc.".');
639         }
640         $form['taxonomy']['tags'][$vocabulary->vid] = array('#type' => 'textfield',
641           '#title' => $vocabulary->name,
642           '#description' => $help,
643           '#required' => $vocabulary->required,
644           '#default_value' => $typed_string,
645           '#autocomplete_path' => 'taxonomy/autocomplete/'. $vocabulary->vid,
646           '#weight' => $vocabulary->weight,
647           '#maxlength' => 100,
648         );
649       }
650       else {
651         // Extract terms belonging to the vocabulary in question.
652         $default_terms = array();
653         foreach ($terms as $term) {
654           if ($term->vid == $vocabulary->vid) {
655             $default_terms[$term->tid] = $term;
656           }
657         }
658         $form['taxonomy'][$vocabulary->vid] = taxonomy_form($vocabulary->vid, array_keys($default_terms), $vocabulary->help);
659         $form['taxonomy'][$vocabulary->vid]['#weight'] = $vocabulary->weight;
660         $form['taxonomy'][$vocabulary->vid]['#required'] = $vocabulary->required;
661       }
662     }
663     if (isset($form['taxonomy'])) {
664       $form['taxonomy'] += array('#type' => 'fieldset', '#title' => t('Categories'), '#collapsible' => TRUE, '#collapsed' => FALSE, '#tree' => TRUE, '#weight' => -3);
665     }
666   }
667 }
668
669 /**
670  * Find all terms associated to the given node, within one vocabulary.
671  */
672 function taxonomy_node_get_terms_by_vocabulary($nid, $vid, $key = 'tid') {
673   $result = db_query(db_rewrite_sql('SELECT t.tid, t.* FROM {term_data} t INNER JOIN {term_node} r ON r.tid = t.tid WHERE t.vid = %d AND r.nid = %d ORDER BY weight', 't', 'tid'), $vid, $nid);
674   $terms = array();
675   while ($term = db_fetch_object($result)) {
676     $terms[$term->$key] = $term;
677   }
678   return $terms;
679 }
680
681 /**
682  * Find all terms associated to the given node, ordered by vocabulary and term weight.
683  */
684 function taxonomy_node_get_terms($nid, $key = 'tid') {
685   static $terms;
686
687   if (!isset($terms[$nid])) {
688     $result = db_query(db_rewrite_sql('SELECT t.* FROM {term_node} r INNER JOIN {term_data} t ON r.tid = t.tid INNER JOIN {vocabulary} v ON t.vid = v.vid WHERE r.nid = %d ORDER BY v.weight, t.weight, t.name', 't', 'tid'), $nid);
689     $terms[$nid] = array();
690     while ($term = db_fetch_object($result)) {
691       $terms[$nid][$term->$key] = $term;
692     }
693   }
694   return $terms[$nid];
695 }
696
697 /**
698  * Make sure incoming vids are free tagging enabled.
699  */
700 function taxonomy_node_validate(&$node) {
701   if ($node->taxonomy) {
702     $terms = $node->taxonomy;
703     if ($terms['tags']) {
704       foreach ($terms['tags'] as $vid => $vid_value) {
705         $vocabulary = taxonomy_get_vocabulary($vid);
706         if (!$vocabulary->tags) {
707           // see form_get_error $key = implode('][', $element['#parents']);
708           // on why this is the key
709           form_set_error("taxonomy][tags][$vid", t('The %name vocabulary can not be modified in this way.', array('%name' => theme('placeholder', $vocabulary->name))));
710         }
711       }
712     }
713   }
714 }
715
716 /**
717  * Save term associations for a given node.
718  */
719 function taxonomy_node_save($nid, $terms) {
720   taxonomy_node_delete($nid);
721
722   // Free tagging vocabularies do not send their tids in the form,
723   // so we'll detect them here and process them independently.
724   if (isset($terms['tags'])) {
725     $typed_input = $terms['tags'];
726     unset($terms['tags']);
727
728     foreach ($typed_input as $vid => $vid_value) {
729       // This regexp allows the following types of user input:
730       // this, "somecmpany, llc", "and ""this"" w,o.rks", foo bar
731       $regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"|(?: [^",]*))%x';
732       preg_match_all($regexp, $vid_value, $matches);
733       $typed_terms = array_unique($matches[1]);
734
735       $inserted = array();
736       foreach ($typed_terms as $typed_term) {
737         // If a user has escaped a term (to demonstrate that it is a group,
738         // or includes a comma or quote character), we remove the escape
739         // formatting so to save the term into the DB as the user intends.
740         $typed_term = str_replace('""', '"', preg_replace('/^"(.*)"$/', '\1', $typed_term));
741         $typed_term = trim($typed_term);
742         if ($typed_term == "") { continue; }
743
744         // See if the term exists in the chosen vocabulary
745         // and return the tid, otherwise, add a new record.
746         $possibilities = taxonomy_get_term_by_name($typed_term);
747         $typed_term_tid = NULL; // tid match if any.
748         foreach ($possibilities as $possibility) {
749           if ($possibility->vid == $vid) {
750             $typed_term_tid = $possibility->tid;
751           }
752         }
753
754         if (!$typed_term_tid) {
755           $edit = array('vid' => $vid, 'name' => $typed_term);
756           $status = taxonomy_save_term($edit);
757           $typed_term_tid = $edit['tid'];
758         }
759
760         // defend against duplicate, different cased tags
761         if (!isset($inserted[$typed_term_tid])) {
762           db_query('INSERT INTO {term_node} (nid, tid) VALUES (%d, %d)', $nid, $typed_term_tid);
763           $inserted[$typed_term_tid] = TRUE;
764         }
765       }
766     }
767   }
768
769   if (is_array($terms)) {
770     foreach ($terms as $term) {
771       if (is_array($term)) {
772         foreach ($term as $tid) {
773           if ($tid) {
774             db_query('INSERT INTO {term_node} (nid, tid) VALUES (%d, %d)', $nid, $tid);
775           }
776         }
777       }
778       else if (is_object($term)) {
779         db_query('INSERT INTO {term_node} (nid, tid) VALUES (%d, %d)', $nid, $term->tid);
780       }
781       else if ($term) {
782         db_query('INSERT INTO {term_node} (nid, tid) VALUES (%d, %d)', $nid, $term);
783       }
784     }
785   }
786 }
787
788 /**
789  * Remove associations of a node to its terms.
790  */
791 function taxonomy_node_delete($nid) {
792   db_query('DELETE FROM {term_node} WHERE nid = %d', $nid);
793 }
794
795 /**
796  * Find all term objects related to a given term ID.
797  */
798 function taxonomy_get_related($tid, $key = 'tid') {
799   if ($tid) {
800     $result = db_query('SELECT t.*, tid1, tid2 FROM {term_relation}, {term_data} t WHERE (t.tid = tid1 OR t.tid = tid2) AND (tid1 = %d OR tid2 = %d) AND t.tid != %d ORDER BY weight, name', $tid, $tid, $tid);
801     $related = array();
802     while ($term = db_fetch_object($result)) {
803       $related[$term->$key] = $term;
804     }
805     return $related;
806   }
807   else {
808     return array();
809   }
810 }
811
812 /**
813  * Find all parents of a given term ID.
814  */
815 function taxonomy_get_parents($tid, $key = 'tid') {
816   if ($tid) {
817     $result = db_query(db_rewrite_sql('SELECT t.tid, t.* FROM {term_data} t INNER JOIN {term_hierarchy} h ON h.parent = t.tid WHERE h.tid = %d ORDER BY weight, name', 't', 'tid'), $tid);
818     $parents = array();
819     while ($parent = db_fetch_object($result)) {
820       $parents[$parent->$key] = $parent;
821     }
822     return $parents;
823   }
824   else {
825     return array();
826   }
827 }
828
829 /**
830  * Find all ancestors of a given term ID.
831  */
832 function taxonomy_get_parents_all($tid) {
833   $parents = array();
834   if ($tid) {
835     $parents[] = taxonomy_get_term($tid);
836     $n = 0;
837     while ($parent = taxonomy_get_parents($parents[$n]->tid)) {
838       $parents = array_merge($parents, $parent);
839       $n++;
840     }
841   }
842   return $parents;
843 }
844
845 /**
846  * Find all children of a term ID.
847  */
848 function taxonomy_get_children($tid, $vid = 0, $key = 'tid') {
849   if ($vid) {
850     $result = db_query(db_rewrite_sql('SELECT t.* FROM {term_data} t INNER JOIN {term_hierarchy} h ON h.tid = t.tid WHERE t.vid = %d AND h.parent = %d ORDER BY weight, name', 't', 'tid'), $vid, $tid);
851   }
852   else {
853     $result = db_query(db_rewrite_sql('SELECT t.* FROM {term_data} t INNER JOIN {term_hierarchy} h ON h.tid = t.tid WHERE parent = %d ORDER BY weight, name', 't', 'tid'), $tid);
854   }
855   $children = array();
856   while ($term = db_fetch_object($result)) {
857     $children[$term->$key] = $term;
858   }
859   return $children;
860 }
861
862 /**
863  * Create a hierarchical representation of a vocabulary.
864  *
865  * @param $vid
866  *   Which vocabulary to generate the tree for.
867  *
868  * @param $parent
869  *   The term ID under which to generate the tree. If 0, generate the tree
870  *   for the entire vocabulary.
871  *
872  * @param $depth
873  *   Internal use only.
874  *
875  * @param $max_depth
876  *   The number of levels of the tree to return. Leave NULL to return all levels.
877  *
878  * @return
879  *   An array of all term objects in the tree. Each term object is extended
880  *   to have "depth" and "parents" attributes in addition to its normal ones.
881  */
882 function taxonomy_get_tree($vid, $parent = 0, $depth = -1, $max_depth = NULL) {
883   static $children, $parents, $terms;
884
885   $depth++;
886
887   // We cache trees, so it's not CPU-intensive to call get_tree() on a term
888   // and its children, too.
889   if (!isset($children[$vid])) {
890     $children[$vid] = array();
891
892     $result = db_query(db_rewrite_sql('SELECT t.tid, t.*, parent FROM {term_data} t INNER JOIN  {term_hierarchy} h ON t.tid = h.tid WHERE t.vid = %d ORDER BY weight, name', 't', 'tid'), $vid);
893     while ($term = db_fetch_object($result)) {
894       $children[$vid][$term->parent][] = $term->tid;
895       $parents[$vid][$term->tid][] = $term->parent;
896       $terms[$vid][$term->tid] = $term;
897     }
898   }
899
900   $max_depth = (is_null($max_depth)) ? count($children[$vid]) : $max_depth;
901   if ($children[$vid][$parent]) {
902     foreach ($children[$vid][$parent] as $child) {
903       if ($max_depth > $depth) {
904         $terms[$vid][$child]->depth = $depth;
905         // The "parent" attribute is not useful, as it would show one parent only.
906         unset($terms[$vid][$child]->parent);
907         $terms[$vid][$child]->parents = $parents[$vid][$child];
908         $tree[] = $terms[$vid][$child];
909
910         if ($children[$vid][$child]) {
911           $tree = array_merge($tree, taxonomy_get_tree($vid, $child, $depth, $max_depth));
912         }
913       }
914     }
915   }
916
917   return $tree ? $tree : array();
918 }
919
920 /**
921  * Return an array of synonyms of the given term ID.
922  */
923 function taxonomy_get_synonyms($tid) {
924   if ($tid) {
925     $result = db_query('SELECT name FROM {term_synonym} WHERE tid = %d', $tid);
926     while ($synonym = db_fetch_array($result)) {
927       $synonyms[] = $synonym['name'];
928     }
929     return $synonyms ? $synonyms : array();
930   }
931   else {
932     return array();
933   }
934 }
935
936 /**
937  * Return the term object that has the given string as a synonym.
938  */
939 function taxonomy_get_synonym_root($synonym) {
940   return db_fetch_object(db_query("SELECT * FROM {term_synonym} s, {term_data} t WHERE t.tid = s.tid AND s.name = '%s'", $synonym));
941 }
942
943 /**
944  * Given a term id, count the number of published nodes in it.
945  */
946 function taxonomy_term_count_nodes($tid, $type = 0) {
947   static $count;
948
949   if (!isset($count[$type])) {
950     // $type == 0 always evaluates true is $type is a string
951     if (is_numeric($type)) {
952       $result = db_query(db_rewrite_sql('SELECT t.tid, COUNT(n.nid) AS c FROM {term_node} t INNER JOIN {node} n ON t.nid = n.nid WHERE n.status = 1 GROUP BY t.tid'));
953     }
954     else {
955       $result = db_query(db_rewrite_sql("SELECT t.tid, COUNT(n.nid) AS c FROM {term_node} t INNER JOIN {node} n ON t.nid = n.nid WHERE n.status = 1 AND n.type = '%s' GROUP BY t.tid"), $type);
956     }
957     while ($term = db_fetch_object($result)) {
958       $count[$type][$term->tid] = $term->c;
959     }
960   }
961
962   foreach (_taxonomy_term_children($tid) as $c) {
963     $children_count += taxonomy_term_count_nodes($c, $type);
964   }
965   return $count[$type][$tid] + $children_count;
966 }
967
968 /**
969  * Helper for taxonomy_term_count_nodes().
970  */
971 function _taxonomy_term_children($tid) {
972   static $children;
973
974   if (!isset($children)) {
975     $result = db_query('SELECT tid, parent FROM {term_hierarchy}');
976     while ($term = db_fetch_object($result)) {
977       $children[$term->parent][] = $term->tid;
978     }
979   }
980   return $children[$tid] ? $children[$tid] : array();
981 }
982
983 /**
984  * Try to map a string to an existing term, as for glossary use.
985  *
986  * Provides a case-insensitive and trimmed mapping, to maximize the
987  * likelihood of a successful match.
988  *
989  * @param name
990  *   Name of the term to search for.
991  *
992  * @return
993  *   An array of matching term objects.
994  */
995 function taxonomy_get_term_by_name($name) {
996   $db_result = db_query(db_rewrite_sql("SELECT t.tid, t.* FROM {term_data} t WHERE LOWER('%s') LIKE LOWER(t.name)", 't', 'tid'), trim($name));
997   $result = array();
998   while ($term = db_fetch_object($db_result)) {
999     $result[] = $term;
1000   }
1001
1002   return $result;
1003 }
1004
1005 /**
1006  * Return the vocabulary object matching a vocabulary ID.
1007  */
1008 function taxonomy_get_vocabulary($vid) {
1009   static $vocabularies = array();
1010
1011   if (!array_key_exists($vid, $vocabularies)) {
1012     $result = db_query('SELECT v.*, n.type FROM {vocabulary} v LEFT JOIN {vocabulary_node_types} n ON v.vid = n.vid WHERE v.vid = %d ORDER BY v.weight, v.name', $vid);
1013     $node_types = array();
1014     while ($voc = db_fetch_object($result)) {
1015       $node_types[] = $voc->type;
1016       unset($voc->type);
1017       $voc->nodes = $node_types;
1018       $vocabularies[$vid] = $voc;
1019     }
1020   }
1021
1022   return $vocabularies[$vid];
1023 }
1024
1025 /**
1026  * Return the term object matching a term ID.
1027  */
1028 function taxonomy_get_term($tid) {
1029   // simple cache using a static var?
1030   return db_fetch_object(db_query('SELECT * FROM {term_data} WHERE tid = %d', $tid));
1031 }
1032
1033 function _taxonomy_term_select($title, $name, $value, $vocabulary_id, $description, $multiple, $blank, $exclude = array()) {
1034   $tree = taxonomy_get_tree($vocabulary_id);
1035   $options = array();
1036
1037   if ($blank) {
1038     $options[0] = $blank;
1039   }
1040   if ($tree) {
1041     foreach ($tree as $term) {
1042       if (!in_array($term->tid, $exclude)) {
1043         $options[$term->tid] = _taxonomy_depth($term->depth, '-') . $term->name;
1044       }
1045     }
1046     if (!$blank && !$value) {
1047       // required but without a predefined value, so set first as predefined
1048       $value = $tree[0]->tid;
1049     }
1050   }
1051
1052   return array('#type' => 'select',
1053     '#title' => $title,
1054     '#default_value' => $value,
1055     '#options' => $options,
1056     '#description' => $description,
1057     '#multiple' => $multiple,
1058     '#size' => $multiple ? min(9, count($options)) : 0,
1059     '#weight' => -15,
1060     '#theme' => 'taxonomy_term_select',
1061   );
1062 }
1063
1064 function theme_taxonomy_term_select($element) {
1065   return theme('select', $element);
1066 }
1067
1068 function _taxonomy_depth($depth, $graphic = '--') {
1069   for ($n = 0; $n < $depth; $n++) {
1070     $result .= $graphic;
1071   }
1072   return $result;
1073 }
1074
1075 /**
1076  * Finds all nodes that match selected taxonomy conditions.
1077  *
1078  * @param $tids
1079  *   An array of term IDs to match.
1080  * @param $operator
1081  *   How to interpret multiple IDs in the array. Can be "or" or "and".
1082  * @param $depth
1083  *   How many levels deep to traverse the taxonomy tree. Can be a nonnegative
1084  *   integer or "all".
1085  * @param $pager
1086  *   Whether the nodes are to be used with a pager (the case on most Drupal
1087  *   pages) or not (in an XML feed, for example).
1088  * @param $order
1089  *   The order clause for the query that retrieve the nodes.
1090  * @return
1091  *   A resource identifier pointing to the query results.
1092  */
1093 function taxonomy_select_nodes($tids = array(), $operator = 'or', $depth = 0, $pager = TRUE, $order = 'n.sticky DESC, n.created DESC') {
1094   if (count($tids) > 0) {
1095     // For each term ID, generate an array of descendant term IDs to the right depth.
1096     $descendant_tids = array();
1097     if ($depth === 'all') {
1098       $depth = NULL;
1099     }
1100     foreach ($tids as $index => $tid) {
1101       $term = taxonomy_get_term($tid);
1102       $tree = taxonomy_get_tree($term->vid, $tid, -1, $depth);
1103       $descendant_tids[] = array_merge(array($tid), array_map('_taxonomy_get_tid_from_term', $tree));
1104     }
1105
1106     if ($operator == 'or') {
1107       $str_tids = implode(',', call_user_func_array('array_merge', $descendant_tids));
1108       $sql = 'SELECT DISTINCT(n.nid), n.sticky, n.title, n.created FROM {node} n INNER JOIN {term_node} tn ON n.nid = tn.nid WHERE tn.tid IN ('. $str_tids .') AND n.status = 1 ORDER BY '. $order;
1109       $sql_count = 'SELECT COUNT(DISTINCT(n.nid)) FROM {node} n INNER JOIN {term_node} tn ON n.nid = tn.nid WHERE tn.tid IN ('. $str_tids .') AND n.status = 1';
1110     }
1111     else {
1112       $joins = '';
1113       $wheres = '';
1114       foreach ($descendant_tids as $index => $tids) {
1115         $joins .= ' INNER JOIN {term_node} tn'. $index .' ON n.nid = tn'. $index .'.nid';
1116         $wheres .= ' AND tn'. $index .'.tid IN ('. implode(',', $tids) .')';
1117       }
1118       $sql = 'SELECT DISTINCT(n.nid), n.sticky, n.title, n.created FROM {node} n '. $joins .' WHERE n.status = 1 '. $wheres .' ORDER BY '. $order;
1119       $sql_count = 'SELECT COUNT(DISTINCT(n.nid)) FROM {node} n '. $joins .' WHERE n.status = 1 ' . $wheres;
1120     }
1121     $sql = db_rewrite_sql($sql);
1122     $sql_count = db_rewrite_sql($sql_count);
1123     if ($pager) {
1124       $result = pager_query($sql, variable_get('default_nodes_main', 10), 0, $sql_count);
1125     }
1126     else {
1127       $result = db_query_range($sql, 0, variable_get('feed_default_items', 10));
1128     }
1129   }
1130
1131   return $result;
1132 }
1133
1134 /**
1135  * Accepts the result of a pager_query() call, such as that performed by
1136  * taxonomy_select_nodes(), and formats each node along with a pager.
1137 */
1138 function taxonomy_render_nodes($result) {
1139   if (db_num_rows($result) > 0) {
1140     while ($node = db_fetch_object($result)) {
1141       $output .= node_view(node_load($node->nid), 1);
1142     }
1143     $output .= theme('pager', NULL, variable_get('default_nodes_main', 10), 0);
1144   }
1145   else {
1146     $output .= t('There are currently no posts in this category.');
1147   }
1148   return $output;
1149 }
1150
1151 /**
1152  * Implementation of hook_nodeapi().
1153  */
1154 function taxonomy_nodeapi($node, $op, $arg = 0) {
1155   switch ($op) {
1156     case 'load':
1157      $output['taxonomy'] = taxonomy_node_get_terms($node->nid);
1158      return $output;
1159     case 'insert':
1160       taxonomy_node_save($node->nid, $node->taxonomy);
1161       break;
1162     case 'update':
1163       taxonomy_node_save($node->nid, $node->taxonomy);
1164       break;
1165     case 'delete':
1166       taxonomy_node_delete($node->nid);
1167       break;
1168     case 'validate':
1169       taxonomy_node_validate($node);
1170       break;
1171     case 'rss item':
1172       return taxonomy_rss_item($node);
1173     case 'update index':
1174       return taxonomy_node_update_index($node);
1175   }
1176 }
1177
1178 /**
1179  * Implementation of hook_nodeapi('update_index').
1180  */
1181 function taxonomy_node_update_index(&$node) {
1182   $output = array();
1183   foreach ($node->taxonomy as $term) {
1184     $output[] = $term->name;
1185   }
1186   if (count($output)) {
1187     return '<strong>('. implode(', ', $output) .')</strong>';
1188   }
1189 }
1190
1191 /**
1192  * Menu callback; displays all nodes associated with a term.
1193  */
1194 function taxonomy_term_page($str_tids = '', $depth = 0, $op = 'page') {
1195   if (preg_match('/^([0-9]+[+ ])+[0-9]+$/', $str_tids)) {
1196     $operator = 'or';
1197     // The '+' character in a query string may be parsed as ' '.
1198     $tids = preg_split('/[+ ]/', $str_tids);
1199   }
1200   else if (preg_match('/^([0-9]+,)*[0-9]+$/', $str_tids)) {
1201     $operator = 'and';
1202     $tids = explode(',', $str_tids);
1203   }
1204   else {
1205     drupal_not_found();
1206   }
1207
1208   if ($tids) {
1209     $result = db_query(db_rewrite_sql('SELECT t.tid, t.name FROM {term_data} t WHERE t.tid IN (%s)', 't', 'tid'), implode(',', $tids));
1210     $tids = array(); // we rebuild the $tids-array so it only contains terms the user has access to.
1211     $names = array();
1212     while ($term = db_fetch_object($result)) {
1213       $tids[] = $term->tid;
1214       $names[] = $term->name;
1215     }
1216
1217     if ($names) {
1218       drupal_set_title($title = check_plain(implode(', ', $names)));
1219
1220       switch ($op) {
1221         case 'page':
1222           // Build breadcrumb based on first hierarchy of first term:
1223           $current->tid = $tids[0];
1224           $breadcrumbs = array(array('path' => $_GET['q']));
1225           while ($parents = taxonomy_get_parents($current->tid)) {
1226             $current = array_shift($parents);
1227             $breadcrumbs[] = array('path' => 'taxonomy/term/'. $current->tid, 'title' => $current->name);
1228           }
1229           $breadcrumbs = array_reverse($breadcrumbs);
1230           menu_set_location($breadcrumbs);
1231
1232           drupal_add_link(array('rel' => 'alternate',
1233                                 'type' => 'application/rss+xml',
1234                                 'title' => 'RSS - '. $title,
1235                                 'href' => url('taxonomy/term/'. $str_tids .'/'. $depth .'/feed')));
1236
1237           $output = taxonomy_render_nodes(taxonomy_select_nodes($tids, $operator, $depth, TRUE));
1238           $output .= theme('feed_icon', url('taxonomy/term/'. $str_tids .'/'. $depth .'/feed'));
1239           return $output;
1240           break;
1241
1242         case 'feed':
1243           $term = taxonomy_get_term($tids[0]);
1244           $channel['link'] = url('taxonomy/term/'. $str_tids .'/'. $depth, NULL, NULL, TRUE);
1245           $channel['title'] = variable_get('site_name', 'drupal') .' - '. $title;
1246           $channel['description'] = $term->description;
1247
1248           $result = taxonomy_select_nodes($tids, $operator, $depth, FALSE);
1249           node_feed($result, $channel);
1250           break;
1251         default:
1252           drupal_not_found();
1253       }
1254     }
1255     else {
1256       drupal_not_found();
1257     }
1258   }
1259 }
1260
1261 /**
1262  * Page to add or edit a vocabulary
1263  */
1264 function taxonomy_admin_vocabulary_edit($vid = NULL) {
1265   if ($_POST['op'] == t('Delete') || $_POST['edit']['confirm']) {
1266     return _taxonomy_confirm_del_vocabulary($vid);
1267   }
1268   elseif ($vid) {
1269     $vocabulary = (array)taxonomy_get_vocabulary($vid);
1270   }
1271   return taxonomy_form_vocabulary($vocabulary);
1272 }
1273
1274 /**
1275  * Page to list terms for a vocabulary
1276  */
1277 function taxonomy_admin_term_edit($tid = NULL) {
1278   if ($_POST['op'] == t('Delete') || $_POST['edit']['confirm']) {
1279     return _taxonomy_confirm_del_term($tid);
1280   }
1281   elseif ($tid) {
1282     $term = (array)taxonomy_get_term($tid);
1283   }
1284   return taxonomy_form_term($term);
1285 }
1286
1287 /**
1288  * Provides category information for rss feeds
1289  */
1290 function taxonomy_rss_item($node) {
1291   $output = array();
1292   foreach ($node->taxonomy as $term) {
1293     $output[] = array('key'   => 'category',
1294                       'value' => check_plain($term->name),
1295                       'attributes' => array('domain' => url('taxonomy/term/'. $term->tid, NULL, NULL, TRUE)));
1296   }
1297   return $output;
1298 }
1299
1300 /**
1301  * Implementation of hook_help().
1302  */
1303 function taxonomy_help($section) {
1304   switch ($section) {
1305     case 'admin/help#taxonomy':
1306       $output = '<p>'. t('The taxonomy module is one of the most popular features because users often want to create categories to organize content by type. It can automatically classify new content, which is very useful for organizing content on-the-fly. A simple example would be organizing a list of music reviews by musical genre.') .'</p>';
1307       $output .= '<p>'. t('Taxonomy is also the study of classification. The taxonomy module allows you to define vocabularies (sets of categories) which are used to classify content. The module supports hierarchical classification and association between terms, allowing for truly flexible information retrieval and classification. The taxonomy module allows multiple lists of categories for classification (controlled vocabularies) and offers the possibility of creating thesauri (controlled vocabularies that indicate the relationship of terms) and taxonomies (controlled vocabularies where relationships are indicated hierarchically). To view and manage the terms of each vocabulary, click on the associated <em>list terms</em> link. To delete a vocabulary and all its terms, choose <em>edit vocabulary.</em>') .'</p>';
1308       $output .= '<p>'. t('A controlled vocabulary is a set of terms to use for describing content (known as descriptors in indexing lingo). Drupal allows you to describe each piece of content (blog, story, etc.) using one or many of these terms. For simple implementations, you might create a set of categories without subcategories, similar to Slashdot\'s sections. For more complex implementations, you might create a hierarchical list of categories.') .'</p>';
1309       $output .= t('<p>You can</p>
1310 <ul>
1311 <li>add a vocabulary at <a href="%admin-taxonomy-add-vocabulary">administer &gt;&gt; categories &gt;&gt;  add vocabulary</a>.</li>
1312 <li>administer taxonomy at <a href="%admin-taxonomy">administer &gt;&gt; categories</a>.</li>
1313 <li>restrict content access by category for specific users roles using the <a href="%external-http-drupal-org-project-taxonomy_access">taxonomy access module</a>.</li>
1314 <li>build a custom view of your categories using the <a href="%external-http-drupal-org-project-taxonomy_browser">taxonomy browser</a>.</li>
1315 </ul>
1316 ', array('%admin-taxonomy-add-vocabulary' => url('admin/taxonomy/add/vocabulary'), '%admin-taxonomy' => url('admin/taxonomy'), '%external-http-drupal-org-project-taxonomy_access' => 'http://drupal.org/project/taxonomy_access', '%external-http-drupal-org-project-taxonomy_browser' => 'http://drupal.org/project/taxonomy_browser'));
1317       $output .= '<p>'. t('For more information please read the configuration and customization handbook <a href="%taxonomy">Taxonomy page</a>.', array('%taxonomy' => 'http://drupal.org/handbook/modules/taxonomy/')) .'</p>';
1318       return $output;
1319     case 'admin/modules#description':
1320       return t('Enables the categorization of content.');
1321     case 'admin/taxonomy':
1322       return t('<p>The taxonomy module allows you to classify content into categories and subcategories; it allows multiple lists of categories for classification (controlled vocabularies) and offers the possibility of creating thesauri (controlled vocabularies that indicate the relationship of terms), taxonomies (controlled vocabularies where relationships are indicated hierarchically), and free vocabularies where terms, or tags, are defined during content creation. To view and manage the terms of each vocabulary, click on the associated <em>list terms</em> link. To delete a vocabulary and all its terms, choose "edit vocabulary".</p>');
1323     case 'admin/taxonomy/add/vocabulary':
1324       return t("<p>When you create a controlled vocabulary you are creating a set of terms to use for describing content (known as descriptors in indexing lingo).  Drupal allows you to describe each piece of content (blog, story, etc.) using one or many of these terms. For simple implementations, you might create a set of categories without subcategories, similar to Slashdot.org's or Kuro5hin.org's sections. For more complex implementations, you might create a hierarchical list of categories.</p>");
1325   }
1326 }
1327
1328 /**
1329  * Helper function for array_map purposes.
1330  */
1331 function _taxonomy_get_tid_from_term($term) {
1332   return $term->tid;
1333 }
1334
1335 /**
1336  * Helper function for autocompletion
1337  */
1338 function taxonomy_autocomplete($vid, $string = '') {
1339   // The user enters a comma-separated list of tags. We only autocomplete the last tag.
1340   // This regexp allows the following types of user input:
1341   // this, "somecmpany, llc", "and ""this"" w,o.rks", foo bar
1342   $regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"|(?: [^",]*))%x';
1343   preg_match_all($regexp, $string, $matches);
1344   $array = $matches[1];
1345
1346   // Fetch last tag
1347   $last_string = trim(array_pop($array));
1348   if ($last_string != '') {
1349     $result = db_query_range(db_rewrite_sql("SELECT t.tid, t.name FROM {term_data} t WHERE t.vid = %d AND LOWER(t.name) LIKE LOWER('%%%s%%')", 't', 'tid'), $vid, $last_string, 0, 10);
1350
1351     $prefix = count($array) ? implode(', ', $array) .', ' : '';
1352
1353     $matches = array();
1354     while ($tag = db_fetch_object($result)) {
1355       $n = $tag->name;
1356       // Commas and quotes in terms are special cases, so encode 'em.
1357       if (preg_match('/,/', $tag->name) || preg_match('/"/', $tag->name)) {
1358         $n = '"'. preg_replace('/"/', '""', $tag->name) .'"';
1359       }
1360       $matches[$prefix . $n] = check_plain($tag->name);
1361     }
1362     print drupal_to_js($matches);
1363     exit();
1364   }
1365 }