/** * Update term based on arguments provided. * * The $args will indiscriminately override all values with the same field name. * Care must be taken to not override important information need to update or * update will fail (or perhaps create a new term, neither would be acceptable). * * Defaults will set 'alias_of', 'description', 'parent', and 'slug' if not * defined in $args already. * * 'alias_of' will create a term group, if it doesn't already exist, and update * it for the $term. * * If the 'slug' argument in $args is missing, then the 'name' in $args will be * used. It should also be noted that if you set 'slug' and it isn't unique then * a WP_Error will be passed back. If you don't pass any slug, then a unique one * will be created for you. * * For what can be overrode in `$args`, check the term scheme can contain and stay * away from the term keys. * * @since 2.3.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $term_id The ID of the term * @param string $taxonomy The context in which to relate the term to the object. * @param array|string $args Optional. Array of get_terms() arguments. Default empty array. * @return array|WP_Error Returns Term ID and Taxonomy Term ID */ function wp_update_term($term_id, $taxonomy, $args = array()) { global $wpdb; if (!taxonomy_exists($taxonomy)) { return new WP_Error('invalid_taxonomy', __('Invalid taxonomy')); } $term_id = (int) $term_id; // First, get all of the original args $term = get_term($term_id, $taxonomy, ARRAY_A); if (is_wp_error($term)) { return $term; } if (!$term) { return new WP_Error('invalid_term', __('Empty Term')); } // Escape data pulled from DB. $term = wp_slash($term); // Merge old and new args with new args overwriting old ones. $args = array_merge($term, $args); $defaults = array('alias_of' => '', 'description' => '', 'parent' => 0, 'slug' => ''); $args = wp_parse_args($args, $defaults); $args = sanitize_term($args, $taxonomy, 'db'); $parsed_args = $args; // expected_slashed ($name) $name = wp_unslash($args['name']); $description = wp_unslash($args['description']); $parsed_args['name'] = $name; $parsed_args['description'] = $description; if ('' == trim($name)) { return new WP_Error('empty_term_name', __('A name is required for this term')); } if ($parsed_args['parent'] > 0 && !term_exists((int) $parsed_args['parent'])) { return new WP_Error('missing_parent', __('Parent term does not exist.')); } $empty_slug = false; if (empty($args['slug'])) { $empty_slug = true; $slug = sanitize_title($name); } else { $slug = $args['slug']; } $parsed_args['slug'] = $slug; $term_group = isset($parsed_args['term_group']) ? $parsed_args['term_group'] : 0; if ($args['alias_of']) { $alias = get_term_by('slug', $args['alias_of'], $taxonomy); if (!empty($alias->term_group)) { // The alias we want is already in a group, so let's use that one. $term_group = $alias->term_group; } elseif (!empty($alias->term_id)) { /* * The alias is not in a group, so we create a new one * and add the alias to it. */ $term_group = $wpdb->get_var("SELECT MAX(term_group) FROM {$wpdb->terms}") + 1; wp_update_term($alias->term_id, $taxonomy, array('term_group' => $term_group)); } $parsed_args['term_group'] = $term_group; } /** * Filter the term parent. * * Hook to this filter to see if it will cause a hierarchy loop. * * @since 3.1.0 * * @param int $parent ID of the parent term. * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. * @param array $parsed_args An array of potentially altered update arguments for the given term. * @param array $args An array of update arguments for the given term. */ $parent = apply_filters('wp_update_term_parent', $args['parent'], $term_id, $taxonomy, $parsed_args, $args); // Check for duplicate slug $duplicate = get_term_by('slug', $slug, $taxonomy); if ($duplicate && $duplicate->term_id != $term_id) { // If an empty slug was passed or the parent changed, reset the slug to something unique. // Otherwise, bail. if ($empty_slug || $parent != $term['parent']) { $slug = wp_unique_term_slug($slug, (object) $args); } else { return new WP_Error('duplicate_term_slug', sprintf(__('The slug “%s” is already in use by another term'), $slug)); } } $tt_id = $wpdb->get_var($wpdb->prepare("SELECT tt.term_taxonomy_id FROM {$wpdb->term_taxonomy} AS tt INNER JOIN {$wpdb->terms} AS t ON tt.term_id = t.term_id WHERE tt.taxonomy = %s AND t.term_id = %d", $taxonomy, $term_id)); // Check whether this is a shared term that needs splitting. $_term_id = _split_shared_term($term_id, $tt_id); if (!is_wp_error($_term_id)) { $term_id = $_term_id; } /** * Fires immediately before the given terms are edited. * * @since 2.9.0 * * @param int $term_id Term ID. * @param string $taxonomy Taxonomy slug. */ do_action('edit_terms', $term_id, $taxonomy); $wpdb->update($wpdb->terms, compact('name', 'slug', 'term_group'), compact('term_id')); if (empty($slug)) { $slug = sanitize_title($name, $term_id); $wpdb->update($wpdb->terms, compact('slug'), compact('term_id')); } /** * Fires immediately after the given terms are edited. * * @since 2.9.0 * * @param int $term_id Term ID * @param string $taxonomy Taxonomy slug. */ do_action('edited_terms', $term_id, $taxonomy); /** * Fires immediate before a term-taxonomy relationship is updated. * * @since 2.9.0 * * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ do_action('edit_term_taxonomy', $tt_id, $taxonomy); $wpdb->update($wpdb->term_taxonomy, compact('term_id', 'taxonomy', 'description', 'parent'), array('term_taxonomy_id' => $tt_id)); /** * Fires immediately after a term-taxonomy relationship is updated. * * @since 2.9.0 * * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ do_action('edited_term_taxonomy', $tt_id, $taxonomy); // Clean the relationship caches for all object types using this term. $objects = $wpdb->get_col($wpdb->prepare("SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id = %d", $tt_id)); $tax_object = get_taxonomy($taxonomy); foreach ($tax_object->object_type as $object_type) { clean_object_term_cache($objects, $object_type); } /** * Fires after a term has been updated, but before the term cache has been cleaned. * * @since 2.3.0 * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ do_action("edit_term", $term_id, $tt_id, $taxonomy); /** * Fires after a term in a specific taxonomy has been updated, but before the term * cache has been cleaned. * * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug. * * @since 2.3.0 * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. */ do_action("edit_{$taxonomy}", $term_id, $tt_id); /** This filter is documented in wp-includes/taxonomy.php */ $term_id = apply_filters('term_id_filter', $term_id, $tt_id); clean_term_cache($term_id, $taxonomy); /** * Fires after a term has been updated, and the term cache has been cleaned. * * @since 2.3.0 * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ do_action("edited_term", $term_id, $tt_id, $taxonomy); /** * Fires after a term for a specific taxonomy has been updated, and the term * cache has been cleaned. * * The dynamic portion of the hook name, `$taxonomy`, refers to the taxonomy slug. * * @since 2.3.0 * * @param int $term_id Term ID. * @param int $tt_id Term taxonomy ID. */ do_action("edited_{$taxonomy}", $term_id, $tt_id); return array('term_id' => $term_id, 'term_taxonomy_id' => $tt_id); }
/** * Splits a batch of shared taxonomy terms. * * @since 4.3.0 * * @global wpdb $wpdb WordPress database abstraction object. */ function _wp_batch_split_terms() { global $wpdb; $lock_name = 'term_split.lock'; // Try to lock. $lock_result = $wpdb->query($wpdb->prepare("INSERT IGNORE INTO `{$wpdb->options}` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", $lock_name, time())); if (!$lock_result) { $lock_result = get_option($lock_name); // Bail if we were unable to create a lock, or if the existing lock is still valid. if (!$lock_result || $lock_result > time() - HOUR_IN_SECONDS) { wp_schedule_single_event(time() + 5 * MINUTE_IN_SECONDS, 'wp_split_shared_term_batch'); return; } } // Update the lock, as by this point we've definitely got a lock, just need to fire the actions. update_option($lock_name, time()); // Get a list of shared terms (those with more than one associated row in term_taxonomy). $shared_terms = $wpdb->get_results("SELECT tt.term_id, t.*, count(*) as term_tt_count FROM {$wpdb->term_taxonomy} tt\n\t\t LEFT JOIN {$wpdb->terms} t ON t.term_id = tt.term_id\n\t\t GROUP BY t.term_id\n\t\t HAVING term_tt_count > 1\n\t\t LIMIT 10"); // No more terms, we're done here. if (!$shared_terms) { update_option('finished_splitting_shared_terms', true); delete_option($lock_name); return; } // Shared terms found? We'll need to run this script again. wp_schedule_single_event(time() + 2 * MINUTE_IN_SECONDS, 'wp_split_shared_term_batch'); // Rekey shared term array for faster lookups. $_shared_terms = array(); foreach ($shared_terms as $shared_term) { $term_id = intval($shared_term->term_id); $_shared_terms[$term_id] = $shared_term; } $shared_terms = $_shared_terms; // Get term taxonomy data for all shared terms. $shared_term_ids = implode(',', array_keys($shared_terms)); $shared_tts = $wpdb->get_results("SELECT * FROM {$wpdb->term_taxonomy} WHERE `term_id` IN ({$shared_term_ids})"); // Split term data recording is slow, so we do it just once, outside the loop. $split_term_data = get_option('_split_terms', array()); $skipped_first_term = $taxonomies = array(); foreach ($shared_tts as $shared_tt) { $term_id = intval($shared_tt->term_id); // Don't split the first tt belonging to a given term_id. if (!isset($skipped_first_term[$term_id])) { $skipped_first_term[$term_id] = 1; continue; } if (!isset($split_term_data[$term_id])) { $split_term_data[$term_id] = array(); } // Keep track of taxonomies whose hierarchies need flushing. if (!isset($taxonomies[$shared_tt->taxonomy])) { $taxonomies[$shared_tt->taxonomy] = 1; } // Split the term. $split_term_data[$term_id][$shared_tt->taxonomy] = _split_shared_term($shared_terms[$term_id], $shared_tt, false); } // Rebuild the cached hierarchy for each affected taxonomy. foreach (array_keys($taxonomies) as $tax) { delete_option("{$tax}_children"); _get_term_hierarchy($tax); } update_option('_split_terms', $split_term_data); delete_option($lock_name); }
/** * Split all shared taxonomy terms. * * @since 4.3.0 */ function split_all_shared_terms() { global $wpdb; // Get a list of shared terms (those with more than one associated row in term_taxonomy). $shared_terms = $wpdb->get_results("SELECT tt.term_id, t.*, count(*) as term_tt_count FROM {$wpdb->term_taxonomy} tt\n\t\t LEFT JOIN {$wpdb->terms} t ON t.term_id = tt.term_id\n\t\t GROUP BY t.term_id\n\t\t HAVING term_tt_count > 1"); if (empty($shared_terms)) { return; } // Rekey shared term array for faster lookups. $_shared_terms = array(); foreach ($shared_terms as $shared_term) { $term_id = intval($shared_term->term_id); $_shared_terms[$term_id] = $shared_term; } $shared_terms = $_shared_terms; // Get term taxonomy data for all shared terms. $shared_term_ids = implode(',', array_keys($shared_terms)); $shared_tts = $wpdb->get_results("SELECT * FROM {$wpdb->term_taxonomy} WHERE `term_id` IN ({$shared_term_ids})"); // Split term data recording is slow, so we do it just once, outside the loop. $suspend = wp_suspend_cache_invalidation(true); $split_term_data = get_option('_split_terms', array()); $skipped_first_term = $taxonomies = array(); foreach ($shared_tts as $shared_tt) { $term_id = intval($shared_tt->term_id); // Don't split the first tt belonging to a given term_id. if (!isset($skipped_first_term[$term_id])) { $skipped_first_term[$term_id] = 1; continue; } if (!isset($split_term_data[$term_id])) { $split_term_data[$term_id] = array(); } // Keep track of taxonomies whose hierarchies need flushing. if (!isset($taxonomies[$shared_tt->taxonomy])) { $taxonomies[$shared_tt->taxonomy] = 1; } // Split the term. $split_term_data[$term_id][$shared_tt->taxonomy] = _split_shared_term($shared_terms[$term_id], $shared_tt, false); } // Rebuild the cached hierarchy for each affected taxonomy. foreach (array_keys($taxonomies) as $tax) { delete_option("{$tax}_children"); _get_term_hierarchy($tax); } wp_suspend_cache_invalidation($suspend); update_option('_split_terms', $split_term_data); }
public function split_shared_term($term_id, $new_term_id, $term_taxonomy_id, $taxonomy) { // avoid recursion static $avoid_recursion = false; if ($avoid_recursion) { return; } $avoid_recursion = true; $lang = $this->model->get_term_language($term_id); foreach ($this->model->get_translations('term', $term_id) as $key => $tr_id) { if ($lang->slug == $key) { $translations[$key] = $new_term_id; } else { $tr_term = get_term($tr_id, $taxonomy); $translations[$key] = _split_shared_term($tr_id, $tr_term->term_taxonomy_id); // hack translation ids sent by the form to avoid overwrite in PLL_Admin_Filters_Term::save_translations if (isset($_POST['term_tr_lang'][$key]) && $_POST['term_tr_lang'][$key] == $tr_id) { $_POST['term_tr_lang'][$key] = $translations[$key]; } } $this->model->set_term_language($translations[$key], $key); } $this->model->save_translations('term', $new_term_id, $translations); $avoid_recursion = false; }
/** * @todo: account for wp 4.2 term splitting (https://developer.wordpress.org/plugins/taxonomy/working-with-split-terms-in-wp-4-2/) */ function test_save_data_array_to_db__from_this_site__term_split() { //create term and term taxonomy $term = $this->new_model_obj_with_dependencies('Term', array('name' => 'Jaguar', 'slug' => 'jag')); $ttcar = $this->new_model_obj_with_dependencies('Term_Taxonomy', array('term_id' => $term->ID(), 'taxonomy' => 'cars', 'description' => 'A fast car')); $ttcat = $this->new_model_obj_with_dependencies('Term_Taxonomy', array('term_id' => $term->ID(), 'taxonomy' => 'cats', 'description' => 'A large black cat that likes to swim')); //create "csv" data for it (pretend exported) $csv_data = array('Term' => array($term->model_field_array()), 'Term_Taxonomy' => array($ttcar->model_field_array(), $ttcat->model_field_array())); $this->assertEquals($ttcat->get('term_id'), $ttcar->get('term_id')); //split the term in the "wp" way. Our model objet $ttcar will NOT get updated on its own $new_term_id_for_car = _split_shared_term($term->ID(), $ttcar->ID()); $ttcar = EEM_Term_Taxonomy::instance()->refresh_entity_map_from_db($ttcar->ID()); // echo "updated term taxonomy:";var_dump($ttcar->model_field_array()); $this->assertNotEquals($ttcat->get('term_id'), $ttcar->get('term_id')); //import it $new_mapping = EE_Import::instance()->save_data_rows_to_db($csv_data, false, array()); $ttcar = EEM_Term_Taxonomy::instance()->refresh_entity_map_from_db($ttcar->ID()); //when it's done importing, we should have saved a term-taxonomy for the new term, not re-inserted a term-taxonomy to the old term //and because it used the models, the model objects we have in scope should already be up-to-date $this->assertEquals($new_term_id_for_car, $ttcar->get('term_id')); }
/** * @ticket 30335 */ public function test_should_update_menus_on_term_split() { global $wpdb; $t1 = wp_insert_term('Foo Menu', 'category'); register_taxonomy('wptests_tax_6', 'post'); $t2 = wp_insert_term('Foo Menu', 'wptests_tax_6'); // Manually modify because shared terms shouldn't naturally occur. $wpdb->update($wpdb->term_taxonomy, array('term_id' => $t1['term_id']), array('term_taxonomy_id' => $t2['term_taxonomy_id']), array('%d'), array('%d')); $menu_id = wp_create_nav_menu(rand_str()); $cat_menu_item = wp_update_nav_menu_item($menu_id, 0, array('menu-item-type' => 'taxonomy', 'menu-item-object' => 'category', 'menu-item-object-id' => $t1['term_id'], 'menu-item-status' => 'publish')); $this->assertEquals($t1['term_id'], get_post_meta($cat_menu_item, '_menu_item_object_id', true)); $new_term_id = _split_shared_term($t1['term_id'], $t1['term_taxonomy_id']); $this->assertNotEquals($new_term_id, $t1['term_id']); $this->assertEquals($new_term_id, get_post_meta($cat_menu_item, '_menu_item_object_id', true)); }
/** * @ticket 33187 * @group navmenus */ public function test_nav_menu_locations_should_be_updated_on_split() { global $wpdb; $cat_term = wp_insert_term('Foo Menu', 'category'); $shared_term_id = $cat_term['term_id']; $nav_term_id = wp_create_nav_menu('Foo Menu'); $nav_term = get_term($nav_term_id, 'nav_menu'); // Manually modify because shared terms shouldn't naturally occur. $wpdb->update($wpdb->term_taxonomy, array('term_id' => $shared_term_id), array('term_taxonomy_id' => $nav_term->term_taxonomy_id)); set_theme_mod('nav_menu_locations', array('foo' => $shared_term_id)); // Splitsville. $new_term_id = _split_shared_term($shared_term_id, $nav_term->term_taxonomy_id); $locations = get_nav_menu_locations(); $this->assertEquals($new_term_id, $locations['foo']); }