Imported drupal-5.5
[drupal.git] / modules / taxonomy / taxonomy.module
blob4160ec559487c77a206e877dd342603cf5f317e1
1 <?php
2 // $Id: taxonomy.module,v 1.330.2.14 2007/12/06 18:16:38 drumm Exp $
4 /**
5  * @file
6  * Enables the organization of content into categories.
7  */
9 /**
10  * Implementation of hook_perm().
11  */
12 function taxonomy_perm() {
13   return array('administer taxonomy');
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_exists('taxonomy')) {
24  *   $terms = taxonomy_link('taxonomy terms', $node);
25  *   print theme('links', $terms);
26  * }
27  */
28 function taxonomy_link($type, $node = NULL) {
29   if ($type == 'taxonomy terms' && $node != NULL) {
30     $links = array();
31     if (array_key_exists('taxonomy', $node)) {
32       foreach ($node->taxonomy as $term) {
33         $links['taxonomy_term_'. $term->tid] = array(
34           'title' => $term->name,
35           'href' => taxonomy_term_path($term),
36           'attributes' => array('rel' => 'tag', 'title' => strip_tags($term->description))
37         );
38       }
39     }
41     // We call this hook again because some modules and themes call taxonomy_link('taxonomy terms') directly
42     foreach (module_implements('link_alter') as $module) {
43       $function = $module .'_link_alter';
44       $function($node, $links);
45     }
47     return $links;
48   }
51 /**
52  * For vocabularies not maintained by taxonomy.module, give the maintaining
53  * module a chance to provide a path for terms in that vocabulary.
54  *
55  * @param $term
56  *   A term object.
57  * @return
58  *   An internal Drupal path.
59  */
61 function taxonomy_term_path($term) {
62   $vocabulary = taxonomy_get_vocabulary($term->vid);
63   if ($vocabulary->module != 'taxonomy' && $path = module_invoke($vocabulary->module, 'term_path', $term)) {
64     return $path;
65   }
66   return 'taxonomy/term/'. $term->tid;
69 /**
70  * Implementation of hook_menu().
71  */
72 function taxonomy_menu($may_cache) {
73   $items = array();
75   if ($may_cache) {
76     $items[] = array('path' => 'admin/content/taxonomy',
77       'title' => t('Categories'),
78       'description' => t('Create vocabularies and terms to categorize your content.'),
79       'callback' => 'taxonomy_overview_vocabularies',
80       'access' => user_access('administer taxonomy'));
82     $items[] = array('path' => 'admin/content/taxonomy/list',
83       'title' => t('List'),
84       'type' => MENU_DEFAULT_LOCAL_TASK,
85       'weight' => -10);
87     $items[] = array('path' => 'admin/content/taxonomy/add/vocabulary',
88       'title' => t('Add vocabulary'),
89       'callback' => 'drupal_get_form',
90       'callback arguments' => array('taxonomy_form_vocabulary'),
91       'access' => user_access('administer taxonomy'),
92       'type' => MENU_LOCAL_TASK);
94     $items[] = array('path' => 'admin/content/taxonomy/edit/vocabulary',
95       'title' => t('Edit vocabulary'),
96       'callback' => 'taxonomy_admin_vocabulary_edit',
97       'access' => user_access('administer taxonomy'),
98       'type' => MENU_CALLBACK);
100     $items[] = array('path' => 'admin/content/taxonomy/edit/term',
101       'title' => t('Edit term'),
102       'callback' => 'taxonomy_admin_term_edit',
103       'access' => user_access('administer taxonomy'),
104       'type' => MENU_CALLBACK);
106     $items[] = array('path' => 'taxonomy/term',
107       'title' => t('Taxonomy term'),
108       'callback' => 'taxonomy_term_page',
109       'access' => user_access('access content'),
110       'type' => MENU_CALLBACK);
112     $items[] = array('path' => 'taxonomy/autocomplete',
113       'title' => t('Autocomplete taxonomy'),
114       'callback' => 'taxonomy_autocomplete',
115       'access' => user_access('access content'),
116       'type' => MENU_CALLBACK);
117   }
118   else {
119     if (arg(0) == 'admin' && arg(1) == 'content' && arg(2) == 'taxonomy' && is_numeric(arg(3))) {
120       $vid = arg(3);
121       $items[] = array('path' => 'admin/content/taxonomy/'. $vid,
122         'title' => t('List terms'),
123         'callback' => 'taxonomy_overview_terms',
124         'callback arguments' => array($vid),
125         'access' => user_access('administer taxonomy'),
126         'type' => MENU_CALLBACK);
128       $items[] = array('path' => 'admin/content/taxonomy/'. $vid .'/list',
129         'title' => t('List'),
130         'type' => MENU_DEFAULT_LOCAL_TASK,
131         'weight' => -10);
133       $items[] = array('path' => 'admin/content/taxonomy/'. $vid .'/add/term',
134         'title' => t('Add term'),
135         'callback' => 'drupal_get_form',
136         'callback arguments' => array('taxonomy_form_term', $vid),
137         'access' => user_access('administer taxonomy'),
138         'type' => MENU_LOCAL_TASK);
139     }
140   }
142   return $items;
146  * List and manage vocabularies.
147  */
148 function taxonomy_overview_vocabularies() {
149   $vocabularies = taxonomy_get_vocabularies();
150   $rows = array();
151   foreach ($vocabularies as $vocabulary) {
152     $types = array();
153     foreach ($vocabulary->nodes as $type) {
154       $node_type = node_get_types('name', $type);
155       $types[] = $node_type ? check_plain($node_type) : check_plain($type);
156     }
157     $rows[] = array('name' => check_plain($vocabulary->name),
158       'type' => implode(', ', $types),
159       'edit' => l(t('edit vocabulary'), "admin/content/taxonomy/edit/vocabulary/$vocabulary->vid"),
160       'list' => l(t('list terms'), "admin/content/taxonomy/$vocabulary->vid"),
161       'add' => l(t('add terms'), "admin/content/taxonomy/$vocabulary->vid/add/term")
162     );
163   }
164   if (empty($rows)) {
165     $rows[] = array(array('data' => t('No categories available.'), 'colspan' => '5'));
166   }
167   $header = array(t('Name'), t('Type'), array('data' => t('Operations'), 'colspan' => '3'));
169   return theme('table', $header, $rows, array('id' => 'taxonomy'));
173  * Display a tree of all the terms in a vocabulary, with options to edit
174  * each one.
175  */
176 function taxonomy_overview_terms($vid) {
177   $destination = drupal_get_destination();
179   $header = array(t('Name'), t('Operations'));
180   $vocabulary = taxonomy_get_vocabulary($vid);
181   if (!$vocabulary) {
182     return drupal_not_found();
183   }
185   drupal_set_title(check_plain($vocabulary->name));
186   $start_from      = $_GET['page'] ? $_GET['page'] : 0;
187   $total_entries   = 0;  // total count for pager
188   $page_increment  = 25; // number of tids per page
189   $displayed_count = 0;  // number of tids shown
191   if ($vocabulary->tags) {
192     // We are not calling taxonomy_get_tree because that might fail with a big
193     // number of tags in the freetagging vocabulary.
194     $results = pager_query(db_rewrite_sql('SELECT t.*, h.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'), $page_increment, 0, NULL, $vid);
195     while ($term = db_fetch_object($results)) {
196       $rows[] = array(
197         l($term->name, "taxonomy/term/$term->tid"),
198         l(t('edit'), "admin/content/taxonomy/edit/term/$term->tid", array(), $destination),
199       );
200     }
201   }
202   else {
203     $tree = taxonomy_get_tree($vocabulary->vid);
204     foreach ($tree as $term) {
205       $total_entries++; // we're counting all-totals, not displayed
206       if (($start_from && ($start_from * $page_increment) >= $total_entries) || ($displayed_count == $page_increment)) {
207         continue;
208       }
209       $rows[] = array(str_repeat('--', $term->depth) .' '. l($term->name, "taxonomy/term/$term->tid"), l(t('edit'), "admin/content/taxonomy/edit/term/$term->tid", array(), $destination));
210       $displayed_count++; // we're counting tids displayed
211     }
213     if (!$rows) {
214       $rows[] = array(array('data' => t('No terms available.'), 'colspan' => '2'));
215     }
217     $GLOBALS['pager_page_array'][] = $start_from;  // FIXME
218     $GLOBALS['pager_total'][] = intval($total_entries / $page_increment) + 1; // FIXME
219   }
221   $output .= theme('table', $header, $rows, array('id' => 'taxonomy'));
222   if ($vocabulary->tags || $total_entries >= $page_increment) {
223     $output .= theme('pager', NULL, $page_increment);
224   }
226   return $output;
230  * Display form for adding and editing vocabularies.
231  */
232 function taxonomy_form_vocabulary($edit = array()) {
233   $form['name'] = array('#type' => 'textfield',
234     '#title' => t('Vocabulary name'),
235     '#default_value' => $edit['name'],
236     '#maxlength' => 255,
237     '#description' => t('The name for this vocabulary. Example: "Topic".'),
238     '#required' => TRUE,
239   );
240   $form['description'] = array('#type' => 'textarea',
241     '#title' => t('Description'),
242     '#default_value' => $edit['description'],
243     '#description' => t('Description of the vocabulary; can be used by modules.'),
244   );
245   $form['help'] = array('#type' => 'textfield',
246     '#title' => t('Help text'),
247     '#maxlength' => 255,
248     '#default_value' => $edit['help'],
249     '#description' => t('Instructions to present to the user when choosing a term.'),
250   );
251   $form['nodes'] = array('#type' => 'checkboxes',
252     '#title' => t('Types'),
253     '#default_value' => $edit['nodes'],
254     '#options' => array_map('check_plain', node_get_types('names')),
255     '#description' => t('A list of node types you want to associate with this vocabulary.'),
256     '#required' => TRUE,
257   );
258   $form['hierarchy'] = array('#type' => 'radios',
259     '#title' => t('Hierarchy'),
260     '#default_value' => $edit['hierarchy'],
261     '#options' => array(t('Disabled'), t('Single'), t('Multiple')),
262     '#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'))),
263   );
264   $form['relations'] = array('#type' => 'checkbox',
265     '#title' => t('Related terms'),
266     '#default_value' => $edit['relations'],
267     '#description' => t('Allows <a href="@help-url">related terms</a> in this vocabulary.', array('@help-url' => url('admin/help/taxonomy', NULL, NULL, 'related-terms'))),
268   );
269   $form['tags'] = array('#type' => 'checkbox',
270     '#title' => t('Free tagging'),
271     '#default_value' => $edit['tags'],
272     '#description' => t('Content is categorized by typing terms instead of choosing from a list.'),
273   );
274   $form['multiple'] = array('#type' => 'checkbox',
275     '#title' => t('Multiple select'),
276     '#default_value' => $edit['multiple'],
277     '#description' => t('Allows nodes to have more than one term from this vocabulary (always true for free tagging).'),
278   );
279   $form['required'] = array('#type' => 'checkbox',
280     '#title' => t('Required'),
281     '#default_value' => $edit['required'],
282     '#description' => t('If enabled, every node <strong>must</strong> have at least one term in this vocabulary.'),
283   );
284   $form['weight'] = array('#type' => 'weight',
285     '#title' => t('Weight'),
286     '#default_value' => $edit['weight'],
287     '#description' => t('In listings, the heavier vocabularies will sink and the lighter vocabularies will be positioned nearer the top.'),
288   );
290   $form['submit'] = array('#type' => 'submit', '#value' => t('Submit'));
291   if ($edit['vid']) {
292     $form['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
293     $form['vid'] = array('#type' => 'value', '#value' => $edit['vid']);
294     $form['module'] = array('#type' => 'value', '#value' => $edit['module']);
295   }
296   return $form;
300  * Accept the form submission for a vocabulary and save the results.
301  */
302 function taxonomy_form_vocabulary_submit($form_id, $form_values) {
303   // Fix up the nodes array to remove unchecked nodes.
304   $form_values['nodes'] = array_filter($form_values['nodes']);
305   switch (taxonomy_save_vocabulary($form_values)) {
306     case SAVED_NEW:
307       drupal_set_message(t('Created new vocabulary %name.', array('%name' => $form_values['name'])));
308       watchdog('taxonomy', t('Created new vocabulary %name.', array('%name' => $form_values['name'])), WATCHDOG_NOTICE, l(t('edit'), 'admin/content/taxonomy/edit/vocabulary/'. $form_values['vid']));
309       break;
310     case SAVED_UPDATED:
311       drupal_set_message(t('Updated vocabulary %name.', array('%name' => $form_values['name'])));
312       watchdog('taxonomy', t('Updated vocabulary %name.', array('%name' => $form_values['name'])), WATCHDOG_NOTICE, l(t('edit'), 'admin/content/taxonomy/edit/vocabulary/'. $form_values['vid']));
313       break;
314   }
315   return 'admin/content/taxonomy';
318 function taxonomy_save_vocabulary(&$edit) {
319   $edit['nodes'] = empty($edit['nodes']) ? array() : $edit['nodes'];
321   if ($edit['vid'] && $edit['name']) {
322     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']);
323     db_query("DELETE FROM {vocabulary_node_types} WHERE vid = %d", $edit['vid']);
324     foreach ($edit['nodes'] as $type => $selected) {
325       db_query("INSERT INTO {vocabulary_node_types} (vid, type) VALUES (%d, '%s')", $edit['vid'], $type);
326     }
327     module_invoke_all('taxonomy', 'update', 'vocabulary', $edit);
328     $status = SAVED_UPDATED;
329   }
330   else if ($edit['vid']) {
331     $status = taxonomy_del_vocabulary($edit['vid']);
332   }
333   else {
334     $edit['vid'] = db_next_id('{vocabulary}_vid');
335     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');
336     foreach ($edit['nodes'] as $type => $selected) {
337       db_query("INSERT INTO {vocabulary_node_types} (vid, type) VALUES (%d, '%s')", $edit['vid'], $type);
338     }
339     module_invoke_all('taxonomy', 'insert', 'vocabulary', $edit);
340     $status = SAVED_NEW;
341   }
343   cache_clear_all();
345   return $status;
349  * Delete a vocabulary.
351  * @param $vid
352  *   A vocabulary ID.
353  * @return
354  *   Constant indicating items were deleted.
355  */
356 function taxonomy_del_vocabulary($vid) {
357   $vocabulary = (array) taxonomy_get_vocabulary($vid);
359   db_query('DELETE FROM {vocabulary} WHERE vid = %d', $vid);
360   db_query('DELETE FROM {vocabulary_node_types} WHERE vid = %d', $vid);
361   $result = db_query('SELECT tid FROM {term_data} WHERE vid = %d', $vid);
362   while ($term = db_fetch_object($result)) {
363     taxonomy_del_term($term->tid);
364   }
366   module_invoke_all('taxonomy', 'delete', 'vocabulary', $vocabulary);
368   cache_clear_all();
370   return SAVED_DELETED;
373 function taxonomy_vocabulary_confirm_delete($vid) {
374   $vocabulary = taxonomy_get_vocabulary($vid);
376   $form['type'] = array('#type' => 'value', '#value' => 'vocabulary');
377   $form['vid'] = array('#type' => 'value', '#value' => $vid);
378   $form['name'] = array('#type' => 'value', '#value' => $vocabulary->name);
379   return confirm_form($form,
380                   t('Are you sure you want to delete the vocabulary %title?',
381                   array('%title' => $vocabulary->name)),
382                   'admin/content/taxonomy',
383                   t('Deleting a vocabulary will delete all the terms in it. This action cannot be undone.'),
384                   t('Delete'),
385                   t('Cancel'));
388 function taxonomy_vocabulary_confirm_delete_submit($form_id, $form_values) {
389   $status = taxonomy_del_vocabulary($form_values['vid']);
390   drupal_set_message(t('Deleted vocabulary %name.', array('%name' => $form_values['name'])));
391   watchdog('taxonomy', t('Deleted vocabulary %name.', array('%name' => $form_values['name'])), WATCHDOG_NOTICE);
392   return 'admin/content/taxonomy';
395 function taxonomy_form_term($vocabulary_id, $edit = array()) {
396   $vocabulary = taxonomy_get_vocabulary($vocabulary_id);
397   drupal_set_title(check_plain($vocabulary->name));
399   $form['name'] = array(
400     '#type' => 'textfield',
401     '#title' => t('Term name'),
402     '#default_value' => $edit['name'],
403     '#maxlength' => 255,
404     '#description' => t('The name of this term.'),
405     '#required' => TRUE);
407   $form['description'] = array(
408     '#type' => 'textarea',
409     '#title' => t('Description'),
410     '#default_value' => $edit['description'],
411     '#description' => t('A description of the term.'));
413   if ($vocabulary->hierarchy) {
414     $parent = array_keys(taxonomy_get_parents($edit['tid']));
415     $children = taxonomy_get_tree($vocabulary_id, $edit['tid']);
417     // A term can't be the child of itself, nor of its children.
418     foreach ($children as $child) {
419       $exclude[] = $child->tid;
420     }
421     $exclude[] = $edit['tid'];
423     if ($vocabulary->hierarchy == 1) {
424       $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);
425     }
426     elseif ($vocabulary->hierarchy == 2) {
427       $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);
428     }
429   }
431   if ($vocabulary->relations) {
432     $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']));
433   }
435   $form['synonyms'] = array(
436     '#type' => 'textarea',
437     '#title' => t('Synonyms'),
438     '#default_value' => implode("\n", taxonomy_get_synonyms($edit['tid'])),
439     '#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'))));
440   $form['weight'] = array(
441     '#type' => 'weight',
442     '#title' => t('Weight'),
443     '#default_value' => $edit['weight'],
444     '#description' => t('In listings, the heavier terms will sink and the lighter terms will be positioned nearer the top.'));
445   $form['vid'] = array(
446     '#type' => 'value',
447     '#value' => $vocabulary->vid);
448   $form['submit'] = array(
449     '#type' => 'submit',
450     '#value' => t('Submit'));
452   if ($edit['tid']) {
453     $form['delete'] = array(
454       '#type' => 'submit',
455       '#value' => t('Delete'));
456     $form['tid'] = array(
457       '#type' => 'value',
458       '#value' => $edit['tid']);
459   }
460   else {
461     $form['destination'] = array('#type' => 'hidden', '#value' => $_GET['q']);
462   }
464   return $form;
468  * Accept the form submission for a taxonomy term and save the result.
469  */
470 function taxonomy_form_term_submit($form_id, $form_values) {
471   switch (taxonomy_save_term($form_values)) {
472     case SAVED_NEW:
473       drupal_set_message(t('Created new term %term.', array('%term' => $form_values['name'])));
474       watchdog('taxonomy', t('Created new term %term.', array('%term' => $form_values['name'])), WATCHDOG_NOTICE, l(t('edit'), 'admin/content/taxonomy/edit/term/'. $form_values['tid']));
475       break;
476     case SAVED_UPDATED:
477       drupal_set_message(t('Updated term %term.', array('%term' => $form_values['name'])));
478       watchdog('taxonomy', t('Updated term %term.', array('%term' => $form_values['name'])), WATCHDOG_NOTICE, l(t('edit'), 'admin/content/taxonomy/edit/term/'. $form_values['tid']));
479       break;
480   }
481   return 'admin/content/taxonomy';
485  * Helper function for taxonomy_form_term_submit().
487  * @param $form_values
488  * @return
489  *   Status constant indicating if term was inserted or updated.
490  */
491 function taxonomy_save_term(&$form_values) {
492   if ($form_values['tid'] && $form_values['name']) {
493     db_query("UPDATE {term_data} SET name = '%s', description = '%s', weight = %d WHERE tid = %d", $form_values['name'], $form_values['description'], $form_values['weight'], $form_values['tid']);
494     $hook = 'update';
495     $status = SAVED_UPDATED;
496   }
497   else if ($form_values['tid']) {
498     return taxonomy_del_term($form_values['tid']);
499   }
500   else {
501     $form_values['tid'] = db_next_id('{term_data}_tid');
502     db_query("INSERT INTO {term_data} (tid, name, description, vid, weight) VALUES (%d, '%s', '%s', %d, %d)", $form_values['tid'], $form_values['name'], $form_values['description'], $form_values['vid'], $form_values['weight']);
503     $hook = 'insert';
504     $status = SAVED_NEW;
505   }
507   db_query('DELETE FROM {term_relation} WHERE tid1 = %d OR tid2 = %d', $form_values['tid'], $form_values['tid']);
508   if ($form_values['relations']) {
509     foreach ($form_values['relations'] as $related_id) {
510       if ($related_id != 0) {
511         db_query('INSERT INTO {term_relation} (tid1, tid2) VALUES (%d, %d)', $form_values['tid'], $related_id);
512       }
513     }
514   }
516   db_query('DELETE FROM {term_hierarchy} WHERE tid = %d', $form_values['tid']);
517   if (!isset($form_values['parent']) || empty($form_values['parent'])) {
518     $form_values['parent'] = array(0);
519   }
520   if (is_array($form_values['parent'])) {
521     foreach ($form_values['parent'] as $parent) {
522       if (is_array($parent)) {
523         foreach ($parent as $tid) {
524           db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $form_values['tid'], $tid);
525         }
526       }
527       else {
528         db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $form_values['tid'], $parent);
529       }
530     }
531   }
532   else {
533     db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $form_values['tid'], $form_values['parent']);
534   }
536   db_query('DELETE FROM {term_synonym} WHERE tid = %d', $form_values['tid']);
537   if ($form_values['synonyms']) {
538     foreach (explode ("\n", str_replace("\r", '', $form_values['synonyms'])) as $synonym) {
539       if ($synonym) {
540         db_query("INSERT INTO {term_synonym} (tid, name) VALUES (%d, '%s')", $form_values['tid'], chop($synonym));
541       }
542     }
543   }
545   if (isset($hook)) {
546     module_invoke_all('taxonomy', $hook, 'term', $form_values);
547   }
549   cache_clear_all();
551   return $status;
555  * Delete a term.
557  * @param $tid
558  *   The term ID.
559  * @return
560  *   Status constant indicating deletion.
561  */
562 function taxonomy_del_term($tid) {
563   $tids = array($tid);
564   while ($tids) {
565     $children_tids = $orphans = array();
566     foreach ($tids as $tid) {
567       // See if any of the term's children are about to be become orphans:
568       if ($children = taxonomy_get_children($tid)) {
569         foreach ($children as $child) {
570           // If the term has multiple parents, we don't delete it.
571           $parents = taxonomy_get_parents($child->tid);
572           if (count($parents) == 1) {
573             $orphans[] = $child->tid;
574           }
575         }
576       }
578       $term = (array) taxonomy_get_term($tid);
580       db_query('DELETE FROM {term_data} WHERE tid = %d', $tid);
581       db_query('DELETE FROM {term_hierarchy} WHERE tid = %d', $tid);
582       db_query('DELETE FROM {term_relation} WHERE tid1 = %d OR tid2 = %d', $tid, $tid);
583       db_query('DELETE FROM {term_synonym} WHERE tid = %d', $tid);
584       db_query('DELETE FROM {term_node} WHERE tid = %d', $tid);
586       module_invoke_all('taxonomy', 'delete', 'term', $term);
587     }
589     $tids = $orphans;
590   }
592   cache_clear_all();
594   return SAVED_DELETED;
597 function taxonomy_term_confirm_delete($tid) {
598   $term = taxonomy_get_term($tid);
600   $form['type'] = array('#type' => 'value', '#value' => 'term');
601   $form['name'] = array('#type' => 'value', '#value' => $term->name);
602   $form['tid'] = array('#type' => 'value', '#value' => $tid);
603   return confirm_form($form,
604                   t('Are you sure you want to delete the term %title?',
605                   array('%title' => $term->name)),
606                   'admin/content/taxonomy',
607                   t('Deleting a term will delete all its children if there are any. This action cannot be undone.'),
608                   t('Delete'),
609                   t('Cancel'));
612 function taxonomy_term_confirm_delete_submit($form_id, $form_values) {
613   taxonomy_del_term($form_values['tid']);
614   drupal_set_message(t('Deleted term %name.', array('%name' => $form_values['name'])));
615   watchdog('taxonomy', t('Deleted term %name.', array('%name' => $form_values['name'])), WATCHDOG_NOTICE);
616   return 'admin/content/taxonomy';
620  * Generate a form element for selecting terms from a vocabulary.
621  */
622 function taxonomy_form($vid, $value = 0, $help = NULL, $name = 'taxonomy') {
623   $vocabulary = taxonomy_get_vocabulary($vid);
624   $help = ($help) ? $help : $vocabulary->help;
625   $blank = 0;
627   if (!$vocabulary->multiple) {
628     $blank = ($vocabulary->required) ? t('- Please choose -') : t('- None selected -');
629   }
631   return _taxonomy_term_select(check_plain($vocabulary->name), $name, $value, $vid, $help, intval($vocabulary->multiple), $blank);
635  * Generate a set of options for selecting a term from all vocabularies.
636  */
637 function taxonomy_form_all($free_tags = 0) {
638   $vocabularies = taxonomy_get_vocabularies();
639   $options = array();
640   foreach ($vocabularies as $vid => $vocabulary) {
641     if ($vocabulary->tags && !$free_tags) { continue; }
642     $tree = taxonomy_get_tree($vid);
643     if ($tree && (count($tree) > 0)) {
644       $options[$vocabulary->name] = array();
645       foreach ($tree as $term) {
646         $options[$vocabulary->name][$term->tid] = str_repeat('-', $term->depth) . $term->name;
647       }
648     }
649   }
650   return $options;
654  * Return an array of all vocabulary objects.
656  * @param $type
657  *   If set, return only those vocabularies associated with this node type.
658  */
659 function taxonomy_get_vocabularies($type = NULL) {
660   if ($type) {
661     $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);
662   }
663   else {
664     $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'));
665   }
667   $vocabularies = array();
668   $node_types = array();
669   while ($voc = db_fetch_object($result)) {
670     $node_types[$voc->vid][] = $voc->type;
671     unset($voc->type);
672     $voc->nodes = $node_types[$voc->vid];
673     $vocabularies[$voc->vid] = $voc;
674   }
676   return $vocabularies;
680  * Implementation of hook_form_alter().
681  * Generate a form for selecting terms to associate with a node.
682  */
683 function taxonomy_form_alter($form_id, &$form) {
684   if (isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id) {
685     $node = $form['#node'];
687     if (!isset($node->taxonomy)) {
688       if ($node->nid) {
689         $terms = taxonomy_node_get_terms($node->nid);
690       }
691       else {
692         $terms = array();
693       }
694     }
695     else {
696       $terms = $node->taxonomy;
697     }
699     $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);
701     while ($vocabulary = db_fetch_object($c)) {
702       if ($vocabulary->tags) {
703         $typed_terms = array();
704         foreach ($terms as $term) {
705           // Extract terms belonging to the vocabulary in question.
706           if ($term->vid == $vocabulary->vid) {
708             // Commas and quotes in terms are special cases, so encode 'em.
709             if (strpos($term->name, ',') !== FALSE || strpos($term->name, '"') !== FALSE) {
710               $term->name = '"'.str_replace('"', '""', $term->name).'"';
711             }
713             $typed_terms[] = $term->name;
714           }
715         }
716         $typed_string = implode(', ', $typed_terms) . (array_key_exists('tags', $terms) ? $terms['tags'][$vocabulary->vid] : NULL);
718         if ($vocabulary->help) {
719           $help = $vocabulary->help;
720         }
721         else {
722           $help = t('A comma-separated list of terms describing this content. Example: funny, bungee jumping, "Company, Inc.".');
723         }
724         $form['taxonomy']['tags'][$vocabulary->vid] = array('#type' => 'textfield',
725           '#title' => $vocabulary->name,
726           '#description' => $help,
727           '#required' => $vocabulary->required,
728           '#default_value' => $typed_string,
729           '#autocomplete_path' => 'taxonomy/autocomplete/'. $vocabulary->vid,
730           '#weight' => $vocabulary->weight,
731           '#maxlength' => 255,
732         );
733       }
734       else {
735         // Extract terms belonging to the vocabulary in question.
736         $default_terms = array();
737         foreach ($terms as $term) {
738           if ($term->vid == $vocabulary->vid) {
739             $default_terms[$term->tid] = $term;
740           }
741         }
742         $form['taxonomy'][$vocabulary->vid] = taxonomy_form($vocabulary->vid, array_keys($default_terms), $vocabulary->help);
743         $form['taxonomy'][$vocabulary->vid]['#weight'] = $vocabulary->weight;
744         $form['taxonomy'][$vocabulary->vid]['#required'] = $vocabulary->required;
745       }
746     }
747     if (is_array($form['taxonomy']) && !empty($form['taxonomy'])) {
748       if (count($form['taxonomy']) > 1) { // Add fieldset only if form has more than 1 element.
749         $form['taxonomy'] += array(
750           '#type' => 'fieldset',
751           '#title' => t('Categories'),
752           '#collapsible' => TRUE,
753           '#collapsed' => FALSE,
754         );
755       }
756       $form['taxonomy']['#weight'] = -3;
757       $form['taxonomy']['#tree'] = TRUE;
758     }
759   }
763  * Find all terms associated with the given node, within one vocabulary.
764  */
765 function taxonomy_node_get_terms_by_vocabulary($nid, $vid, $key = 'tid') {
766   $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);
767   $terms = array();
768   while ($term = db_fetch_object($result)) {
769     $terms[$term->$key] = $term;
770   }
771   return $terms;
775  * Find all terms associated with the given node, ordered by vocabulary and term weight.
776  */
777 function taxonomy_node_get_terms($nid, $key = 'tid') {
778   static $terms;
780   if (!isset($terms[$nid][$key])) {
781     $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);
782     $terms[$nid][$key] = array();
783     while ($term = db_fetch_object($result)) {
784       $terms[$nid][$key][$term->$key] = $term;
785     }
786   }
787   return $terms[$nid][$key];
791  * Make sure incoming vids are free tagging enabled.
792  */
793 function taxonomy_node_validate(&$node) {
794   if ($node->taxonomy) {
795     $terms = $node->taxonomy;
796     if ($terms['tags']) {
797       foreach ($terms['tags'] as $vid => $vid_value) {
798         $vocabulary = taxonomy_get_vocabulary($vid);
799         if (!$vocabulary->tags) {
800           // see form_get_error $key = implode('][', $element['#parents']);
801           // on why this is the key
802           form_set_error("taxonomy][tags][$vid", t('The %name vocabulary can not be modified in this way.', array('%name' => $vocabulary->name)));
803         }
804       }
805     }
806   }
810  * Save term associations for a given node.
811  */
812 function taxonomy_node_save($nid, $terms) {
813   taxonomy_node_delete($nid);
815   // Free tagging vocabularies do not send their tids in the form,
816   // so we'll detect them here and process them independently.
817   if (isset($terms['tags'])) {
818     $typed_input = $terms['tags'];
819     unset($terms['tags']);
821     foreach ($typed_input as $vid => $vid_value) {
822       // This regexp allows the following types of user input:
823       // this, "somecmpany, llc", "and ""this"" w,o.rks", foo bar
824       $regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"|(?: [^",]*))%x';
825       preg_match_all($regexp, $vid_value, $matches);
826       $typed_terms = array_unique($matches[1]);
828       $inserted = array();
829       foreach ($typed_terms as $typed_term) {
830         // If a user has escaped a term (to demonstrate that it is a group,
831         // or includes a comma or quote character), we remove the escape
832         // formatting so to save the term into the database as the user intends.
833         $typed_term = str_replace('""', '"', preg_replace('/^"(.*)"$/', '\1', $typed_term));
834         $typed_term = trim($typed_term);
835         if ($typed_term == "") { continue; }
837         // See if the term exists in the chosen vocabulary
838         // and return the tid; otherwise, add a new record.
839         $possibilities = taxonomy_get_term_by_name($typed_term);
840         $typed_term_tid = NULL; // tid match, if any.
841         foreach ($possibilities as $possibility) {
842           if ($possibility->vid == $vid) {
843             $typed_term_tid = $possibility->tid;
844           }
845         }
847         if (!$typed_term_tid) {
848           $edit = array('vid' => $vid, 'name' => $typed_term);
849           $status = taxonomy_save_term($edit);
850           $typed_term_tid = $edit['tid'];
851         }
853         // Defend against duplicate, differently cased tags
854         if (!isset($inserted[$typed_term_tid])) {
855           db_query('INSERT INTO {term_node} (nid, tid) VALUES (%d, %d)', $nid, $typed_term_tid);
856           $inserted[$typed_term_tid] = TRUE;
857         }
858       }
859     }
860   }
862   if (is_array($terms)) {
863     foreach ($terms as $term) {
864       if (is_array($term)) {
865         foreach ($term as $tid) {
866           if ($tid) {
867             db_query('INSERT INTO {term_node} (nid, tid) VALUES (%d, %d)', $nid, $tid);
868           }
869         }
870       }
871       else if (is_object($term)) {
872         db_query('INSERT INTO {term_node} (nid, tid) VALUES (%d, %d)', $nid, $term->tid);
873       }
874       else if ($term) {
875         db_query('INSERT INTO {term_node} (nid, tid) VALUES (%d, %d)', $nid, $term);
876       }
877     }
878   }
882  * Remove associations of a node to its terms.
883  */
884 function taxonomy_node_delete($nid) {
885   db_query('DELETE FROM {term_node} WHERE nid = %d', $nid);
889  * Implementation of hook_node_type().
890  */
891 function taxonomy_node_type($op, $info) {
892   if ($op == 'update' && !empty($info->old_type) && $info->type != $info->old_type) {
893     db_query("UPDATE {vocabulary_node_types} SET type = '%s' WHERE type = '%s'", $info->type, $info->old_type);
894   }
895   elseif ($op == 'delete') {
896     db_query("DELETE FROM {vocabulary_node_types} WHERE type = '%s'", $info->type);
897   }
901  * Find all term objects related to a given term ID.
902  */
903 function taxonomy_get_related($tid, $key = 'tid') {
904   if ($tid) {
905     $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);
906     $related = array();
907     while ($term = db_fetch_object($result)) {
908       $related[$term->$key] = $term;
909     }
910     return $related;
911   }
912   else {
913     return array();
914   }
918  * Find all parents of a given term ID.
919  */
920 function taxonomy_get_parents($tid, $key = 'tid') {
921   if ($tid) {
922     $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);
923     $parents = array();
924     while ($parent = db_fetch_object($result)) {
925       $parents[$parent->$key] = $parent;
926     }
927     return $parents;
928   }
929   else {
930     return array();
931   }
935  * Find all ancestors of a given term ID.
936  */
937 function taxonomy_get_parents_all($tid) {
938   $parents = array();
939   if ($tid) {
940     $parents[] = taxonomy_get_term($tid);
941     $n = 0;
942     while ($parent = taxonomy_get_parents($parents[$n]->tid)) {
943       $parents = array_merge($parents, $parent);
944       $n++;
945     }
946   }
947   return $parents;
951  * Find all children of a term ID.
952  */
953 function taxonomy_get_children($tid, $vid = 0, $key = 'tid') {
954   if ($vid) {
955     $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);
956   }
957   else {
958     $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);
959   }
960   $children = array();
961   while ($term = db_fetch_object($result)) {
962     $children[$term->$key] = $term;
963   }
964   return $children;
968  * Create a hierarchical representation of a vocabulary.
970  * @param $vid
971  *   Which vocabulary to generate the tree for.
973  * @param $parent
974  *   The term ID under which to generate the tree. If 0, generate the tree
975  *   for the entire vocabulary.
977  * @param $depth
978  *   Internal use only.
980  * @param $max_depth
981  *   The number of levels of the tree to return. Leave NULL to return all levels.
983  * @return
984  *   An array of all term objects in the tree. Each term object is extended
985  *   to have "depth" and "parents" attributes in addition to its normal ones.
986  *   Results are statically cached.
987  */
988 function taxonomy_get_tree($vid, $parent = 0, $depth = -1, $max_depth = NULL) {
989   static $children, $parents, $terms;
991   $depth++;
993   // We cache trees, so it's not CPU-intensive to call get_tree() on a term
994   // and its children, too.
995   if (!isset($children[$vid])) {
996     $children[$vid] = array();
998     $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);
999     while ($term = db_fetch_object($result)) {
1000       $children[$vid][$term->parent][] = $term->tid;
1001       $parents[$vid][$term->tid][] = $term->parent;
1002       $terms[$vid][$term->tid] = $term;
1003     }
1004   }
1006   $max_depth = (is_null($max_depth)) ? count($children[$vid]) : $max_depth;
1007   if ($children[$vid][$parent]) {
1008     foreach ($children[$vid][$parent] as $child) {
1009       if ($max_depth > $depth) {
1010         $term = drupal_clone($terms[$vid][$child]);
1011         $term->depth = $depth;
1012         // The "parent" attribute is not useful, as it would show one parent only.
1013         unset($term->parent);
1014         $term->parents = $parents[$vid][$child];
1015         $tree[] = $term;
1017         if ($children[$vid][$child]) {
1018           $tree = array_merge($tree, taxonomy_get_tree($vid, $child, $depth, $max_depth));
1019         }
1020       }
1021     }
1022   }
1024   return $tree ? $tree : array();
1028  * Return an array of synonyms of the given term ID.
1029  */
1030 function taxonomy_get_synonyms($tid) {
1031   if ($tid) {
1032     $result = db_query('SELECT name FROM {term_synonym} WHERE tid = %d', $tid);
1033     while ($synonym = db_fetch_array($result)) {
1034       $synonyms[] = $synonym['name'];
1035     }
1036     return $synonyms ? $synonyms : array();
1037   }
1038   else {
1039     return array();
1040   }
1044  * Return the term object that has the given string as a synonym.
1045  */
1046 function taxonomy_get_synonym_root($synonym) {
1047   return db_fetch_object(db_query("SELECT * FROM {term_synonym} s, {term_data} t WHERE t.tid = s.tid AND s.name = '%s'", $synonym));
1051  * Count the number of published nodes classified by a term.
1053  * @param $tid
1054  *   The term's ID
1056  * @param $type
1057  *   The $node->type. If given, taxonomy_term_count_nodes only counts
1058  *   nodes of $type that are classified with the term $tid.
1060  * @return int
1061  *   An integer representing a number of nodes.
1062  *   Results are statically cached.
1063  */
1064 function taxonomy_term_count_nodes($tid, $type = 0) {
1065   static $count;
1067   if (!isset($count[$type])) {
1068     // $type == 0 always evaluates TRUE if $type is a string
1069     if (is_numeric($type)) {
1070       $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'));
1071     }
1072     else {
1073       $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);
1074     }
1075     while ($term = db_fetch_object($result)) {
1076       $count[$type][$term->tid] = $term->c;
1077     }
1078   }
1080   foreach (_taxonomy_term_children($tid) as $c) {
1081     $children_count += taxonomy_term_count_nodes($c, $type);
1082   }
1083   return $count[$type][$tid] + $children_count;
1087  * Helper for taxonomy_term_count_nodes(). Used to find out
1088  * which terms are children of a parent term.
1090  * @param $tid
1091  *   The parent term's ID
1093  * @return array
1094  *   An array of term IDs representing the children of $tid.
1095  *   Results are statically cached.
1097  */
1098 function _taxonomy_term_children($tid) {
1099   static $children;
1101   if (!isset($children)) {
1102     $result = db_query('SELECT tid, parent FROM {term_hierarchy}');
1103     while ($term = db_fetch_object($result)) {
1104       $children[$term->parent][] = $term->tid;
1105     }
1106   }
1107   return $children[$tid] ? $children[$tid] : array();
1111  * Try to map a string to an existing term, as for glossary use.
1113  * Provides a case-insensitive and trimmed mapping, to maximize the
1114  * likelihood of a successful match.
1116  * @param name
1117  *   Name of the term to search for.
1119  * @return
1120  *   An array of matching term objects.
1121  */
1122 function taxonomy_get_term_by_name($name) {
1123   $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));
1124   $result = array();
1125   while ($term = db_fetch_object($db_result)) {
1126     $result[] = $term;
1127   }
1129   return $result;
1133  * Return the vocabulary object matching a vocabulary ID.
1135  * @param $vid
1136  *   The vocabulary's ID
1138  * @return Object
1139  *   The vocabulary object with all of its metadata.
1140  *   Results are statically cached.
1141  */
1142 function taxonomy_get_vocabulary($vid) {
1143   static $vocabularies = array();
1145   if (!array_key_exists($vid, $vocabularies)) {
1146     $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);
1147     $node_types = array();
1148     while ($voc = db_fetch_object($result)) {
1149       $node_types[] = $voc->type;
1150       unset($voc->type);
1151       $voc->nodes = $node_types;
1152       $vocabularies[$vid] = $voc;
1153     }
1154   }
1156   return $vocabularies[$vid];
1160  * Return the term object matching a term ID.
1162  * @param $tid
1163  *   A term's ID
1165  * @return Object
1166  *   A term object. Results are statically cached.
1167  */
1168 function taxonomy_get_term($tid) {
1169   static $terms = array();
1171   if (!isset($terms[$tid])) {
1172     $terms[$tid] = db_fetch_object(db_query('SELECT * FROM {term_data} WHERE tid = %d', $tid));
1173   }
1175   return $terms[$tid];
1178 function _taxonomy_term_select($title, $name, $value, $vocabulary_id, $description, $multiple, $blank, $exclude = array()) {
1179   $tree = taxonomy_get_tree($vocabulary_id);
1180   $options = array();
1182   if ($blank) {
1183     $options[''] = $blank;
1184   }
1185   if ($tree) {
1186     foreach ($tree as $term) {
1187       if (!in_array($term->tid, $exclude)) {
1188         $choice = new stdClass();
1189         $choice->option = array($term->tid => str_repeat('-', $term->depth) . $term->name);
1190         $options[] = $choice;
1191       }
1192     }
1193   }
1195   return array('#type' => 'select',
1196     '#title' => $title,
1197     '#default_value' => $value,
1198     '#options' => $options,
1199     '#description' => $description,
1200     '#multiple' => $multiple,
1201     '#size' => $multiple ? min(9, count($options)) : 0,
1202     '#weight' => -15,
1203     '#theme' => 'taxonomy_term_select',
1204   );
1208  * We use the default selection field for choosing terms.
1209  */
1210 function theme_taxonomy_term_select($element) {
1211   return theme('select', $element);
1215  * Finds all nodes that match selected taxonomy conditions.
1217  * @param $tids
1218  *   An array of term IDs to match.
1219  * @param $operator
1220  *   How to interpret multiple IDs in the array. Can be "or" or "and".
1221  * @param $depth
1222  *   How many levels deep to traverse the taxonomy tree. Can be a nonnegative
1223  *   integer or "all".
1224  * @param $pager
1225  *   Whether the nodes are to be used with a pager (the case on most Drupal
1226  *   pages) or not (in an XML feed, for example).
1227  * @param $order
1228  *   The order clause for the query that retrieve the nodes.
1229  * @return
1230  *   A resource identifier pointing to the query results.
1231  */
1232 function taxonomy_select_nodes($tids = array(), $operator = 'or', $depth = 0, $pager = TRUE, $order = 'n.sticky DESC, n.created DESC') {
1233   if (count($tids) > 0) {
1234     // For each term ID, generate an array of descendant term IDs to the right depth.
1235     $descendant_tids = array();
1236     if ($depth === 'all') {
1237       $depth = NULL;
1238     }
1239     foreach ($tids as $index => $tid) {
1240       $term = taxonomy_get_term($tid);
1241       $tree = taxonomy_get_tree($term->vid, $tid, -1, $depth);
1242       $descendant_tids[] = array_merge(array($tid), array_map('_taxonomy_get_tid_from_term', $tree));
1243     }
1245     if ($operator == 'or') {
1246       $args = call_user_func_array('array_merge', $descendant_tids);
1247       $placeholders = implode(',', array_fill(0, count($args), '%d'));
1248       $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 ('. $placeholders .') AND n.status = 1 ORDER BY '. $order;
1249       $sql_count = 'SELECT COUNT(DISTINCT(n.nid)) FROM {node} n INNER JOIN {term_node} tn ON n.nid = tn.nid WHERE tn.tid IN ('. $placeholders .') AND n.status = 1';
1250     }
1251     else {
1252       $joins = '';
1253       $wheres = '';
1254       $args = array();
1255       foreach ($descendant_tids as $index => $tids) {
1256         $joins .= ' INNER JOIN {term_node} tn'. $index .' ON n.nid = tn'. $index .'.nid';
1257         $placeholders = implode(',', array_fill(0, count($tids), '%d'));
1258         $wheres .= ' AND tn'. $index .'.tid IN ('. $placeholders .')';
1259         $args = array_merge($args, $tids);
1260       }
1261       $sql = 'SELECT DISTINCT(n.nid), n.sticky, n.title, n.created FROM {node} n '. $joins .' WHERE n.status = 1 '. $wheres .' ORDER BY '. $order;
1262       $sql_count = 'SELECT COUNT(DISTINCT(n.nid)) FROM {node} n '. $joins .' WHERE n.status = 1 '. $wheres;
1263     }
1264     $sql = db_rewrite_sql($sql);
1265     $sql_count = db_rewrite_sql($sql_count);
1266     if ($pager) {
1267       $result = pager_query($sql, variable_get('default_nodes_main', 10), 0, $sql_count, $args);
1268     }
1269     else {
1270       $result = db_query_range($sql, $args, 0, variable_get('feed_default_items', 10));
1271     }
1272   }
1274   return $result;
1278  * Accepts the result of a pager_query() call, such as that performed by
1279  * taxonomy_select_nodes(), and formats each node along with a pager.
1281 function taxonomy_render_nodes($result) {
1282   $output = '';
1283   if (db_num_rows($result) > 0) {
1284     while ($node = db_fetch_object($result)) {
1285       $output .= node_view(node_load($node->nid), 1);
1286     }
1287     $output .= theme('pager', NULL, variable_get('default_nodes_main', 10), 0);
1288   }
1289   else {
1290     $output .= '<p>'. t('There are currently no posts in this category.') .'</p>';
1291   }
1292   return $output;
1296  * Implementation of hook_nodeapi().
1297  */
1298 function taxonomy_nodeapi($node, $op, $arg = 0) {
1299   switch ($op) {
1300     case 'load':
1301      $output['taxonomy'] = taxonomy_node_get_terms($node->nid);
1302      return $output;
1303     case 'insert':
1304       taxonomy_node_save($node->nid, $node->taxonomy);
1305       break;
1306     case 'update':
1307       taxonomy_node_save($node->nid, $node->taxonomy);
1308       break;
1309     case 'delete':
1310       taxonomy_node_delete($node->nid);
1311       break;
1312     case 'validate':
1313       taxonomy_node_validate($node);
1314       break;
1315     case 'rss item':
1316       return taxonomy_rss_item($node);
1317     case 'update index':
1318       return taxonomy_node_update_index($node);
1319   }
1323  * Implementation of hook_nodeapi('update_index').
1324  */
1325 function taxonomy_node_update_index(&$node) {
1326   $output = array();
1327   foreach ($node->taxonomy as $term) {
1328     $output[] = $term->name;
1329   }
1330   if (count($output)) {
1331     return '<strong>('. implode(', ', $output) .')</strong>';
1332   }
1336  * Parses a comma or plus separated string of term IDs.
1338  * @param $str_tids
1339  *   A string of term IDs, separated by plus or comma.
1340  *   comma (,) means AND
1341  *   plus (+) means OR
1343  * @return an associative array with an operator key (either 'and'
1344  *   or 'or') and a tid key containing an array of the term ids.
1345  */
1346 function taxonomy_terms_parse_string($str_tids) {
1347   $terms = array();
1348   if (preg_match('/^([0-9]+[+ ])+[0-9]+$/', $str_tids)) {
1349     $terms['operator'] = 'or';
1350     // The '+' character in a query string may be parsed as ' '.
1351     $terms['tids'] = preg_split('/[+ ]/', $str_tids);
1352   }
1353   else if (preg_match('/^([0-9]+,)*[0-9]+$/', $str_tids)) {
1354     $terms['operator'] = 'and';
1355     $terms['tids'] = explode(',', $str_tids);
1356   }
1357   return $terms;
1362  * Menu callback; displays all nodes associated with a term.
1363  */
1364 function taxonomy_term_page($str_tids = '', $depth = 0, $op = 'page') {
1365   $terms = taxonomy_terms_parse_string($str_tids);
1366   if ($terms['operator'] != 'and' && $terms['operator'] != 'or') {
1367     drupal_not_found();
1368   }
1370   if ($terms['tids']) {
1371     $placeholders = implode(',', array_fill(0, count($terms['tids']), '%d'));
1372     $result = db_query(db_rewrite_sql('SELECT t.tid, t.name FROM {term_data} t WHERE t.tid IN ('. $placeholders .')', 't', 'tid'), $terms['tids']);
1373     $tids = array(); // we rebuild the $tids-array so it only contains terms the user has access to.
1374     $names = array();
1375     while ($term = db_fetch_object($result)) {
1376       $tids[] = $term->tid;
1377       $names[] = $term->name;
1378     }
1380     if ($names) {
1381       $title = check_plain(implode(', ', $names));
1382       drupal_set_title($title);
1384       switch ($op) {
1385         case 'page':
1386           // Build breadcrumb based on first hierarchy of first term:
1387           $current->tid = $tids[0];
1388           $breadcrumbs = array(array('path' => $_GET['q'], 'title' => $names[0]));
1389           while ($parents = taxonomy_get_parents($current->tid)) {
1390             $current = array_shift($parents);
1391             $breadcrumbs[] = array('path' => 'taxonomy/term/'. $current->tid, 'title' => $current->name);
1392           }
1393           $breadcrumbs = array_reverse($breadcrumbs);
1394           menu_set_location($breadcrumbs);
1396           $output = taxonomy_render_nodes(taxonomy_select_nodes($tids, $terms['operator'], $depth, TRUE));
1397           drupal_add_feed(url('taxonomy/term/'. $str_tids .'/'. $depth .'/feed'), 'RSS - '. $title);
1398           return $output;
1399           break;
1401         case 'feed':
1402           $term = taxonomy_get_term($tids[0]);
1403           $channel['link'] = url('taxonomy/term/'. $str_tids .'/'. $depth, NULL, NULL, TRUE);
1404           $channel['title'] = variable_get('site_name', 'Drupal') .' - '. $title;
1405           $channel['description'] = $term->description;
1407           $result = taxonomy_select_nodes($tids, $terms['operator'], $depth, FALSE);
1408           node_feed($result, $channel);
1409           break;
1410         default:
1411           drupal_not_found();
1412       }
1413     }
1414     else {
1415       drupal_not_found();
1416     }
1417   }
1421  * Page to edit a vocabulary.
1422  */
1423 function taxonomy_admin_vocabulary_edit($vid = NULL) {
1424   if ($_POST['op'] == t('Delete') || $_POST['confirm']) {
1425     return drupal_get_form('taxonomy_vocabulary_confirm_delete', $vid);
1426   }
1427   if ($vocabulary = (array)taxonomy_get_vocabulary($vid)) {
1428     return drupal_get_form('taxonomy_form_vocabulary', $vocabulary);
1429   }
1430   return drupal_not_found();
1434  * Page to edit a vocabulary term.
1435  */
1436 function taxonomy_admin_term_edit($tid) {
1437   if ($_POST['op'] == t('Delete') || $_POST['confirm']) {
1438     return drupal_get_form('taxonomy_term_confirm_delete', $tid);
1439   }
1440   if ($term = (array)taxonomy_get_term($tid)) {
1441     return drupal_get_form('taxonomy_form_term', $term['vid'], $term);
1442   }
1443   return drupal_not_found();
1447  * Provides category information for RSS feeds.
1448  */
1449 function taxonomy_rss_item($node) {
1450   $output = array();
1451   foreach ($node->taxonomy as $term) {
1452     $output[] = array('key'   => 'category',
1453                       'value' => check_plain($term->name),
1454                       'attributes' => array('domain' => url('taxonomy/term/'. $term->tid, NULL, NULL, TRUE)));
1455   }
1456   return $output;
1460  * Implementation of hook_help().
1461  */
1462 function taxonomy_help($section) {
1463   switch ($section) {
1464     case 'admin/help#taxonomy':
1465       $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. A simple example would be organizing a list of music reviews by musical genre.') .'</p>';
1466       $output .= '<p>'. t('Taxonomy is 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>';
1467       $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>';
1468       $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>';
1469       return $output;
1470     case 'admin/content/taxonomy':
1471       return '<p>'. t('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>';
1472     case 'admin/content/taxonomy/add/vocabulary':
1473       return '<p>'. t("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. For more complex implementations, you might create a hierarchical list of categories.") .'</p>';
1474   }
1478  * Helper function for array_map purposes.
1479  */
1480 function _taxonomy_get_tid_from_term($term) {
1481   return $term->tid;
1485  * Helper function for autocompletion
1486  */
1487 function taxonomy_autocomplete($vid, $string = '') {
1488   // The user enters a comma-separated list of tags. We only autocomplete the last tag.
1489   // This regexp allows the following types of user input:
1490   // this, "somecmpany, llc", "and ""this"" w,o.rks", foo bar
1491   $regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"|(?: [^",]*))%x';
1492   preg_match_all($regexp, $string, $matches);
1493   $array = $matches[1];
1495   // Fetch last tag
1496   $last_string = trim(array_pop($array));
1497   if ($last_string != '') {
1498     $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);
1500     $prefix = count($array) ? implode(', ', $array) .', ' : '';
1502     $matches = array();
1503     while ($tag = db_fetch_object($result)) {
1504       $n = $tag->name;
1505       // Commas and quotes in terms are special cases, so encode 'em.
1506       if (strpos($tag->name, ',') !== FALSE || strpos($tag->name, '"') !== FALSE) {
1507         $n = '"'. str_replace('"', '""', $tag->name) .'"';
1508       }
1509       $matches[$prefix . $n] = check_plain($tag->name);
1510     }
1511     print drupal_to_js($matches);
1512     exit();
1513   }