taxonomy.module 63.3 KB
Newer Older
Dries's avatar
   
Dries committed
1
<?php
Dries's avatar
   
Dries committed
2

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

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * Denotes that no term in the vocabulary has a parent.
 */
const TAXONOMY_HIERARCHY_DISABLED = 0;

/**
 * Denotes that one or more terms in the vocabulary has a single parent.
 */
const TAXONOMY_HIERARCHY_SINGLE = 1;

/**
 * Denotes that one or more terms in the vocabulary have multiple parents.
 */
const TAXONOMY_HIERARCHY_MULTIPLE = 2;

23
24
25
26
27
28
29
30
/**
 * Users can create new terms in a free-tagging vocabulary when
 * submitting a taxonomy_autocomplete_widget. We store a term object
 * whose tid is 'autocreate' as a field data item during widget
 * validation and then actually create the term if/when that field
 * data item makes it to taxonomy_field_insert/update().
 */

31
/**
32
 * Implements hook_help().
33
34
35
36
37
38
39
40
41
42
 */
function taxonomy_help($path, $arg) {
  switch ($path) {
    case 'admin/help#taxonomy':
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('The Taxonomy module allows you to classify the content of your website. To classify content, you define <em>vocabularies</em> that contain related <em>terms</em>, and then assign the vocabularies to content types. For more information, see the online handbook entry for the <a href="@taxonomy">Taxonomy module</a>.', array('@taxonomy' => 'http://drupal.org/handbook/modules/taxonomy/')) . '</p>';
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Creating vocabularies') . '</dt>';
43
      $output .= '<dd>' . t('Users with sufficient <a href="@perm">permissions</a> can create <em>vocabularies</em> and <em>terms</em> through the <a href="@taxo">Taxonomy page</a>. The page listing the terms provides a drag-and-drop interface for controlling the order of the terms and sub-terms within a vocabulary, in a hierarchical fashion. A <em>controlled vocabulary</em> classifying music by genre with terms and sub-terms could look as follows:', array('@taxo' => url('admin/structure/taxonomy'), '@perm' => url('admin/people/permissions', array('fragment'=>'module-taxonomy'))));
44
      $output .= '<ul><li>' . t('<em>vocabulary</em>: Music') . '</li>';
45
46
47
48
49
50
51
52
53
      $output .= '<ul><li>' . t('<em>term</em>: Jazz') . '</li>';
      $output .= '<ul><li>' . t('<em>sub-term</em>: Swing') . '</li>';
      $output .= '<li>' . t('<em>sub-term</em>: Fusion') . '</li></ul></ul>';
      $output .= '<ul><li>' . t('<em>term</em>: Rock') . '</li>';
      $output .= '<ul><li>' . t('<em>sub-term</em>: Country rock') . '</li>';
      $output .= '<li>' . t('<em>sub-term</em>: Hard rock') . '</li></ul></ul></ul>';
      $output .= t('You can assign a sub-term to multiple parent terms. For example, <em>fusion</em> can be assigned to both <em>rock</em> and <em>jazz</em>.') . '</dd>';
      $output .= '<dd>' . t('Terms in a <em>free-tagging vocabulary</em> can be built gradually as you create or edit content. This is often done used for blogs or photo management applications.') . '</dd>';
      $output .= '<dt>' . t('Assigning vocabularies to content types') . '</dt>';
54
      $output .= '<dd>' . t('Before you can use a new vocabulary to classify your content, a new Taxonomy term field must be added to a <a href="@ctedit">content type</a> on its <em>manage fields</em> page. When adding a taxonomy field, you choose a <em>widget</em> to use to enter the taxonomy information on the content editing page: a select list, checkboxes, radio buttons, or an auto-complete field (to build a free-tagging vocabulary). After choosing the field type and widget, on the subsequent <em>field settings</em> page you can choose the desired vocabulary, whether one or multiple terms can be chosen from the vocabulary, and other settings. The same vocabulary can be added to multiple content types, by using the "Add existing field" section on the manage fields page.', array('@ctedit' => url('admin/structure/types'))) . '</dd>';
55
      $output .= '<dt>' . t('Classifying content') . '</dt>';
56
      $output .= '<dd>' . t('After the vocabulary is assigned to the content type, you can start classifying content. The field with terms will appear on the content editing screen when you edit or <a href="@addnode">add new content</a>.', array('@addnode' => url('node/add'))) . '</dd>';
57
58
59
60
61
62
63
      $output .= '<dt>' . t('Viewing listings and RSS feeds by term') . '</dt>';
      $output .= '<dd>' . t("Each taxonomy term automatically provides a page listing content that has its classification, and a corresponding RSS feed. For example, if the taxonomy term <em>country rock</em> has the ID 123 (you can see this by looking at the URL when hovering on the linked term, which you can click to navigate to the listing page), then you will find this list at the path <em>taxonomy/term/123</em>. The RSS feed will use the path <em>taxonomy/term/123/feed</em> (the RSS icon for this term's listing will automatically display in your browser's address bar when viewing the listing page).") . '</dd>';
      $output .= '<dt>' . t('Extending Taxonomy module') . '</dt>';
      $output .= '<dd>' . t('There are <a href="@taxcontrib">many contributed modules</a> that extend the behavior of the Taxonomy module for both display and organization of terms.', array('@taxcontrib' => 'http://drupal.org/project/modules?filters=tid:71&solrsort=sis_project_release_usage%20desc'));
      $output .= '</dl>';
      return $output;
    case 'admin/structure/taxonomy':
64
      $output = '<p>' . t('Taxonomy is for categorizing content. Terms are grouped into vocabularies. For example, a vocabulary called "Fruit" would contain the terms "Apple" and "Banana".') . '</p>';
65
66
      return $output;
    case 'admin/structure/taxonomy/%':
67
      $vocabulary = taxonomy_vocabulary_machine_name_load($arg[3]);
68
      switch ($vocabulary->hierarchy) {
69
        case TAXONOMY_HIERARCHY_DISABLED:
70
          return '<p>' . t('You can reorganize the terms in %capital_name using their drag-and-drop handles, and group terms under a parent term by sliding them under and to the right of the parent.', array('%capital_name' => drupal_ucfirst($vocabulary->name), '%name' => $vocabulary->name)) . '</p>';
71
        case TAXONOMY_HIERARCHY_SINGLE:
72
          return '<p>' . t('%capital_name contains terms grouped under parent terms. You can reorganize the terms in %capital_name using their drag-and-drop handles.', array('%capital_name' => drupal_ucfirst($vocabulary->name), '%name' => $vocabulary->name)) . '</p>';
73
        case TAXONOMY_HIERARCHY_MULTIPLE:
74
          return '<p>' . t('%capital_name contains terms with multiple parents. Drag and drop of terms with multiple parents is not supported, but you can re-enable drag-and-drop support by editing each term to include only a single parent.', array('%capital_name' => drupal_ucfirst($vocabulary->name))) . '</p>';
75
76
77
78
      }
  }
}

Dries's avatar
   
Dries committed
79
/**
80
 * Implements hook_permission().
Dries's avatar
   
Dries committed
81
 */
82
function taxonomy_permission() {
83
  $permissions = array(
84
    'administer taxonomy' => array(
85
      'title' => t('Administer vocabularies and terms'),
86
    ),
87
  );
88
  foreach (taxonomy_vocabulary_load_multiple(FALSE) as $vocabulary) {
89
90
91
92
93
94
95
    $permissions += array(
      'edit terms in ' . $vocabulary->vid => array(
        'title' => t('Edit terms in %vocabulary', array('%vocabulary' => $vocabulary->name)),
      ),
    );
    $permissions += array(
       'delete terms in ' . $vocabulary->vid => array(
96
         'title' => t('Delete terms from %vocabulary', array('%vocabulary' => $vocabulary->name)),
97
98
99
100
      ),
    );
  }
  return $permissions;
Kjartan's avatar
Kjartan committed
101
}
Dries's avatar
   
Dries committed
102

103
/**
104
 * Implements hook_entity_info().
105
 */
106
function taxonomy_entity_info() {
107
108
  $return = array(
    'taxonomy_term' => array(
109
      'label' => t('Taxonomy term'),
110
111
      'controller class' => 'TaxonomyTermController',
      'base table' => 'taxonomy_term_data',
112
      'uri callback' => 'taxonomy_term_uri',
113
      'fieldable' => TRUE,
114
      'entity keys' => array(
115
116
        'id' => 'tid',
        'bundle' => 'vocabulary_machine_name',
117
        'label' => 'name',
118
119
120
121
122
      ),
      'bundle keys' => array(
        'bundle' => 'machine_name',
      ),
      'bundles' => array(),
123
124
125
126
      'view modes' => array(
        // @todo View mode for display as a field (when attached to nodes etc).
        'full' => array(
          'label' => t('Taxonomy term page'),
127
          'custom settings' => FALSE,
128
129
        ),
      ),
130
131
    ),
  );
132
133
  foreach (taxonomy_vocabulary_get_names() as $machine_name => $vocabulary) {
    $return['taxonomy_term']['bundles'][$machine_name] = array(
134
135
      'label' => $vocabulary->name,
      'admin' => array(
136
137
        'path' => 'admin/structure/taxonomy/%taxonomy_vocabulary_machine_name',
        'real path' => 'admin/structure/taxonomy/' . $machine_name,
138
139
140
141
142
        'bundle argument' => 3,
        'access arguments' => array('administer taxonomy'),
      ),
    );
  }
143
144
145
146
  $return['taxonomy_vocabulary'] = array(
    'label' => t('Taxonomy vocabulary'),
    'controller class' => 'TaxonomyVocabularyController',
    'base table' => 'taxonomy_vocabulary',
147
    'entity keys' => array(
148
      'id' => 'vid',
149
      'label' => 'name',
150
151
152
153
    ),
    'fieldable' => FALSE,
  );

154
155
156
  return $return;
}

157
/**
158
 * Entity uri callback.
159
 */
160
161
162
163
function taxonomy_term_uri($term) {
  return array(
    'path' => 'taxonomy/term/' . $term->tid,
  );
164
165
}

166
167
168
169
170
/**
 * Implements hook_field_extra_fields().
 */
function taxonomy_field_extra_fields() {
  $return = array();
171
172
173
  $info = entity_get_info('taxonomy_term');
  foreach (array_keys($info['bundles']) as $bundle) {
    $return['taxonomy_term'][$bundle] = array(
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
      'form' => array(
        'name' => array(
          'label' => t('Name'),
          'description' => t('Term name textfield'),
          'weight' => -5,
        ),
        'description' => array(
          'label' => t('Description'),
          'description' => t('Term description textarea'),
          'weight' => 0,
        ),
      ),
      'display' => array(
        'description' => array(
          'label' => t('Description'),
          'description' => t('Term description'),
          'weight' => 0,
        ),
192
193
194
195
196
197
198
      ),
    );
  }

  return $return;
}

199
200
201
202
203
204
205
/**
 * Return nodes attached to a term across all field instances.
 *
 * This function requires taxonomy module to be maintaining its own tables,
 * and will return an empty array if it is not. If using other field storage
 * methods alternatives methods for listing terms will need to be used.
 *
206
207
 * @param $tid
 *   The term ID.
208
209
 * @param $pager
 *   Boolean to indicate whether a pager should be used.
210
211
212
 * @param $limit
 *   Integer. The maximum number of nodes to find.
 *   Set to FALSE for no limit.
213
214
215
216
217
218
 * @order
 *   An array of fields and directions.
 *
 * @return
 *   An array of nids matching the query.
 */
219
function taxonomy_select_nodes($tid, $pager = TRUE, $limit = FALSE, $order = array('t.sticky' => 'DESC', 't.created' => 'DESC')) {
220
221
222
223
224
  if (!variable_get('taxonomy_maintain_index_table', TRUE)) {
    return array();
  }
  $query = db_select('taxonomy_index', 't');
  $query->addTag('node_access');
225
  $query->condition('tid', $tid);
226
227
228
229
  if ($pager) {
    $count_query = clone $query;
    $count_query->addExpression('COUNT(t.nid)');

230
231
232
233
    $query = $query->extend('PagerDefault');
    if ($limit !== FALSE) {
      $query = $query->limit($limit);
    }
234
235
236
    $query->setCountQuery($count_query);
  }
  else {
237
238
239
    if ($limit !== FALSE) {
      $query->range(0, $limit);
    }
240
241
242
243
244
245
246
247
248
249
250
251
252
  }
  $query->addField('t', 'nid');
  $query->addField('t', 'tid');
  foreach ($order as $field => $direction) {
    $query->orderBy($field, $direction);
    // ORDER BY fields need to be loaded too, assume they are in the form
    // table_alias.name
    list($table_alias, $name) = explode('.', $field);
    $query->addField($table_alias, $name);
  }
  return $query->execute()->fetchCol();
}

253
/**
254
 * Implements hook_theme().
255
256
257
 */
function taxonomy_theme() {
  return array(
258
    'taxonomy_overview_vocabularies' => array(
259
      'render element' => 'form',
260
261
    ),
    'taxonomy_overview_terms' => array(
262
      'render element' => 'form',
263
    ),
264
265
266
267
    'taxonomy_term' => array(
      'render element' => 'elements',
      'template' => 'taxonomy-term',
    ),
268
269
270
  );
}

Dries's avatar
   
Dries committed
271
/**
272
 * Implements hook_menu().
Dries's avatar
   
Dries committed
273
 */
274
function taxonomy_menu() {
275
  $items['admin/structure/taxonomy'] = array(
276
277
    'title' => 'Taxonomy',
    'description' => 'Manage tagging, categorization, and classification of your content.',
278
279
    'page callback' => 'drupal_get_form',
    'page arguments' => array('taxonomy_overview_vocabularies'),
280
    'access arguments' => array('administer taxonomy'),
281
    'file' => 'taxonomy.admin.inc',
282
  );
283
  $items['admin/structure/taxonomy/list'] = array(
284
    'title' => 'List',
285
286
287
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
288
  $items['admin/structure/taxonomy/add'] = array(
289
    'title' => 'Add vocabulary',
290
291
    'page callback' => 'drupal_get_form',
    'page arguments' => array('taxonomy_form_vocabulary'),
292
    'access arguments' => array('administer taxonomy'),
293
    'type' => MENU_LOCAL_ACTION,
294
    'file' => 'taxonomy.admin.inc',
295
296
  );

297
  $items['taxonomy/term/%taxonomy_term'] = array(
298
    'title' => 'Taxonomy term',
299
300
    'title callback' => 'taxonomy_term_title',
    'title arguments' => array(2),
301
302
303
    'page callback' => 'taxonomy_term_page',
    'page arguments' => array(2),
    'access arguments' => array('access content'),
304
    'file' => 'taxonomy.pages.inc',
305
  );
306
  $items['taxonomy/term/%taxonomy_term/view'] = array(
307
308
    'title' => 'View',
    'type' => MENU_DEFAULT_LOCAL_TASK,
309
  );
310
  $items['taxonomy/term/%taxonomy_term/edit'] = array(
311
    'title' => 'Edit',
312
    'page callback' => 'drupal_get_form',
313
314
315
    // Pass a NULL argument to ensure that additional path components are not
    // passed to taxonomy_form_term() as the vocabulary machine name argument.
    'page arguments' => array('taxonomy_form_term', 2, NULL),
316
317
    'access callback' => 'taxonomy_term_edit_access',
    'access arguments' => array(2),
318
319
    'type' => MENU_LOCAL_TASK,
    'weight' => 10,
320
    'file' => 'taxonomy.admin.inc',
321
  );
322
323
324
325
326
327
328
329
  $items['taxonomy/term/%taxonomy_term/feed'] = array(
    'title' => 'Taxonomy term',
    'title callback' => 'taxonomy_term_title',
    'title arguments' => array(2),
    'page callback' => 'taxonomy_term_feed',
    'page arguments' => array(2),
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
330
    'file' => 'taxonomy.pages.inc',
331
  );
332
  $items['taxonomy/autocomplete'] = array(
333
    'title' => 'Autocomplete taxonomy',
334
335
336
    'page callback' => 'taxonomy_autocomplete',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
337
    'file' => 'taxonomy.pages.inc',
338
  );
339

340
  $items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name'] = array(
341
342
    'title callback' => 'taxonomy_admin_vocabulary_title_callback',
    'title arguments' => array(3),
343
344
    'page callback' => 'drupal_get_form',
    'page arguments' => array('taxonomy_overview_terms', 3),
345
    'access arguments' => array('administer taxonomy'),
346
    'file' => 'taxonomy.admin.inc',
347
  );
348
  $items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name/list'] = array(
349
    'title' => 'List',
350
    'type' => MENU_DEFAULT_LOCAL_TASK,
351
352
    'weight' => -20,
  );
353
  $items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name/edit'] = array(
354
    'title' => 'Edit',
355
    'page callback' => 'drupal_get_form',
356
    'page arguments' => array('taxonomy_form_vocabulary', 3),
357
358
    'access arguments' => array('administer taxonomy'),
    'type' => MENU_LOCAL_TASK,
359
    'weight' => -10,
360
    'file' => 'taxonomy.admin.inc',
361
  );
362

363
  $items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name/add'] = array(
364
    'title' => 'Add term',
365
    'page callback' => 'drupal_get_form',
366
    'page arguments' => array('taxonomy_form_term', array(), 3),
367
    'access arguments' => array('administer taxonomy'),
368
    'type' => MENU_LOCAL_ACTION,
369
    'file' => 'taxonomy.admin.inc',
370
  );
Dries's avatar
   
Dries committed
371

Dries's avatar
   
Dries committed
372
373
  return $items;
}
Dries's avatar
   
Dries committed
374

375
376
377
378
379
380
381
382
383
384
/**
 * Implements hook_admin_paths().
 */
function taxonomy_admin_paths() {
  $paths = array(
    'taxonomy/term/*/edit' => TRUE,
  );
  return $paths;
}

385
386
387
388
389
390
391
/**
 * Return edit access for a given term.
 */
function taxonomy_term_edit_access($term) {
  return user_access("edit terms in $term->vid") || user_access('administer taxonomy');
}

392
393
394
395
396
397
398
399
/**
 * Return the vocabulary name given the vocabulary object.
 */
function taxonomy_admin_vocabulary_title_callback($vocabulary) {
  return check_plain($vocabulary->name);
}

/**
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
 * Saves a vocabulary.
 *
 * @param $vocabulary
 *   A vocabulary object with the following properties:
 *   - vid: The ID of the vocabulary.
 *   - name: The human-readable name of the vocabulary.
 *   - machine_name: The machine name of the vocabulary.
 *   - description: (optional) The vocabulary's description.
 *   - hierarchy: The hierarchy level of the vocabulary.
 *   - module: (optional) The module altering the vocabulary.
 *   - weight: (optional) The weight of this vocabulary in relation to other
 *     vocabularies.
 *   - original: (optional) The original vocabulary object before any changes
 *     are applied.
 *   - old_machine_name: (optional) The original machine name of the
 *     vocabulary.
 *
 * @return
 *   Status constant indicating whether the vocabulary was inserted (SAVED_NEW)
 *   or updated(SAVED_UPDATED).
420
 */
421
function taxonomy_vocabulary_save($vocabulary) {
422
  // Prevent leading and trailing spaces in vocabulary names.
423
424
425
  if (!empty($vocabulary->name)) {
    $vocabulary->name = trim($vocabulary->name);
  }
426
427
428
429
430
431
432
433
434
  // Load the stored entity, if any.
  if (!empty($vocabulary->vid)) {
    if (!isset($vocabulary->original)) {
      $vocabulary->original = entity_load_unchanged('taxonomy_vocabulary', $vocabulary->vid);
    }
    // Make sure machine name changes are easily detected.
    // @todo: Remove in Drupal 8, as it is deprecated by directly reading from
    // $vocabulary->original.
    $vocabulary->old_machine_name = $vocabulary->original->machine_name;
435
  }
Dries's avatar
   
Dries committed
436

437
  module_invoke_all('taxonomy_vocabulary_presave', $vocabulary);
438
  module_invoke_all('entity_presave', $vocabulary, 'taxonomy_vocabulary');
439

440
  if (!empty($vocabulary->vid) && !empty($vocabulary->name)) {
441
    $status = drupal_write_record('taxonomy_vocabulary', $vocabulary, 'vid');
442
    taxonomy_vocabulary_static_reset(array($vocabulary->vid));
443
444
445
    if ($vocabulary->old_machine_name != $vocabulary->machine_name) {
      field_attach_rename_bundle('taxonomy_term', $vocabulary->old_machine_name, $vocabulary->machine_name);
    }
446
    module_invoke_all('taxonomy_vocabulary_update', $vocabulary);
447
    module_invoke_all('entity_update', $vocabulary, 'taxonomy_vocabulary');
Kjartan's avatar
Kjartan committed
448
  }
449
  elseif (empty($vocabulary->vid)) {
450
    $status = drupal_write_record('taxonomy_vocabulary', $vocabulary);
451
    taxonomy_vocabulary_static_reset();
452
    field_attach_create_bundle('taxonomy_term', $vocabulary->machine_name);
453
    module_invoke_all('taxonomy_vocabulary_insert', $vocabulary);
454
    module_invoke_all('entity_insert', $vocabulary, 'taxonomy_vocabulary');
Kjartan's avatar
Kjartan committed
455
  }
Dries's avatar
   
Dries committed
456

457
  unset($vocabulary->original);
Dries's avatar
   
Dries committed
458
  cache_clear_all();
Dries's avatar
   
Dries committed
459

460
  return $status;
Kjartan's avatar
Kjartan committed
461
}
Dries's avatar
   
Dries committed
462

463
464
465
466
467
468
469
/**
 * Delete a vocabulary.
 *
 * @param $vid
 *   A vocabulary ID.
 * @return
 *   Constant indicating items were deleted.
470
471
472
 *
 * @see hook_taxonomy_vocabulary_predelete()
 * @see hook_taxonomy_vocabulary_delete()
473
 */
474
function taxonomy_vocabulary_delete($vid) {
475
  $vocabulary = taxonomy_vocabulary_load($vid);
Dries's avatar
   
Dries committed
476

477
478
  $transaction = db_transaction();
  try {
479
480
481
482
    // Allow modules to act before vocabulary deletion.
    module_invoke_all('taxonomy_vocabulary_predelete', $vocabulary);
    module_invoke_all('entity_predelete', $vocabulary, 'taxonomy_vocabulary');

483
484
485
486
487
488
489
490
    // Only load terms without a parent, child terms will get deleted too.
    $result = db_query('SELECT t.tid FROM {taxonomy_term_data} t INNER JOIN {taxonomy_term_hierarchy} th ON th.tid = t.tid WHERE t.vid = :vid AND th.parent = 0', array(':vid' => $vid))->fetchCol();
    foreach ($result as $tid) {
      taxonomy_term_delete($tid);
    }
    db_delete('taxonomy_vocabulary')
      ->condition('vid', $vid)
      ->execute();
491

492
    field_attach_delete_bundle('taxonomy_term', $vocabulary->machine_name);
493
494

    // Allow modules to respond to vocabulary deletion.
495
496
    module_invoke_all('taxonomy_vocabulary_delete', $vocabulary);
    module_invoke_all('entity_delete', $vocabulary, 'taxonomy_vocabulary');
497

498
    cache_clear_all();
499
    taxonomy_vocabulary_static_reset();
500

501
502
503
504
505
506
    return SAVED_DELETED;
  }
  catch (Exception $e) {
    $transaction->rollback();
    watchdog_exception('taxonomy', $e);
    throw $e;
Dries's avatar
   
Dries committed
507
  }
Dries's avatar
   
Dries committed
508
509
}

510
/**
511
 * Implements hook_taxonomy_vocabulary_update().
512
 */
513
514
515
516
function taxonomy_taxonomy_vocabulary_update($vocabulary) {
  // Reflect machine name changes in the definitions of existing 'taxonomy'
  // fields.
  if (!empty($vocabulary->old_machine_name) && $vocabulary->old_machine_name != $vocabulary->machine_name) {
517
518
519
520
521
    $fields = field_read_fields();
    foreach ($fields as $field_name => $field) {
      $update = FALSE;
      if ($field['type'] == 'taxonomy_term_reference') {
        foreach ($field['settings']['allowed_values'] as $key => &$value) {
522
523
          if ($value['vocabulary'] == $vocabulary->old_machine_name) {
            $value['vocabulary'] = $vocabulary->machine_name;
524
525
526
527
528
529
530
531
532
533
534
            $update = TRUE;
          }
        }
        if ($update) {
          field_update_field($field);
        }
      }
    }
  }
}

535
/**
536
 * Checks and updates the hierarchy flag of a vocabulary.
537
 *
538
 * Checks the current parents of all terms in a vocabulary and updates the
539
 * vocabulary's hierarchy setting to the lowest possible level. If no term
540
541
542
543
544
 * has parent terms then the vocabulary will be given a hierarchy of
 * TAXONOMY_HIERARCHY_DISABLED. If any term has a single parent then the
 * vocabulary will be given a hierarchy of TAXONOMY_HIERARCHY_SINGLE. If any
 * term has multiple parents then the vocabulary will be given a hierarchy of
 * TAXONOMY_HIERARCHY_MULTIPLE.
545
 *
546
 * @param $vocabulary
547
 *   A vocabulary object.
548
549
 * @param $changed_term
 *   An array of the term structure that was updated.
550
551
552
 *
 * @return
 *   An integer that represents the level of the vocabulary's hierarchy.
553
554
 */
function taxonomy_check_vocabulary_hierarchy($vocabulary, $changed_term) {
555
  $tree = taxonomy_get_tree($vocabulary->vid);
556
  $hierarchy = TAXONOMY_HIERARCHY_DISABLED;
557
  foreach ($tree as $term) {
558
    // Update the changed term with the new parent value before comparison.
559
    if ($term->tid == $changed_term['tid']) {
560
      $term = (object) $changed_term;
561
562
563
564
      $term->parents = $term->parent;
    }
    // Check this term's parent count.
    if (count($term->parents) > 1) {
565
      $hierarchy = TAXONOMY_HIERARCHY_MULTIPLE;
566
567
568
      break;
    }
    elseif (count($term->parents) == 1 && 0 !== array_shift($term->parents)) {
569
      $hierarchy = TAXONOMY_HIERARCHY_SINGLE;
570
571
    }
  }
572
573
574
  if ($hierarchy != $vocabulary->hierarchy) {
    $vocabulary->hierarchy = $hierarchy;
    taxonomy_vocabulary_save($vocabulary);
575
576
577
578
579
  }

  return $hierarchy;
}

580
/**
581
 * Saves a term object to the database.
582
 *
583
 * @param $term
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
 *   The taxonomy term object with the following properties:
 *   - vid: The ID of the vocabulary the term is assigned to.
 *   - name: The name of the term.
 *   - tid: (optional) The unique ID for the term being saved. If $term->tid is
 *     empty or omitted, a new term will be inserted.
 *   - description: (optional) The term's description.
 *   - format: (optional) The text format for the term's description.
 *   - weight: (optional) The weight of this term in relation to other terms
 *     within the same vocabulary.
 *   - parent: (optional) The parent term(s) for this term. This can be a single
 *     term ID or an array of term IDs. A value of 0 means this term does not
 *     have any parents. When omitting this variable during an update, the
 *     existing hierarchy for the term remains unchanged.
 *   - vocabulary_machine_name: (optional) The machine name of the vocabulary
 *     the term is assigned to. If not given, this value will be set
 *     automatically by loading the vocabulary based on $term->vid.
 *   - original: (optional) The original taxonomy term object before any changes
 *     were applied. When omitted, the unchanged taxonomy term object is
 *     loaded from the database and stored in this property.
 *   Since a taxonomy term is an entity, any fields contained in the term object
 *   are saved alongside the term object.
 *
606
 * @return
607
608
609
 *   Status constant indicating whether term was inserted (SAVED_NEW) or updated
 *   (SAVED_UPDATED). When inserting a new term, $term->tid will contain the
 *   term ID of the newly created term.
610
 */
611
function taxonomy_term_save($term) {
612
613
  // Prevent leading and trailing spaces in term names.
  $term->name = trim($term->name);
614
615
616
617
618
  if (!isset($term->vocabulary_machine_name)) {
    $vocabulary = taxonomy_vocabulary_load($term->vid);
    $term->vocabulary_machine_name = $vocabulary->machine_name;
  }

619
620
621
622
623
  // Load the stored entity, if any.
  if (!empty($term->tid) && !isset($term->original)) {
    $term->original = entity_load_unchanged('taxonomy_term', $term->tid);
  }

624
  field_attach_presave('taxonomy_term', $term);
625
  module_invoke_all('taxonomy_term_presave', $term);
626
  module_invoke_all('entity_presave', $term, 'taxonomy_term');
627

628
  if (empty($term->tid)) {
629
    $op = 'insert';
630
    $status = drupal_write_record('taxonomy_term_data', $term);
631
    field_attach_insert('taxonomy_term', $term);
632
633
634
    if (!isset($term->parent)) {
      $term->parent = array(0);
    }
Kjartan's avatar
Kjartan committed
635
  }
636
  else {
637
    $op = 'update';
638
639
640
641
642
643
644
    $status = drupal_write_record('taxonomy_term_data', $term, 'tid');
    field_attach_update('taxonomy_term', $term);
    if (isset($term->parent)) {
      db_delete('taxonomy_term_hierarchy')
        ->condition('tid', $term->tid)
        ->execute();
    }
645
  }
646

647
648
649
650
651
652
  if (isset($term->parent)) {
    if (!is_array($term->parent)) {
      $term->parent = array($term->parent);
    }
    $query = db_insert('taxonomy_term_hierarchy')
      ->fields(array('tid', 'parent'));
653
654
655
    foreach ($term->parent as $parent) {
      if (is_array($parent)) {
        foreach ($parent as $tid) {
656
657
          $query->values(array(
            'tid' => $term->tid,
658
            'parent' => $tid
659
          ));
660
661
        }
      }
662
663
664
665
666
667
      else {
        $query->values(array(
          'tid' => $term->tid,
          'parent' => $parent
        ));
      }
Dries's avatar
   
Dries committed
668
    }
669
    $query->execute();
Kjartan's avatar
Kjartan committed
670
  }
671
672

  // Reset the taxonomy term static variables.
673
  taxonomy_terms_static_reset();
Dries's avatar
   
Dries committed
674

675
676
677
  // Invoke the taxonomy hooks.
  module_invoke_all("taxonomy_term_$op", $term);
  module_invoke_all("entity_$op", $term, 'taxonomy_term');
678
  unset($term->original);
679

680
  return $status;
Kjartan's avatar
Kjartan committed
681
}
Dries's avatar
   
Dries committed
682

683
684
685
686
687
688
689
/**
 * Delete a term.
 *
 * @param $tid
 *   The term ID.
 * @return
 *   Status constant indicating deletion.
690
691
692
 *
 * @see hook_taxonomy_term_predelete()
 * @see hook_taxonomy_term_delete()
693
 */
694
function taxonomy_term_delete($tid) {
695
696
697
698
699
700
  $transaction = db_transaction();
  try {
    $tids = array($tid);
    while ($tids) {
      $children_tids = $orphans = array();
      foreach ($tids as $tid) {
701
702
703
704
705
706
        // Allow modules to act before term deletion.
        if ($term = taxonomy_term_load($tid)) {
          module_invoke_all('taxonomy_term_predelete', $term);
          module_invoke_all('entity_predelete', $term, 'taxonomy_term');
        }

707
        // See if any of the term's children are about to be become orphans:
708
        if ($children = taxonomy_term_load_children($tid)) {
709
710
          foreach ($children as $child) {
            // If the term has multiple parents, we don't delete it.
711
            $parents = taxonomy_term_load_parents($child->tid);
712
713
714
            if (count($parents) == 1) {
              $orphans[] = $child->tid;
            }
715
716
          }
        }
Dries's avatar
   
Dries committed
717

718
        if ($term) {
719
720
721
722
723
724
725
726
          db_delete('taxonomy_term_data')
            ->condition('tid', $tid)
            ->execute();
          db_delete('taxonomy_term_hierarchy')
            ->condition('tid', $tid)
            ->execute();

          field_attach_delete('taxonomy_term', $term);
727
728

          // Allow modules to respond to term deletion.
729
730
731
732
          module_invoke_all('taxonomy_term_delete', $term);
          module_invoke_all('entity_delete', $term, 'taxonomy_term');
          taxonomy_terms_static_reset();
        }
733
      }
Dries's avatar
   
Dries committed
734

735
736
737
738
739
740
741
742
      $tids = $orphans;
    }
    return SAVED_DELETED;
  }
  catch (Exception $e) {
    $transaction->rollback();
    watchdog_exception('taxonomy', $e);
    throw $e;
743
  }
Dries's avatar
   
Dries committed
744
745
}

746
747
748
749
750
751
752
/**
 * Generate an array for rendering the given term.
 *
 * @param $term
 *   A term object.
 * @param $view_mode
 *   View mode, e.g. 'full', 'teaser'...
753
754
755
 * @param $langcode
 *   (optional) A language code to use for rendering. Defaults to the global
 *   content language of the current request.
756
757
758
759
 *
 * @return
 *   An array as expected by drupal_render().
 */
760
761
function taxonomy_term_view($term, $view_mode = 'full', $langcode = NULL) {
  if (!isset($langcode)) {
762
    $langcode = $GLOBALS['language_content']->langcode;
763
764
  }

765
766
  field_attach_prepare_view('taxonomy_term', array($term->tid => $term), $view_mode, $langcode);
  entity_prepare_view('taxonomy_term', array($term->tid => $term), $langcode);
767
768
769
770
771

  $build = array(
    '#theme' => 'taxonomy_term',
    '#term' => $term,
    '#view_mode' => $view_mode,
772
    '#language' => $langcode,
773
774
  );

775
  $build += field_attach_view('taxonomy_term', $term, $view_mode, $langcode);
776

777
778
779
780
781
782
783
784
785
  // Add term description if the term has one.
  if (!empty($term->description)) {
    $build['description'] = array(
      '#markup' => check_markup($term->description, $term->format, '', TRUE),
      '#weight' => 0,
      '#prefix' => '<div class="taxonomy-term-description">',
      '#suffix' => '</div>',
    );
  }
786
787
788

  $build['#attached']['css'][] = drupal_get_path('module', 'taxonomy') . '/taxonomy.css';

789
790
791
792
  // Allow modules to modify the structured term.
  $type = 'taxonomy_term';
  drupal_alter(array('taxonomy_term_view', 'entity_view'), $build, $type);

793
794
795
796
797
798
799
800
801
802
803
  return $build;
}

/**
 * Process variables for taxonomy-term.tpl.php.
 */
function template_preprocess_taxonomy_term(&$variables) {
  $variables['view_mode'] = $variables['elements']['#view_mode'];
  $variables['term'] = $variables['elements']['#term'];
  $term = $variables['term'];

804
805
  $uri = entity_uri('taxonomy_term', $term);
  $variables['term_url']  = url($uri['path'], $uri['options']);
806
  $variables['term_name'] = check_plain($term->name);
807
  $variables['page']      = $variables['view_mode'] == 'full' && taxonomy_term_is_page($term);
808
809

  // Flatten the term object's member fields.
810
  $variables = array_merge((array) $term, $variables);
811
812

  // Helpful $content variable for templates.
813
  $variables['content'] = array();
814
815
816
817
818
819
820
821
822
823
  foreach (element_children($variables['elements']) as $key) {
    $variables['content'][$key] = $variables['elements'][$key];
  }

  // field_attach_preprocess() overwrites the $[field_name] variables with the
  // values of the field in the language that was selected for display, instead
  // of the raw values in $term->[field_name], which contain all values in all
  // languages.
  field_attach_preprocess('taxonomy_term', $term, $variables['content'], $variables);

824
  // Gather classes, and clean up name so there are no underscores.
825
826
827
  $vocabulary_name_css = str_replace('_', '-', $term->vocabulary_machine_name);
  $variables['classes_array'][] = 'vocabulary-' . $vocabulary_name_css;

828
829
  $variables['theme_hook_suggestions'][] = 'taxonomy_term__' . $term->vocabulary_machine_name;
  $variables['theme_hook_suggestions'][] = 'taxonomy_term__' . $term->tid;
830
831
832
}

/**
833
 * Returns whether the current page is the page of the passed-in term.
834
835
836
837
838
839
840
841
842
 *
 * @param $term
 *   A term object.
 */
function taxonomy_term_is_page($term) {
  $page_term = menu_get_object('taxonomy_term', 2);
  return (!empty($page_term) ? $page_term->tid == $term->tid : FALSE);
}

843
/**
844
 * Clear all static cache variables for terms.
845
846
847
848
 */
function taxonomy_terms_static_reset() {
  drupal_static_reset('taxonomy_term_count_nodes');
  drupal_static_reset('taxonomy_get_tree');
849
850
  drupal_static_reset('taxonomy_get_tree:parents');
  drupal_static_reset('taxonomy_get_tree:terms');
851
852
853
  drupal_static_reset('taxonomy_term_load_parents');
  drupal_static_reset('taxonomy_term_load_parents_all');
  drupal_static_reset('taxonomy_term_load_children');
854
  entity_get_controller('taxonomy_term')->resetCache();
855
856
}

857
858
/**
 * Clear all static cache variables for vocabularies.
859
 *
860
861
862
863
864
865
866
867
 * @param $ids
 * An array of ids to reset in entity controller cache.
 */
function taxonomy_vocabulary_static_reset($ids = NULL) {
  drupal_static_reset('taxonomy_vocabulary_get_names');
  entity_get_controller('taxonomy_vocabulary')->resetCache($ids);
}

868
869
870
871
/**
 * Get names for all taxonomy vocabularies.
 *
 * @return
872
 *   An array of vocabulary ids, names, machine names, keyed by machine name.
873
874
 */
function taxonomy_vocabulary_get_names() {
875
876
877
878
879
  $names = &drupal_static(__FUNCTION__);

  if (!isset($names)) {
    $names = db_query('SELECT name, machine_name, vid FROM {taxonomy_vocabulary}')->fetchAllAssoc('machine_name');
  }
880

881
882
883
  return $names;
}

Dries's avatar
   
Dries committed
884
/**
885
886
887
888
889
890
 * Finds all parents of a given term ID.
 *
 * @param $tid
 *   A taxonomy term ID.
 *
 * @return
891
892
 *   An array of term objects which are the parents of the term $tid, or an
 *   empty array if parents are not found.
Dries's avatar
   
Dries committed
893
 */
894
function taxonomy_term_load_parents($tid) {
895
896
897
898
899
900
901
902
903
904
905
906
  $parents = &drupal_static(__FUNCTION__, array());

  if ($tid && !isset($parents[$tid])) {
    $query = db_select('taxonomy_term_data', 't');
    $query->join('taxonomy_term_hierarchy', 'h', 'h.parent = t.tid');
    $query->addField('t', 'tid');
    $query->condition('h.tid', $tid);
    $query->addTag('term_access');
    $query->orderBy('t.weight');
    $query->orderBy('t.name');
    $tids = $query->execute()->fetchCol();
    $parents[$tid] = taxonomy_term_load_multiple($tids);
Kjartan's avatar
Kjartan committed
907
  }
908
909

  return isset($parents[$tid]) ? $parents[$tid] : array();
Kjartan's avatar
Kjartan committed
910
}
Dries's avatar
   
Dries committed
911

Dries's avatar
   
Dries committed
912
913
914
/**
 * Find all ancestors of a given term ID.
 */
915
function taxonomy_term_load_parents_all($tid) {
916
917
918
919
920
921
  $cache = &drupal_static(__FUNCTION__, array());

  if (isset($cache[$tid])) {
    return $cache[$tid];
  }

Dries's avatar
   
Dries committed
922
  $parents = array();
923
924
  if ($term = taxonomy_term_load($tid)) {
    $parents[] = $term;
Dries's avatar
   
Dries committed
925
    $n = 0;
926
    while ($parent = taxonomy_term_load_parents($parents[$n]->tid)) {
Dries's avatar
   
Dries committed
927
928
929
930
      $parents = array_merge($parents, $parent);
      $n++;
    }
  }
931
932
933

  $cache[$tid] = $parents;

Dries's avatar
   
Dries committed
934
935
936
  return $parents;
}

Dries's avatar
   
Dries committed
937
/**
938
939
940
941
942
943
944
945
 * Finds all children of a term ID.
 *
 * @param $tid
 *   A taxonomy term ID.
 * @param $vid
 *   An optional vocabulary ID to restrict the child search.
 *
 * @return
946
947
 *   An array of term objects that are the children of the term $tid, or an
 *   empty array when no children exist.
Dries's avatar
   
Dries committed
948
 */
949
function taxonomy_term_load_children($tid, $vid = 0) {
950
951
952
  $children = &drupal_static(__FUNCTION__, array());

  if ($tid && !isset($children[$tid])) {
953
954
    $query = db_select('taxonomy_term_data', 't');
    $query->join('taxonomy_term_hierarchy', 'h', 'h.tid = t.tid');
955
956
    $query->addField('t', 'tid');
    $query->condition('h.parent', $tid);
957
958
959
    if ($vid) {
      $query->condition('t.vid', $vid);
    }
960
961
962
963
964
    $query->addTag('term_access');
    $query->orderBy('t.weight');
    $query->orderBy('t.name');
    $tids = $query->execute()->fetchCol();
    $children[$tid] = taxonomy_term_load_multiple($tids);
Kjartan's avatar
Kjartan committed
965
  }
966
967

  return isset($children[$tid]) ? $children[$tid] : array();
Kjartan's avatar
Kjartan committed
968
}
Dries's avatar
   
Dries committed
969

Dries's avatar
   
Dries committed
970
971
972
973
974
975
976
977
/**
 * 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.
Dries's avatar
   
Dries committed
978
979
 * @param $max_depth
 *   The number of levels of the tree to return. Leave NULL to return all levels.
980
981
982
983
984
 * @param $load_entities
 *   If TRUE, a full entity load will occur on the term objects. Otherwise they
 *   are partial objects queried directly from the {taxonomy_term_data} table to
 *   save execution time and memory consumption when listing large numbers of
 *   terms. Defaults to FALSE.
985
 *
Dries's avatar
   
Dries committed
986
987
988
 * @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.
989
990
 *   Results are statically cached. Term objects will be partial or complete
 *   depending on the $load_entities parameter.
Dries's avatar
   
Dries committed
991
 */
992
function taxonomy_get_tree($vid, $parent = 0, $max_depth = NULL, $load_entities = FALSE) {
993
  $children = &drupal_static(__FUNCTION__, array());
994
995
  $parents = &drupal_static(__FUNCTION__ . ':parents', array());
  $terms = &drupal_static(__FUNCTION__ . ':terms', array());
996

Dries's avatar
   
Dries committed
997
998
999
1000
  // 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();
1001
1002
    $parents[$vid] = array();
    $terms[$vid] = array();
Dries's avatar
   
Dries committed
1003

1004
1005
    $query = db_select('taxonomy_term_data', 't');
    $query->join('taxonomy_term_hierarchy', 'h', 'h.tid = t.tid');