diff --git a/src/wp-admin/includes/upgrade.php b/src/wp-admin/includes/upgrade.php index f2f9aa7f85..e7d7dd782a 100644 --- a/src/wp-admin/includes/upgrade.php +++ b/src/wp-admin/includes/upgrade.php @@ -1505,8 +1505,9 @@ function upgrade_430() { upgrade_430_fix_comments(); } + // Shared terms are split in a separate process. if ( $wp_current_db_version < 32814 ) { - split_all_shared_terms(); + wp_schedule_single_event( time() + ( 1 * MINUTE_IN_SECONDS ), 'wp_split_shared_term_batch' ); } if ( $wp_current_db_version < 33055 && 'utf8mb4' === $wpdb->charset ) { @@ -1880,76 +1881,6 @@ function maybe_convert_table_to_utf8mb4( $table ) { return $wpdb->query( "ALTER TABLE $table CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci" ); } -/** - * Splits all shared taxonomy terms. - * - * @since 4.3.0 - * - * @global wpdb $wpdb WordPress database abstraction object. - */ -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 - LEFT JOIN {$wpdb->terms} t ON t.term_id = tt.term_id - GROUP BY t.term_id - 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 ); -} - /** * Retrieve all options as it was for 1.2. * diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 46fafc18b3..b6f0ce303e 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -328,9 +328,11 @@ add_filter( 'determine_current_user', 'wp_validate_auth_cookie' ); add_filter( 'determine_current_user', 'wp_validate_logged_in_cookie', 20 ); // Split term updates. +add_action( 'admin_init', '_wp_check_for_scheduled_split_terms' ); add_action( 'split_shared_term', '_wp_check_split_default_terms', 10, 4 ); add_action( 'split_shared_term', '_wp_check_split_terms_in_menus', 10, 4 ); add_action( 'split_shared_term', '_wp_check_split_nav_menu_terms', 10, 4 ); +add_action( 'wp_split_shared_term_batch', '_wp_batch_split_terms' ); /** * Filters formerly mixed into wp-includes diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index 8e3e03d8d5..ba8156bc63 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -4249,12 +4249,6 @@ function _split_shared_term( $term_id, $term_taxonomy_id, $record = true ) { $term_taxonomy_id = intval( $term_taxonomy->term_taxonomy_id ); } - // Don't try to split terms if database schema does not support shared slugs. - $current_db_version = get_option( 'db_version' ); - if ( $current_db_version < 30133 ) { - return $term_id; - } - // If there are no shared term_taxonomy rows, there's nothing to do here. $shared_tt_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_taxonomy tt WHERE tt.term_id = %d AND tt.term_taxonomy_id != %d", $term_id, $term_taxonomy_id ) ); @@ -4262,6 +4256,15 @@ function _split_shared_term( $term_id, $term_taxonomy_id, $record = true ) { return $term_id; } + /* + * Verify that the term_taxonomy_id passed to the function is actually associated with the term_id. + * If there's a mismatch, it may mean that the term is already split. Return the actual term_id from the db. + */ + $check_term_id = $wpdb->get_var( $wpdb->prepare( "SELECT term_id FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = %d", $term_taxonomy_id ) ); + if ( $check_term_id != $term_id ) { + return $check_term_id; + } + // Pull up data about the currently shared slug, which we'll use to populate the new one. if ( empty( $shared_term ) ) { $shared_term = $wpdb->get_row( $wpdb->prepare( "SELECT t.* FROM $wpdb->terms t WHERE t.term_id = %d", $term_id ) ); @@ -4338,6 +4341,116 @@ function _split_shared_term( $term_id, $term_taxonomy_id, $record = true ) { return $new_term_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 + LEFT JOIN {$wpdb->terms} t ON t.term_id = tt.term_id + GROUP BY t.term_id + HAVING term_tt_count > 1 + LIMIT 20" + ); + + // 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. + $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 ); + + delete_option( $lock_name ); +} + +/** + * In order to avoid the wp_batch_split_terms() job being accidentally removed, + * check that it's still scheduled while we haven't finished splitting terms. + * + * @ignore + * @since 4.3.0 + */ +function _wp_check_for_scheduled_split_terms() { + if ( ! get_option( 'finished_splitting_shared_terms' ) && ! wp_next_scheduled( 'wp_batch_split_terms' ) ) { + wp_schedule_single_event( 'wp_batch_split_terms', time() + MINUTE_IN_SECONDS ); + } +} + /** * Check default categories when a term gets split to see if any of them need to be updated. *