delete_orphaned_terms.module in Delete Orphaned Terms 7.2
Same filename and directory in other branches
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.moduleView 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
Name![]() |
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). |