Spike PHPCoverage Details: taxonomy.module

Line #FrequencySource Line
1 <?php
2 // $Id: taxonomy.module,v 1.417 2008/03/02 05:58:40 dries Exp $
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(
14     'administer taxonomy' => t('Manage taxonomy vocabularies and terms.'),
15   );
16 }
17 
18 /**
19  * Implementation of hook_theme()
20  */
21 function taxonomy_theme() {
22   return array(
23     'taxonomy_term_select' => array(
24       'arguments' => array('element' => NULL),
25     ),
26     'taxonomy_term_page' => array(
27       'arguments' => array('tids' => array(), 'result' => NULL),
28     ),
29     'taxonomy_overview_vocabularies' => array(
30       'arguments' => array('form' => array()),
31     ),
32     'taxonomy_overview_terms' => array(
33       'arguments' => array('form' => array()),
34     ),
35   );
36 }
37 
38 /**
39  * Implementation of hook_link().
40  *
41  * This hook is extended with $type = 'taxonomy terms' to allow themes to
42  * print lists of terms associated with a node. Themes can print taxonomy
43  * links with:
44  *
45  * if (module_exists('taxonomy')) {
46  *   $terms = taxonomy_link('taxonomy terms', $node);
47  *   print theme('links', $terms);
48  * }
49  */
50 function taxonomy_link($type, $node = NULL) {
51   if ($type == 'taxonomy terms' && $node != NULL) {
52     $links = array();
53     // If previewing, the terms must be converted to objects first.
54     if ($node->build_mode == NODE_BUILD_PREVIEW) {
55       $node->taxonomy = taxonomy_preview_terms($node);
56     }
57     if (!empty($node->taxonomy)) {
58       foreach ($node->taxonomy as $term) {
59         // During preview the free tagging terms are in an array unlike the
60         // other terms which are objects. So we have to check if a $term
61         // is an object or not.
62         if (is_object($term)) {
63           $links['taxonomy_term_'. $term->tid] = array(
64             'title' => $term->name,
65             'href' => taxonomy_term_path($term),
66             'attributes' => array('rel' => 'tag', 'title' => strip_tags($term->description))
67           );
68         }
69         // Previewing free tagging terms; we don't link them because the
70         // term-page might not exist yet.
71         else {
72           foreach ($term as $free_typed) {
73             $typed_terms = drupal_explode_tags($free_typed);
74             foreach ($typed_terms as $typed_term) {
75               $links['taxonomy_preview_term_'. $typed_term] = array(
76                 'title' => $typed_term,
77               );
78             }
79           }
80         }
81       }
82     }
83 
84     // We call this hook again because some modules and themes
85     // call taxonomy_link('taxonomy terms') directly.
86     drupal_alter('link', $links, $node);
87 
88     return $links;
89   }
90 }
91 
92 /**
93  * For vocabularies not maintained by taxonomy.module, give the maintaining
94  * module a chance to provide a path for terms in that vocabulary.
95  *
96  * @param $term
97  *   A term object.
98  * @return
99  *   An internal Drupal path.
100  */
101 
102 function taxonomy_term_path($term) {
103   $vocabulary = taxonomy_vocabulary_load($term->vid);
104   if ($vocabulary->module != 'taxonomy' && $path = module_invoke($vocabulary->module, 'term_path', $term)) {
105     return $path;
106   }
107   return 'taxonomy/term/'. $term->tid;
108 }
109 
110 /**
111  * Implementation of hook_menu().
112  */
113 function taxonomy_menu() {
114   $items['admin/content/taxonomy'] = array(
1151    'title' => 'Taxonomy',
116     'description' => 'Manage tagging, categorization, and classification of your content.',
117     'page callback' => 'drupal_get_form',
118     'page arguments' => array('taxonomy_overview_vocabularies'),
119     'access arguments' => array('administer taxonomy'),
120     'file' => 'taxonomy.admin.inc',
121   );
122 
123   $items['admin/content/taxonomy/list'] = array(
1241    'title' => 'List',
125     'type' => MENU_DEFAULT_LOCAL_TASK,
126     'weight' => -10,
127   );
128 
129   $items['admin/content/taxonomy/add/vocabulary'] = array(
1301    'title' => 'Add vocabulary',
131     'page callback' => 'drupal_get_form',
132     'page arguments' => array('taxonomy_form_vocabulary'),
133     'type' => MENU_LOCAL_TASK,
134     'parent' => 'admin/content/taxonomy',
135     'file' => 'taxonomy.admin.inc',
136   );
137 
138   $items['admin/content/taxonomy/edit/vocabulary/%taxonomy_vocabulary'] = array(
1391    'title' => 'Edit vocabulary',
140     'page callback' => 'taxonomy_admin_vocabulary_edit',
141     'page arguments' => array(5),
142     'type' => MENU_CALLBACK,
143     'file' => 'taxonomy.admin.inc',
144   );
145 
146   $items['admin/content/taxonomy/edit/term'] = array(
1471    'title' => 'Edit term',
148     'page callback' => 'taxonomy_admin_term_edit',
149     'type' => MENU_CALLBACK,
150     'file' => 'taxonomy.admin.inc',
151   );
152 
153   $items['taxonomy/term/%'] = array(
1541    'title' => 'Taxonomy term',
155     'page callback' => 'taxonomy_term_page',
156     'page arguments' => array(2),
157     'access arguments' => array('access content'),
158     'type' => MENU_CALLBACK,
159     'file' => 'taxonomy.pages.inc',
160   );
161 
162   $items['taxonomy/autocomplete'] = array(
1631    'title' => 'Autocomplete taxonomy',
164     'page callback' => 'taxonomy_autocomplete',
165     'access arguments' => array('access content'),
166     'type' => MENU_CALLBACK,
167     'file' => 'taxonomy.pages.inc',
168   );
169   $items['admin/content/taxonomy/%taxonomy_vocabulary'] = array(
1701    'title' => 'List terms',
171     'page callback' => 'drupal_get_form',
172     'page arguments' => array('taxonomy_overview_terms', 3),
173     'access arguments' => array('administer taxonomy'),
174     'type' => MENU_CALLBACK,
175     'file' => 'taxonomy.admin.inc',
176   );
177 
178   $items['admin/content/taxonomy/%taxonomy_vocabulary/list'] = array(
1791    'title' => 'List',
180     'type' => MENU_DEFAULT_LOCAL_TASK,
181     'weight' => -10,
182   );
183 
184   $items['admin/content/taxonomy/%taxonomy_vocabulary/add/term'] = array(
1851    'title' => 'Add term',
186     'page callback' => 'taxonomy_add_term_page',
187     'page arguments' => array(3),
188     'type' => MENU_LOCAL_TASK,
189     'parent' => 'admin/content/taxonomy/%taxonomy_vocabulary',
190     'file' => 'taxonomy.admin.inc',
191   );
192 
1931  return $items;
194 }
195 
196 function taxonomy_save_vocabulary(&$edit) {
197   $edit['nodes'] = empty($edit['nodes']) ? array() : $edit['nodes'];
198 
199   if (!isset($edit['module'])) {
200     $edit['module'] = 'taxonomy';
201   }
202 
203   if (!empty($edit['vid']) && !empty($edit['name'])) {
204     drupal_write_record('vocabulary', $edit, 'vid');
205     db_query("DELETE FROM {vocabulary_node_types} WHERE vid = %d", $edit['vid']);
206     foreach ($edit['nodes'] as $type => $selected) {
207       db_query("INSERT INTO {vocabulary_node_types} (vid, type) VALUES (%d, '%s')", $edit['vid'], $type);
208     }
209     module_invoke_all('taxonomy', 'update', 'vocabulary', $edit);
210     $status = SAVED_UPDATED;
211   }
212   else if (!empty($edit['vid'])) {
213     $status = taxonomy_del_vocabulary($edit['vid']);
214   }
215   else {
216     drupal_write_record('vocabulary', $edit);
217     foreach ($edit['nodes'] as $type => $selected) {
218       db_query("INSERT INTO {vocabulary_node_types} (vid, type) VALUES (%d, '%s')", $edit['vid'], $type);
219     }
220     module_invoke_all('taxonomy', 'insert', 'vocabulary', $edit);
221     $status = SAVED_NEW;
222   }
223 
224   cache_clear_all();
225 
226   return $status;
227 }
228 
229 /**
230  * Delete a vocabulary.
231  *
232  * @param $vid
233  *   A vocabulary ID.
234  * @return
235  *   Constant indicating items were deleted.
236  */
237 function taxonomy_del_vocabulary($vid) {
238   $vocabulary = (array) taxonomy_vocabulary_load($vid);
239 
240   db_query('DELETE FROM {vocabulary} WHERE vid = %d', $vid);
241   db_query('DELETE FROM {vocabulary_node_types} WHERE vid = %d', $vid);
242   $result = db_query('SELECT tid FROM {term_data} WHERE vid = %d', $vid);
243   while ($term = db_fetch_object($result)) {
244     taxonomy_del_term($term->tid);
245   }
246 
247   module_invoke_all('taxonomy', 'delete', 'vocabulary', $vocabulary);
248 
249   cache_clear_all();
250 
251   return SAVED_DELETED;
252 }
253 
254 /**
255  * Dynamicly check and update the hierarachy flag of a vocabulary.
256  *
257  * Checks the current parents of all terms in a vocabulary and updates the
258  * vocabularies hierarchy setting to the lowest possible level. A hierarchy with
259  * no parents in any of its terms will be given a hierarchy of 0. If terms
260  * contain at most a single parent, the vocabulary will be given a hierarchy of
261  * 1. If any term contain multiple parents, the vocabulary will be given a
262  * hieararchy of 2.
263  *
264  * @param $vocabulary
265  *   An array of the vocabulary structure.
266  * @param $changed_term
267  *   An array of the term structure that was updated.
268  */
269 function taxonomy_check_vocabulary_hierarchy($vocabulary, $changed_term) {
270   $tree = taxonomy_get_tree($vocabulary['vid']);
271   $hierarchy = 0;
272   foreach ($tree as $term) {
273     // Update the changed term with the new parent value before comparision.
274     if ($term->tid == $changed_term['tid']) {
275       $term = (object)$changed_term;
276       $term->parents = $term->parent;
277     }
278     // Check this term's parent count.
279     if (count($term->parents) > 1) {
280       $hierarchy = 2;
281       break;
282     }
283     elseif (count($term->parents) == 1 && 0 !== array_shift($term->parents)) {
284       $hierarchy = 1;
285     }
286   }
287   if ($hierarchy != $vocabulary['hierarchy']) {
288     $vocabulary['hierarchy'] = $hierarchy;
289     taxonomy_save_vocabulary($vocabulary);
290   }
291 
292   return $hierarchy;
293 }
294 
295 /**
296  * Helper function for taxonomy_form_term_submit().
297  *
298  * @param $form_state['values']
299  * @return
300  *   Status constant indicating if term was inserted or updated.
301  */
302 function taxonomy_save_term(&$form_values) {
303   $form_values += array(
304     'description' => '',
305     'weight' => 0
306   );
307 
308   if (!empty($form_values['tid']) && $form_values['name']) {
309     drupal_write_record('term_data', $form_values, 'tid');
310     $hook = 'update';
311     $status = SAVED_UPDATED;
312   }
313   else if (!empty($form_values['tid'])) {
314     return taxonomy_del_term($form_values['tid']);
315   }
316   else {
317     drupal_write_record('term_data', $form_values);
318     $hook = 'insert';
319     $status = SAVED_NEW;
320   }
321 
322   db_query('DELETE FROM {term_relation} WHERE tid1 = %d OR tid2 = %d', $form_values['tid'], $form_values['tid']);
323   if (!empty($form_values['relations'])) {
324     foreach ($form_values['relations'] as $related_id) {
325       if ($related_id != 0) {
326         db_query('INSERT INTO {term_relation} (tid1, tid2) VALUES (%d, %d)', $form_values['tid'], $related_id);
327       }
328     }
329   }
330 
331   db_query('DELETE FROM {term_hierarchy} WHERE tid = %d', $form_values['tid']);
332   if (!isset($form_values['parent']) || empty($form_values['parent'])) {
333     $form_values['parent'] = array(0);
334   }
335   if (is_array($form_values['parent'])) {
336     foreach ($form_values['parent'] as $parent) {
337       if (is_array($parent)) {
338         foreach ($parent as $tid) {
339           db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $form_values['tid'], $tid);
340         }
341       }
342       else {
343         db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $form_values['tid'], $parent);
344       }
345     }
346   }
347   else {
348     db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $form_values['tid'], $form_values['parent']);
349   }
350 
351   db_query('DELETE FROM {term_synonym} WHERE tid = %d', $form_values['tid']);
352   if (!empty($form_values['synonyms'])) {
353     foreach (explode ("\n", str_replace("\r", '', $form_values['synonyms'])) as $synonym) {
354       if ($synonym) {
355         db_query("INSERT INTO {term_synonym} (tid, name) VALUES (%d, '%s')", $form_values['tid'], chop($synonym));
356       }
357     }
358   }
359 
360   if (isset($hook)) {
361     module_invoke_all('taxonomy', $hook, 'term', $form_values);
362   }
363 
364   cache_clear_all();
365 
366   return $status;
367 }
368 
369 /**
370  * Delete a term.
371  *
372  * @param $tid
373  *   The term ID.
374  * @return
375  *   Status constant indicating deletion.
376  */
377 function taxonomy_del_term($tid) {
378   $tids = array($tid);
379   while ($tids) {
380     $children_tids = $orphans = array();
381     foreach ($tids as $tid) {
382       // See if any of the term's children are about to be become orphans:
383       if ($children = taxonomy_get_children($tid)) {
384         foreach ($children as $child) {
385           // If the term has multiple parents, we don't delete it.
386           $parents = taxonomy_get_parents($child->tid);
387           if (count($parents) == 1) {
388             $orphans[] = $child->tid;
389           }
390         }
391       }
392 
393       $term = (array) taxonomy_get_term($tid);
394 
395       db_query('DELETE FROM {term_data} WHERE tid = %d', $tid);
396       db_query('DELETE FROM {term_hierarchy} WHERE tid = %d', $tid);
397       db_query('DELETE FROM {term_relation} WHERE tid1 = %d OR tid2 = %d', $tid, $tid);
398       db_query('DELETE FROM {term_synonym} WHERE tid = %d', $tid);
399       db_query('DELETE FROM {term_node} WHERE tid = %d', $tid);
400 
401       module_invoke_all('taxonomy', 'delete', 'term', $term);
402     }
403 
404     $tids = $orphans;
405   }
406 
407   cache_clear_all();
408 
409   return SAVED_DELETED;
410 }
411 
412 /**
413  * Generate a form element for selecting terms from a vocabulary.
414  */
415 function taxonomy_form($vid, $value = 0, $help = NULL, $name = 'taxonomy') {
416   $vocabulary = taxonomy_vocabulary_load($vid);
417   $help = ($help) ? $help : $vocabulary->help;
418 
419   if (!$vocabulary->multiple) {
420     $blank = ($vocabulary->required) ? t('- Please choose -') : t('- None selected -');
421   }
422   else {
423     $blank = ($vocabulary->required) ? 0 : t('- None -');
424   }
425 
426   return _taxonomy_term_select(check_plain($vocabulary->name), $name, $value, $vid, $help, intval($vocabulary->multiple), $blank);
427 }
428 
429 /**
430  * Generate a set of options for selecting a term from all vocabularies.
431  */
432 function taxonomy_form_all($free_tags = 0) {
433   $vocabularies = taxonomy_get_vocabularies();
434   $options = array();
435   foreach ($vocabularies as $vid => $vocabulary) {
436     if ($vocabulary->tags && !$free_tags) {
437       continue;
438     }
439     $tree = taxonomy_get_tree($vid);
440     if ($tree && (count($tree) > 0)) {
441       $options[$vocabulary->name] = array();
442       foreach ($tree as $term) {
443         $options[$vocabulary->name][$term->tid] = str_repeat('-', $term->depth) . $term->name;
444       }
445     }
446   }
447   return $options;
448 }
449 
450 /**
451  * Return an array of all vocabulary objects.
452  *
453  * @param $type
454  *   If set, return only those vocabularies associated with this node type.
455  */
456 function taxonomy_get_vocabularies($type = NULL) {
457   if ($type) {
458     $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);
459   }
460   else {
461     $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'));
462   }
463 
464   $vocabularies = array();
465   $node_types = array();
466   while ($voc = db_fetch_object($result)) {
467     // If no node types are associated with a vocabulary, the LEFT JOIN will
468     // return a NULL value for type.
469     if (isset($voc->type)) {
470       $node_types[$voc->vid][$voc->type] = $voc->type;
471       unset($voc->type);
472       $voc->nodes = $node_types[$voc->vid];
473     }
474     elseif (!isset($voc->nodes)) {
475       $voc->nodes = array();
476     }
477     $vocabularies[$voc->vid] = $voc;
478   }
479 
480   return $vocabularies;
481 }
482 
483 /**
484  * Implementation of hook_form_alter().
485  * Generate a form for selecting terms to associate with a node.
486  * We check for taxonomy_override_selector before loading the full
487  * vocabulary, so contrib modules can intercept before hook_form_alter
488  *  and provide scalable alternatives.
489  */
490 function taxonomy_form_alter(&$form, $form_state, $form_id) {
491   if (isset($form['type']) && isset($form['#node']) && (!variable_get('taxonomy_override_selector', FALSE)) && $form['type']['#value'] .'_node_form' == $form_id) {
492     $node = $form['#node'];
493 
494     if (!isset($node->taxonomy)) {
495       $terms = empty($node->nid) ? array() : taxonomy_node_get_terms($node);
496     }
497     else {
498       // After preview the terms must be converted to objects.
499       if (isset($form_state['node_preview'])) {
500         $node->taxonomy = taxonomy_preview_terms($node);
501       }
502       $terms = $node->taxonomy;
503     }
504 
505     $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);
506 
507     while ($vocabulary = db_fetch_object($c)) {
508       if ($vocabulary->tags) {
509         if (isset($form_state['node_preview'])) {
510           // Typed string can be changed by the user before preview,
511           // so we just insert the tags directly as provided in the form.
512           $typed_string = $node->taxonomy['tags'][$vocabulary->vid];
513         }
514         else {
515           $typed_string = taxonomy_implode_tags($terms, $vocabulary->vid) . (array_key_exists('tags', $terms) ? $terms['tags'][$vocabulary->vid] : NULL);
516         }
517         if ($vocabulary->help) {
518           $help = $vocabulary->help;
519         }
520         else {
521           $help = t('A comma-separated list of terms describing this content. Example: funny, bungee jumping, "Company, Inc.".');
522         }
523         $form['taxonomy']['tags'][$vocabulary->vid] = array('#type' => 'textfield',
524           '#title' => $vocabulary->name,
525           '#description' => $help,
526           '#required' => $vocabulary->required,
527           '#default_value' => $typed_string,
528           '#autocomplete_path' => 'taxonomy/autocomplete/'. $vocabulary->vid,
529           '#weight' => $vocabulary->weight,
530           '#maxlength' => 255,
531         );
532       }
533       else {
534         // Extract terms belonging to the vocabulary in question.
535         $default_terms = array();
536         foreach ($terms as $term) {
537           // Free tagging has no default terms and also no vid after preview.
538           if (isset($term->vid) && $term->vid == $vocabulary->vid) {
539             $default_terms[$term->tid] = $term;
540           }
541         }
542         $form['taxonomy'][$vocabulary->vid] = taxonomy_form($vocabulary->vid, array_keys($default_terms), $vocabulary->help);
543         $form['taxonomy'][$vocabulary->vid]['#weight'] = $vocabulary->weight;
544         $form['taxonomy'][$vocabulary->vid]['#required'] = $vocabulary->required;
545       }
546     }
547     if (!empty($form['taxonomy']) && is_array($form['taxonomy'])) {
548       if (count($form['taxonomy']) > 1) {
549         // Add fieldset only if form has more than 1 element.
550         $form['taxonomy'] += array(
551           '#type' => 'fieldset',
552           '#title' => t('Vocabularies'),
553           '#collapsible' => TRUE,
554           '#collapsed' => FALSE,
555         );
556       }
557       $form['taxonomy']['#weight'] = -3;
558       $form['taxonomy']['#tree'] = TRUE;
559     }
560   }
561 }
562 
563 /**
564  * Helper function to convert terms after a preview.
565  *
566  * After preview the tags are an array instead of proper objects. This function
567  * converts them back to objects with the exception of 'free tagging' terms,
568  * because new tags can be added by the user before preview and those do not
569  * yet exist in the database. We therefore save those tags as a string so
570  * we can fill the form again after the preview.
571  */
572 function taxonomy_preview_terms($node) {
573   $taxonomy = array();
574   if (isset($node->taxonomy)) {
575     foreach ($node->taxonomy as $key => $term) {
576       unset($node->taxonomy[$key]);
577       // A 'Multiple select' and a 'Free tagging' field returns an array.
578       if (is_array($term)) {
579         foreach ($term as $tid) {
580           if ($key == 'tags') {
581             // Free tagging; the values will be saved for later as strings
582             // instead of objects to fill the form again.
583             $taxonomy['tags'] = $term;
584           }
585           else {
586             $taxonomy[$tid] = taxonomy_get_term($tid);
587           }
588         }
589       }
590       // A 'Single select' field returns the term id.
591       elseif ($term) {
592         $taxonomy[$term] = taxonomy_get_term($term);
593       }
594     }
595   }
596   return $taxonomy;
597 }
598 
599 /**
600  * Find all terms associated with the given node, within one vocabulary.
601  */
602 function taxonomy_node_get_terms_by_vocabulary($node, $vid, $key = 'tid') {
603   $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.vid = %d ORDER BY weight', 't', 'tid'), $vid, $node->vid);
604   $terms = array();
605   while ($term = db_fetch_object($result)) {
606     $terms[$term->$key] = $term;
607   }
608   return $terms;
609 }
610 
611 /**
612  * Find all terms associated with the given node, ordered by vocabulary and term weight.
613  */
614 function taxonomy_node_get_terms($node, $key = 'tid') {
615   static $terms;
616 
617   if (!isset($terms[$node->vid][$key])) {
618     $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.vid = %d ORDER BY v.weight, t.weight, t.name', 't', 'tid'), $node->vid);
619     $terms[$node->vid][$key] = array();
620     while ($term = db_fetch_object($result)) {
621       $terms[$node->vid][$key][$term->$key] = $term;
622     }
623   }
624   return $terms[$node->vid][$key];
625 }
626 
627 /**
628  * Make sure incoming vids are free tagging enabled.
629  */
630 function taxonomy_node_validate(&$node) {
631   if (!empty($node->taxonomy)) {
632     $terms = $node->taxonomy;
633     if (!empty($terms['tags'])) {
634       foreach ($terms['tags'] as $vid => $vid_value) {
635         $vocabulary = taxonomy_vocabulary_load($vid);
636         if (empty($vocabulary->tags)) {
637           // see form_get_error $key = implode('][', $element['#parents']);
638           // on why this is the key
639           form_set_error("taxonomy][tags][$vid", t('The %name vocabulary can not be modified in this way.', array('%name' => $vocabulary->name)));
640         }
641       }
642     }
643   }
644 }
645 
646 /**
647  * Save term associations for a given node.
648  */
649 function taxonomy_node_save($node, $terms) {
650 
651   taxonomy_node_delete_revision($node);
652 
653   // Free tagging vocabularies do not send their tids in the form,
654   // so we'll detect them here and process them independently.
655   if (isset($terms['tags'])) {
656     $typed_input = $terms['tags'];
657     unset($terms['tags']);
658 
659     foreach ($typed_input as $vid => $vid_value) {
660       $typed_terms = drupal_explode_tags($vid_value);
661 
662       $inserted = array();
663       foreach ($typed_terms as $typed_term) {
664         // See if the term exists in the chosen vocabulary
665         // and return the tid; otherwise, add a new record.
666         $possibilities = taxonomy_get_term_by_name($typed_term);
667         $typed_term_tid = NULL; // tid match, if any.
668         foreach ($possibilities as $possibility) {
669           if ($possibility->vid == $vid) {
670             $typed_term_tid = $possibility->tid;
671           }
672         }
673 
674         if (!$typed_term_tid) {
675           $edit = array('vid' => $vid, 'name' => $typed_term);
676           $status = taxonomy_save_term($edit);
677           $typed_term_tid = $edit['tid'];
678         }
679 
680         // Defend against duplicate, differently cased tags
681         if (!isset($inserted[$typed_term_tid])) {
682           db_query('INSERT INTO {term_node} (nid, vid, tid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $typed_term_tid);
683           $inserted[$typed_term_tid] = TRUE;
684         }
685       }
686     }
687   }
688 
689   if (is_array($terms)) {
690     foreach ($terms as $term) {
691       if (is_array($term)) {
692         foreach ($term as $tid) {
693           if ($tid) {
694             db_query('INSERT INTO {term_node} (nid, vid, tid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $tid);
695           }
696         }
697       }
698       else if (is_object($term)) {
699         db_query('INSERT INTO {term_node} (nid, vid, tid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $term->tid);
700       }
701       else if ($term) {
702         db_query('INSERT INTO {term_node} (nid, vid, tid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $term);
703       }
704     }
705   }
706 }
707 
708 /**
709  * Remove associations of a node to its terms.
710  */
711 function taxonomy_node_delete($node) {
712   db_query('DELETE FROM {term_node} WHERE nid = %d', $node->nid);
713 }
714 
715 /**
716  * Remove associations of a node to its terms.
717  */
718 function taxonomy_node_delete_revision($node) {
719   db_query('DELETE FROM {term_node} WHERE vid = %d', $node->vid);
720 }
721 
722 /**
723  * Implementation of hook_node_type().
724  */
725 function taxonomy_node_type($op, $info) {
7261  if ($op == 'update' && !empty($info->old_type) && $info->type != $info->old_type) {
727     db_query("UPDATE {vocabulary_node_types} SET type = '%s' WHERE type = '%s'", $info->type, $info->old_type);
728   }
7291  elseif ($op == 'delete') {
730     db_query("DELETE FROM {vocabulary_node_types} WHERE type = '%s'", $info->type);
731   }
732 }
733 
734 /**
735  * Find all term objects related to a given term ID.
736  */
737 function taxonomy_get_related($tid, $key = 'tid') {
738   if ($tid) {
739     $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);
740     $related = array();
741     while ($term = db_fetch_object($result)) {
742       $related[$term->$key] = $term;
743     }
744     return $related;
745   }
746   else {
747     return array();
748   }
749 }
750 
751 /**
752  * Find all parents of a given term ID.
753  */
754 function taxonomy_get_parents($tid, $key = 'tid') {
755   if ($tid) {
756     $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);
757     $parents = array();
758     while ($parent = db_fetch_object($result)) {
759       $parents[$parent->$key] = $parent;
760     }
761     return $parents;
762   }
763   else {
764     return array();
765   }
766 }
767 
768 /**
769  * Find all ancestors of a given term ID.
770  */
771 function taxonomy_get_parents_all($tid) {
772   $parents = array();
773   if ($tid) {
774     $parents[] = taxonomy_get_term($tid);
775     $n = 0;
776     while ($parent = taxonomy_get_parents($parents[$n]->tid)) {
777       $parents = array_merge($parents, $parent);
778       $n++;
779     }
780   }
781   return $parents;
782 }
783 
784 /**
785  * Find all children of a term ID.
786  */
787 function taxonomy_get_children($tid, $vid = 0, $key = 'tid') {
788   if ($vid) {
789     $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);
790   }
791   else {
792     $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);
793   }
794   $children = array();
795   while ($term = db_fetch_object($result)) {
796     $children[$term->$key] = $term;
797   }
798   return $children;
799 }
800 
801 /**
802  * Create a hierarchical representation of a vocabulary.
803  *
804  * @param $vid
805  *   Which vocabulary to generate the tree for.
806  *
807  * @param $parent
808  *   The term ID under which to generate the tree. If 0, generate the tree
809  *   for the entire vocabulary.
810  *
811  * @param $depth
812  *   Internal use only.
813  *
814  * @param $max_depth
815  *   The number of levels of the tree to return. Leave NULL to return all levels.
816  *
817  * @return
818  *   An array of all term objects in the tree. Each term object is extended
819  *   to have "depth" and "parents" attributes in addition to its normal ones.
820  *   Results are statically cached.
821  */
822 function taxonomy_get_tree($vid, $parent = 0, $depth = -1, $max_depth = NULL) {
823   static $children, $parents, $terms;
824 
825   $depth++;
826 
827   // We cache trees, so it's not CPU-intensive to call get_tree() on a term
828   // and its children, too.
829   if (!isset($children[$vid])) {
830     $children[$vid] = array();
831 
832     $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);
833     while ($term = db_fetch_object($result)) {
834       $children[$vid][$term->parent][] = $term->tid;
835       $parents[$vid][$term->tid][] = $term->parent;
836       $terms[$vid][$term->tid] = $term;
837     }
838   }
839 
840   $max_depth = (is_null($max_depth)) ? count($children[$vid]) : $max_depth;
841   $tree = array();
842   if (!empty($children[$vid][$parent])) {
843     foreach ($children[$vid][$parent] as $child) {
844       if ($max_depth > $depth) {
845         $term = clone $terms[$vid][$child];
846         $term->depth = $depth;
847         // The "parent" attribute is not useful, as it would show one parent only.
848         unset($term->parent);
849         $term->parents = $parents[$vid][$child];
850         $tree[] = $term;
851 
852         if (!empty($children[$vid][$child])) {
853           $tree = array_merge($tree, taxonomy_get_tree($vid, $child, $depth, $max_depth));
854         }
855       }
856     }
857   }
858 
859   return $tree;
860 }
861 
862 /**
863  * Return an array of synonyms of the given term ID.
864  */
865 function taxonomy_get_synonyms($tid) {
866   if ($tid) {
867     $synonyms = array();
868     $result = db_query('SELECT name FROM {term_synonym} WHERE tid = %d', $tid);
869     while ($synonym = db_fetch_array($result)) {
870       $synonyms[] = $synonym['name'];
871     }
872     return $synonyms;
873   }
874   else {
875     return array();
876   }
877 }
878 
879 /**
880  * Return the term object that has the given string as a synonym.
881  */
882 function taxonomy_get_synonym_root($synonym) {
883   return db_fetch_object(db_query("SELECT * FROM {term_synonym} s, {term_data} t WHERE t.tid = s.tid AND s.name = '%s'", $synonym));
884 }
885 
886 /**
887  * Count the number of published nodes classified by a term.
888  *
889  * @param $tid
890  *   The term's ID
891  *
892  * @param $type
893  *   The $node->type. If given, taxonomy_term_count_nodes only counts
894  *   nodes of $type that are classified with the term $tid.
895  *
896  * @return int
897  *   An integer representing a number of nodes.
898  *   Results are statically cached.
899  */
900 function taxonomy_term_count_nodes($tid, $type = 0) {
901   static $count;
902 
903   if (!isset($count[$type])) {
904     // $type == 0 always evaluates TRUE if $type is a string
905     if (is_numeric($type)) {
906       $result = db_query(db_rewrite_sql('SELECT t.tid, COUNT(n.nid) AS c FROM {term_node} t INNER JOIN {node} n ON t.vid = n.vid WHERE n.status = 1 GROUP BY t.tid'));
907     }
908     else {
909       $result = db_query(db_rewrite_sql("SELECT t.tid, COUNT(n.nid) AS c FROM {term_node} t INNER JOIN {node} n ON t.vid = n.vid WHERE n.status = 1 AND n.type = '%s' GROUP BY t.tid"), $type);
910     }
911     while ($term = db_fetch_object($result)) {
912       $count[$type][$term->tid] = $term->c;
913     }
914   }
915   $children_count = 0;
916   foreach (_taxonomy_term_children($tid) as $c) {
917     $children_count += taxonomy_term_count_nodes($c, $type);
918   }
919   return $children_count + (isset($count[$type][$tid]) ? $count[$type][$tid] : 0);
920 }
921 
922 /**
923  * Helper for taxonomy_term_count_nodes(). Used to find out
924  * which terms are children of a parent term.
925  *
926  * @param $tid
927  *   The parent term's ID
928  *
929  * @return array
930  *   An array of term IDs representing the children of $tid.
931  *   Results are statically cached.
932  *
933  */
934 function _taxonomy_term_children($tid) {
935   static $children;
936 
937   if (!isset($children)) {
938     $result = db_query('SELECT tid, parent FROM {term_hierarchy}');
939     while ($term = db_fetch_object($result)) {
940       $children[$term->parent][] = $term->tid;
941     }
942   }
943   return isset($children[$tid]) ? $children[$tid] : array();
944 }
945 
946 /**
947  * Try to map a string to an existing term, as for glossary use.
948  *
949  * Provides a case-insensitive and trimmed mapping, to maximize the
950  * likelihood of a successful match.
951  *
952  * @param name
953  *   Name of the term to search for.
954  *
955  * @return
956  *   An array of matching term objects.
957  */
958 function taxonomy_get_term_by_name($name) {
959   $db_result = db_query(db_rewrite_sql("SELECT t.tid, t.* FROM {term_data} t WHERE LOWER(t.name) LIKE LOWER('%s')", 't', 'tid'), trim($name));
960   $result = array();
961   while ($term = db_fetch_object($db_result)) {
962     $result[] = $term;
963   }
964 
965   return $result;
966 }
967 
968 /**
969  * Return the vocabulary object matching a vocabulary ID.
970  *
971  * @param $vid
972  *   The vocabulary's ID
973  *
974  * @return
975  *   The vocabulary object with all of its metadata, if exists, NULL otherwise.
976  *   Results are statically cached.
977  */
978 function taxonomy_vocabulary_load($vid) {
979   static $vocabularies = array();
980 
981   if (!isset($vocabularies[$vid])) {
982     // Initialize so if this vocabulary does not exist, we have
983     // that cached, and we will not try to load this later.
984     $vocabularies[$vid] = FALSE;
985     // Try to load the data and fill up the object.
986     $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', $vid);
987     $node_types = array();
988     while ($voc = db_fetch_object($result)) {
989       if (!empty($voc->type)) {
990         $node_types[$voc->type] = $voc->type;
991       }
992       unset($voc->type);
993       $voc->nodes = $node_types;
994       $vocabularies[$vid] = $voc;
995     }
996   }
997 
998   // Return NULL if this vocabulary does not exist.
999   return !empty($vocabularies[$vid]) ? $vocabularies[$vid] : NULL;
1000 }
1001 
1002 /**
1003  * Return the term object matching a term ID.
1004  *
1005  * @param $tid
1006  *   A term's ID
1007  *
1008  * @return Object
1009  *   A term object. Results are statically cached.
1010  */
1011 function taxonomy_get_term($tid) {
1012   static $terms = array();
1013 
1014   if (!isset($terms[$tid])) {
1015     $terms[$tid] = db_fetch_object(db_query('SELECT * FROM {term_data} WHERE tid = %d', $tid));
1016   }
1017 
1018   return $terms[$tid];
1019 }
1020 
1021 function _taxonomy_term_select($title, $name, $value, $vocabulary_id, $description, $multiple, $blank, $exclude = array()) {
1022   $tree = taxonomy_get_tree($vocabulary_id);
1023   $options = array();
1024 
1025   if ($blank) {
1026     $options[''] = $blank;
1027   }
1028   if ($tree) {
1029     foreach ($tree as $term) {
1030       if (!in_array($term->tid, $exclude)) {
1031         $choice = new stdClass();
1032         $choice->option = array($term->tid => str_repeat('-', $term->depth) . $term->name);
1033         $options[] = $choice;
1034       }
1035     }
1036   }
1037 
1038   return array('#type' => 'select',
1039     '#title' => $title,
1040     '#default_value' => $value,
1041     '#options' => $options,
1042     '#description' => $description,
1043     '#multiple' => $multiple,
1044     '#size' => $multiple ? min(9, count($options)) : 0,
1045     '#weight' => -15,
1046     '#theme' => 'taxonomy_term_select',
1047   );
1048 }
1049 
1050 /**
1051  * Format the selection field for choosing terms
1052  * (by deafult the default selection field is used).
1053  *
1054  * @ingroup themeable
1055  */
1056 function theme_taxonomy_term_select($element) {
1057   return theme('select', $element);
1058 }
1059 
1060 /**
1061  * Finds all nodes that match selected taxonomy conditions.
1062  *
1063  * @param $tids
1064  *   An array of term IDs to match.
1065  * @param $operator
1066  *   How to interpret multiple IDs in the array. Can be "or" or "and".
1067  * @param $depth
1068  *   How many levels deep to traverse the taxonomy tree. Can be a nonnegative
1069  *   integer or "all".
1070  * @param $pager
1071  *   Whether the nodes are to be used with a pager (the case on most Drupal
1072  *   pages) or not (in an XML feed, for example).
1073  * @param $order
1074  *   The order clause for the query that retrieve the nodes.
1075  * @return
1076  *   A resource identifier pointing to the query results.
1077  */
1078 function taxonomy_select_nodes($tids = array(), $operator = 'or', $depth = 0, $pager = TRUE, $order = 'n.sticky DESC, n.created DESC') {
1079   if (count($tids) > 0) {
1080     // For each term ID, generate an array of descendant term IDs to the right depth.
1081     $descendant_tids = array();
1082     if ($depth === 'all') {
1083       $depth = NULL;
1084     }
1085     foreach ($tids as $index => $tid) {
1086       $term = taxonomy_get_term($tid);
1087       $tree = taxonomy_get_tree($term->vid, $tid, -1, $depth);
1088       $descendant_tids[] = array_merge(array($tid), array_map('_taxonomy_get_tid_from_term', $tree));
1089     }
1090 
1091     if ($operator == 'or') {
1092       $args = call_user_func_array('array_merge', $descendant_tids);
1093       $placeholders = db_placeholders($args, 'int');
1094       $sql = 'SELECT DISTINCT(n.nid), n.sticky, n.title, n.created FROM {node} n INNER JOIN {term_node} tn ON n.vid = tn.vid WHERE tn.tid IN ('. $placeholders .') AND n.status = 1 ORDER BY '. $order;
1095       $sql_count = 'SELECT COUNT(DISTINCT(n.nid)) FROM {node} n INNER JOIN {term_node} tn ON n.vid = tn.vid WHERE tn.tid IN ('. $placeholders .') AND n.status = 1';
1096     }
1097     else {
1098       $joins = '';
1099       $wheres = '';
1100       $args = array();
1101       foreach ($descendant_tids as $index => $tids) {
1102         $joins .= ' INNER JOIN {term_node} tn'. $index .' ON n.vid = tn'. $index .'.vid';
1103         $wheres .= ' AND tn'. $index .'.tid IN ('. db_placeholders($tids, 'int') .')';
1104         $args = array_merge($args, $tids);
1105       }
1106       $sql = 'SELECT DISTINCT(n.nid), n.sticky, n.title, n.created FROM {node} n '. $joins .' WHERE n.status = 1 '. $wheres .' ORDER BY '. $order;
1107       $sql_count = 'SELECT COUNT(DISTINCT(n.nid)) FROM {node} n '. $joins .' WHERE n.status = 1 '. $wheres;
1108     }
1109     $sql = db_rewrite_sql($sql);
1110     $sql_count = db_rewrite_sql($sql_count);
1111     if ($pager) {
1112       $result = pager_query($sql, variable_get('default_nodes_main', 10), 0, $sql_count, $args);
1113     }
1114     else {
1115       $result = db_query_range($sql, $args, 0, variable_get('feed_default_items', 10));
1116     }
1117   }
1118 
1119   return $result;
1120 }
1121 
1122 /**
1123  * Accepts the result of a pager_query() call, such as that performed by
1124  * taxonomy_select_nodes(), and formats each node along with a pager.
1125  */
1126 function taxonomy_render_nodes($result) {
1127   $output = '';
1128   $has_rows = FALSE;
1129   while ($node = db_fetch_object($result)) {
1130     $output .= node_view(node_load($node->nid), 1);
1131     $has_rows = TRUE;
1132   }
1133   if ($has_rows) {
1134     $output .= theme('pager', NULL, variable_get('default_nodes_main', 10), 0);
1135   }
1136   else {
1137     $output .= '<p>'. t('There are currently no posts in this category.') .'</p>';
1138   }
1139   return $output;
1140 }
1141 
1142 /**
1143  * Implementation of hook_nodeapi().
1144  */
1145 function taxonomy_nodeapi($node, $op, $arg = 0) {
1146   switch ($op) {
1147     case 'load':
1148       $output['taxonomy'] = taxonomy_node_get_terms($node);
1149       return $output;
1150 
1151     case 'insert':
1152       if (!empty($node->taxonomy)) {
1153         taxonomy_node_save($node, $node->taxonomy);
1154       }
1155       break;
1156 
1157     case 'update':
1158       if (!empty($node->taxonomy)) {
1159         taxonomy_node_save($node, $node->taxonomy);
1160       }
1161       break;
1162 
1163     case 'delete':
1164       taxonomy_node_delete($node);
1165       break;
1166 
1167     case 'delete revision':
1168       taxonomy_node_delete_revision($node);
1169       break;
1170 
1171     case 'validate':
1172       taxonomy_node_validate($node);
1173       break;
1174 
1175     case 'rss item':
1176       return taxonomy_rss_item($node);
1177 
1178     case 'update index':
1179       return taxonomy_node_update_index($node);
1180   }
1181 }
1182 
1183 /**
1184  * Implementation of hook_nodeapi('update_index').
1185  */
1186 function taxonomy_node_update_index(&$node) {
1187   $output = array();
1188   foreach ($node->taxonomy as $term) {
1189     $output[] = $term->name;
1190   }
1191   if (count($output)) {
1192     return '<strong>('. implode(', ', $output) .')</strong>';
1193   }
1194 }
1195 
1196 /**
1197  * Parses a comma or plus separated string of term IDs.
1198  *
1199  * @param $str_tids
1200  *   A string of term IDs, separated by plus or comma.
1201  *   comma (,) means AND
1202  *   plus (+) means OR
1203  *
1204  * @return an associative array with an operator key (either 'and'
1205  *   or 'or') and a tid key containing an array of the term ids.
1206  */
1207 function taxonomy_terms_parse_string($str_tids) {
1208   $terms = array('operator' => '', 'tids' => array());
1209   if (preg_match('/^([0-9]+[+ ])+[0-9]+$/', $str_tids)) {
1210     $terms['operator'] = 'or';
1211     // The '+' character in a query string may be parsed as ' '.
1212     $terms['tids'] = preg_split('/[+ ]/', $str_tids);
1213   }
1214   else if (preg_match('/^([0-9]+,)*[0-9]+$/', $str_tids)) {
1215     $terms['operator'] = 'and';
1216     $terms['tids'] = explode(',', $str_tids);
1217   }
1218   return $terms;
1219 }
1220 
1221 /**
1222  * Provides category information for RSS feeds.
1223  */
1224 function taxonomy_rss_item($node) {
1225   $output = array();
1226   foreach ($node->taxonomy as $term) {
1227     $output[] = array('key'   => 'category',
1228                       'value' => check_plain($term->name),
1229                       'attributes' => array('domain' => url('taxonomy/term/'. $term->tid, array('absolute' => TRUE))));
1230   }
1231   return $output;
1232 }
1233 
1234 /**
1235  * Implementation of hook_help().
1236  */
1237 function taxonomy_help($path, $arg) {
1238   switch ($path) {
1239     case 'admin/help#taxonomy':
1240       $output = '<p>'. t('The taxonomy module allows you to categorize content using various systems of classification. Free-tagging vocabularies are created by users on the fly when they submit posts (as commonly found in blogs and social bookmarking applications). Controlled vocabularies allow for administrator-defined short lists of terms as well as complex hierarchies with multiple relationships between different terms. These methods can be applied to different content types and combined together to create a powerful and flexible method of classifying and presenting your content.') .'</p>';
1241       $output .= '<p>'. t('For example, when creating a recipe site, you might want to classify posts by both the type of meal and preparation time. A vocabulary for each allows you to categorize using each criteria independently instead of creating a tag for every possible combination.') .'</p>';
1242       $output .= '<p>'. t('Type of Meal: <em>Appetizer, Main Course, Salad, Dessert</em>') .'</p>';
1243       $output .= '<p>'. t('Preparation Time: <em>0-30mins, 30-60mins, 1-2 hrs, 2hrs+</em>') .'</p>';
1244       $output .= '<p>'. t("Each taxonomy term (often called a 'category' or 'tag' in other systems) automatically provides lists of posts and a corresponding RSS feed. These taxonomy/term URLs can be manipulated to generate AND and OR lists of posts classified with terms. In our recipe site example, it then becomes easy to create pages displaying 'Main courses', '30 minute recipes', or '30 minute main courses and appetizers' by using terms on their own or in combination with others. There are a significant number of contributed modules which you to alter and extend the behavior of the core module for both display and organization of terms.") .'</p>';
1245       $output .= '<p>'. t("Terms can also be organized in parent/child relationships from the admin interface. An example would be a vocabulary grouping countries under their parent geo-political regions. The taxonomy module also enables advanced implementations of hierarchy, for example placing Turkey in both the 'Middle East' and 'Europe'.") .'</p>';
1246       $output .= '<p>'. t('The taxonomy module supports the use of both synonyms and related terms, but does not directly use this functionality. However, optional contributed or custom modules may make full use of these advanced features.') .'</p>';
1247       $output .= '<p>'. t('For more information, see the online handbook entry for <a href="@taxonomy">Taxonomy module</a>.', array('@taxonomy' => 'http://drupal.org/handbook/modules/taxonomy/')) .'</p>';
1248       return $output;
1249     case 'admin/content/taxonomy':
1250       $output = '<p>'. t("The taxonomy module allows you to categorize your content using both tags and administrator defined terms. It is a flexible tool for classifying content with many advanced features. To begin, create a 'Vocabulary' to hold one set of terms or tags. You can create one free-tagging vocabulary for everything, or separate controlled vocabularies to define the various properties of your content, for example 'Countries' or 'Colors'.") .'</p>';
1251       $output .= '<p>'. t('Use the list below to configure and review the vocabularies defined on your site, or to list and manage the terms (tags) they contain. A vocabulary may (optionally) be tied to specific content types as shown in the <em>Type</em> column and, if so, will be displayed when creating or editing posts of that type. Multiple vocabularies tied to the same content type will be displayed in the order shown below. To change the order of a vocabulary, grab a drag-and-drop handle under the <em>Name</em> column and drag it to a new location in the list. (Grab a handle by clicking and holding the mouse while hovering over a handle icon.) Remember that your changes will not be saved until you click the <em>Save</em> button at the bottom of the page.') .'</p>';
1252       return $output;
1253     case 'admin/content/taxonomy/%':
1254       $vocabulary = taxonomy_vocabulary_load($arg[3]);
1255       if ($vocabulary->tags) {
1256         return '<p>'. t('%capital_name is a free-tagging vocabulary. To change the name or description of a term, click the <em>edit</em> link next to the term.', array('%capital_name' => drupal_ucfirst($vocabulary->name))) .'</p>';
1257       }
1258       switch ($vocabulary->hierarchy) {
1259         case 0:
1260           return '<p>'. t('%capital_name is a flat vocabulary. You may organize the terms in the %name vocabulary by using the handles on the left side of the table. To change the name or description of a term, click the <em>edit</em> link next to the term.', array('%capital_name' => drupal_ucfirst($vocabulary->name), '%name' => $vocabulary->name)) .'</p>';
1261         case 1:
1262           return '<p>'. t('%capital_name is a single hierarchy vocabulary. You may organize the terms in the %name vocabulary by using the handles on the left side of the table. To change the name or description of a term, click the <em>edit</em> link next to the term.', array('%capital_name' => drupal_ucfirst($vocabulary->name), '%name' => $vocabulary->name)) .'</p>';
1263         case 2:
1264           return '<p>'. t('%capital_name is a multiple hierarchy vocabulary. To change the name or description of a term, click the <em>edit</em> link next to the term. Drag and drop of multiple hierarchies is not supported, but you can re-enable drag and drop support by editing each term to include only a single parent.', array('%capital_name' => drupal_ucfirst($vocabulary->name))) .'</p>';
1265       }
1266     case 'admin/content/taxonomy/add/vocabulary':
1267       return '<p>'. t('Define how your vocabulary will be presented to administrators and users, and which content types to categorize with it. Tags allows users to create terms when submitting posts by typing a comma separated list. Otherwise terms are chosen from a select list and can only be created by users with the "administer taxonomy" permission.') .'</p>';
1268   }
1269 }
1270 
1271 /**
1272  * Helper function for array_map purposes.
1273  */
1274 function _taxonomy_get_tid_from_term($term) {
1275   return $term->tid;
1276 }
1277 
1278 /**
1279  * Implode a list of tags of a certain vocabulary into a string.
1280  */
1281 function taxonomy_implode_tags($tags, $vid = NULL) {
1282   $typed_tags = array();
1283   foreach ($tags as $tag) {
1284     // Extract terms belonging to the vocabulary in question.
1285     if (is_null($vid) || $tag->vid == $vid) {
1286 
1287       // Commas and quotes in tag names are special cases, so encode 'em.
1288       if (strpos($tag->name, ',') !== FALSE || strpos($tag->name, '"') !== FALSE) {
1289         $tag->name = '"'. str_replace('"', '""', $tag->name) .'"';
1290       }
1291 
1292       $typed_tags[] = $tag->name;
1293     }
1294   }
1295   return implode(', ', $typed_tags);
1296 }
1297 
1298 /**
1299  * Implementation of hook_hook_info().
1300  */
1301 function taxonomy_hook_info() {
1302   return array(
1303     'taxonomy' => array(
1304       'taxonomy' => array(
1305         'insert' => array(
1306           'runs when' => t('After saving a new term to the database'),
1307         ),
1308         'update' => array(
1309           'runs when' => t('After saving an updated term to the database'),
1310         ),
1311         'delete' => array(
1312           'runs when' => t('After deleting a term')
1313         ),
1314       ),
1315     ),
1316   );
1317 }