taxonomy.module 43.8 KB
Newer Older
Dries's avatar
 
Dries committed
1
<?php
Kjartan's avatar
Kjartan committed
2
// $Id$
Dries's avatar
 
Dries committed
3

Dries's avatar
 
Dries committed
4 5 6 7 8
/**
 * @file
 * Enables the organization of content into categories.
 */

Dries's avatar
 
Dries committed
9 10 11
/**
 * Implementation of hook_perm().
 */
Kjartan's avatar
Kjartan committed
12
function taxonomy_perm() {
Dries's avatar
 
Dries committed
13
  return array('administer taxonomy');
Kjartan's avatar
Kjartan committed
14
}
Dries's avatar
 
Dries committed
15

16 17 18 19 20 21 22
/**
 * Implementation of hook_theme()
 */
function taxonomy_theme() {
  return array(
    'taxonomy_term_select' => array(
      'arguments' => array('element' => NULL),
23 24 25
    ),
    'taxonomy_term_page' => array(
      'arguments' => array('tids' => array(), 'result' => NULL),
26
    ),
27 28 29 30 31 32
    'taxonomy_overview_vocabularies' => array(
      'arguments' => array('form' => array()),
    ),
    'taxonomy_overview_terms' => array(
      'arguments' => array('form' => array()),
    ),
33 34 35
  );
}

Dries's avatar
 
Dries committed
36 37 38 39 40 41 42
/**
 * Implementation of hook_link().
 *
 * This hook is extended with $type = 'taxonomy terms' to allow themes to
 * print lists of terms associated with a node. Themes can print taxonomy
 * links with:
 *
43
 * if (module_exists('taxonomy')) {
44 45
 *   $terms = taxonomy_link('taxonomy terms', $node);
 *   print theme('links', $terms);
Dries's avatar
 
Dries committed
46 47
 * }
 */
Dries's avatar
 
Dries committed
48
function taxonomy_link($type, $node = NULL) {
Dries's avatar
 
Dries committed
49
  if ($type == 'taxonomy terms' && $node != NULL) {
Kjartan's avatar
 
Kjartan committed
50
    $links = array();
51 52 53 54
    // If previewing, the terms must be converted to objects first.
    if ($node->build_mode == NODE_BUILD_PREVIEW) {
      $node->taxonomy = taxonomy_preview_terms($node);
    }
55
    if (!empty($node->taxonomy)) {
56
      foreach ($node->taxonomy as $term) {
57
        // During preview the free tagging terms are in an array unlike the
58
        // other terms which are objects. So we have to check if a $term
59
        // is an object or not.
60 61 62 63 64 65 66
        if (is_object($term)) {
          $links['taxonomy_term_'. $term->tid] = array(
            'title' => $term->name,
            'href' => taxonomy_term_path($term),
            'attributes' => array('rel' => 'tag', 'title' => strip_tags($term->description))
          );
        }
67 68
        // Previewing free tagging terms; we don't link them because the
        // term-page might not exist yet.
69 70 71 72 73 74
        else {
          foreach ($term as $free_typed) {
            $typed_terms = drupal_explode_tags($free_typed);
            foreach($typed_terms as $typed_term) {
              $links['taxonomy_preview_term_'. $typed_term] = array(
                'title' => $typed_term,
75
              );
76 77
            }
          }
78
        }
Dries's avatar
 
Dries committed
79
      }
Dries's avatar
 
Dries committed
80
    }
Kjartan's avatar
Kjartan committed
81

82 83
    // We call this hook again because some modules and themes
    // call taxonomy_link('taxonomy terms') directly.
84
    drupal_alter('link', $links, $node);
85 86

    return $links;
87 88 89
  }
}

90 91 92 93 94 95 96 97 98 99
/**
 * For vocabularies not maintained by taxonomy.module, give the maintaining
 * module a chance to provide a path for terms in that vocabulary.
 *
 * @param $term
 *   A term object.
 * @return
 *   An internal Drupal path.
 */

100
function taxonomy_term_path($term) {
101
  $vocabulary = taxonomy_vocabulary_load($term->vid);
102 103 104 105 106 107
  if ($vocabulary->module != 'taxonomy' && $path = module_invoke($vocabulary->module, 'term_path', $term)) {
    return $path;
  }
  return 'taxonomy/term/'. $term->tid;
}

Dries's avatar
 
Dries committed
108 109 110
/**
 * Implementation of hook_menu().
 */
111 112
function taxonomy_menu() {
  $items['admin/content/taxonomy'] = array(
113 114
    'title' => 'Taxonomy',
    'description' => 'Manage tagging, categorization, and classification of your content.',
115 116
    'page callback' => 'drupal_get_form',
    'page arguments' => array('taxonomy_overview_vocabularies'),
117
    'access arguments' => array('administer taxonomy'),
118
    'file' => 'taxonomy.admin.inc',
119 120 121
  );

  $items['admin/content/taxonomy/list'] = array(
122
    'title' => 'List',
123 124 125 126 127
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );

  $items['admin/content/taxonomy/add/vocabulary'] = array(
128
    'title' => 'Add vocabulary',
129 130 131
    'page callback' => 'drupal_get_form',
    'page arguments' => array('taxonomy_form_vocabulary'),
    'type' => MENU_LOCAL_TASK,
132
    'parent' => 'admin/content/taxonomy',
133
    'file' => 'taxonomy.admin.inc',
134 135
  );

136
  $items['admin/content/taxonomy/edit/vocabulary/%taxonomy_vocabulary'] = array(
137
    'title' => 'Edit vocabulary',
138 139 140
    'page callback' => 'taxonomy_admin_vocabulary_edit',
    'page arguments' => array(5),
    'type' => MENU_CALLBACK,
141
    'file' => 'taxonomy.admin.inc',
142 143 144
  );

  $items['admin/content/taxonomy/edit/term'] = array(
145
    'title' => 'Edit term',
146 147
    'page callback' => 'taxonomy_admin_term_edit',
    'type' => MENU_CALLBACK,
148
    'file' => 'taxonomy.admin.inc',
149 150 151
  );

  $items['taxonomy/term'] = array(
152
    'title' => 'Taxonomy term',
153 154 155
    'page callback' => 'taxonomy_term_page',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
156
    'file' => 'taxonomy.pages.inc',
157 158 159
  );

  $items['taxonomy/autocomplete'] = array(
160
    'title' => 'Autocomplete taxonomy',
161 162 163
    'page callback' => 'taxonomy_autocomplete',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
164
    'file' => 'taxonomy.pages.inc',
165
  );
166
  $items['admin/content/taxonomy/%taxonomy_vocabulary'] = array(
167
    'title' => 'List terms',
168 169
    'page callback' => 'drupal_get_form',
    'page arguments' => array('taxonomy_overview_terms', 3),
170 171
    'access arguments' => array('administer taxonomy'),
    'type' => MENU_CALLBACK,
172
    'file' => 'taxonomy.admin.inc',
173 174
  );

175
  $items['admin/content/taxonomy/%taxonomy_vocabulary/list'] = array(
176
    'title' => 'List',
177 178 179 180
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );

181
  $items['admin/content/taxonomy/%taxonomy_vocabulary/add/term'] = array(
182
    'title' => 'Add term',
183 184
    'page callback' => 'taxonomy_add_term_page',
    'page arguments' => array(3),
185
    'type' => MENU_LOCAL_TASK,
186
    'parent' => 'admin/content/taxonomy/%taxonomy_vocabulary',
187
    'file' => 'taxonomy.admin.inc',
188
  );
Dries's avatar
 
Dries committed
189

Dries's avatar
 
Dries committed
190 191
  return $items;
}
Dries's avatar
 
Dries committed
192

193
function taxonomy_save_vocabulary(&$edit) {
194
  $edit['nodes'] = empty($edit['nodes']) ? array() : $edit['nodes'];
Dries's avatar
 
Dries committed
195

196 197 198 199
  if (!isset($edit['module'])) {
    $edit['module'] = 'taxonomy';
  }

200
  if (!empty($edit['vid']) && !empty($edit['name'])) {
201
    drupal_write_record('vocabulary', $edit, 'vid');
Dries's avatar
 
Dries committed
202
    db_query("DELETE FROM {vocabulary_node_types} WHERE vid = %d", $edit['vid']);
203
    foreach ($edit['nodes'] as $type => $selected) {
Dries's avatar
 
Dries committed
204 205
      db_query("INSERT INTO {vocabulary_node_types} (vid, type) VALUES (%d, '%s')", $edit['vid'], $type);
    }
Dries's avatar
 
Dries committed
206
    module_invoke_all('taxonomy', 'update', 'vocabulary', $edit);
207
    $status = SAVED_UPDATED;
Dries's avatar
 
Dries committed
208
  }
209
  else if (!empty($edit['vid'])) {
210
    $status = taxonomy_del_vocabulary($edit['vid']);
Kjartan's avatar
Kjartan committed
211 212
  }
  else {
213
    drupal_write_record('vocabulary', $edit);
214
    foreach ($edit['nodes'] as $type => $selected) {
Dries's avatar
 
Dries committed
215 216
      db_query("INSERT INTO {vocabulary_node_types} (vid, type) VALUES (%d, '%s')", $edit['vid'], $type);
    }
Dries's avatar
 
Dries committed
217
    module_invoke_all('taxonomy', 'insert', 'vocabulary', $edit);
218
    $status = SAVED_NEW;
Kjartan's avatar
Kjartan committed
219
  }
Dries's avatar
 
Dries committed
220 221

  cache_clear_all();
Dries's avatar
 
Dries committed
222

223
  return $status;
Kjartan's avatar
Kjartan committed
224
}
Dries's avatar
 
Dries committed
225

226 227 228 229 230 231 232 233
/**
 * Delete a vocabulary.
 *
 * @param $vid
 *   A vocabulary ID.
 * @return
 *   Constant indicating items were deleted.
 */
Kjartan's avatar
Kjartan committed
234
function taxonomy_del_vocabulary($vid) {
235
  $vocabulary = (array) taxonomy_vocabulary_load($vid);
Dries's avatar
 
Dries committed
236

Dries's avatar
 
Dries committed
237
  db_query('DELETE FROM {vocabulary} WHERE vid = %d', $vid);
Dries's avatar
 
Dries committed
238
  db_query('DELETE FROM {vocabulary_node_types} WHERE vid = %d', $vid);
Dries's avatar
 
Dries committed
239
  $result = db_query('SELECT tid FROM {term_data} WHERE vid = %d', $vid);
Kjartan's avatar
Kjartan committed
240 241
  while ($term = db_fetch_object($result)) {
    taxonomy_del_term($term->tid);
Dries's avatar
 
Dries committed
242
  }
Dries's avatar
 
Dries committed
243

Dries's avatar
 
Dries committed
244
  module_invoke_all('taxonomy', 'delete', 'vocabulary', $vocabulary);
Dries's avatar
 
Dries committed
245

Dries's avatar
 
Dries committed
246 247
  cache_clear_all();

248
  return SAVED_DELETED;
Dries's avatar
 
Dries committed
249 250
}

251 252
/**
 * Dynamicly check and update the hierarachy flag of a vocabulary.
253
 *
254 255 256 257 258 259
 * Checks the current parents of all terms in a vocabulary and updates the
 * vocabularies hierarchy setting to the lowest possible level. A hierarchy with
 * no parents in any of its terms will be given a hierarchy of 0. If terms
 * contain at most a single parent, the vocabulary will be given a hierarchy of
 * 1. If any term contain multiple parents, the vocabulary will be given a
 * hieararchy of 2.
260
 *
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
 * @param $vocabulary
 *   An array of the vocabulary structure.
 * @param $changed_term
 *   An array of the term structure that was updated.
 */
function taxonomy_check_vocabulary_hierarchy($vocabulary, $changed_term) {
  $tree = taxonomy_get_tree($vocabulary['vid']);
  $hierarchy = 0;
  foreach ($tree as $term) {
    // Update the changed term with the new parent value before comparision.
    if ($term->tid == $changed_term['tid']) {
      $term = (object)$changed_term;
      $term->parents = $term->parent;
    }
    // Check this term's parent count.
    if (count($term->parents) > 1) {
      $hierarchy = 2;
      break;
    }
    elseif (count($term->parents) == 1 && 0 !== array_shift($term->parents)) {
      $hierarchy = 1;
    }
  }
  if ($hierarchy != $vocabulary['hierarchy']) {
    $vocabulary['hierarchy'] = $hierarchy;
    taxonomy_save_vocabulary($vocabulary);
  }

  return $hierarchy;
}

292 293 294
/**
 * Helper function for taxonomy_form_term_submit().
 *
295
 * @param $form_state['values']
296 297 298 299
 * @return
 *   Status constant indicating if term was inserted or updated.
 */
function taxonomy_save_term(&$form_values) {
300 301 302 303 304
  $form_values += array(
    'description' => '',
    'weight' => 0
  );

305
  if (!empty($form_values['tid']) && $form_values['name']) {
306
    drupal_write_record('term_data', $form_values, 'tid');
307
    $hook = 'update';
308
    $status = SAVED_UPDATED;
Kjartan's avatar
Kjartan committed
309
  }
310
  else if (!empty($form_values['tid'])) {
311
    return taxonomy_del_term($form_values['tid']);
Kjartan's avatar
Kjartan committed
312 313
  }
  else {
314
    drupal_write_record('term_data', $form_values);
315
    $hook = 'insert';
316
    $status = SAVED_NEW;
Kjartan's avatar
Kjartan committed
317
  }
Dries's avatar
 
Dries committed
318

319
  db_query('DELETE FROM {term_relation} WHERE tid1 = %d OR tid2 = %d', $form_values['tid'], $form_values['tid']);
320
  if (!empty($form_values['relations'])) {
321
    foreach ($form_values['relations'] as $related_id) {
Kjartan's avatar
Kjartan committed
322
      if ($related_id != 0) {
323
        db_query('INSERT INTO {term_relation} (tid1, tid2) VALUES (%d, %d)', $form_values['tid'], $related_id);
Dries's avatar
 
Dries committed
324
      }
Kjartan's avatar
Kjartan committed
325
    }
Kjartan's avatar
Kjartan committed
326
  }
Dries's avatar
 
Dries committed
327

328 329 330
  db_query('DELETE FROM {term_hierarchy} WHERE tid = %d', $form_values['tid']);
  if (!isset($form_values['parent']) || empty($form_values['parent'])) {
    $form_values['parent'] = array(0);
Kjartan's avatar
Kjartan committed
331
  }
332 333
  if (is_array($form_values['parent'])) {
    foreach ($form_values['parent'] as $parent) {
334 335
      if (is_array($parent)) {
        foreach ($parent as $tid) {
336
          db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $form_values['tid'], $tid);
337 338 339
        }
      }
      else {
340
        db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $form_values['tid'], $parent);
341
      }
Dries's avatar
 
Dries committed
342
    }
Kjartan's avatar
Kjartan committed
343
  }
344
  else {
345
    db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $form_values['tid'], $form_values['parent']);
346
  }
Dries's avatar
 
Dries committed
347

348
  db_query('DELETE FROM {term_synonym} WHERE tid = %d', $form_values['tid']);
349
  if (!empty($form_values['synonyms'])) {
350
    foreach (explode ("\n", str_replace("\r", '', $form_values['synonyms'])) as $synonym) {
Dries's avatar
 
Dries committed
351
      if ($synonym) {
352
        db_query("INSERT INTO {term_synonym} (tid, name) VALUES (%d, '%s')", $form_values['tid'], chop($synonym));
Dries's avatar
 
Dries committed
353
      }
Kjartan's avatar
Kjartan committed
354
    }
Dries's avatar
 
Dries committed
355
  }
Dries's avatar
 
Dries committed
356

357
  if (isset($hook)) {
358
    module_invoke_all('taxonomy', $hook, 'term', $form_values);
359 360
  }

Dries's avatar
 
Dries committed
361 362
  cache_clear_all();

363
  return $status;
Kjartan's avatar
Kjartan committed
364
}
Dries's avatar
 
Dries committed
365

366 367 368 369 370 371 372 373
/**
 * Delete a term.
 *
 * @param $tid
 *   The term ID.
 * @return
 *   Status constant indicating deletion.
 */
Kjartan's avatar
Kjartan committed
374
function taxonomy_del_term($tid) {
375 376 377 378 379 380 381 382 383 384 385 386 387 388
  $tids = array($tid);
  while ($tids) {
    $children_tids = $orphans = array();
    foreach ($tids as $tid) {
      // See if any of the term's children are about to be become orphans:
      if ($children = taxonomy_get_children($tid)) {
        foreach ($children as $child) {
          // If the term has multiple parents, we don't delete it.
          $parents = taxonomy_get_parents($child->tid);
          if (count($parents) == 1) {
            $orphans[] = $child->tid;
          }
        }
      }
Dries's avatar
 
Dries committed
389

390
      $term = (array) taxonomy_get_term($tid);
Dries's avatar
 
Dries committed
391

392 393 394 395 396
      db_query('DELETE FROM {term_data} WHERE tid = %d', $tid);
      db_query('DELETE FROM {term_hierarchy} WHERE tid = %d', $tid);
      db_query('DELETE FROM {term_relation} WHERE tid1 = %d OR tid2 = %d', $tid, $tid);
      db_query('DELETE FROM {term_synonym} WHERE tid = %d', $tid);
      db_query('DELETE FROM {term_node} WHERE tid = %d', $tid);
Dries's avatar
 
Dries committed
397

398 399
      module_invoke_all('taxonomy', 'delete', 'term', $term);
    }
Dries's avatar
 
Dries committed
400

401 402
    $tids = $orphans;
  }
Dries's avatar
 
Dries committed
403

Dries's avatar
 
Dries committed
404
  cache_clear_all();
405 406

  return SAVED_DELETED;
Dries's avatar
 
Dries committed
407 408
}

Dries's avatar
 
Dries committed
409 410 411
/**
 * Generate a form element for selecting terms from a vocabulary.
 */
412
function taxonomy_form($vid, $value = 0, $help = NULL, $name = 'taxonomy') {
413
  $vocabulary = taxonomy_vocabulary_load($vid);
414
  $help = ($help) ? $help : $vocabulary->help;
415
  $blank = 0;
416

417 418
  if (!$vocabulary->multiple) {
    $blank = ($vocabulary->required) ? t('- Please choose -') : t('- None selected -');
Kjartan's avatar
Kjartan committed
419
  }
Dries's avatar
 
Dries committed
420

421
  return _taxonomy_term_select(check_plain($vocabulary->name), $name, $value, $vid, $help, intval($vocabulary->multiple), $blank);
Kjartan's avatar
Kjartan committed
422
}
Dries's avatar
 
Dries committed
423

424
/**
425
 * Generate a set of options for selecting a term from all vocabularies.
426 427 428 429 430 431 432
 */
function taxonomy_form_all($free_tags = 0) {
  $vocabularies = taxonomy_get_vocabularies();
  $options = array();
  foreach ($vocabularies as $vid => $vocabulary) {
    if ($vocabulary->tags && !$free_tags) { continue; }
    $tree = taxonomy_get_tree($vid);
433
    if ($tree && (count($tree) > 0)) {
434
      $options[$vocabulary->name] = array();
435
      foreach ($tree as $term) {
436
        $options[$vocabulary->name][$term->tid] = str_repeat('-', $term->depth) . $term->name;
437 438 439 440 441 442
      }
    }
  }
  return $options;
}

Dries's avatar
 
Dries committed
443 444 445 446 447 448
/**
 * Return an array of all vocabulary objects.
 *
 * @param $type
 *   If set, return only those vocabularies associated with this node type.
 */
Dries's avatar
 
Dries committed
449
function taxonomy_get_vocabularies($type = NULL) {
Kjartan's avatar
Kjartan committed
450
  if ($type) {
451
    $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);
Kjartan's avatar
Kjartan committed
452 453
  }
  else {
454
    $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'));
Kjartan's avatar
Kjartan committed
455
  }
Dries's avatar
 
Dries committed
456

Kjartan's avatar
Kjartan committed
457
  $vocabularies = array();
Dries's avatar
 
Dries committed
458
  $node_types = array();
Kjartan's avatar
Kjartan committed
459
  while ($voc = db_fetch_object($result)) {
460 461 462
    // If no node types are associated with a vocabulary, the LEFT JOIN will
    // return a NULL value for type.
    if (isset($voc->type)) {
463
      $node_types[$voc->vid][$voc->type] = $voc->type;
464 465 466 467 468 469
      unset($voc->type);
      $voc->nodes = $node_types[$voc->vid];
    }
    elseif (!isset($voc->nodes)) {
      $voc->nodes = array();
    }
Dries's avatar
 
Dries committed
470
    $vocabularies[$voc->vid] = $voc;
Dries's avatar
 
Dries committed
471
  }
Dries's avatar
 
Dries committed
472

Kjartan's avatar
Kjartan committed
473 474
  return $vocabularies;
}
Dries's avatar
 
Dries committed
475

Dries's avatar
 
Dries committed
476
/**
477
 * Implementation of hook_form_alter().
Dries's avatar
 
Dries committed
478
 * Generate a form for selecting terms to associate with a node.
Dries's avatar
Dries committed
479
 * We check for taxonomy_override_selector before loading the full
480 481
 * vocabulary, so contrib modules can intercept before hook_form_alter
 *  and provide scalable alternatives.
Dries's avatar
 
Dries committed
482
 */
483
function taxonomy_form_alter(&$form, $form_state, $form_id) {
484
  if (isset($form['type']) && isset($form['#node']) && (!variable_get('taxonomy_override_selector', FALSE)) && $form['type']['#value'] .'_node_form' == $form_id) {
485 486
    $node = $form['#node'];

487
    if (!isset($node->taxonomy)) {
488
      $terms = empty($node->nid) ? array() : taxonomy_node_get_terms($node);
Kjartan's avatar
Kjartan committed
489 490
    }
    else {
491 492 493 494
      // After preview the terms must be converted to objects.
      if (isset($form_state['node_preview'])) {
        $node->taxonomy = taxonomy_preview_terms($node);
      }
495
      $terms = $node->taxonomy;
Dries's avatar
 
Dries committed
496 497
    }

498
    $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);
Dries's avatar
Dries committed
499

500 501
    while ($vocabulary = db_fetch_object($c)) {
      if ($vocabulary->tags) {
502
        if (isset($form_state['node_preview'])) {
503
          // Typed string can be changed by the user before preview,
504
          // so we just insert the tags directly as provided in the form.
505 506 507 508 509
          $typed_string = $node->taxonomy['tags'][$vocabulary->vid];
        }
        else {
          $typed_string = taxonomy_implode_tags($terms, $vocabulary->vid) . (array_key_exists('tags', $terms) ? $terms['tags'][$vocabulary->vid] : NULL);
        }
510 511 512 513
        if ($vocabulary->help) {
          $help = $vocabulary->help;
        }
        else {
514
          $help = t('A comma-separated list of terms describing this content. Example: funny, bungee jumping, "Company, Inc.".');
515
        }
516 517 518 519 520 521 522
        $form['taxonomy']['tags'][$vocabulary->vid] = array('#type' => 'textfield',
          '#title' => $vocabulary->name,
          '#description' => $help,
          '#required' => $vocabulary->required,
          '#default_value' => $typed_string,
          '#autocomplete_path' => 'taxonomy/autocomplete/'. $vocabulary->vid,
          '#weight' => $vocabulary->weight,
523
          '#maxlength' => 255,
524
        );
525 526
      }
      else {
527 528
        // Extract terms belonging to the vocabulary in question.
        $default_terms = array();
529
        foreach ($terms as $term) {
530 531
          // Free tagging has no default terms and also no vid after preview.
          if (isset($term->vid) && $term->vid == $vocabulary->vid) {
532 533 534 535
            $default_terms[$term->tid] = $term;
          }
        }
        $form['taxonomy'][$vocabulary->vid] = taxonomy_form($vocabulary->vid, array_keys($default_terms), $vocabulary->help);
536
        $form['taxonomy'][$vocabulary->vid]['#weight'] = $vocabulary->weight;
537
        $form['taxonomy'][$vocabulary->vid]['#required'] = $vocabulary->required;
538
      }
Dries's avatar
Dries committed
539
    }
540
    if (!empty($form['taxonomy']) && is_array($form['taxonomy'])) {
541
      if (count($form['taxonomy']) > 1) {
542
        // Add fieldset only if form has more than 1 element.
543 544
        $form['taxonomy'] += array(
          '#type' => 'fieldset',
545
          '#title' => t('Vocabularies'),
546 547 548 549
          '#collapsible' => TRUE,
          '#collapsed' => FALSE,
        );
      }
550 551
      $form['taxonomy']['#weight'] = -3;
      $form['taxonomy']['#tree'] = TRUE;
552
    }
553 554 555 556
  }
}

/**
557
 * Helper function to convert terms after a preview.
558 559 560
 *
 * After preview the tags are an array instead of proper objects. This function
 * converts them back to objects with the exception of 'free tagging' terms,
561 562
 * because new tags can be added by the user before preview and those do not
 * yet exist in the database. We therefore save those tags as a string so
563 564 565 566
 * we can fill the form again after the preview.
 */
function taxonomy_preview_terms($node) {
  $taxonomy = array();
567 568 569 570 571 572 573 574 575 576 577 578 579 580
  if (isset($node->taxonomy)) {
    foreach ($node->taxonomy as $key => $term) {
      unset($node->taxonomy[$key]);
      // A 'Multiple select' and a 'Free tagging' field returns an array.
      if (is_array($term)) {
        foreach ($term as $tid) {
          if ($key == 'tags') {
            // Free tagging; the values will be saved for later as strings
            // instead of objects to fill the form again.
            $taxonomy['tags'] = $term;
          }
          else {
            $taxonomy[$tid] = taxonomy_get_term($tid);
          }
581 582
        }
      }
583 584 585 586
      // A 'Single select' field returns the term id.
      elseif ($term) {
        $taxonomy[$term] = taxonomy_get_term($term);
      }
Dries's avatar
Dries committed
587
    }
Dries's avatar
 
Dries committed
588
  }
589
  return $taxonomy;
Kjartan's avatar
Kjartan committed
590
}
Dries's avatar
 
Dries committed
591

Dries's avatar
 
Dries committed
592
/**
593
 * Find all terms associated with the given node, within one vocabulary.
Dries's avatar
 
Dries committed
594
 */
595 596
function taxonomy_node_get_terms_by_vocabulary($node, $vid, $key = 'tid') {
  $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);
Kjartan's avatar
Kjartan committed
597 598 599
  $terms = array();
  while ($term = db_fetch_object($result)) {
    $terms[$term->$key] = $term;
Dries's avatar
 
Dries committed
600
  }
Kjartan's avatar
Kjartan committed
601 602 603
  return $terms;
}

Dries's avatar
 
Dries committed
604
/**
605
 * Find all terms associated with the given node, ordered by vocabulary and term weight.
Dries's avatar
 
Dries committed
606
 */
607
function taxonomy_node_get_terms($node, $key = 'tid') {
Kjartan's avatar
Kjartan committed
608
  static $terms;
Dries's avatar
 
Dries committed
609

610
  if (!isset($terms[$node->vid][$key])) {
611
    $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);
612
    $terms[$node->vid][$key] = array();
Dries's avatar
 
Dries committed
613
    while ($term = db_fetch_object($result)) {
614
      $terms[$node->vid][$key][$term->$key] = $term;
Dries's avatar
 
Dries committed
615 616
    }
  }
617
  return $terms[$node->vid][$key];
Kjartan's avatar
Kjartan committed
618
}
Dries's avatar
 
Dries committed
619

Dries's avatar
Dries committed
620 621 622
/**
 * Make sure incoming vids are free tagging enabled.
 */
623
function taxonomy_node_validate(&$node) {
624
  if (!empty($node->taxonomy)) {
625
    $terms = $node->taxonomy;
626
    if (!empty($terms['tags'])) {
Dries's avatar
Dries committed
627
      foreach ($terms['tags'] as $vid => $vid_value) {
628
        $vocabulary = taxonomy_vocabulary_load($vid);
629
        if (empty($vocabulary->tags)) {
630 631
          // see form_get_error $key = implode('][', $element['#parents']);
          // on why this is the key
632
          form_set_error("taxonomy][tags][$vid", t('The %name vocabulary can not be modified in this way.', array('%name' => $vocabulary->name)));
Dries's avatar
Dries committed
633 634 635 636 637 638
        }
      }
    }
  }
}

Dries's avatar
 
Dries committed
639 640 641
/**
 * Save term associations for a given node.
 */
642 643 644
function taxonomy_node_save($node, $terms) {

  taxonomy_node_delete_revision($node);
Dries's avatar
Dries committed
645 646 647

  // Free tagging vocabularies do not send their tids in the form,
  // so we'll detect them here and process them independently.
648 649 650
  if (isset($terms['tags'])) {
    $typed_input = $terms['tags'];
    unset($terms['tags']);
Dries's avatar
Dries committed
651 652

    foreach ($typed_input as $vid => $vid_value) {
653
      $typed_terms = drupal_explode_tags($vid_value);
Dries's avatar
Dries committed
654

655
      $inserted = array();
Dries's avatar
Dries committed
656 657
      foreach ($typed_terms as $typed_term) {
        // See if the term exists in the chosen vocabulary
658
        // and return the tid; otherwise, add a new record.
Dries's avatar
Dries committed
659
        $possibilities = taxonomy_get_term_by_name($typed_term);
660
        $typed_term_tid = NULL; // tid match, if any.
Dries's avatar
Dries committed
661 662 663 664 665 666 667
        foreach ($possibilities as $possibility) {
          if ($possibility->vid == $vid) {
            $typed_term_tid = $possibility->tid;
          }
        }

        if (!$typed_term_tid) {
668 669 670
          $edit = array('vid' => $vid, 'name' => $typed_term);
          $status = taxonomy_save_term($edit);
          $typed_term_tid = $edit['tid'];
Dries's avatar
Dries committed
671 672
        }

673
        // Defend against duplicate, differently cased tags
674
        if (!isset($inserted[$typed_term_tid])) {
675
          db_query('INSERT INTO {term_node} (nid, vid, tid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $typed_term_tid);
676 677
          $inserted[$typed_term_tid] = TRUE;
        }
Dries's avatar
Dries committed
678 679 680
      }
    }
  }
Dries's avatar
 
Dries committed
681

682 683
  if (is_array($terms)) {
    foreach ($terms as $term) {
684 685 686
      if (is_array($term)) {
        foreach ($term as $tid) {
          if ($tid) {
687
            db_query('INSERT INTO {term_node} (nid, vid, tid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $tid);
688 689 690
          }
        }
      }
691
      else if (is_object($term)) {
692
        db_query('INSERT INTO {term_node} (nid, vid, tid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $term->tid);
693
      }
694
      else if ($term) {
695
        db_query('INSERT INTO {term_node} (nid, vid, tid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $term);
696
      }
Dries's avatar
 
Dries committed
697 698
    }
  }
Kjartan's avatar
Kjartan committed
699
}
Dries's avatar
 
Dries committed
700

Dries's avatar
 
Dries committed
701 702 703
/**
 * Remove associations of a node to its terms.
 */
704
function taxonomy_node_delete($node) {
705
  db_query('DELETE FROM {term_node} WHERE nid = %d', $node->nid);
706 707 708 709 710 711
}

/**
 * Remove associations of a node to its terms.
 */
function taxonomy_node_delete_revision($node) {
712
  db_query('DELETE FROM {term_node} WHERE vid = %d', $node->vid);
Kjartan's avatar
Kjartan committed
713
}
Dries's avatar
 
Dries committed
714

715 716 717 718 719
/**
 * Implementation of hook_node_type().
 */
function taxonomy_node_type($op, $info) {
  if ($op == 'update' && !empty($info->old_type) && $info->type != $info->old_type) {
720 721 722 723
    db_query("UPDATE {vocabulary_node_types} SET type = '%s' WHERE type = '%s'", $info->type, $info->old_type);
  }
  elseif ($op == 'delete') {
    db_query("DELETE FROM {vocabulary_node_types} WHERE type = '%s'", $info->type);
724 725 726
  }
}

Dries's avatar
 
Dries committed
727 728 729 730
/**
 * Find all term objects related to a given term ID.
 */
function taxonomy_get_related($tid, $key = 'tid') {
Kjartan's avatar
Kjartan committed
731
  if ($tid) {
Dries's avatar
 
Dries committed
732
    $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);
Kjartan's avatar
Kjartan committed
733 734 735
    $related = array();
    while ($term = db_fetch_object($result)) {
      $related[$term->$key] = $term;
Dries's avatar
 
Dries committed
736
    }
Kjartan's avatar
Kjartan committed
737
    return $related;
Dries's avatar
 
Dries committed
738
  }
Kjartan's avatar
Kjartan committed
739 740
  else {
    return array();
Dries's avatar
 
Dries committed
741
  }
Kjartan's avatar
Kjartan committed
742
}
Dries's avatar
 
Dries committed
743

Dries's avatar
 
Dries committed
744 745 746 747
/**
 * Find all parents of a given term ID.
 */
function taxonomy_get_parents($tid, $key = 'tid') {
Kjartan's avatar
Kjartan committed
748
  if ($tid) {
749
    $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);
Kjartan's avatar
Kjartan committed
750 751 752
    $parents = array();
    while ($parent = db_fetch_object($result)) {
      $parents[$parent->$key] = $parent;
Dries's avatar
 
Dries committed
753
    }
Kjartan's avatar
Kjartan committed
754
    return $parents;
Dries's avatar
 
Dries committed
755
  }
Kjartan's avatar
Kjartan committed
756 757 758 759
  else {
    return array();
  }
}
Dries's avatar
 
Dries committed
760

Dries's avatar
 
Dries committed
761 762 763 764
/**
 * Find all ancestors of a given term ID.
 */
function taxonomy_get_parents_all($tid) {
Dries's avatar
 
Dries committed
765 766 767 768 769 770 771 772 773 774 775 776
  $parents = array();
  if ($tid) {
    $parents[] = taxonomy_get_term($tid);
    $n = 0;
    while ($parent = taxonomy_get_parents($parents[$n]->tid)) {
      $parents = array_merge($parents, $parent);
      $n++;
    }
  }
  return $parents;
}

Dries's avatar
 
Dries committed
777 778 779 780
/**
 * Find all children of a term ID.
 */
function taxonomy_get_children($tid, $vid = 0, $key = 'tid') {
Kjartan's avatar
Kjartan committed
781
  if ($vid) {
782
    $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);
Dries's avatar
 
Dries committed
783
  }
Kjartan's avatar
Kjartan committed
784
  else {
785
    $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);
Kjartan's avatar
Kjartan committed
786 787 788 789 790 791 792
  }
  $children = array();
  while ($term = db_fetch_object($result)) {
    $children[$term->$key] = $term;
  }
  return $children;
}
Dries's avatar
 
Dries committed
793

Dries's avatar
 
Dries committed
794 795 796 797 798 799 800 801 802 803 804 805 806
/**
 * Create a hierarchical representation of a vocabulary.
 *
 * @param $vid
 *   Which vocabulary to generate the tree for.
 *
 * @param $parent
 *   The term ID under which to generate the tree. If 0, generate the tree
 *   for the entire vocabulary.
 *
 * @param $depth
 *   Internal use only.
 *
Dries's avatar
 
Dries committed
807 808 809
 * @param $max_depth
 *   The number of levels of the tree to return. Leave NULL to return all levels.
 *
Dries's avatar
 
Dries committed
810 811 812
 * @return
 *   An array of all term objects in the tree. Each term object is extended
 *   to have "depth" and "parents" attributes in addition to its normal ones.
813
 *   Results are statically cached.
Dries's avatar
 
Dries committed
814 815
 */
function taxonomy_get_tree($vid, $parent = 0, $depth = -1, $max_depth = NULL) {
Dries's avatar
 
Dries committed
816
  static $children, $parents, $terms;
Dries's avatar
 
Dries committed
817

Kjartan's avatar
Kjartan committed
818
  $depth++;
Dries's avatar
 
Dries committed
819

Dries's avatar
 
Dries committed
820 821 822 823
  // We cache trees, so it's not CPU-intensive to call get_tree() on a term
  // and its children, too.
  if (!isset($children[$vid])) {
    $children[$vid] = array();
Dries's avatar
 
Dries committed
824

825
    $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);
Dries's avatar
 
Dries committed
826
    while ($term = db_fetch_object($result)) {
Dries's avatar
 
Dries committed
827 828 829
      $children[$vid][$term->parent][] = $term->tid;
      $parents[$vid][$term->tid][] = $term->parent;
      $terms[$vid][$term->tid] = $term;
Dries's avatar
 
Dries committed
830 831
    }
  }
Dries's avatar
 
Dries committed
832

Dries's avatar
 
Dries committed
833
  $max_depth = (is_null($max_depth)) ? count($children[$vid]) : $max_depth;
834 835
  $tree = array();
  if (!empty($children[$vid][$parent])) {
Dries's avatar
Dries committed
836 837
    foreach ($children[$vid][$parent] as $child) {
      if ($max_depth > $depth) {
838 839
        $term = drupal_clone($terms[$vid][$child]);
        $term->depth = $depth;
Dries's avatar
Dries committed
840
        // The "parent" attribute is not useful, as it would show one parent only.
841 842 843
        unset($term->parent);
        $term->parents = $parents[$vid][$child];
        $tree[] = $term;
Dries's avatar
Dries committed
844

845
        if (!empty($children[$vid][$child])) {
Dries's avatar
Dries committed
846 847
          $tree = array_merge($tree, taxonomy_get_tree($vid, $child, $depth, $max_depth));
        }
Dries's avatar
 
Dries committed
848
      }
Dries's avatar
 
Dries committed
849
    }
Kjartan's avatar
Kjartan committed
850
  }
Dries's avatar
 
Dries committed
851

852
  return $tree;
Kjartan's avatar
Kjartan committed
853
}
Dries's avatar
 
Dries committed
854

Dries's avatar
 
Dries committed
855 856 857
/**
 * Return an array of synonyms of the given term ID.
 */
Kjartan's avatar
Kjartan committed
858 859
function taxonomy_get_synonyms($tid) {
  if ($tid) {
860
    $synonyms = array();
Dries's avatar
 
Dries committed
861
    $result = db_query('SELECT name FROM {term_synonym} WHERE tid = %d', $tid);
Kjartan's avatar
Kjartan committed
862
    while ($synonym = db_fetch_array($result)) {
Dries's avatar
 
Dries committed
863
      $synonyms[] = $synonym['name'];
Dries's avatar
 
Dries committed
864
    }
865
    return $synonyms;
Dries's avatar
 
Dries committed
866
  }
Kjartan's avatar
Kjartan committed
867 868
  else {
    return array();
Dries's avatar
 
Dries committed
869
  }
Kjartan's avatar
Kjartan committed
870
}
Dries's avatar
 
Dries committed
871

Dries's avatar
 
Dries committed
872 873 874 875 876
/**
 * Return the term object that has the given string as a synonym.
 */
function taxonomy_get_synonym_root($synonym) {
  return db_fetch_object(db_query("SELECT * FROM {term_synonym} s, {term_data} t WHERE t.tid = s.tid AND s.name = '%s'", $synonym));
Kjartan's avatar
Kjartan committed
877
}
Dries's avatar
 
Dries committed
878

Dries's avatar
 
Dries committed
879
/**
880 881 882 883 884 885 886 887 888 889 890 891
 * Count the number of published nodes classified by a term.
 *
 * @param $tid
 *   The term's ID
 *
 * @param $type
 *   The $node->type. If given, taxonomy_term_count_nodes only counts
 *   nodes of $type that are classified with the term $tid.
 *
 * @return int
 *   An integer representing a number of nodes.
 *   Results are statically cached.
Dries's avatar
 
Dries committed
892
 */
Dries's avatar
 
Dries committed
893
function taxonomy_term_count_nodes($tid, $type = 0) {
Kjartan's avatar
Kjartan committed
894
  static $count;
Dries's avatar
 
Dries committed
895

Dries's avatar
 
Dries committed
896
  if (!isset($count[$type])) {
897
    // $type == 0 always evaluates TRUE if $type is a string
Dries's avatar
 
Dries committed
898
    if (is_numeric($type)) {
899
      $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'));
Dries's avatar
 
Dries committed
900 901
    }
    else {
902
      $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);
Dries's avatar
 
Dries committed
903
    }
Kjartan's avatar
Kjartan committed
904
    while ($term = db_fetch_object($result)) {
Dries's avatar
 
Dries committed
905
      $count[$type][$term->tid] = $term->c;
Dries's avatar
 
Dries committed
906 907
    }
  }
908
  $children_count = 0;
Kjartan's avatar
Kjartan committed
909
  foreach (_taxonomy_term_children($tid) as $c) {
Dries's avatar
 
Dries committed
910
    $children_count += taxonomy_term_count_nodes($c, $type);
Kjartan's avatar
Kjartan committed
911
  }
912
  return $children_count + (isset($count[$type][$tid]) ? $count[$type][$tid] : 0);
Kjartan's avatar
Kjartan committed
913 914
}

Dries's avatar
 
Dries committed
915
/**
916 917 918 919 920 921 922 923 924 925
 * Helper for taxonomy_term_count_nodes(). Used to find out
 * which terms are children of a parent term.
 *
 * @param $tid
 *   The parent term's ID
 *
 * @return array
 *   An array of term IDs representing the children of $tid.
 *   Results are statically cached.
 *
Dries's avatar
 
Dries committed
926
 */
Kjartan's avatar
Kjartan committed
927 928
function _taxonomy_term_children($tid) {
  static $children;
Dries's avatar
 
Dries committed
929

Dries's avatar
 
Dries committed
930
  if (!isset($children)) {
Dries's avatar
 
Dries committed
931
    $result = db_query('SELECT tid, parent FROM {term_hierarchy}');
Kjartan's avatar
Kjartan committed
932 933
    while ($term = db_fetch_object($result)) {
      $children[$term->parent][] = $term->tid;
Dries's avatar
 
Dries committed
934
    }
Dries's avatar
 
Dries committed
935
  }
936
  return isset($children[$tid]) ? $children[$tid] : array();
Kjartan's avatar
Kjartan committed
937
}
Dries's avatar
 
Dries committed
938

Dries's avatar
 
Dries committed
939
/**
Dries's avatar
 
Dries committed
940
 * Try to map a string to an existing term, as for glossary use.