taxonomy.module 64.2 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
 * @param $order
214
215
216
217
218
 *   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_access',
    'access arguments' => array('edit', 2),
318
319
    'type' => MENU_LOCAL_TASK,
    'weight' => 10,
320
    'file' => 'taxonomy.admin.inc',
321
  );
322
323
324
325
326
327
328
329
330
331
  $items['taxonomy/term/%taxonomy_term/delete'] = array(
    'title' => 'Delete',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('taxonomy_term_confirm_delete', 2),
    'access callback' => 'taxonomy_term_access',
    'access arguments' => array('delete', 2),
    'type' => MENU_LOCAL_TASK,
    'weight' => 11,
    'file' => 'taxonomy.admin.inc',
  );
332
333
334
335
336
337
338
339
  $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,
340
    'file' => 'taxonomy.pages.inc',
341
  );
342
  $items['taxonomy/autocomplete'] = array(
343
    'title' => 'Autocomplete taxonomy',
344
345
346
    'page callback' => 'taxonomy_autocomplete',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
347
    'file' => 'taxonomy.pages.inc',
348
  );
349

350
  $items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name'] = array(
351
352
    'title callback' => 'taxonomy_admin_vocabulary_title_callback',
    'title arguments' => array(3),
353
354
    'page callback' => 'drupal_get_form',
    'page arguments' => array('taxonomy_overview_terms', 3),
355
    'access arguments' => array('administer taxonomy'),
356
    'file' => 'taxonomy.admin.inc',
357
  );
358
  $items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name/list'] = array(
359
    'title' => 'List',
360
    'type' => MENU_DEFAULT_LOCAL_TASK,
361
362
    'weight' => -20,
  );
363
  $items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name/edit'] = array(
364
    'title' => 'Edit',
365
    'page callback' => 'drupal_get_form',
366
    'page arguments' => array('taxonomy_form_vocabulary', 3),
367
368
    'access arguments' => array('administer taxonomy'),
    'type' => MENU_LOCAL_TASK,
369
    'weight' => -10,
370
    'file' => 'taxonomy.admin.inc',
371
  );
372

373
  $items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name/add'] = array(
374
    'title' => 'Add term',
375
    'page callback' => 'drupal_get_form',
376
    'page arguments' => array('taxonomy_form_term', array(), 3),
377
    'access arguments' => array('administer taxonomy'),
378
    'type' => MENU_LOCAL_ACTION,
379
    'file' => 'taxonomy.admin.inc',
380
  );
Dries's avatar
   
Dries committed
381

Dries's avatar
   
Dries committed
382
383
  return $items;
}
Dries's avatar
   
Dries committed
384

385
386
387
388
389
390
/**
 * Implements hook_admin_paths().
 */
function taxonomy_admin_paths() {
  $paths = array(
    'taxonomy/term/*/edit' => TRUE,
391
    'taxonomy/term/*/delete' => TRUE,
392
393
394
395
  );
  return $paths;
}

396
/**
397
398
399
400
401
402
403
404
405
406
407
408
409
410
 * Access callback: Checks a user's permission for performing a taxonomy term
 * operation.
 *
 * @param $op
 *   The operation to be performed on the taxonomy term. Possible values are:
 *   - "edit"
 *   - "delete"
 * @param $term
 *   The $term object on which the operation is to be performed.
 *
 * @return
 *   TRUE if the operation may be performed, FALSE otherwise.
 *
 * @see taxonomy_menu()
411
 */
412
413
414
415
416
417
418
419
function taxonomy_term_access($op, $term) {
  if (!$term || !in_array($op, array('edit', 'delete'), TRUE)) {
    // If there was no term to check against, or the $op was not one of the
    // supported ones, we return access denied.
    return FALSE;
  }

  return user_access("$op terms in $term->vid") || user_access('administer taxonomy');
420
421
}

422
423
424
425
426
427
428
429
/**
 * Return the vocabulary name given the vocabulary object.
 */
function taxonomy_admin_vocabulary_title_callback($vocabulary) {
  return check_plain($vocabulary->name);
}

/**
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
 * 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).
450
 */
451
function taxonomy_vocabulary_save($vocabulary) {
452
  // Prevent leading and trailing spaces in vocabulary names.
453
454
455
  if (!empty($vocabulary->name)) {
    $vocabulary->name = trim($vocabulary->name);
  }
456
457
458
459
460
461
462
463
464
  // 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;
465
  }
Dries's avatar
   
Dries committed
466

467
  module_invoke_all('taxonomy_vocabulary_presave', $vocabulary);
468
  module_invoke_all('entity_presave', $vocabulary, 'taxonomy_vocabulary');
469

470
  if (!empty($vocabulary->vid) && !empty($vocabulary->name)) {
471
    $status = drupal_write_record('taxonomy_vocabulary', $vocabulary, 'vid');
472
    taxonomy_vocabulary_static_reset(array($vocabulary->vid));
473
474
475
    if ($vocabulary->old_machine_name != $vocabulary->machine_name) {
      field_attach_rename_bundle('taxonomy_term', $vocabulary->old_machine_name, $vocabulary->machine_name);
    }
476
    module_invoke_all('taxonomy_vocabulary_update', $vocabulary);
477
    module_invoke_all('entity_update', $vocabulary, 'taxonomy_vocabulary');
Kjartan's avatar
Kjartan committed
478
  }
479
  elseif (empty($vocabulary->vid)) {
480
    $status = drupal_write_record('taxonomy_vocabulary', $vocabulary);
481
    taxonomy_vocabulary_static_reset();
482
    field_attach_create_bundle('taxonomy_term', $vocabulary->machine_name);
483
    module_invoke_all('taxonomy_vocabulary_insert', $vocabulary);
484
    module_invoke_all('entity_insert', $vocabulary, 'taxonomy_vocabulary');
Kjartan's avatar
Kjartan committed
485
  }
Dries's avatar
   
Dries committed
486

487
  unset($vocabulary->original);
Dries's avatar
   
Dries committed
488
  cache_clear_all();
Dries's avatar
   
Dries committed
489

490
  return $status;
Kjartan's avatar
Kjartan committed
491
}
Dries's avatar
   
Dries committed
492

493
494
495
496
497
498
499
/**
 * Delete a vocabulary.
 *
 * @param $vid
 *   A vocabulary ID.
 * @return
 *   Constant indicating items were deleted.
500
501
502
 *
 * @see hook_taxonomy_vocabulary_predelete()
 * @see hook_taxonomy_vocabulary_delete()
503
 */
504
function taxonomy_vocabulary_delete($vid) {
505
  $vocabulary = taxonomy_vocabulary_load($vid);
Dries's avatar
   
Dries committed
506

507
508
  $transaction = db_transaction();
  try {
509
510
511
512
    // Allow modules to act before vocabulary deletion.
    module_invoke_all('taxonomy_vocabulary_predelete', $vocabulary);
    module_invoke_all('entity_predelete', $vocabulary, 'taxonomy_vocabulary');

513
514
515
516
517
518
519
520
    // 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();
521

522
    field_attach_delete_bundle('taxonomy_term', $vocabulary->machine_name);
523
524

    // Allow modules to respond to vocabulary deletion.
525
526
    module_invoke_all('taxonomy_vocabulary_delete', $vocabulary);
    module_invoke_all('entity_delete', $vocabulary, 'taxonomy_vocabulary');
527

528
    cache_clear_all();
529
    taxonomy_vocabulary_static_reset();
530

531
532
533
534
535
536
    return SAVED_DELETED;
  }
  catch (Exception $e) {
    $transaction->rollback();
    watchdog_exception('taxonomy', $e);
    throw $e;
Dries's avatar
   
Dries committed
537
  }
Dries's avatar
   
Dries committed
538
539
}

540
/**
541
 * Implements hook_taxonomy_vocabulary_update().
542
 */
543
544
545
546
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) {
547
548
549
550
551
    $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) {
552
553
          if ($value['vocabulary'] == $vocabulary->old_machine_name) {
            $value['vocabulary'] = $vocabulary->machine_name;
554
555
556
557
558
559
560
561
562
563
564
            $update = TRUE;
          }
        }
        if ($update) {
          field_update_field($field);
        }
      }
    }
  }
}

565
/**
566
 * Checks and updates the hierarchy flag of a vocabulary.
567
 *
568
 * Checks the current parents of all terms in a vocabulary and updates the
569
 * vocabulary's hierarchy setting to the lowest possible level. If no term
570
571
572
573
574
 * 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.
575
 *
576
 * @param $vocabulary
577
 *   A vocabulary object.
578
579
 * @param $changed_term
 *   An array of the term structure that was updated.
580
581
582
 *
 * @return
 *   An integer that represents the level of the vocabulary's hierarchy.
583
584
 */
function taxonomy_check_vocabulary_hierarchy($vocabulary, $changed_term) {
585
  $tree = taxonomy_get_tree($vocabulary->vid);
586
  $hierarchy = TAXONOMY_HIERARCHY_DISABLED;
587
  foreach ($tree as $term) {
588
    // Update the changed term with the new parent value before comparison.
589
    if ($term->tid == $changed_term['tid']) {
590
      $term = (object) $changed_term;
591
592
593
594
      $term->parents = $term->parent;
    }
    // Check this term's parent count.
    if (count($term->parents) > 1) {
595
      $hierarchy = TAXONOMY_HIERARCHY_MULTIPLE;
596
597
598
      break;
    }
    elseif (count($term->parents) == 1 && 0 !== array_shift($term->parents)) {
599
      $hierarchy = TAXONOMY_HIERARCHY_SINGLE;
600
601
    }
  }
602
603
604
  if ($hierarchy != $vocabulary->hierarchy) {
    $vocabulary->hierarchy = $hierarchy;
    taxonomy_vocabulary_save($vocabulary);
605
606
607
608
609
  }

  return $hierarchy;
}

610
/**
611
 * Saves a term object to the database.
612
 *
613
 * @param $term
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
 *   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.
 *
636
 * @return
637
638
639
 *   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.
640
 */
641
function taxonomy_term_save($term) {
642
643
  // Prevent leading and trailing spaces in term names.
  $term->name = trim($term->name);
644
645
646
647
648
  if (!isset($term->vocabulary_machine_name)) {
    $vocabulary = taxonomy_vocabulary_load($term->vid);
    $term->vocabulary_machine_name = $vocabulary->machine_name;
  }

649
650
651
652
653
  // Load the stored entity, if any.
  if (!empty($term->tid) && !isset($term->original)) {
    $term->original = entity_load_unchanged('taxonomy_term', $term->tid);
  }

654
  field_attach_presave('taxonomy_term', $term);
655
  module_invoke_all('taxonomy_term_presave', $term);
656
  module_invoke_all('entity_presave', $term, 'taxonomy_term');
657

658
  if (empty($term->tid)) {
659
    $op = 'insert';
660
    $status = drupal_write_record('taxonomy_term_data', $term);
661
    field_attach_insert('taxonomy_term', $term);
662
663
664
    if (!isset($term->parent)) {
      $term->parent = array(0);
    }
Kjartan's avatar
Kjartan committed
665
  }
666
  else {
667
    $op = 'update';
668
669
670
671
672
673
674
    $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();
    }
675
  }
676

677
678
679
680
681
682
  if (isset($term->parent)) {
    if (!is_array($term->parent)) {
      $term->parent = array($term->parent);
    }
    $query = db_insert('taxonomy_term_hierarchy')
      ->fields(array('tid', 'parent'));
683
684
685
    foreach ($term->parent as $parent) {
      if (is_array($parent)) {
        foreach ($parent as $tid) {
686
687
          $query->values(array(
            'tid' => $term->tid,
688
            'parent' => $tid
689
          ));
690
691
        }
      }
692
693
694
695
696
697
      else {
        $query->values(array(
          'tid' => $term->tid,
          'parent' => $parent
        ));
      }
Dries's avatar
   
Dries committed
698
    }
699
    $query->execute();
Kjartan's avatar
Kjartan committed
700
  }
701
702

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

705
706
707
  // Invoke the taxonomy hooks.
  module_invoke_all("taxonomy_term_$op", $term);
  module_invoke_all("entity_$op", $term, 'taxonomy_term');
708
  unset($term->original);
709

710
  return $status;
Kjartan's avatar
Kjartan committed
711
}
Dries's avatar
   
Dries committed
712

713
714
715
716
717
718
719
/**
 * Delete a term.
 *
 * @param $tid
 *   The term ID.
 * @return
 *   Status constant indicating deletion.
720
721
722
 *
 * @see hook_taxonomy_term_predelete()
 * @see hook_taxonomy_term_delete()
723
 */
724
function taxonomy_term_delete($tid) {
725
726
727
728
729
730
  $transaction = db_transaction();
  try {
    $tids = array($tid);
    while ($tids) {
      $children_tids = $orphans = array();
      foreach ($tids as $tid) {
731
732
733
734
735
736
        // 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');
        }

737
        // See if any of the term's children are about to be become orphans:
738
        if ($children = taxonomy_term_load_children($tid)) {
739
740
          foreach ($children as $child) {
            // If the term has multiple parents, we don't delete it.
741
            $parents = taxonomy_term_load_parents($child->tid);
742
743
744
            if (count($parents) == 1) {
              $orphans[] = $child->tid;
            }
745
746
          }
        }
Dries's avatar
   
Dries committed
747

748
        if ($term) {
749
750
751
752
753
754
755
756
          db_delete('taxonomy_term_data')
            ->condition('tid', $tid)
            ->execute();
          db_delete('taxonomy_term_hierarchy')
            ->condition('tid', $tid)
            ->execute();

          field_attach_delete('taxonomy_term', $term);
757
758

          // Allow modules to respond to term deletion.
759
760
761
762
          module_invoke_all('taxonomy_term_delete', $term);
          module_invoke_all('entity_delete', $term, 'taxonomy_term');
          taxonomy_terms_static_reset();
        }
763
      }
Dries's avatar
   
Dries committed
764

765
766
767
768
769
770
771
772
      $tids = $orphans;
    }
    return SAVED_DELETED;
  }
  catch (Exception $e) {
    $transaction->rollback();
    watchdog_exception('taxonomy', $e);
    throw $e;
773
  }
Dries's avatar
   
Dries committed
774
775
}

776
777
778
779
780
781
782
/**
 * Generate an array for rendering the given term.
 *
 * @param $term
 *   A term object.
 * @param $view_mode
 *   View mode, e.g. 'full', 'teaser'...
783
784
785
 * @param $langcode
 *   (optional) A language code to use for rendering. Defaults to the global
 *   content language of the current request.
786
787
788
789
 *
 * @return
 *   An array as expected by drupal_render().
 */
790
791
function taxonomy_term_view($term, $view_mode = 'full', $langcode = NULL) {
  if (!isset($langcode)) {
792
    $langcode = $GLOBALS['language_content']->langcode;
793
794
  }

795
796
  field_attach_prepare_view('taxonomy_term', array($term->tid => $term), $view_mode, $langcode);
  entity_prepare_view('taxonomy_term', array($term->tid => $term), $langcode);
797
798
799
800
801

  $build = array(
    '#theme' => 'taxonomy_term',
    '#term' => $term,
    '#view_mode' => $view_mode,
802
    '#language' => $langcode,
803
804
  );

805
  $build += field_attach_view('taxonomy_term', $term, $view_mode, $langcode);
806

807
808
809
810
811
812
813
814
815
  // 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>',
    );
  }
816
817
818

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

819
820
821
822
  // Allow modules to modify the structured term.
  $type = 'taxonomy_term';
  drupal_alter(array('taxonomy_term_view', 'entity_view'), $build, $type);

823
824
825
826
827
828
829
830
831
832
833
  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'];

834
835
  $uri = entity_uri('taxonomy_term', $term);
  $variables['term_url']  = url($uri['path'], $uri['options']);
836
  $variables['term_name'] = check_plain($term->name);
837
  $variables['page']      = $variables['view_mode'] == 'full' && taxonomy_term_is_page($term);
838
839

  // Flatten the term object's member fields.
840
  $variables = array_merge((array) $term, $variables);
841
842

  // Helpful $content variable for templates.
843
  $variables['content'] = array();
844
845
846
847
848
849
850
851
852
853
  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);

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

858
859
  $variables['theme_hook_suggestions'][] = 'taxonomy_term__' . $term->vocabulary_machine_name;
  $variables['theme_hook_suggestions'][] = 'taxonomy_term__' . $term->tid;
860
861
862
}

/**
863
 * Returns whether the current page is the page of the passed-in term.
864
865
866
867
868
869
870
871
872
 *
 * @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);
}

873
/**
874
 * Clear all static cache variables for terms.
875
876
877
878
 */
function taxonomy_terms_static_reset() {
  drupal_static_reset('taxonomy_term_count_nodes');
  drupal_static_reset('taxonomy_get_tree');
879
880
  drupal_static_reset('taxonomy_get_tree:parents');
  drupal_static_reset('taxonomy_get_tree:terms');
881
882
883
  drupal_static_reset('taxonomy_term_load_parents');
  drupal_static_reset('taxonomy_term_load_parents_all');
  drupal_static_reset('taxonomy_term_load_children');
884
  entity_get_controller('taxonomy_term')->resetCache();
885
886
}

887
888
/**
 * Clear all static cache variables for vocabularies.
889
 *
890
891
892
893
894
895
896
897
 * @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);
}

898
899
900
901
/**
 * Get names for all taxonomy vocabularies.
 *
 * @return
902
 *   An array of vocabulary ids, names, machine names, keyed by machine name.
903
904
 */
function taxonomy_vocabulary_get_names() {
905
906
907
908
909
  $names = &drupal_static(__FUNCTION__);

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

911
912
913
  return $names;
}

Dries's avatar
   
Dries committed
914
/**
915
916
917
918
919
920
 * Finds all parents of a given term ID.
 *
 * @param $tid
 *   A taxonomy term ID.
 *
 * @return
921
922
 *   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
923
 */
924
function taxonomy_term_load_parents($tid) {
925
926
927
928
929
930
931
932
933
934
935
936
  $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
937
  }
938
939

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

Dries's avatar
   
Dries committed
942
943
944
/**
 * Find all ancestors of a given term ID.
 */
945
function taxonomy_term_load_parents_all($tid) {
946
947
948
949
950
951
  $cache = &drupal_static(__FUNCTION__, array());

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

Dries's avatar
   
Dries committed
952
  $parents = array();
953
954
  if ($term = taxonomy_term_load($tid)) {
    $parents[] = $term;
Dries's avatar
   
Dries committed
955
    $n = 0;
956
    while ($parent = taxonomy_term_load_parents($parents[$n]->tid)) {
Dries's avatar
   
Dries committed
957
958
959
960
      $parents = array_merge($parents, $parent);
      $n++;
    }
  }
961
962
963

  $cache[$tid] = $parents;

Dries's avatar
   
Dries committed
964
965
966
  return $parents;
}

Dries's avatar
   
Dries committed
967
/**
968
969
970
971
972
973
974
975
 * 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
976
977
 *   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
978
 */
979
function taxonomy_term_load_children($tid, $vid = 0) {
980
981
982
  $children = &drupal_static(__FUNCTION__, array());

  if ($tid && !isset($children[$tid])) {
983
984
    $query = db_select('taxonomy_term_data', 't');
    $query->join('taxonomy_term_hierarchy', 'h', 'h.tid = t.tid');
985
986
    $query->addField('t', 'tid');
    $query->condition('h.parent', $tid);
987
988
989
    if ($vid) {
      $query->condition('t.vid', $vid);
    }
990
991
992
993
994
    $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
995
  }
996
997

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

Dries's avatar
   
Dries committed
1000
1001
1002
1003
1004
1005
1006
1007
/**
 * 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
1008
1009
 * @param $max_depth
 *   The number of levels of the tree to return. Leave NULL to return all levels.
1010
1011
1012
1013
1014
 * @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.
1015
 *
Dries's avatar
   
Dries committed
1016
1017
1018
 * @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.
1019
1020
 *   Results are statically cached. Term objects will be partial or complete
 *   depending on the $load_entities parameter.
Dries's avatar
   
Dries committed
1021
 */
1022
function taxonomy_get_tree($vid, $parent = 0, $max_depth = NULL, $load_entities = FALSE) {
1023
  $children = &drupal_static(__FUNCTION__, array());
1024
1025
  $parents = &drupal_static(__FUNCTION__ . ':parents', array());
  $terms = &drupal_static(__FUNCTION__ . ':terms', array());
1026

Dries's avatar
   
Dries committed
1027
1028
1029
1030
  // 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();
1031
1032
    $parents[$vid] = array();
    $terms[$vid] = array();
Dries's avatar
   
Dries committed
1033

1034
1035
    $query = db_select('taxonomy_term_data', 't');
    $query->join('taxonomy_term_hierarchy', 'h', 'h.tid = t.tid');
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
    $result = $query
      ->addTag('translatable')
      ->addTag('term_access')
      ->fields('t')
      ->fields('h', array('parent'))
      ->condition('t.vid', $vid)
      ->orderBy('t.weight')
      ->orderBy('t.name')
      ->execute();

    foreach ($result as $term) {
      $children[$vid][$term->parent][] = $term->tid;
      $parents[$vid][$term->tid][] = $term->parent;
      $terms[$vid][$term->tid] = $term;
Dries's avatar