You are here

delete_orphaned_terms.module in Delete Orphaned Terms 7.2

Same filename and directory in other branches
  1. 6.2 delete_orphaned_terms.module
  2. 6 delete_orphaned_terms.module

Delete Orphaned Terms - deletes orphaned terms from taxonomy.

The "Delete orphaned terms" module allows for manual and automatic pruning of taxonomy terms that are no longer referenced by a minimum (configurable) number of nodes, optionally via hook_cron().

File

delete_orphaned_terms.module
View source
<?php

/**
 * @file
 * Delete Orphaned Terms - deletes orphaned terms from taxonomy.
 *
 * The "Delete orphaned terms" module allows for manual and automatic pruning
 * of taxonomy terms that are no longer referenced by a minimum (configurable)
 * number of nodes, optionally via hook_cron().
 */

/**
 * Implements hook_menu().
 */
function delete_orphaned_terms_menu() {
  $items = array();
  $items['dot'] = array(
    'access arguments' => array(
      'administer taxonomy',
    ),
    'description' => "Delete terms with no content associated with them.",
    'menu_name' => 'management',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'delete_orphaned_terms_form_manual',
    ),
    'title' => 'Orphaned Terms Deletion',
  );
  $items['admin/config/content/delete_orphaned_terms'] = array(
    'access arguments' => array(
      'administer taxonomy',
    ),
    'file' => 'delete_orphaned_terms.admin.inc',
    'description' => 'Configure orphaned terms removal on your taxonomy.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'delete_orphaned_terms_admin',
    ),
    'title' => 'Delete Orphaned Terms',
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/config/content/delete_orphaned_terms/settings'] = array(
    'access arguments' => array(
      'administer taxonomy',
    ),
    'file' => 'delete_orphaned_terms.admin.inc',
    'title' => 'Automatic Pruning Settings',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -1,
  );
  $items['admin/config/content/delete_orphaned_terms/parameters'] = array(
    'access arguments' => array(
      'administer taxonomy',
    ),
    'file' => 'delete_orphaned_terms.admin.inc',
    'page arguments' => array(
      'delete_orphaned_terms_admin_parameters',
    ),
    'title' => 'Parameters',
    'type' => MENU_LOCAL_TASK,
  );
  return $items;
}

/**
 * Returns default values, used by _dot_get().
 *
 * it's not a good idea to fill memory with define()s
 */
function __delete_orphaned_terms_defaults($var) {
  switch ($var) {
    case 'delete_orphaned_terms_failsafe':
      return 50;
      break;
    case 'delete_orphaned_terms_failsafe_all':
      return 25;
      break;
    case 'delete_orphaned_terms_threshold':
      return 0;
      break;
    case 'delete_orphaned_terms_whitelist':
    case 'delete_orphaned_terms_blacklist':
      return array();
      break;
    case 'delete_orphaned_terms_tagsonly':
      return TRUE;
      break;
    case 'delete_orphaned_terms_removeemptyvoc':
      return FALSE;
      break;
    case 'delete_orphaned_terms_vocabs':
      return array();
      break;
    case 'delete_orphaned_terms_enablecron':
      return FALSE;
      break;
    case 'delete_orphaned_terms_voctags':
      return array(
        1,
      );

      // default tags on new Drupal 7.x install
      break;
    default:
      die('Don\'t know this option');
  }
}

/**
 * Wrapper for variable_get().
 */
function _dot_get($var) {
  return variable_get($var, __delete_orphaned_terms_defaults($var));
}

/**
 * Get count of vocabularies that are a tag based field on some content type (bundle).
 *
 * replacement for taxonomy_term_count_nodes()
 */
function __delete_orphaned_terms_tax_term_count_entities($tid, $count = TRUE) {
  $cnt = 0;
  $instances = field_read_instances();
  foreach ($instances as $instance) {

    // check if this has to do with taxonomy, otherwise there will be no 'tid' column
    if ($instance['widget']['module'] == 'taxonomy') {
      $field = field_info_field_by_id($instance['field_id']);
      $query = new EntityFieldQuery();
      $query
        ->fieldCondition($field, 'tid', $tid)
        ->count();
      $cnt += $query
        ->execute();
    }
  }
  return $cnt;
}

/**
 * Check if a vocabulary is used as a source for at least one auto-complete term widget (tag).
 */
function _delete_orphaned_terms_voc_is_tag($vid) {
  $fi_auto = array();

  // get all field instances
  $instances = field_read_instances();
  foreach ($instances as $instance) {

    // check if instance is of autocomplete type
    if (isset($instance['widget']['type']) && $instance['widget']['type'] == 'taxonomy_autocomplete') {
      $fi_auto[] = $instance;
    }
  }
  $ret = FALSE;
  $terms = taxonomy_get_tree($vid);
  foreach ($terms as $term) {
    foreach ($fi_auto as $fi) {
      if (isset($term->id)) {

        // if a field exists for that term and in the previously compiled list,
        // then we have a tagging term
        $field = field_info_field_by_id($fi['field_id']);
        $query = new EntityFieldQuery();
        $query
          ->fieldCondition($field, 'tid', $term->id);
        $ok = $query
          ->execute();
        if (!empty($ok)) {
          $ret = TRUE;
          break 2;
        }
      }
    }
  }
  return $ret;

  //return in_array($vid, _dot_get('delete_orphaned_terms_voctags'));
}

/**
 * Helper function: retrieves vocabularies' names
 */
function _delete_orphaned_terms_vocab_names($verbose = FALSE, $show_if_tags = FALSE) {
  if (function_exists('i18n_selection_mode')) {
    i18n_selection_mode('off');
  }
  $thres = _dot_get('delete_orphaned_terms_threshold');
  $vocab_names = array();
  $vocabularies = taxonomy_get_vocabularies();
  foreach ($vocabularies as $vocabulary) {
    $vocab_names[$vocabulary->vid] = $vocabulary->name;
    $count = count(taxonomy_get_tree($vocabulary->vid));
    if ($verbose) {
      $vocab_names[$vocabulary->vid] .= ' (' . $count . ' terms)';
    }
    else {
      $vocab_names[$vocabulary->vid] .= ' (' . $count . ')';
    }
    if ($show_if_tags && _delete_orphaned_terms_voc_is_tag($vocabulary->vid)) {
      $vocab_names[$vocabulary->vid] .= ' [t]';
    }
  }
  return $vocab_names;
}

/**
 * Verifies the syntax of the given percentage value.
 *
 * @param $element
 *   The form element to be validated.
 * @param $form_state
 *   A reference to the entire form containing the element to be validated.
 *   Since this parameter is passed by reference one can also modify the form.
 */
function _delete_orphaned_terms_element_validate_percentage($element, &$form_state) {
  if ($element['#value'] < 0 || $element['#value'] > 100) {
    form_error($element, t('@title must be a number between 0 and 100.', array(
      '@title' => $element['#title'],
    )));
  }

  //$form_state['values']['delete_orphaned_terms_failsafe'] = (int)$element['#value'];
}

/**
 * Verifies the syntax of the given threshold value.
 *
 * @param $element
 *   The form element to be validated.
 * @param $form_state
 *   A reference to the entire form containing the element to be validated.
 *   Since this parameter is passed by reference one can also modify the form.
 */
function _delete_orphaned_terms_element_validate_threshold($element, &$form_state) {
  if (!is_numeric($element['#value']) || $element['#value'] < 0) {
    form_error($element, t('@title must contain a non-negative number.', array(
      '@title' => $element['#title'],
    )));
  }
}

/**
 * Checks which elements of the form were used and prunes the taxonomy
 * according to those settings. (internal function, not to be considered as API)
 *
 * @param $form
 *   The form
 * @param $form_state
 *   The state of the form
 */
function _delete_orphaned_terms($form, $form_state) {

  // disable language selection, i.e. don't consider node language
  // when searching for referencing terms etc.
  if (function_exists('i18n_selection_mode')) {
    i18n_selection_mode('off');
  }

  // initialize state array
  $state = _delete_orphaned_terms_initialize_state($form, $form_state);
  $whitelist = _dot_get('delete_orphaned_terms_whitelist');
  $blacklist = _dot_get('delete_orphaned_terms_blacklist');
  $tagsonly = _dot_get('delete_orphaned_terms_tagsonly');
  if ($state['sim']) {
    drupal_set_message(t('%sim', array(
      '%sim' => 'Simulation',
    )));
  }
  $vids = array();
  if ($state['cron'] || $state['cronman'] || $state['all']) {
    $vocabs = taxonomy_get_vocabularies();
    foreach ($vocabs as $vocab) {
      if ($state['all'] && !$state['cron'] && !$state['cronman']) {
        $vids[] = $vocab->vid;
      }
      elseif (in_array($vocab->vid, $whitelist) || !in_array($vocab->vid, $blacklist) && _delete_orphaned_terms_voc_is_tag($vocab->vid) == $tagsonly) {
        $vids[] = $vocab->vid;
      }
    }
  }
  else {
    $vids = $form_state['values']['delete_orphaned_terms_vocabs'];
    variable_set('delete_orphaned_terms_vocabs', $form_state['values']['delete_orphaned_terms_vocabs']);
  }
  $thres = _dot_get('delete_orphaned_terms_threshold');

  // assemble the terms to process and generate output
  $prune = array();
  $all_terms = 0;
  foreach ($vids as $vid) {
    if ($vid <= 0) {
      continue;
    }
    $vocab = taxonomy_vocabulary_load($vid);
    $terms = taxonomy_get_tree($vid);
    $del_term = 0;
    $prune_local = array();
    foreach ($terms as $term) {
      if (__delete_orphaned_terms_tax_term_count_entities($term->tid) <= $thres) {
        $simulated_deletions = delete_orphaned_terms_simulate_delete($term->tid);
        $del_term = $del_term + count($simulated_deletions);
        foreach ($simulated_deletions as $simulated_deletion) {

          // PHP arrays are really hash tables which can be used to guarantee uniqueness for their keys
          $prune_local[$simulated_deletion] = $simulated_deletion;
        }
      }
    }
    foreach ($terms as $term) {
      if (in_array($term->tid, $prune_local)) {
        if ($state['sim']) {
          drupal_set_message(t('Would delete term #@tid: @name (@vname)', array(
            '@tid' => $term->tid,
            '@name' => $term->name,
            '@vname' => $vocab->name,
          )));
        }
        else {
          if (!$state['cron']) {
            drupal_set_message(t('Deleting term #@tid: @name (@vname)', array(
              '@tid' => $term->tid,
              '@name' => $term->name,
              '@vname' => $vocab->name,
            )));
          }
        }
      }
    }

    // issue an error to the log if the configured failsafe (any) is out of bounds
    if (count($terms) > 0 && count($prune) / count($terms) * 100 > _dot_get('delete_orphaned_terms_failsafe')) {
      $errmsg = t('[cron] FAILSAFE (any): did not delete @count/@total terms from @vname', array(
        '@count' => count($prune),
        '@total' => count($terms),
        '@vname' => $vocab->name,
      ));
      if ($state['cron']) {
        watchdog('delete_orphaned_terms', $errmsg, array(), WATCHDOG_ERROR);
        continue;

        // in order not to be triggered in manual mode, this is here and not below
      }
      elseif ($state['cronman']) {
        drupal_set_message($errmsg);
        continue;
      }
    }
    if ($del_term > 0 && !$state['cron']) {
      drupal_set_message(t('Previous @vname term count: @count', array(
        '@vname' => $vocab->name,
        '@count' => count($terms),
      )));
    }
    $all_terms += count($terms);
    $prune = array_merge($prune, $prune_local);
  }

  // issue an error and do not do anything if the configured failsafe (all) is out of bounds
  if ($all_terms > 0 && count($prune) / $all_terms * 100 > _dot_get('delete_orphaned_terms_failsafe_all')) {
    $errmsg = t('[cron] FAILSAFE (all): did not delete @count/@total terms', array(
      '@count' => count($prune),
      '@total' => $all_terms,
    ));
    if ($state['cron']) {
      watchdog('delete_orphaned_terms', $errmsg, array(), WATCHDOG_ERROR);
      return FALSE;

      // in order not to be triggered in manual mode, this is here and not below
    }
    elseif ($state['cronman']) {
      drupal_set_message($errmsg);
      return FALSE;
    }
  }

  // do the processing
  if (!$state['sim']) {
    foreach ($prune as $p) {
      taxonomy_term_delete($p);
    }
  }
  $del_voc = 0;

  // scan vocabularies and remove empty ones
  if (($state['cron'] || $state['cronman']) && _dot_get('delete_orphaned_terms_removeemptyvoc')) {
    $del_voc = _delete_orphaned_terms_remove_empty_voc($del_voc, $blacklist, $state, $prune);
  }

  // give a final message
  _delete_orphaned_terms_final_message($prune, $del_voc, $state);
}

/**
 *
 * @param array $form
 * @param array $form_state
 *
 * @return array
 *   An array of initialized variables that dictate how to proceed with the deletion process.
 */
function _delete_orphaned_terms_initialize_state($form, $form_state) {
  $state = array(
    'cron' => FALSE,
    // are we running from cron?
    'sim' => TRUE,
    // are we simulating?
    'cronman' => FALSE,
    // was a manual cron run selected? (delete_orphaned_terms' submit buttons carry this attribute)
    'all' => FALSE,
  );
  if ($form === NULL && $form_state === NULL) {
    $state['cron'] = TRUE;
    $state['sim'] = $state['cronman'] = $state['all'] = FALSE;
  }
  else {
    $state['sim'] = $form_state['clicked_button']['#sim'];
    $state['cronman'] = $form_state['clicked_button']['#cronman'];
    if (isset($form_state['values']['delete_orphaned_terms_vocabs']['all'])) {
      $state['all'] = $form_state['values']['delete_orphaned_terms_vocabs']['all'];
    }
  }
  return $state;
}

/**
 * Scan vocabularies and remove empty ones.
 *
 * @param int   $del_voc
 * @param array $blacklist
 * @param array $state
 * @param int   $prune
 *
 * @return int
 *   A count of the number of vocabularies removed.
 */
function _delete_orphaned_terms_remove_empty_voc($del_voc, $blacklist, $state, $prune) {
  $vocabularies = taxonomy_get_vocabularies();
  foreach ($vocabularies as $vocabulary) {

    // account for vocabulary in blacklist
    if (in_array($vocabulary->vid, $blacklist)) {
      continue;
    }
    $terms = taxonomy_get_tree($vocabulary->vid);

    // account for terms that would have been removed in the previous step of a simulation
    if ($state['sim']) {
      foreach ($terms as $k => $term) {
        if (in_array($term->tid, $prune)) {
          unset($terms[$k]);
        }
      }
    }
    if (count($terms) != 0) {
      continue;
    }
    $del_voc++;
    if ($state['sim']) {
      drupal_set_message(t('Would delete vocabulary #@vid: @vname', array(
        '@vid' => $vocabulary->vid,
        '@vname' => $vocabulary->name,
      )));
    }
    else {
      if (!$state['cron']) {
        drupal_set_message(t('Deleting vocabulary #@vid: @vname', array(
          '@vid' => $vocabulary->vid,
          '@vname' => $vocabulary->name,
        )));
      }
      taxonomy_vocabulary_delete($vocabulary->vid);
    }
  }
  return $del_voc;
}

/**
 * Provide a message to the user, detailing what has been done.
 *
 * @param int $prune
 * @param int $del_voc
 */
function _delete_orphaned_terms_final_message($prune, $del_voc, $state) {
  $logmsg = t('Taxonomy pruned.');
  if (count($prune) > 0) {
    $logmsgdet[] = t('@count @terms', array(
      '@count' => count($prune),
      '@terms' => format_plural(count($prune), 'term', 'terms'),
    ));
  }
  if ($del_voc > 0) {
    $logmsgdet[] = t('@count @vocs', array(
      '@count' => $del_voc,
      '@vocs' => format_plural($del_voc, 'vocabulary', 'vocabularies'),
    ));
  }
  if (count($prune) + $del_voc > 0) {
    $logmsg .= ' (' . implode(', ', $logmsgdet) . ')';
  }
  if (!$state['cron']) {
    if (count($prune) + $del_voc > 0) {
      drupal_set_message($logmsg);
    }
    else {
      drupal_set_message(t('Nothing to do.'));
    }
  }
  else {
    if (count($prune) + $del_voc > 0) {
      watchdog('delete_orphaned_terms', '[cron] ' . $logmsg, array(), WATCHDOG_INFO);
    }
  }
}

/**
 * The user menu.
 */
function delete_orphaned_terms_form_manual() {
  if (count(_delete_orphaned_terms_vocab_names()) <= 0) {
    $form['info'] = array(
      '#value' => t('There are no vocabularies.'),
    );
    return $form;
  }
  $form['delete_orphaned_terms_vocabs'] = array(
    // '#default_value' => array_fill(0, count(taxonomy_get_vocabularies()), FALSE),
    '#default_value' => _dot_get('delete_orphaned_terms_vocabs'),
    '#description' => t('Select one or more or all vocabularies to delete orphaned terms from. Current orphan threshold is @threshold.', array(
      '@threshold' => _dot_get('delete_orphaned_terms_threshold'),
    )),
    '#multiple' => TRUE,
    '#options' => _delete_orphaned_terms_vocab_names(TRUE),
    '#type' => 'checkboxes',
    '#title' => t('Vocabularies'),
  );
  $form['delete_orphaned_terms_vocabs']['#options']['all'] = t('%all', array(
    '%all' => 'All',
  ));
  $form['delete_orphaned_terms_runnow']['go'] = array(
    '#cronman' => FALSE,
    '#sim' => FALSE,
    '#submit' => array(
      '_delete_orphaned_terms',
    ),
    '#type' => 'submit',
    '#value' => t('Delete orphaned terms now'),
  );
  $form['delete_orphaned_terms_runnow']['sim'] = array(
    '#cronman' => FALSE,
    '#sim' => TRUE,
    '#submit' => array(
      '_delete_orphaned_terms',
    ),
    '#type' => 'submit',
    '#value' => t('Simulate'),
  );
  return $form;
}

/**
 * Implements hook_cron().
 */
function delete_orphaned_terms_cron() {
  if (_dot_get('delete_orphaned_terms_enablecron')) {
    _delete_orphaned_terms(NULL, NULL);
  }
}

/**
 * Mimics taxonomy_term_delete, to more accurately count terms to be deleted.
 */
function delete_orphaned_terms_simulate_delete($tid) {
  $simulated_deletions = array();
  try {
    $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;
            }
          }
        }
        if ($term = taxonomy_term_load($tid)) {
          $simulated_deletions[] = $tid;
        }
      }
      $tids = $orphans;
    }
    return $simulated_deletions;
  } catch (Exception $e) {
    $transaction
      ->rollback();
    watchdog_exception('delete_orphaned_terms', $e);
    throw $e;
  }
}

/**
 * Implements hook_help().
 */
function delete_orphaned_terms_help($path, $arg) {
  $output = '';

  // declare output variable, otherwise NULL might be returned
  switch ($path) {
    case "admin/settings/delete_orphaned_terms":
      $output = '<p>' . t('These options, when enabled, prune taxonomy regularly according to several parameters.') . '</p>';
      break;
    case "admin/settings/delete_orphaned_terms/parameters":
      $output = '<p>' . t('These parameters are applied to both automatic and manual removal of orphaned terms.') . '</p>';
      break;
  }
  return $output;
}

Functions

Namesort descending Description
delete_orphaned_terms_cron Implements hook_cron().
delete_orphaned_terms_form_manual The user menu.
delete_orphaned_terms_help Implements hook_help().
delete_orphaned_terms_menu Implements hook_menu().
delete_orphaned_terms_simulate_delete Mimics taxonomy_term_delete, to more accurately count terms to be deleted.
_delete_orphaned_terms Checks which elements of the form were used and prunes the taxonomy according to those settings. (internal function, not to be considered as API)
_delete_orphaned_terms_element_validate_percentage Verifies the syntax of the given percentage value.
_delete_orphaned_terms_element_validate_threshold Verifies the syntax of the given threshold value.
_delete_orphaned_terms_final_message Provide a message to the user, detailing what has been done.
_delete_orphaned_terms_initialize_state _state
_delete_orphaned_terms_remove_empty_voc Scan vocabularies and remove empty ones.
_delete_orphaned_terms_vocab_names Helper function: retrieves vocabularies' names
_delete_orphaned_terms_voc_is_tag Check if a vocabulary is used as a source for at least one auto-complete term widget (tag).
_dot_get Wrapper for variable_get().
__delete_orphaned_terms_defaults Returns default values, used by _dot_get().
__delete_orphaned_terms_tax_term_count_entities Get count of vocabularies that are a tag based field on some content type (bundle).