'fieldset',
'#collapsible' => TRUE,
'#title' => t('Menu to taxonomy'),
'#weight' => 10,
'#tree' => TRUE,
);
$item = array('mlid' => 0);
$menu_items = menu_parent_options(menu_get_menus(), $item);
array_unshift($menu_items, t('= DISABLED ='));
// The vid isn't set when a new vocabulary is being created.
if (isset($form['vid']['#value'])) {
$vocab_parent_default = variable_get(_menu_to_taxonomy_build_variable('vocab_menu', $form['vid']['#value']), NULL) . ':' .
variable_get(_menu_to_taxonomy_build_variable('vocab_parent', $form['vid']['#value']), NULL);
if (!isset($menu_items[$vocab_parent_default])) {
$vocab_parent_default = 0;
}
}
else {
$vocab_parent_default = 0;
}
// TODO: UX for this select box is problematic with big hierarchies and
// several menus. To look into hierarchical select integration.
$form['menu_to_taxonomy']['vocab_parent'] = array(
'#type' => 'select',
'#title' => t('Menu to sync from'),
'#default_value' => $vocab_parent_default,
'#options' => $menu_items,
'#description' => t('The menu and parent from which we will insert taxonomy terms into this vocabulary.'),
'#attributes' => array('class' => array('menu-title-select')),
);
// Set up maximum depth.
$form['menu_to_taxonomy']['max_depth'] = array(
'#type' => 'select',
'#title' => t('Maximum depth'),
'#description' => t('Limit how many levels of the menu tree to process.'),
'#options' => array(0 => t('Unlimited'), 1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 6, 7 => 7, 8 => 8, 9 => 9),
'#default_value' => isset ($form['vid']['#value']) ? variable_get(_menu_to_taxonomy_build_variable('max_depth', $form['vid']['#value']), 0) : 0,
);
// Rebuild the menu.
$form['menu_to_taxonomy']['rebuild'] = array(
'#type' => 'checkbox',
'#title' => t('Select to rebuild the vocabulary on submit.'),
'#default_value' => 0,
'#weight' => 20,
'#description' => t('Rebuild the vocabulary on submit. Warning: This will delete then re-create all of the taxonomy terms ever linked to menu items in this vocabulary. Only use this option if you are experiencing issues like missing taxonomy terms or other inconsistencies.'),
);
// Move the buttons to the bottom of the form.
$form['submit']['#weight'] = 49;
$form['delete']['#weight'] = 50;
// Add an extra submit handler to save these settings.
$form['#submit'][] = 'menu_to_taxonomy_vocab_submit';
}
}
/**
* Submit handler for the extra settings added to the taxonomy vocab form.
*
* Check to see if the user has selected a different menu, and only rebuild
* if this is the case.
*
* @see taxonomy_menu_vocab_submit
*/
function menu_to_taxonomy_vocab_submit($form, &$form_state) {
$vid = $form_state['values']['vid'];
$changed = FALSE;
if (is_numeric($form_state['values']['menu_to_taxonomy']['vocab_parent'])) {
// Menu location has been set to disabled, don't want to throw notices.
$form_state['values']['menu_to_taxonomy']['vocab_parent'] = '0:0';
}
// Split the menu location into menu name and menu item id.
list($vocab_parent['vocab_menu'], $vocab_parent['vocab_parent']) = explode(':', $form_state['values']['menu_to_taxonomy']['vocab_parent']);
// Add maximum depth.
$vocab_parent['max_depth'] = $form_state['values']['menu_to_taxonomy']['max_depth'];
// Init flag variables to avoid notices if changes haven't happened.
$changed_menu = FALSE;
// Set the menu name and check for changes.
$variable_name = _menu_to_taxonomy_build_variable('vocab_menu', $vid);
if (_menu_to_taxonomy_check_variable($variable_name, $vocab_parent['vocab_menu'])) {
$changed_menu = TRUE;
}
variable_set($variable_name, $vocab_parent['vocab_menu']);
// Set the menu parent item and check for changes.
$variable_name = _menu_to_taxonomy_build_variable('vocab_parent', $vid);
if (_menu_to_taxonomy_check_variable($variable_name, $vocab_parent['vocab_parent'])) {
$changed_menu = TRUE;
}
variable_set($variable_name, $vocab_parent['vocab_parent']);
// Set up the maximum depth for the menu and check for changes.
$variable_name = _menu_to_taxonomy_build_variable('max_depth', $vid);
if (_menu_to_taxonomy_check_variable($variable_name, $vocab_parent['max_depth'])) {
$changed_menu = TRUE;
}
variable_set($variable_name, $vocab_parent['max_depth']);
// If the menu hasn't changed and is disabled then do not do anything else.
if ($form_state['values']['menu_to_taxonomy']['rebuild'] ||
$changed_menu ||
(!$changed_menu && variable_get(_menu_to_taxonomy_build_variable('vocab_menu', $vid), FALSE) == 0)) {
// Rebuild if rebuild is selected or menu has changed.
if ($form_state['values']['menu_to_taxonomy']['rebuild'] || $changed_menu) {
$message = _menu_to_taxonomy_rebuild($vid);
}
// If setting has changed and a menu item is enabled, then update all of
// the menu items.
elseif ($changed && variable_get(_menu_to_taxonomy_build_variable('vocab_menu', $vid), FALSE)) {
$message = _menu_to_taxonomy_update_link_items($vid);
}
// Only send a message if one has been created.
if (isset($message) && $message) {
// $message is sanitized coming out of its source function,
// no need to reclean it here.
drupal_set_message($message, 'status');
}
}
}
/**
* Builds a variable from the supplied name and machine name of the vocabulary.
*
* @param string $name
* String to be added to the returned variable.
* @param int $vid
* VID of the vocabulary from which the machine name will be taken.
*
* @return bool|string
* The variable name or FALSE.
*/
function _menu_to_taxonomy_build_variable($name, $vid) {
$vocabulary = taxonomy_vocabulary_load($vid);
if ($vocabulary) {
return 'menu_to_taxonomy_' . $name . '_' . $vocabulary->machine_name;
}
else {
return FALSE;
}
}
/**
* Checks to see if the variable has changed.
*
* @param string $variable
* The name of the variable.
*
* @return bool
* TRUE if it has changed.
*/
function _menu_to_taxonomy_check_variable($variable, $new_value) {
if ($new_value != variable_get($variable, FALSE)) {
return TRUE;
}
return FALSE;
}
/**
* Implements hook_menu_link_insert().
*/
function menu_to_taxonomy_menu_link_insert($link) {
_menu_to_taxonomy_term_save($link);
}
/**
* Implements hook_menu_link_update().
*/
function menu_to_taxonomy_menu_link_update($link) {
_menu_to_taxonomy_term_save($link);
}
/**
* Implements hook_menu_link_delete().
*/
function menu_to_taxonomy_menu_link_delete($link) {
$vocabularies_to_update = _menu_to_taxonomy_get_vocabularies_to_update($link);
foreach ($vocabularies_to_update as $vid) {
$tid = _menu_to_taxonomy_get_tid($link['mlid'], $vid);
$term = taxonomy_term_load($tid);
if (empty($term)) {
return;
}
// No worries about the children, as these already have been reasssigned to
// the parent of the deleted term.
taxonomy_term_delete($term->tid);
}
}
/**
* Find out which vocabularies we need to update when adding a given link.
*
* @param array $link
* Menu link item.
*
* @return array
* Vids to update
*/
function _menu_to_taxonomy_get_vocabularies_to_update(array $link) {
$vocabularies_to_update = array();
// Load all vocabularies.
$vocabularies = taxonomy_vocabulary_load_multiple(FALSE);
foreach ($vocabularies as $vocabulary) {
// Check whether this vocabulary needs to be synced with the menu this link
// belongs to.
$variable_name = _menu_to_taxonomy_build_variable('vocab_menu', $vocabulary->vid);
if (variable_get($variable_name, FALSE) == $link['menu_name']) {
// Get the menu parent item under which we start syncing terms.
$parent_mlid = variable_get(_menu_to_taxonomy_build_variable('vocab_parent', $vocabulary->vid), FALSE);
if (!empty($parent_mlid)) {
$parent_link = array(
'mlid' => $parent_mlid,
'menu_name' => $link['menu_name'],
);
// Get all children of the menu parent item under which we start syncing
// terms.
$children = _menu_to_taxonomy_get_all_menu_children($parent_link);
// Test whether the parent of the menu link item being saved is amongst
// children of the menu parent item under which we start syncing terms.
if ($link['plid'] == $parent_link['mlid'] || in_array($link['plid'], $children)) {
// Do nothing.
}
else {
// No need to update this vocabulary.
continue;
}
}
// Menu link item complies with the menu condition (in any case) and the
// parent condition (if defined), meaning this vocabulary will be updated.
$vocabularies_to_update[] = $vocabulary->vid;
}
}
return $vocabularies_to_update;
}
/**
* Updates/creates a linked term and saves a DB record for the mlid/tid link.
*
* @param array $link
* Menu link item array.
*/
function _menu_to_taxonomy_term_save(array $link) {
global $menu_to_taxonomy_skip_term_access;
if (empty($link['mlid']) || empty($link['menu_name']) || empty($link['link_title'])) {
return;
}
// Cleanup title if needed.
$link['link_title'] = str_replace('
', ' ', $link['link_title']);
$link['link_title'] = str_replace('
', ' ', $link['link_title']);
$link['link_title'] = str_replace('
', ' ', $link['link_title']);
$link['link_title'] = strip_tags($link['link_title']);
// Find out which vocabularies we need to update.
$vocabularies_to_update = _menu_to_taxonomy_get_vocabularies_to_update($link);
// Defaults to use, whether creating a new term or updating an existing one.
$term_defaults = (object) array(
// Use the Link title as the term name.
'name' => $link['link_title'],
// NOTE: this copies the weight verbatim!
'weight' => $link['weight'],
// Using the termstatus module.
'status' => ($link['hidden'] == 1) ? 0 : 1,
);
// Get the description from the menu link item, if it has been set.
if (!empty($link['options']['attributes']['title'])) {
$term_defaults->description = $link['options']['attributes']['title'];
}
foreach ($vocabularies_to_update as $vid) {
// Get the terms linked to this menu item for this vocabulary.
$tid = _menu_to_taxonomy_get_tid($link['mlid'], $vid);
// Make sure that the link isn't deeper than the maximum depth defined for
// this vocabulary.
$max_depth = variable_get(_menu_to_taxonomy_build_variable('max_depth', $vid), 0);
if ($max_depth > 0 && $link['depth'] > $max_depth) {
// Check if a term already exists.
if (!empty($tid)) {
// Term is out-of-scope, delete it.
taxonomy_term_delete($tid);
}
// Skip this vocabulary.
continue;
}
// Set default.
$term = clone $term_defaults;
// Get the parent for this vocabulary.
if (isset($link['plid'])) {
// Get the terms associated to the parent's menu link item ID.
$parent_tid = _menu_to_taxonomy_get_tid($link['plid'], $vid);
if (!empty($parent_tid)) {
$term->parent = array($parent_tid);
}
}
if (empty($tid)) {
// This will be a new term.
$term->vid = $vid;
}
else {
// This is an existing term.
$term_existing = _menu_to_taxonomy_load_term_without_access_control($tid);
if (empty($term_existing)) {
throw new Exception('Could not synchronize term.');
}
// Overwrite values of existing term.
foreach ($term as $property => $value) {
$term_existing->{$property} = $value;
}
// Save the term with the overwitten values.
unset($term);
$term = clone $term_existing;
}
// Save the term and its menu link item sync record.
_menu_to_taxonomy_term_save_term_and_sync($term, $link);
}
}
/**
* Loads a taxonomy term while ignoring access control query tags.
*
* @param int $tid
* Term ID.
*
* @return object
* The taxonomy term object.
*/
function _menu_to_taxonomy_load_term_without_access_control($tid) {
// Get original implementations for term_access query tag into
// "module_implements" static cache, so we can temporarily override these.
module_implements('query_term_access_alter');
// Override statically cached "module_implements" implementations, skipping
// term_access query tag during term load so we also load inaccessible terms.
// Alternatively we could have just reset the cache by calling
// "module_implements(FALSE, FALSE, TRUE);" but this would have been slower.
$implementations = &drupal_static('module_implements');
if (isset($implementations['query_term_access_alter'])) {
// Save original query tags so we can restore these later.
$original = $implementations['query_term_access_alter'];
}
// Temporarily override the query tags, turning off term access control.
$implementations['query_term_access_alter'] = array();
// Reset the drupal_alter cache so that module_implements data will be reloaded.
// for loading the query tag hooks.
drupal_static_reset('drupal_alter');
// Reset the taxonomy term cache so we don't get any old results.
entity_get_controller('taxonomy_term')->resetCache(array($tid));
// Get the term.
$term = taxonomy_term_load($tid);
// As we only needed to override during the previous taxonomy_term_load()
// call, stop overriding here and reset static cache to original value.
if (isset($original)) {
$implementations['query_term_access_alter'] = $original;
drupal_static_reset('drupal_alter');
// Reset the cached term.
entity_get_controller('taxonomy_term')->resetCache(array($tid));
}
return $term;
}
/**
* Saves a taxonomy term while ignoring access control query tags.
*
* @param object $term
* The taxonomy term object.
*/
function _menu_to_taxonomy_save_term_without_access_control($term) {
// Get original implementations for term_access query tag into
// "module_implements" static cache, so we can temporarily override these.
module_implements('query_term_access_alter');
// Override statically cached "module_implements" implementations, skipping
// term_access query tag during term load so we also load inaccessible terms.
// Alternatively we could have just reset the cache by calling
// "module_implements(FALSE, FALSE, TRUE);" but this would have been slower.
$implementations = &drupal_static('module_implements');
if (isset($implementations['query_term_access_alter'])) {
// Save original query tags so we can restore these later.
$original = $implementations['query_term_access_alter'];
}
// Temporarily override the query tags, turning off term access control.
$implementations['query_term_access_alter'] = array();
// Reset the drupal_alter cache so that module_implements data will be reloaded.
// for loading the query tag hooks.
drupal_static_reset('drupal_alter');
// Reset the taxonomy term cache so we don't get any old results.
entity_get_controller('taxonomy_term')->resetCache(array($term->tid));
// Save the term.
taxonomy_term_save($term);
// As we only needed to override during the previous taxonomy_term_load()
// call, stop overriding here and reset static cache to original value.
if (isset($original)) {
$implementations['query_term_access_alter'] = $original;
drupal_static_reset('drupal_alter');
// Reset the cached term.
entity_get_controller('taxonomy_term')->resetCache(array($term->tid));
}
return $term;
}
/**
* Saves a term and its menu link item sync record.
*
* @param object $term
* The term object.
* @param array $link
* The link array.
*
* @throws Exception
* If something goes wrong during save.
*/
function _menu_to_taxonomy_term_save_term_and_sync($term, array $link) {
$transaction = db_transaction();
try {
// Save the term.
_menu_to_taxonomy_save_term_without_access_control($term);
// Save the link between the menu link item <=> term ID.
$record = array(
'tid' => $term->tid,
'vid' => $term->vid,
'mlid' => $link['mlid'],
);
_menu_to_taxonomy_record_save($record);
}
catch (Exception $e) {
$transaction->rollback();
watchdog_exception('menu_to_taxonomy', $e);
throw $e;
}
// Clear cache for this term.
entity_get_controller('taxonomy_term')->resetCache(array($term->tid));
}
/**
* Implements hook_taxonomy_term_delete().
*
* Invoked by taxonomy_term_delete() which will also run upon vocabulary
* deletion or menu link item deletion (through
* menu_to_taxonomy_menu_link_delete).
*/
function menu_to_taxonomy_taxonomy_term_delete($term) {
// When a term is deleted, delete the sync record.
db_delete('menu_to_taxonomy')
->condition('tid', $term->tid)
->execute();
}
/**
* Rebuilds a vocabulary.
*
* @param int $vid
* The vocabulary ID.
*
* @return string
* Message that is displayed.
*/
function _menu_to_taxonomy_rebuild($vid) {
// Remove all of the menu items for this vocabulary.
_menu_to_taxonomy_delete_all_terms($vid);
// Only insert the terms if a menu is set.
if (variable_get(_menu_to_taxonomy_build_variable('vocab_menu', $vid), FALSE)) {
_menu_to_taxonomy_insert_terms_batch($vid);
return t('The Menu structure in this vocabulary has been rebuilt.');
}
return t('The terms linked to menu items in this vocabulary have been removed.');
}