taxonomy.module 39.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
    ),
  );
}

Dries's avatar
 
Dries committed
30 31 32 33 34 35 36
/**
 * 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:
 *
37
 * if (module_exists('taxonomy')) {
38 39
 *   $terms = taxonomy_link('taxonomy terms', $node);
 *   print theme('links', $terms);
Dries's avatar
 
Dries committed
40 41
 * }
 */
Dries's avatar
 
Dries committed
42
function taxonomy_link($type, $node = NULL) {
Dries's avatar
 
Dries committed
43
  if ($type == 'taxonomy terms' && $node != NULL) {
Kjartan's avatar
 
Kjartan committed
44
    $links = array();
45 46 47 48
    // If previewing, the terms must be converted to objects first.
    if ($node->build_mode == NODE_BUILD_PREVIEW) {
      $node->taxonomy = taxonomy_preview_terms($node);
    }
49
    if (!empty($node->taxonomy)) {
50
      foreach ($node->taxonomy as $term) {
51
        // During preview the free tagging terms are in an array unlike the
52
        // other terms which are objects. So we have to check if a $term
53
        // is an object or not.
54 55 56 57 58 59 60
        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))
          );
        }
61 62
        // Previewing free tagging terms; we don't link them because the
        // term-page might not exist yet.
63 64 65 66 67 68
        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,
69
              );
70 71
            }
          }
72
        }
Dries's avatar
 
Dries committed
73
      }
Dries's avatar
 
Dries committed
74
    }
Kjartan's avatar
Kjartan committed
75

76 77
    // We call this hook again because some modules and themes
    // call taxonomy_link('taxonomy terms') directly.
78
    drupal_alter('link', $links, $node);
79 80

    return $links;
81 82 83
  }
}

84 85 86 87 88 89 90 91 92 93
/**
 * 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.
 */

94
function taxonomy_term_path($term) {
95
  $vocabulary = taxonomy_vocabulary_load($term->vid);
96 97 98 99 100 101
  if ($vocabulary->module != 'taxonomy' && $path = module_invoke($vocabulary->module, 'term_path', $term)) {
    return $path;
  }
  return 'taxonomy/term/'. $term->tid;
}

Dries's avatar
 
Dries committed
102 103 104
/**
 * Implementation of hook_menu().
 */
105 106
function taxonomy_menu() {
  $items['admin/content/taxonomy'] = array(
107 108
    'title' => 'Categories',
    'description' => 'Create vocabularies and terms to categorize your content.',
109 110
    'page callback' => 'taxonomy_overview_vocabularies',
    'access arguments' => array('administer taxonomy'),
111
    'file' => 'taxonomy.admin.inc',
112 113 114
  );

  $items['admin/content/taxonomy/list'] = array(
115
    'title' => 'List',
116 117 118 119 120
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );

  $items['admin/content/taxonomy/add/vocabulary'] = array(
121
    'title' => 'Add vocabulary',
122 123 124
    'page callback' => 'drupal_get_form',
    'page arguments' => array('taxonomy_form_vocabulary'),
    'type' => MENU_LOCAL_TASK,
125
    'parent' => 'admin/content/taxonomy',
126
    'file' => 'taxonomy.admin.inc',
127 128
  );

129
  $items['admin/content/taxonomy/edit/vocabulary/%taxonomy_vocabulary'] = array(
130
    'title' => 'Edit vocabulary',
131 132 133
    'page callback' => 'taxonomy_admin_vocabulary_edit',
    'page arguments' => array(5),
    'type' => MENU_CALLBACK,
134
    'file' => 'taxonomy.admin.inc',
135 136 137
  );

  $items['admin/content/taxonomy/edit/term'] = array(
138
    'title' => 'Edit term',
139 140
    'page callback' => 'taxonomy_admin_term_edit',
    'type' => MENU_CALLBACK,
141
    'file' => 'taxonomy.admin.inc',
142 143 144
  );

  $items['taxonomy/term'] = array(
145
    'title' => 'Taxonomy term',
146 147 148
    'page callback' => 'taxonomy_term_page',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
149
    'file' => 'taxonomy.pages.inc',
150 151 152
  );

  $items['taxonomy/autocomplete'] = array(
153
    'title' => 'Autocomplete taxonomy',
154 155 156
    'page callback' => 'taxonomy_autocomplete',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
157
    'file' => 'taxonomy.pages.inc',
158
  );
159
  $items['admin/content/taxonomy/%taxonomy_vocabulary'] = array(
160
    'title' => 'List terms',
161 162 163 164
    'page callback' => 'taxonomy_overview_terms',
    'page arguments' => array(3),
    'access arguments' => array('administer taxonomy'),
    'type' => MENU_CALLBACK,
165
    'file' => 'taxonomy.admin.inc',
166 167
  );

168
  $items['admin/content/taxonomy/%taxonomy_vocabulary/list'] = array(
169
    'title' => 'List',
170 171 172 173
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );

174
  $items['admin/content/taxonomy/%taxonomy_vocabulary/add/term'] = array(
175
    'title' => 'Add term',
176 177
    'page callback' => 'taxonomy_add_term_page',
    'page arguments' => array(3),
178
    'type' => MENU_LOCAL_TASK,
179
    'parent' => 'admin/content/taxonomy/%taxonomy_vocabulary',
180
    'file' => 'taxonomy.admin.inc',
181
  );
Dries's avatar
 
Dries committed
182

Dries's avatar
 
Dries committed
183 184
  return $items;
}
Dries's avatar
 
Dries committed
185

186
function taxonomy_save_vocabulary(&$edit) {
187
  $edit['nodes'] = empty($edit['nodes']) ? array() : $edit['nodes'];
Dries's avatar
 
Dries committed
188

189 190 191 192
  if (!isset($edit['module'])) {
    $edit['module'] = 'taxonomy';
  }

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

  cache_clear_all();
Dries's avatar
 
Dries committed
215

216
  return $status;
Kjartan's avatar
Kjartan committed
217
}
Dries's avatar
 
Dries committed
218

219 220 221 222 223 224 225 226
/**
 * Delete a vocabulary.
 *
 * @param $vid
 *   A vocabulary ID.
 * @return
 *   Constant indicating items were deleted.
 */
Kjartan's avatar
Kjartan committed
227
function taxonomy_del_vocabulary($vid) {
228
  $vocabulary = (array) taxonomy_vocabulary_load($vid);
Dries's avatar
 
Dries committed
229

Dries's avatar
 
Dries committed
230
  db_query('DELETE FROM {vocabulary} WHERE vid = %d', $vid);
Dries's avatar
 
Dries committed
231
  db_query('DELETE FROM {vocabulary_node_types} WHERE vid = %d', $vid);
Dries's avatar
 
Dries committed
232
  $result = db_query('SELECT tid FROM {term_data} WHERE vid = %d', $vid);
Kjartan's avatar
Kjartan committed
233 234
  while ($term = db_fetch_object($result)) {
    taxonomy_del_term($term->tid);
Dries's avatar
 
Dries committed
235
  }
Dries's avatar
 
Dries committed
236

Dries's avatar
 
Dries committed
237
  module_invoke_all('taxonomy', 'delete', 'vocabulary', $vocabulary);
Dries's avatar
 
Dries committed
238

Dries's avatar
 
Dries committed
239 240
  cache_clear_all();

241
  return SAVED_DELETED;
Dries's avatar
 
Dries committed
242 243
}

244 245 246
/**
 * Helper function for taxonomy_form_term_submit().
 *
247
 * @param $form_state['values']
248 249 250 251
 * @return
 *   Status constant indicating if term was inserted or updated.
 */
function taxonomy_save_term(&$form_values) {
252 253 254 255 256
  $form_values += array(
    'description' => '',
    'weight' => 0
  );

257
  if (!empty($form_values['tid']) && $form_values['name']) {
258
    drupal_write_record('term_data', $form_values, 'tid');
259
    $hook = 'update';
260
    $status = SAVED_UPDATED;
Kjartan's avatar
Kjartan committed
261
  }
262
  else if (!empty($form_values['tid'])) {
263
    return taxonomy_del_term($form_values['tid']);
Kjartan's avatar
Kjartan committed
264 265
  }
  else {
266
    drupal_write_record('term_data', $form_values);
267
    $hook = 'insert';
268
    $status = SAVED_NEW;
Kjartan's avatar
Kjartan committed
269
  }
Dries's avatar
 
Dries committed
270

271
  db_query('DELETE FROM {term_relation} WHERE tid1 = %d OR tid2 = %d', $form_values['tid'], $form_values['tid']);
272
  if (!empty($form_values['relations'])) {
273
    foreach ($form_values['relations'] as $related_id) {
Kjartan's avatar
Kjartan committed
274
      if ($related_id != 0) {
275
        db_query('INSERT INTO {term_relation} (tid1, tid2) VALUES (%d, %d)', $form_values['tid'], $related_id);
Dries's avatar
 
Dries committed
276
      }
Kjartan's avatar
Kjartan committed
277
    }
Kjartan's avatar
Kjartan committed
278
  }
Dries's avatar
 
Dries committed
279

280 281 282
  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
283
  }
284 285
  if (is_array($form_values['parent'])) {
    foreach ($form_values['parent'] as $parent) {
286 287
      if (is_array($parent)) {
        foreach ($parent as $tid) {
288
          db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $form_values['tid'], $tid);
289 290 291
        }
      }
      else {
292
        db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $form_values['tid'], $parent);
293
      }
Dries's avatar
 
Dries committed
294
    }
Kjartan's avatar
Kjartan committed
295
  }
296
  else {
297
    db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $form_values['tid'], $form_values['parent']);
298
  }
Dries's avatar
 
Dries committed
299

300
  db_query('DELETE FROM {term_synonym} WHERE tid = %d', $form_values['tid']);
301
  if (!empty($form_values['synonyms'])) {
302
    foreach (explode ("\n", str_replace("\r", '', $form_values['synonyms'])) as $synonym) {
Dries's avatar
 
Dries committed
303
      if ($synonym) {
304
        db_query("INSERT INTO {term_synonym} (tid, name) VALUES (%d, '%s')", $form_values['tid'], chop($synonym));
Dries's avatar
 
Dries committed
305
      }
Kjartan's avatar
Kjartan committed
306
    }
Dries's avatar
 
Dries committed
307
  }
Dries's avatar
 
Dries committed
308

309
  if (isset($hook)) {
310
    module_invoke_all('taxonomy', $hook, 'term', $form_values);
311 312
  }

Dries's avatar
 
Dries committed
313 314
  cache_clear_all();

315
  return $status;
Kjartan's avatar
Kjartan committed
316
}
Dries's avatar
 
Dries committed
317

318 319 320 321 322 323 324 325
/**
 * Delete a term.
 *
 * @param $tid
 *   The term ID.
 * @return
 *   Status constant indicating deletion.
 */
Kjartan's avatar
Kjartan committed
326
function taxonomy_del_term($tid) {
327 328 329 330 331 332 333 334 335 336 337 338 339 340
  $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
341

342
      $term = (array) taxonomy_get_term($tid);
Dries's avatar
 
Dries committed
343

344 345 346 347 348
      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
349

350 351
      module_invoke_all('taxonomy', 'delete', 'term', $term);
    }
Dries's avatar
 
Dries committed
352

353 354
    $tids = $orphans;
  }
Dries's avatar
 
Dries committed
355

Dries's avatar
 
Dries committed
356
  cache_clear_all();
357 358

  return SAVED_DELETED;
Dries's avatar
 
Dries committed
359 360
}

Dries's avatar
 
Dries committed
361 362 363
/**
 * Generate a form element for selecting terms from a vocabulary.
 */
364
function taxonomy_form($vid, $value = 0, $help = NULL, $name = 'taxonomy') {
365
  $vocabulary = taxonomy_vocabulary_load($vid);
366
  $help = ($help) ? $help : $vocabulary->help;
367
  $blank = 0;
368

369 370
  if (!$vocabulary->multiple) {
    $blank = ($vocabulary->required) ? t('- Please choose -') : t('- None selected -');
Kjartan's avatar
Kjartan committed
371
  }
Dries's avatar
 
Dries committed
372

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

376
/**
377
 * Generate a set of options for selecting a term from all vocabularies.
378 379 380 381 382 383 384
 */
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);
385
    if ($tree && (count($tree) > 0)) {
386
      $options[$vocabulary->name] = array();
387
      foreach ($tree as $term) {
388
        $options[$vocabulary->name][$term->tid] = str_repeat('-', $term->depth) . $term->name;
389 390 391 392 393 394
      }
    }
  }
  return $options;
}

Dries's avatar
 
Dries committed
395 396 397 398 399 400
/**
 * 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
401
function taxonomy_get_vocabularies($type = NULL) {
Kjartan's avatar
Kjartan committed
402
  if ($type) {
403
    $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
404 405
  }
  else {
406
    $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
407
  }
Dries's avatar
 
Dries committed
408

Kjartan's avatar
Kjartan committed
409
  $vocabularies = array();
Dries's avatar
 
Dries committed
410
  $node_types = array();
Kjartan's avatar
Kjartan committed
411
  while ($voc = db_fetch_object($result)) {
412 413 414 415 416 417 418 419 420 421
    // If no node types are associated with a vocabulary, the LEFT JOIN will
    // return a NULL value for type.
    if (isset($voc->type)) {
      $node_types[$voc->vid][] = $voc->type;
      unset($voc->type);
      $voc->nodes = $node_types[$voc->vid];
    }
    elseif (!isset($voc->nodes)) {
      $voc->nodes = array();
    }
Dries's avatar
 
Dries committed
422
    $vocabularies[$voc->vid] = $voc;
Dries's avatar
 
Dries committed
423
  }
Dries's avatar
 
Dries committed
424

Kjartan's avatar
Kjartan committed
425 426
  return $vocabularies;
}
Dries's avatar
 
Dries committed
427

Dries's avatar
 
Dries committed
428
/**
429
 * Implementation of hook_form_alter().
Dries's avatar
 
Dries committed
430 431
 * Generate a form for selecting terms to associate with a node.
 */
432
function taxonomy_form_alter(&$form, $form_state, $form_id) {
433
  if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id) {
434 435
    $node = $form['#node'];

436
    if (!isset($node->taxonomy)) {
437
      $terms = empty($node->nid) ? array() : taxonomy_node_get_terms($node);
Kjartan's avatar
Kjartan committed
438 439
    }
    else {
440 441 442 443
      // After preview the terms must be converted to objects.
      if (isset($form_state['node_preview'])) {
        $node->taxonomy = taxonomy_preview_terms($node);
      }
444
      $terms = $node->taxonomy;
Dries's avatar
 
Dries committed
445 446
    }

447
    $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
448

449 450
    while ($vocabulary = db_fetch_object($c)) {
      if ($vocabulary->tags) {
451
        if (isset($form_state['node_preview'])) {
452
          // Typed string can be changed by the user before preview,
453
          // so we just insert the tags directly as provided in the form.
454 455 456 457 458
          $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);
        }
459 460 461 462
        if ($vocabulary->help) {
          $help = $vocabulary->help;
        }
        else {
463
          $help = t('A comma-separated list of terms describing this content. Example: funny, bungee jumping, "Company, Inc.".');
464
        }
465 466 467 468 469 470 471
        $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,
472
          '#maxlength' => 255,
473
        );
474 475
      }
      else {
476 477
        // Extract terms belonging to the vocabulary in question.
        $default_terms = array();
478
        foreach ($terms as $term) {
479 480
          // Free tagging has no default terms and also no vid after preview.
          if (isset($term->vid) && $term->vid == $vocabulary->vid) {
481 482 483 484
            $default_terms[$term->tid] = $term;
          }
        }
        $form['taxonomy'][$vocabulary->vid] = taxonomy_form($vocabulary->vid, array_keys($default_terms), $vocabulary->help);
485
        $form['taxonomy'][$vocabulary->vid]['#weight'] = $vocabulary->weight;
486
        $form['taxonomy'][$vocabulary->vid]['#required'] = $vocabulary->required;
487
      }
Dries's avatar
Dries committed
488
    }
489
    if (!empty($form['taxonomy']) && is_array($form['taxonomy'])) {
490
      if (count($form['taxonomy']) > 1) {
491
        // Add fieldset only if form has more than 1 element.
492 493 494 495 496 497 498
        $form['taxonomy'] += array(
          '#type' => 'fieldset',
          '#title' => t('Categories'),
          '#collapsible' => TRUE,
          '#collapsed' => FALSE,
        );
      }
499 500
      $form['taxonomy']['#weight'] = -3;
      $form['taxonomy']['#tree'] = TRUE;
501
    }
502 503 504 505 506 507 508 509
  }
}

/**
 * Helper function to covert terms after a preview.
 *
 * 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,
510 511
 * 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
512 513 514 515 516 517 518 519 520 521
 * we can fill the form again after the preview.
 */
function taxonomy_preview_terms($node) {
  $taxonomy = array();
  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') {
522 523
          // Free tagging; the values will be saved for later as strings
    // instead of objects to fill the form again.
524 525 526 527 528 529 530 531 532 533
          $taxonomy['tags'] = $term;
        }
        else {
          $taxonomy[$tid] = taxonomy_get_term($tid);
        }
      }
    }
    // A 'Single select' field returns the term id.
    elseif ($term) {
      $taxonomy[$term] = taxonomy_get_term($term);
Dries's avatar
Dries committed
534
    }
Dries's avatar
 
Dries committed
535
  }
536
  return $taxonomy;
Kjartan's avatar
Kjartan committed
537
}
Dries's avatar
 
Dries committed
538

Dries's avatar
 
Dries committed
539
/**
540
 * Find all terms associated with the given node, within one vocabulary.
Dries's avatar
 
Dries committed
541
 */
542 543
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
544 545 546
  $terms = array();
  while ($term = db_fetch_object($result)) {
    $terms[$term->$key] = $term;
Dries's avatar
 
Dries committed
547
  }
Kjartan's avatar
Kjartan committed
548 549 550
  return $terms;
}

Dries's avatar
 
Dries committed
551
/**
552
 * Find all terms associated with the given node, ordered by vocabulary and term weight.
Dries's avatar
 
Dries committed
553
 */
554
function taxonomy_node_get_terms($node, $key = 'tid') {
Kjartan's avatar
Kjartan committed
555
  static $terms;
Dries's avatar
 
Dries committed
556

557
  if (!isset($terms[$node->vid][$key])) {
558
    $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);
559
    $terms[$node->vid][$key] = array();
Dries's avatar
 
Dries committed
560
    while ($term = db_fetch_object($result)) {
561
      $terms[$node->vid][$key][$term->$key] = $term;
Dries's avatar
 
Dries committed
562 563
    }
  }
564
  return $terms[$node->vid][$key];
Kjartan's avatar
Kjartan committed
565
}
Dries's avatar
 
Dries committed
566

Dries's avatar
Dries committed
567 568 569
/**
 * Make sure incoming vids are free tagging enabled.
 */
570
function taxonomy_node_validate(&$node) {
571
  if (!empty($node->taxonomy)) {
572
    $terms = $node->taxonomy;
573
    if (!empty($terms['tags'])) {
Dries's avatar
Dries committed
574
      foreach ($terms['tags'] as $vid => $vid_value) {
575
        $vocabulary = taxonomy_vocabulary_load($vid);
576
        if (empty($vocabulary->tags)) {
577 578
          // see form_get_error $key = implode('][', $element['#parents']);
          // on why this is the key
579
          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
580 581 582 583 584 585
        }
      }
    }
  }
}

Dries's avatar
 
Dries committed
586 587 588
/**
 * Save term associations for a given node.
 */
589 590 591
function taxonomy_node_save($node, $terms) {

  taxonomy_node_delete_revision($node);
Dries's avatar
Dries committed
592 593 594

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

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

602
      $inserted = array();
Dries's avatar
Dries committed
603 604
      foreach ($typed_terms as $typed_term) {
        // See if the term exists in the chosen vocabulary
605
        // and return the tid; otherwise, add a new record.
Dries's avatar
Dries committed
606
        $possibilities = taxonomy_get_term_by_name($typed_term);
607
        $typed_term_tid = NULL; // tid match, if any.
Dries's avatar
Dries committed
608 609 610 611 612 613 614
        foreach ($possibilities as $possibility) {
          if ($possibility->vid == $vid) {
            $typed_term_tid = $possibility->tid;
          }
        }

        if (!$typed_term_tid) {
615 616 617
          $edit = array('vid' => $vid, 'name' => $typed_term);
          $status = taxonomy_save_term($edit);
          $typed_term_tid = $edit['tid'];
Dries's avatar
Dries committed
618 619
        }

620
        // Defend against duplicate, differently cased tags
621
        if (!isset($inserted[$typed_term_tid])) {
622
          db_query('INSERT INTO {term_node} (nid, vid, tid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $typed_term_tid);
623 624
          $inserted[$typed_term_tid] = TRUE;
        }
Dries's avatar
Dries committed
625 626 627
      }
    }
  }
Dries's avatar
 
Dries committed
628

629 630
  if (is_array($terms)) {
    foreach ($terms as $term) {
631 632 633
      if (is_array($term)) {
        foreach ($term as $tid) {
          if ($tid) {
634
            db_query('INSERT INTO {term_node} (nid, vid, tid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $tid);
635 636 637
          }
        }
      }
638
      else if (is_object($term)) {
639
        db_query('INSERT INTO {term_node} (nid, vid, tid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $term->tid);
640
      }
641
      else if ($term) {
642
        db_query('INSERT INTO {term_node} (nid, vid, tid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $term);
643
      }
Dries's avatar
 
Dries committed
644 645
    }
  }
Kjartan's avatar
Kjartan committed
646
}
Dries's avatar
 
Dries committed
647

Dries's avatar
 
Dries committed
648 649 650
/**
 * Remove associations of a node to its terms.
 */
651
function taxonomy_node_delete($node) {
652
  db_query('DELETE FROM {term_node} WHERE nid = %d', $node->nid);
653 654 655 656 657 658
}

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

662 663 664 665 666
/**
 * Implementation of hook_node_type().
 */
function taxonomy_node_type($op, $info) {
  if ($op == 'update' && !empty($info->old_type) && $info->type != $info->old_type) {
667 668 669 670
    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);
671 672 673
  }
}

Dries's avatar
 
Dries committed
674 675 676 677
/**
 * Find all term objects related to a given term ID.
 */
function taxonomy_get_related($tid, $key = 'tid') {
Kjartan's avatar
Kjartan committed
678
  if ($tid) {
Dries's avatar
 
Dries committed
679
    $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
680 681 682
    $related = array();
    while ($term = db_fetch_object($result)) {
      $related[$term->$key] = $term;
Dries's avatar
 
Dries committed
683
    }
Kjartan's avatar
Kjartan committed
684
    return $related;
Dries's avatar
 
Dries committed
685
  }
Kjartan's avatar
Kjartan committed
686 687
  else {
    return array();
Dries's avatar
 
Dries committed
688
  }
Kjartan's avatar
Kjartan committed
689
}
Dries's avatar
 
Dries committed
690

Dries's avatar
 
Dries committed
691 692 693 694
/**
 * Find all parents of a given term ID.
 */
function taxonomy_get_parents($tid, $key = 'tid') {
Kjartan's avatar
Kjartan committed
695
  if ($tid) {
696
    $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
697 698 699
    $parents = array();
    while ($parent = db_fetch_object($result)) {
      $parents[$parent->$key] = $parent;
Dries's avatar
 
Dries committed
700
    }
Kjartan's avatar
Kjartan committed
701
    return $parents;
Dries's avatar
 
Dries committed
702
  }
Kjartan's avatar
Kjartan committed
703 704 705 706
  else {
    return array();
  }
}
Dries's avatar
 
Dries committed
707

Dries's avatar
 
Dries committed
708 709 710 711
/**
 * Find all ancestors of a given term ID.
 */
function taxonomy_get_parents_all($tid) {
Dries's avatar
 
Dries committed
712 713 714 715 716 717 718 719 720 721 722 723
  $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
724 725 726 727
/**
 * Find all children of a term ID.
 */
function taxonomy_get_children($tid, $vid = 0, $key = 'tid') {
Kjartan's avatar
Kjartan committed
728
  if ($vid) {
729
    $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
730
  }
Kjartan's avatar
Kjartan committed
731
  else {
732
    $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
733 734 735 736 737 738 739
  }
  $children = array();
  while ($term = db_fetch_object($result)) {
    $children[$term->$key] = $term;
  }
  return $children;
}
Dries's avatar
 
Dries committed
740

Dries's avatar
 
Dries committed
741 742 743 744 745 746 747 748 749 750 751 752 753
/**
 * 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
754 755 756
 * @param $max_depth
 *   The number of levels of the tree to return. Leave NULL to return all levels.
 *
Dries's avatar
 
Dries committed
757 758 759
 * @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.
760
 *   Results are statically cached.
Dries's avatar
 
Dries committed
761 762
 */
function taxonomy_get_tree($vid, $parent = 0, $depth = -1, $max_depth = NULL) {
Dries's avatar
 
Dries committed
763
  static $children, $parents, $terms;
Dries's avatar
 
Dries committed
764

Kjartan's avatar
Kjartan committed
765
  $depth++;
Dries's avatar
 
Dries committed
766

Dries's avatar
 
Dries committed
767 768 769 770
  // 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
771

772
    $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
773
    while ($term = db_fetch_object($result)) {
Dries's avatar
 
Dries committed
774 775 776
      $children[$vid][$term->parent][] = $term->tid;
      $parents[$vid][$term->tid][] = $term->parent;
      $terms[$vid][$term->tid] = $term;
Dries's avatar
 
Dries committed
777 778
    }
  }
Dries's avatar
 
Dries committed
779

Dries's avatar
 
Dries committed
780
  $max_depth = (is_null($max_depth)) ? count($children[$vid]) : $max_depth;
781 782
  $tree = array();
  if (!empty($children[$vid][$parent])) {
Dries's avatar
Dries committed
783 784
    foreach ($children[$vid][$parent] as $child) {
      if ($max_depth > $depth) {
785 786
        $term = drupal_clone($terms[$vid][$child]);
        $term->depth = $depth;
Dries's avatar
Dries committed
787
        // The "parent" attribute is not useful, as it would show one parent only.
788 789 790
        unset($term->parent);
        $term->parents = $parents[$vid][$child];
        $tree[] = $term;
Dries's avatar
Dries committed
791