diff --git a/wp-includes/default-filters.php b/wp-includes/default-filters.php index a47f68642..3a7c22e06 100644 --- a/wp-includes/default-filters.php +++ b/wp-includes/default-filters.php @@ -117,6 +117,10 @@ foreach ( array( 'term_name_rss' ) as $filter ) { add_filter( $filter, 'convert_chars' ); } +// Pre save hierarchy +add_filter( 'wp_insert_post_parent', 'wp_check_post_hierarchy_for_loops', 10, 2 ); +add_filter( 'wp_update_term_parent', 'wp_check_term_hierarchy_for_loops', 10, 3 ); + // Display filters add_filter( 'the_title', 'wptexturize' ); add_filter( 'the_title', 'convert_chars' ); diff --git a/wp-includes/functions.php b/wp-includes/functions.php index db5e2e863..6427c77a7 100644 --- a/wp-includes/functions.php +++ b/wp-includes/functions.php @@ -4338,4 +4338,74 @@ function _wp_mysql_week( $column ) { } } -?> +/** + * Finds hierarchy loops using a callback function that maps objects to parents. + * + * @since 3.1 + * + * @param callback $callback function that accepts ( ID, callback_arg, ... ) and outputs parent_ID + * @param $start The ID to start the loop check at + * @param $start_parent the parent_ID of $start to use instead of calling $callback( $start ). Use null to always use $callback + * @param array $override an array of ( ID => parent_ID, ... ) to use instead of $callback + * @param array $callback_arg optional additional arguments to send to $callback + * + * @internal + * + * @return array IDs of all members of loop + */ +function wp_find_hierarchy_loop( $callback, $start, $start_parent, $callback_args = array() ) { + $override = is_null( $start_parent ) ? array() : array( $start => $start_parent ); + + echo "wp_find_hierarchy_loop: $callback, $start, $callback_args\n"; + if ( !$arbitrary_loop_member = wp_find_hierarchy_loop_tortoise_hare( $callback, $start, $override, $callback_args ) ) + return array(); + + return wp_find_hierarchy_loop_tortoise_hare( $callback, $arbitrary_loop_member, $override, $callback_args, true ); +} + +/** + * Uses the "The Tortoise and the Hare" algorithm to detect loops. + * + * For every step of the algorithm, the hare takes two steps and the tortoise one. + * If the hare ever laps the tortoise, there must be a loop. + * + * @since 3.1 + * + * @param callback $callback function that accupts ( ID, callback_arg, ... ) and outputs parent_ID + * @param $start The ID to start the loop check at + * @param array $override an array of ( ID => parent_ID, ... ) to use instead of $callback + * @param array $callback_args optional additional arguments to send to $callback + * @param bool $_return_loop Return loop members or just detect presence of loop? + * Only set to true if you already know the given $start is part of a loop + * (otherwise the returned array might include branches) + * + * @internal + * + * @return mixed scalar ID of some arbitrary member of the loop, or array of IDs of all members of loop if $_return_loop + */ +function wp_find_hierarchy_loop_tortoise_hare( $callback, $start, $override = array(), $callback_args = array(), $_return_loop = false ) { + $tortoise = $hare = $evanescent_hare = $start; + $return = array(); + + // Set evanescent_hare to one past hare + // Increment hare two steps + while ( + $tortoise + && + ( $evanescent_hare = isset( $override[$hare] ) ? $override[$hare] : call_user_func_array( $callback, array_merge( array( $hare ), $callback_args ) ) ) + && + ( $hare = isset( $override[$evanescent_hare] ) ? $override[$evanescent_hare] : call_user_func_array( $callback, array_merge( array( $evanescent_hare ), $callback_args ) ) ) + ) { + if ( $_return_loop ) + $return[$tortoise] = $return[$evanescent_hare] = $return[$hare] = true; + + // tortoise got lapped - must be a loop + if ( $tortoise == $evanescent_hare || $tortoise == $hare ) + return $_return_loop ? $return : $tortoise; + + // Increment tortoise by one step + $tortoise = isset( $override[$tortoise] ) ? $override[$tortoise] : call_user_func_array( $callback, array_merge( array( $tortoise ), $callback_args ) ); + } + + return false; +} diff --git a/wp-includes/post.php b/wp-includes/post.php index 57f0795e9..9b270244e 100644 --- a/wp-includes/post.php +++ b/wp-includes/post.php @@ -2337,17 +2337,8 @@ function wp_insert_post($postarr, $wp_error = false) { else $post_parent = 0; - if ( !empty($post_ID) ) { - if ( $post_parent == $post_ID ) { - // Post can't be its own parent - $post_parent = 0; - } elseif ( !empty($post_parent) ) { - $parent_post = get_post($post_parent); - // Check for circular dependency - if ( isset( $parent_post->post_parent ) && $parent_post->post_parent == $post_ID ) - $post_parent = 0; - } - } + // Check the post_parent to see if it will cause a hierarchy loop + $post_parent = apply_filters( 'wp_insert_post_parent', $post_parent, $post_ID, compact( array_keys( $postarr ) ), $postarr ); if ( isset($menu_order) ) $menu_order = (int) $menu_order; @@ -4804,6 +4795,64 @@ function _show_post_preview() { } } +/** + * Returns the post's parent's post_ID + * + * @since 3.1 + * + * @param int $post_id + * + * @return int|bool false on error + */ +function wp_get_post_parent_id( $post_ID ) { + $post = get_post( $post_ID ); + if ( !$post || is_wp_error( $post ) ) + return false; + return (int) $post->post_parent; +} + +/** + * Checks the given subset of the post hierarchy for hierarchy loops. + * Prevents loops from forming and breaks those that it finds. + * + * Attached to the wp_insert_post_parent filter. + * + * @since 3.1 + * @uses wp_find_hierarchy_loop() + * + * @param int $post_parent ID of the parent for the post we're checking. + * @parem int $post_ID ID of the post we're checking. + * + * @return int The new post_parent for the post. + */ +function wp_check_post_hierarchy_for_loops( $post_parent, $post_ID ) { + // Nothing fancy here - bail + if ( !$post_parent ) + return 0; + + // New post can't cause a loop + if ( empty( $post_ID ) ) + return $post_parent; + + // Can't be its own parent + if ( $post_parent == $post_ID ) + return 0; + + // Now look for larger loops + + if ( !$loop = wp_find_hierarchy_loop( 'wp_get_post_parent_id', $post_ID, $post_parent ) ) + return $post_parent; // No loop + + // Setting $post_parent to the given value causes a loop + if ( isset( $loop[$post_ID] ) ) + return 0; + + // There's a loop, but it doesn't contain $post_ID. Break the loop. + foreach ( array_keys( $loop ) as $loop_member ) + wp_update_post( array( 'ID' => $loop_member, 'post_parent' => 0 ) ); + + return $post_parent; +} /** * Default post information to use when populating the "Write Post" form. diff --git a/wp-includes/taxonomy.php b/wp-includes/taxonomy.php index 04699f177..548ce8209 100644 --- a/wp-includes/taxonomy.php +++ b/wp-includes/taxonomy.php @@ -2039,6 +2039,9 @@ function wp_update_term( $term_id, $taxonomy, $args = array() ) { } } + // Check $parent to see if it will cause a hierarchy loop + $parent = apply_filters( 'wp_update_term_parent', $parent, $term_id, $taxonomy, compact( array_keys( $args ) ), $args ); + // Check for duplicate slug $id = $wpdb->get_var( $wpdb->prepare( "SELECT term_id FROM $wpdb->terms WHERE slug = %s", $slug ) ); if ( $id && ($id != $term_id) ) { @@ -2879,3 +2882,62 @@ function get_terms_to_edit( $post_id, $taxonomy = 'post_tag' ) { return $tags_to_edit; } + +/** + * Returns the term's parent's term_ID + * + * @since 3.1 + * + * @param int $term_id + * @param string $taxonomy + * + * @return int|bool false on error + */ +function wp_get_term_taxonomy_parent_id( $term_id, $taxonomy ) { + $term = get_term( $term_id, $taxonomy ); + if ( !$term || is_wp_error( $term ) ) + return false; + return (int) $term->parent; +} + +/** + * Checks the given subset of the term hierarchy for hierarchy loops. + * Prevents loops from forming and breaks those that it finds. + * + * Attached to the wp_update_term_parent filter. + * + * @since 3.1 + * @uses wp_find_hierarchy_loop() + * + * @param int $parent term_id of the parent for the term we're checking. + * @param int $term_id The term we're checking. + * @param string $taxonomy The taxonomy of the term we're checking. + * + * @return int The new parent for the term. + */ +function wp_check_term_hierarchy_for_loops( $parent, $term_id, $taxonomy ) { + // Nothing fancy here - bail + if ( !$parent ) + return 0; + + // Can't be its own parent + if ( $parent == $term_id ) + return 0; + + echo "larger loops\n"; + + // Now look for larger loops + + if ( !$loop = wp_find_hierarchy_loop( 'wp_get_term_taxonomy_parent_id', $term_id, $parent, array( $taxonomy ) ) ) + return $parent; // No loop + + // Setting $parent to the given value causes a loop + if ( isset( $loop[$term_id] ) ) + return 0; + + // There's a loop, but it doesn't contain $term_id. Break the loop. + foreach ( array_keys( $loop ) as $loop_member ) + wp_update_term( $loop_member, $taxonomy, array( 'parent' => 0 ) ); + + return $parent; +}