taxonomy.module 45.2 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
        else {
          foreach ($term as $free_typed) {
            $typed_terms = drupal_explode_tags($free_typed);
72
            foreach ($typed_terms as $typed_term) {
73
74
              $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
    'page callback' => 'taxonomy_term_page',
154
    'page arguments' => array(2),
155
156
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
157
    'file' => 'taxonomy.pages.inc',
158
159
160
  );

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

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

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

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

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

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

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

  cache_clear_all();
Dries's avatar
   
Dries committed
223

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

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

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

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

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

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

252
253
/**
 * Dynamicly check and update the hierarachy flag of a vocabulary.
254
 *
255
256
257
258
259
260
 * 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.
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
292
 * @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;
}

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

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

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

329
330
331
  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
332
  }
333
334
  if (is_array($form_values['parent'])) {
    foreach ($form_values['parent'] as $parent) {
335
336
      if (is_array($parent)) {
        foreach ($parent as $tid) {
337
          db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $form_values['tid'], $tid);
338
339
340
        }
      }
      else {
341
        db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $form_values['tid'], $parent);
342
      }
Dries's avatar
   
Dries committed
343
    }
Kjartan's avatar
Kjartan committed
344
  }
345
  else {
346
    db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $form_values['tid'], $form_values['parent']);
347
  }
Dries's avatar
   
Dries committed
348

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

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

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

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

367
368
369
370
371
372
373
374
/**
 * Delete a term.
 *
 * @param $tid
 *   The term ID.
 * @return
 *   Status constant indicating deletion.
 */
Kjartan's avatar
Kjartan committed
375
function taxonomy_del_term($tid) {
376
377
378
379
380
381
382
383
384
385
386
387
388
389
  $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
390

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

393
394
395
396
397
      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
398

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

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

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

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

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

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

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

427
/**
428
 * Generate a set of options for selecting a term from all vocabularies.
429
430
431
432
433
434
435
 */
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);
436
    if ($tree && (count($tree) > 0)) {
437
      $options[$vocabulary->name] = array();
438
      foreach ($tree as $term) {
439
        $options[$vocabulary->name][$term->tid] = str_repeat('-', $term->depth) . $term->name;
440
441
442
443
444
445
      }
    }
  }
  return $options;
}

Dries's avatar
   
Dries committed
446
447
448
449
450
451
/**
 * 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
452
function taxonomy_get_vocabularies($type = NULL) {
Kjartan's avatar
Kjartan committed
453
  if ($type) {
454
    $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
455
456
  }
  else {
457
    $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
458
  }
Dries's avatar
   
Dries committed
459

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

Kjartan's avatar
Kjartan committed
476
477
  return $vocabularies;
}
Dries's avatar
   
Dries committed
478

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

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

501
    $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
502

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

/**
560
 * Helper function to convert terms after a preview.
561
562
563
 *
 * 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,
564
565
 * 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
566
567
568
569
 * we can fill the form again after the preview.
 */
function taxonomy_preview_terms($node) {
  $taxonomy = array();
570
571
572
573
574
575
576
577
578
579
580
581
582
583
  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);
          }
584
585
        }
      }
586
587
588
589
      // A 'Single select' field returns the term id.
      elseif ($term) {
        $taxonomy[$term] = taxonomy_get_term($term);
      }
Dries's avatar
Dries committed
590
    }
Dries's avatar
   
Dries committed
591
  }
592
  return $taxonomy;
Kjartan's avatar
Kjartan committed
593
}
Dries's avatar
   
Dries committed
594

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

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

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

Dries's avatar
Dries committed
623
624
625
/**
 * Make sure incoming vids are free tagging enabled.
 */
626
function taxonomy_node_validate(&$node) {
627
  if (!empty($node->taxonomy)) {
628
    $terms = $node->taxonomy;
629
    if (!empty($terms['tags'])) {
Dries's avatar
Dries committed
630
      foreach ($terms['tags'] as $vid => $vid_value) {
631
        $vocabulary = taxonomy_vocabulary_load($vid);
632
        if (empty($vocabulary->tags)) {
633
634
          // see form_get_error $key = implode('][', $element['#parents']);
          // on why this is the key
635
          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
636
637
638
639
640
641
        }
      }
    }
  }
}

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

  taxonomy_node_delete_revision($node);
Dries's avatar
Dries committed
648
649
650

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

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

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

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

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

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

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

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

718
719
720
721
722
/**
 * Implementation of hook_node_type().
 */
function taxonomy_node_type($op, $info) {
  if ($op == 'update' && !empty($info->old_type) && $info->type != $info->old_type) {
723
724
725
726
    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);
727
728
729
  }
}

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

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

Dries's avatar
   
Dries committed
764
765
766
767
/**
 * Find all ancestors of a given term ID.
 */
function taxonomy_get_parents_all($tid) {
Dries's avatar
   
Dries committed
768
769
770
771
772
773
774
775
776
777
778
779
  $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
780
781
782
783
/**
 * Find all children of a term ID.
 */
function taxonomy_get_children($tid, $vid = 0, $key = 'tid') {
Kjartan's avatar
Kjartan committed
784
  if ($vid) {
785
    $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
786
  }
Kjartan's avatar
Kjartan committed
787
  else {
788
    $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
789
790
791
792
793
794
795
  }
  $children = array();
  while ($term = db_fetch_object($result)) {
    $children[$term->$key] = $term;
  }
  return $children;
}
Dries's avatar
   
Dries committed
796

Dries's avatar
   
Dries committed
797
798
799
800
801
802
803
804
805
806
807
808
809
/**
 * 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
810
811
812
 * @param $max_depth
 *   The number of levels of the tree to return. Leave NULL to return all levels.
 *
Dries's avatar
   
Dries committed
813
814
815
 * @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.
816
 *   Results are statically cached.
Dries's avatar
   
Dries committed
817
818
 */
function taxonomy_get_tree($vid, $parent = 0, $depth = -1, $max_depth = NULL) {
Dries's avatar
   
Dries committed
819
  static $children, $parents, $terms;
Dries's avatar
   
Dries committed
820

Kjartan's avatar
Kjartan committed
821
  $depth++;
Dries's avatar
   
Dries committed
822

Dries's avatar
   
Dries committed
823
824
825
826
  // 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
827

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

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

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

855
  return $tree;
Kjartan's avatar
Kjartan committed
856
}
Dries's avatar
   
Dries committed
857

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

Dries's avatar
   
Dries committed
875
876
877
878
879
/**
 * 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
880
}
Dries's avatar
   
Dries committed
881

Dries's avatar
   
Dries committed
882
/**
883
884
885
886
887
888
889
890
891
892
893
894
 * 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
895
 */
Dries's avatar
   
Dries committed
896
function taxonomy_term_count_nodes($tid, $type = 0) {
Kjartan's avatar
Kjartan committed
897
  static $count;
Dries's avatar
   
Dries committed
898

Dries's avatar
   
Dries committed
899
  if (!isset($count[$type])) {
900
    // $type == 0 always evaluates TRUE if $type is a string
Dries's avatar
   
Dries committed
901
    if (is_numeric($type)) {
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 GROUP BY t.tid'));
Dries's avatar
   
Dries committed
903
904
    }
    else {
905
      $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
906
    }
Kjartan's avatar
Kjartan committed
907
    while ($term = db_fetch_object($result)) {
Dries's avatar
   
Dries committed
908
      $count[$type][$term->tid] = $term->c;
Dries's avatar
   
Dries committed
909
910
    }
  }