You are here

delete_orphaned_terms.module in Delete Orphaned Terms 6.2

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

Taxonomy pruner - 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
 * Taxonomy pruner - 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().
 */

/**
 * 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;
    default:
      die('Don\'t know this option');
  }
}

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

/**
 * 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 && $vocabulary->tags == 1) {
      $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');
  }

  // are we running from cron?
  if ($form === NULL && $form_state === NULL) {
    $cron = TRUE;
    $sim = $cronman = $all = FALSE;
  }
  else {
    $cron = FALSE;

    // are we simulating?
    $sim = $form_state['clicked_button']['#sim'];

    // was "All" selected in the manual pruning dialog?
    $all = $form_state['values']['delete_orphaned_terms_vocabs']['all'];

    // find out which submit button was clicked
    $cronman = $form_state['clicked_button']['#cronman'];
  }
  $w = _dot_get('delete_orphaned_terms_whitelist');
  $b = _dot_get('delete_orphaned_terms_blacklist');
  $t = _dot_get('delete_orphaned_terms_tagsonly');
  if ($sim) {
    drupal_set_message(t('%sim', array(
      '%sim' => 'Simulation',
    )));
  }
  $vids = array();
  if ($cron || $cronman || $all) {
    $vocabs = taxonomy_get_vocabularies();
    foreach ($vocabs as $vocab) {
      if ($all && !$cron && !$cronman) {
        $vids[] = $vocab->vid;
      }
      elseif (in_array($vocab->vid, $w) || !in_array($vocab->vid, $b) && $vocab->tags >= $t) {
        $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 (taxonomy_term_count_nodes($term->tid) <= $thres) {
        $prune_local[] = $term->tid;
        $del_term++;
        if ($sim) {
          drupal_set_message(t('Would delete term #!tid: !name (!vname)', array(
            '!tid' => $term->tid,
            '!name' => $term->name,
            '!vname' => $vocab->name,
          )));
        }
        else {
          if (!$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 ($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 ($cronman) {
        drupal_set_message($errmsg);
        continue;
      }
    }
    if ($del_term > 0 && !$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 ($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 ($cronman) {
      drupal_set_message($errmsg);
      return FALSE;
    }
  }

  // do the processing
  if (!$sim) {
    foreach ($prune as $p) {
      taxonomy_del_term($p);
    }
  }

  // scan vocabularies and remove empty ones
  $del_voc = 0;
  if (($cron || $cronman) && _dot_get('delete_orphaned_terms_removeemptyvoc')) {
    $vocabularies = taxonomy_get_vocabularies();
    foreach ($vocabularies as $vocabulary) {

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

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

  // give a final message
  $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 (!$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;
}

/**
 * The admin menu. (Tab 1)
 */
function delete_orphaned_terms_admin() {
  drupal_add_css(drupal_get_path('module', 'delete_orphaned_terms') . "/delete_orphaned_terms.css", 'module', 'all', FALSE);
  $form['dot_cron']['delete_orphaned_terms_enablecron'] = array(
    '#default_value' => _dot_get('delete_orphaned_terms_enablecron'),
    '#title' => '<strong>' . t('Enable automatic pruning') . '</strong>',
    '#type' => 'checkbox',
  );
  $form['dot_cron']['lists'] = array(
    '#description' => t('Use these options to force or prevent automatic pruning of your taxonomy.') . '<br/><small>' . t('[t] indicates a tag based vocabulary.') . '</small><br/><span style="text-decoration: underline; font-weight: bold;">' . t('Warning') . '</span>: ' . t('First save at the bottom of this dialog to apply this setting before testing the pruning.') . '<br/><small>' . t('Ctrl-Click for multi select') . '</small>',
    '#title' => t('Whitelisting and blacklisting of vocabularies'),
    '#type' => 'fieldset',
  );
  $form['dot_cron']['lists']['delete_orphaned_terms_whitelist'] = array(
    '#default_value' => _dot_get('delete_orphaned_terms_whitelist'),
    '#description' => t('Which vocabularies to always prune, even if tag based or blacklisted.'),
    '#multiple' => TRUE,
    '#options' => _delete_orphaned_terms_vocab_names(FALSE, TRUE),
    '#title' => t('Whitelist'),
    '#type' => 'select',
  );
  $form['dot_cron']['lists']['delete_orphaned_terms_blacklist'] = array(
    '#default_value' => _dot_get('delete_orphaned_terms_blacklist'),
    '#description' => t('Which vocabularies to never prune.') . '<br/><em>' . t('Note') . '</em>: ' . t('This has no effect if a same item is already selected in the whitelist.'),
    '#multiple' => TRUE,
    '#options' => $form['dot_cron']['lists']['delete_orphaned_terms_whitelist']['#options'],
    '#title' => t('Blacklist'),
    '#type' => 'select',
  );
  $form['dot_cron']['delete_orphaned_terms_tagsonly'] = array(
    '#default_value' => _dot_get('delete_orphaned_terms_tagsonly'),
    '#description' => t('Only act on terms that are part of vocabularies which are tag based.') . '<br/><em>' . t('Note') . '</em>: ' . t('This condition does not apply to items in the whitelist, which are always pruned.'),
    '#title' => t('Prune tag based vocabularies only'),
    '#type' => 'checkbox',
  );
  $form['dot_cron']['delete_orphaned_terms_removeemptyvoc'] = array(
    '#default_value' => _dot_get('delete_orphaned_terms_removeemptyvoc'),
    '#description' => t('Remove vocabularies altogether that might have become empty after pruning (no matter if tag based or not).') . '<br/><em>' . t('Note') . '</em>: ' . t('This does not apply to vocabularies in the blacklist, which will never be deleted.'),
    '#title' => t('Remove empty vocabularies'),
    '#type' => 'checkbox',
  );
  $form['dot_cron']['failsafe'] = array(
    '#collapsible' => TRUE,
    '#description' => t('Use these options to define upper bounds on how many terms should be automatically deleted.'),
    '#title' => t('Failsafe'),
    '#type' => 'fieldset',
  );
  $form['dot_cron']['failsafe']['delete_orphaned_terms_failsafe'] = array(
    '#default_value' => _dot_get('delete_orphaned_terms_failsafe'),
    '#description' => t('Maximum percentage of tags to delete from any one vocabulary in automatic mode.'),
    '#element_validate' => array(
      '_delete_orphaned_terms_element_validate_percentage',
    ),
    '#field_suffix' => '%',
    '#maxlength' => 3,
    '#size' => 3,
    '#title' => t('Any'),
    '#type' => 'textfield',
  );
  $form['dot_cron']['failsafe']['delete_orphaned_terms_failsafe_all'] = array(
    '#default_value' => _dot_get('delete_orphaned_terms_failsafe_all'),
    '#description' => t('Maximum percentage of tags to delete from all vocabularies combined in automatic mode.'),
    '#element_validate' => array(
      '_delete_orphaned_terms_element_validate_percentage',
    ),
    '#field_suffix' => '%',
    '#maxlength' => 3,
    '#size' => 3,
    '#title' => t('All'),
    '#type' => 'textfield',
  );
  $form['dot_cron']['simulation'] = array(
    '#collapsible' => FALSE,
    '#description' => t('Use these buttons to simulate what automatic pruning would do to your taxonomy.'),
    '#title' => t('Simulation'),
    '#type' => 'fieldset',
  );
  $form['dot_cron']['simulation']['sim'] = array(
    '#cronman' => TRUE,
    '#sim' => TRUE,
    '#submit' => array(
      '_delete_orphaned_terms',
    ),
    '#type' => 'submit',
    '#value' => t('Simulate cron'),
  );
  $form['dot_cron']['simulation']['go'] = array(
    '#cronman' => TRUE,
    '#sim' => FALSE,
    '#submit' => array(
      '_delete_orphaned_terms',
    ),
    '#type' => 'submit',
    '#value' => t('Prune taxonomy now (stored cron settings)'),
  );
  return system_settings_form($form);
}

/**
 * The admin menu. (Tab 2)
 */
function delete_orphaned_terms_admin_parameters() {
  $form['dot_param']['delete_orphaned_terms_threshold'] = array(
    '#description' => t('Number of nodes associated to a term below (or equal) to which it is considered as orphaned.'),
    '#default_value' => _dot_get('delete_orphaned_terms_threshold'),
    '#element_validate' => array(
      '_delete_orphaned_terms_element_validate_threshold',
    ),
    '#maxlength' => 1,
    '#size' => 1,
    '#title' => t('Orphan threshold'),
    '#type' => 'textfield',
  );
  return system_settings_form($form);
}

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

/**
 * Implementation of 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;
}

/**
 * Implementation of 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.",
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'delete_orphaned_terms_form_manual',
    ),
    'title' => 'Delete Orphaned Terms',
  );
  $items['admin/settings/delete_orphaned_terms'] = array(
    'access arguments' => array(
      'administer taxonomy',
    ),
    'description' => 'Configure orphaned terms removal on your taxonomy.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'delete_orphaned_terms_admin',
    ),
    'title' => 'Taxonomy Pruner',
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/settings/delete_orphaned_terms/settings'] = array(
    'access arguments' => array(
      'administer taxonomy',
    ),
    'title' => 'Automatic Pruning Settings',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -1,
  );
  $items['admin/settings/delete_orphaned_terms/parameters'] = array(
    'access arguments' => array(
      'administer taxonomy',
    ),
    'page arguments' => array(
      'delete_orphaned_terms_admin_parameters',
    ),
    'title' => 'Parameters',
    'type' => MENU_LOCAL_TASK,
  );
  return $items;
}

Functions

Namesort descending Description
delete_orphaned_terms_admin The admin menu. (Tab 1)
delete_orphaned_terms_admin_parameters The admin menu. (Tab 2)
delete_orphaned_terms_cron Implementation of hook_cron().
delete_orphaned_terms_form_manual The user menu.
delete_orphaned_terms_help Implementation of hook_help().
delete_orphaned_terms_menu Implementation of hook_menu().
_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_vocab_names Helper function: retrieves vocabularies' names
_dot_get Wrapper for variable_get().
__delete_orphaned_terms_defaults Returns default values, used by _dot_get().