Index: wp-includes/post-template.php =================================================================== --- wp-includes/post-template.php (revision 11807) +++ wp-includes/post-template.php (working copy) @@ -702,6 +702,8 @@ ); $r = wp_parse_args( $args, $defaults ); + if ( 'menu_order, post_title' == $r['sort_column'] && -1 != $r['depth'] ) + $r['sort_column'] = 'lft'; extract( $r, EXTR_SKIP ); $pages = get_pages($r); @@ -713,7 +715,10 @@ $output .= "\t"; if ( $show_option_none ) $output .= "\t\n"; - $output .= walk_page_dropdown_tree($pages, $depth, $r); + if ( 'lft' == $sort_solumn ) + $output .= walk_page_dropdown_tree($pages, $depth, $r, true); + else + $output .= walk_page_dropdown_tree($pages, $depth, $r, false); $output .= "\n"; } @@ -737,13 +742,15 @@ $defaults = array( 'depth' => 0, 'show_date' => '', 'date_format' => get_option('date_format'), - 'child_of' => 0, 'exclude' => '', + 'child_of' => 0, 'ancestor_of' => 0, 'exclude' => '', 'title_li' => __('Pages'), 'echo' => 1, 'authors' => '', 'sort_column' => 'menu_order, post_title', 'link_before' => '', 'link_after' => '' ); $r = wp_parse_args( $args, $defaults ); + if ( 'menu_order, post_title' == $r['sort_column'] && -1 != $r['depth'] ) + $r['sort_column'] = 'lft'; extract( $r, EXTR_SKIP ); $output = ''; @@ -766,7 +773,10 @@ global $wp_query; if ( is_page() || is_attachment() || $wp_query->is_posts_page ) $current_page = $wp_query->get_queried_object_id(); - $output .= walk_page_tree($pages, $r['depth'], $current_page, $r); + if ( 'lft' == $r['sort_column'] ) + $output .= walk_page_tree($pages, $r['depth'], $current_page, $r, true); + else + $output .= walk_page_tree($pages, $r['depth'], $current_page, $r, false); if ( $r['title_li'] ) $output .= ''; @@ -806,7 +816,7 @@ * @param array|string $args */ function wp_page_menu( $args = array() ) { - $defaults = array('sort_column' => 'menu_order, post_title', 'menu_class' => 'menu', 'echo' => true, 'link_before' => '', 'link_after' => ''); + $defaults = array('sort_column' => 'lft', 'menu_class' => 'menu', 'echo' => true, 'link_before' => '', 'link_after' => ''); $args = wp_parse_args( $args, $defaults ); $args = apply_filters( 'wp_page_menu_args', $args ); @@ -861,9 +871,13 @@ * @since 2.1.0 * @see Walker_Page::walk() for parameters and return description. */ -function walk_page_tree($pages, $depth, $current_page, $r) { - if ( empty($r['walker']) ) - $walker = new Walker_Page; +function walk_page_tree($pages, $depth, $current_page, $r, $mptt = false) { + if ( empty($r['walker']) ) { + if ( $mptt ) + $walker = new MPTT_Walker_Page; + else + $walker = new Walker_Page; + } else $walker = $r['walker']; @@ -880,8 +894,12 @@ */ function walk_page_dropdown_tree() { $args = func_get_args(); - if ( empty($args[2]['walker']) ) // the user's options are the third parameter - $walker = new Walker_PageDropdown; + if ( empty($args[2]['walker']) ) { // the user's options are the third parameter + if ( $args[3] ) + $walker = new MPTT_Walker_PageDropdown; + else + $walker = new Walker_PageDropdown; + } else $walker = $args[2]['walker']; Index: wp-includes/category.php =================================================================== --- wp-includes/category.php (revision 11807) +++ wp-includes/category.php (working copy) @@ -37,7 +37,7 @@ * @return array List of categories. */ function &get_categories( $args = '' ) { - $defaults = array( 'type' => 'category' ); + $defaults = array( 'type' => 'category', 'page' => 0, 'per_page' => 20 ); $args = wp_parse_args( $args, $defaults ); $taxonomy = apply_filters( 'get_categories_taxonomy', 'category', $args ); Index: wp-includes/taxonomy.php =================================================================== --- wp-includes/taxonomy.php (revision 11807) +++ wp-includes/taxonomy.php (working copy) @@ -434,7 +434,7 @@ $term_id = intval( $term_id ); - $terms = _get_term_hierarchy($taxonomy); + $terms = _get_term_hierarchy($taxonomy, 0, $term_id); if ( ! isset($terms[$term_id]) ) return array(); @@ -449,6 +449,7 @@ return $children; } + /** * Get sanitized Term field. * @@ -624,8 +625,9 @@ $defaults = array('orderby' => 'name', 'order' => 'ASC', 'hide_empty' => true, 'exclude' => '', 'exclude_tree' => '', 'include' => '', 'number' => '', 'fields' => 'all', 'slug' => '', 'parent' => '', - 'hierarchical' => true, 'child_of' => 0, 'get' => '', 'name__like' => '', - 'pad_counts' => false, 'offset' => '', 'search' => ''); + 'hierarchical' => true, 'child_of' => 0, 'ancestor_of' => 0, 'get' => '', + 'name__like' => '', 'pad_counts' => false, 'offset' => '', 'search' => '', + 'page' => 0, 'per_page' => 20); $args = wp_parse_args( $args, $defaults ); $args['number'] = absint( $args['number'] ); $args['offset'] = absint( $args['offset'] ); @@ -642,20 +644,15 @@ $args['hierarchical'] = false; $args['pad_counts'] = false; } - extract($args, EXTR_SKIP); - if ( $child_of ) { - $hierarchy = _get_term_hierarchy($taxonomies[0]); - if ( !isset($hierarchy[$child_of]) ) - return $empty_array; + if ( 0 < $args['page'] ) { + if ( 0 >= $args['per_page'] ) + $args['per_page'] = 20; + $args['number'] = $args['per_page']; + $args['offset'] = ($args['page'] - 1) * $args['per_page']; } + extract($args, EXTR_SKIP); - if ( $parent ) { - $hierarchy = _get_term_hierarchy($taxonomies[0]); - if ( !isset($hierarchy[$parent]) ) - return $empty_array; - } - // $args can be whatever, only use the args defined in defaults to compute the key $filter_key = ( has_filter('list_terms_exclusions') ) ? serialize($GLOBALS['wp_filter']['list_terms_exclusions']) : ''; $key = md5( serialize( compact(array_keys($defaults)) ) . serialize( $taxonomies ) . $filter_key ); @@ -680,6 +677,10 @@ $orderby = 't.slug'; else if ( 'term_group' == $_orderby ) $orderby = 't.term_group'; + else if ( 'lft' == $_orderby ) + $orderby = 'tt.lft'; + else if ( 'rgt' == $_orderby ) + $orderby = 'tt.rgt'; elseif ( empty($_orderby) || 'id' == $_orderby ) $orderby = 't.term_id'; @@ -754,7 +755,7 @@ $where .= ' AND tt.count > 0'; // don't limit the query results when we have to descend the family tree - if ( ! empty($number) && ! $hierarchical && empty( $child_of ) && '' === $parent ) { + if ( ! empty($number) && ( 'tt.lft' == $orderby || 'tt.rgt' == $orderby || ( ! $hierarchical && empty( $child_of ) && '' === $parent ) ) ) { if( $offset ) $limit = 'LIMIT ' . $offset . ',' . $number; else @@ -768,6 +769,22 @@ $where .= " AND (t.name LIKE '%$search%')"; } + if ( $child_of > 0 ) { + $parent_bounds = $wpdb->get_row( "SELECT lft, rgt FROM $wpdb->term_taxonomy WHERE taxonomy IN ($in_taxonomies) AND term_id = $child_of LIMIT 1" ); + $lft = $parent_bounds->lft; + $rgt = $parent_bounds->rgt; + $bounds = "AND tt.lft > $lft AND tt.rgt < $rgt "; + } else { + $bounds = ""; + } + + if ( $ancestor_of > 0 ) { + $child_bounds = $wpdb->get_row( "SELECT lft, rgt FROM $wpdb->term_taxonomy WHERE taxonomy IN ($in_taxonomies) AND term_id = $ancestor_of LIMIT 1" ); + $lft = $child_bounds->lft; + $rgt = $child_bounds->rgt; + $bounds .= "AND tt.lft <= $lft AND tt.rgt >= $rgt "; + } + $selects = array(); if ( 'all' == $fields ) $selects = array('t.*', 'tt.*'); @@ -777,7 +794,7 @@ $selects = array('t.term_id', 'tt.parent', 'tt.count', 't.name'); $select_this = implode(', ', apply_filters( 'get_terms_fields', $selects, $args )); - $query = "SELECT $select_this FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id WHERE tt.taxonomy IN ($in_taxonomies) $where ORDER BY $orderby $order $limit"; + $query = "SELECT $select_this FROM $wpdb->terms AS t INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id WHERE tt.taxonomy IN ($in_taxonomies) $where $bounds ORDER BY $orderby $order $limit"; $terms = $wpdb->get_results($query); if ( 'all' == $fields ) { @@ -790,12 +807,6 @@ return $terms; } - if ( $child_of ) { - $children = _get_term_hierarchy($taxonomies[0]); - if ( ! empty($children) ) - $terms = & _get_term_children($child_of, $terms, $taxonomies[0]); - } - // Update term counts to include children. if ( $pad_counts && 'all' == $fields ) _pad_term_counts($terms, $taxonomies[0]); @@ -1099,6 +1110,7 @@ * @since 2.3.0 * * @uses $wpdb + * @uses MPTT * @uses do_action() Calls both 'delete_term' and 'delete_$taxonomy' action * hooks, passing term object, term id. 'delete_term' gets an additional * parameter with the $taxonomy parameter. @@ -1155,6 +1167,11 @@ wp_set_object_terms($object, $terms, $taxonomy); } + if ( 'category' == $taxonomy ) { + $mptt = new MPTT_Category; + $mptt->remove_node( $term ); + } + $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->term_taxonomy WHERE term_taxonomy_id = %d", $tt_id ) ); // Delete the term if no taxonomies use it. @@ -1330,6 +1347,7 @@ * @subpackage Taxonomy * @since 2.3.0 * @uses $wpdb + * @uses MPTT * * @uses do_action() Calls 'create_term' hook with the term id and taxonomy id as parameters. * @uses do_action() Calls 'create_$taxonomy' hook with term id and taxonomy id as parameters. @@ -1354,7 +1372,7 @@ if ( '' == trim($term) ) return new WP_Error('empty_term_name', __('A name is required for this term')); - $defaults = array( 'alias_of' => '', 'description' => '', 'parent' => 0, 'slug' => ''); + $defaults = array( 'alias_of' => '', 'description' => '', 'parent' => 0, 'slug' => '', 'lft' => 0, 'rgt' => 0 ); $args = wp_parse_args($args, $defaults); $args['name'] = $term; $args['taxonomy'] = $taxonomy; @@ -1404,9 +1422,14 @@ if ( !empty($tt_id) ) return array('term_id' => $term_id, 'term_taxonomy_id' => $tt_id); - $wpdb->insert( $wpdb->term_taxonomy, compact( 'term_id', 'taxonomy', 'description', 'parent') + array( 'count' => 0 ) ); + $wpdb->insert( $wpdb->term_taxonomy, compact( 'term_id', 'taxonomy', 'description', 'parent' ) + array( 'count' => 0 ) ); $tt_id = (int) $wpdb->insert_id; + if ( 'category' == $taxonomy ) { + $mptt = new MPTT_Category; + $mptt->insert_node( $term_id ); + } + do_action("create_term", $term_id, $tt_id); do_action("create_$taxonomy", $term_id, $tt_id); @@ -1591,6 +1614,7 @@ * @since 2.3.0 * * @uses $wpdb + * @uses MPTT * @uses do_action() Will call both 'edit_term' and 'edit_$taxonomy' twice. * @uses apply_filters() Will call the 'term_id_filter' filter and pass the term * id and taxonomy id. @@ -1609,18 +1633,18 @@ $term_id = (int) $term_id; // First, get all of the original args - $term = get_term ($term_id, $taxonomy, ARRAY_A); + $old_term = get_term ($term_id, $taxonomy, ARRAY_A); - if ( is_wp_error( $term ) ) - return $term; + if ( is_wp_error( $old_term ) ) + return $old_term; // Escape data pulled from DB. - $term = add_magic_quotes($term); + $term = add_magic_quotes($old_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' => ''); + $defaults = array( 'alias_of' => '', 'description' => '', 'parent' => 0, 'slug' => '' ); $args = wp_parse_args($args, $defaults); $args = sanitize_term($args, $taxonomy, 'db'); extract($args, EXTR_SKIP); @@ -1668,10 +1692,18 @@ $wpdb->update( $wpdb->terms, compact( 'slug' ), compact( 'term_id' ) ); } - $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) ); + $tt_id = $old_term['term_taxonomy_id']; + $tt_parent = $old_term['parent']; + $lft = $old_term['lft']; + $rgt = $old_term['rgt']; $wpdb->update( $wpdb->term_taxonomy, compact( 'term_id', 'taxonomy', 'description', 'parent' ), array( 'term_taxonomy_id' => $tt_id ) ); + if ( 'category' == $taxonomy && ( $tt_parent != $parent || $old_term['name'] != $name ) ) { + $mptt = new MPTT_Category; + $mptt->move_subtree( $term_id ); + } + do_action("edit_term", $term_id, $tt_id); do_action("edit_$taxonomy", $term_id, $tt_id); @@ -1989,26 +2021,37 @@ * @since 2.3.0 * * @uses update_option() Stores all of the children in "$taxonomy_children" - * option. That is the name of the taxonomy, immediately followed by '_children'. + * option if the full hierarchy is retrieved. That is the name of the taxonomy, + * immediately followed by '_children'. * * @param string $taxonomy Taxonomy Name * @return array Empty if $taxonomy isn't hierarachical or returns children as Term IDs. */ -function _get_term_hierarchy($taxonomy) { +function _get_term_hierarchy($taxonomy, $terms = 0, $root = 0) { if ( !is_taxonomy_hierarchical($taxonomy) ) return array(); - $children = get_option("{$taxonomy}_children"); - if ( is_array($children) ) - return $children; - $children = array(); - $terms = get_terms($taxonomy, 'get=all'); + if ( empty( $terms ) ) { + $cache = true; + $children = get_option("{$taxonomy}_children"); + if ( is_array($children) ) + return $children; + + $children = array(); + if ( $root > 0 ) + $terms = get_terms( $taxonomy, array( "child_of" => $root ) ); + else + $terms = get_terms($taxonomy, 'get=all'); + } + foreach ( $terms as $term ) { if ( $term->parent > 0 ) $children[$term->parent][] = $term->term_id; } - update_option("{$taxonomy}_children", $children); + if ($cache && ! $root) + update_option("{$taxonomy}_children", $children); + return $children; } Index: wp-includes/post.php =================================================================== --- wp-includes/post.php (revision 11807) +++ wp-includes/post.php (working copy) @@ -1172,6 +1172,9 @@ delete_option('page_for_posts'); } + $mptt = new MPTT_Page; + $mptt->remove_node( $postid ); + // Point children of this page to its parent, also clean the cache of affected children $children_query = $wpdb->prepare("SELECT * FROM $wpdb->posts WHERE post_parent = %d AND post_type='page'", $postid); $children = $wpdb->get_results($children_query); @@ -1627,6 +1630,14 @@ $where = array( 'ID' => $post_ID ); } + if ( $post_type == 'page' ) { + $mptt = new MPTT_Page; + if ( $update ) + $mptt->move_subtree( $post_ID ); + else + $mptt->insert_node( $post_ID ); + } + if ( empty($data['post_name']) && !in_array( $data['post_status'], array( 'draft', 'pending' ) ) ) { $data['post_name'] = sanitize_title($data['post_title'], $post_ID); $wpdb->update( $wpdb->posts, array( 'post_name' => $data['post_name'] ), $where ); @@ -2246,13 +2257,8 @@ */ function get_page_hierarchy($posts, $parent = 0) { $result = array ( ); - if ($posts) { foreach ( (array) $posts as $post) { - if ($post->post_parent == $parent) { - $result[$post->ID] = $post->post_name; - $children = get_page_hierarchy($posts, $post->ID); - $result += $children; //append $children to $result - } - } } + if ($posts) + usort( $posts, array( 'MPTT_Page', 'comparator' ) ); return $result; } @@ -2268,16 +2274,17 @@ */ function get_page_uri($page_id) { $page = get_page($page_id); - $uri = $page->post_name; + $uri = ''; // A page cannot be it's own parent. if ( $page->post_parent == $page->ID ) return $uri; - while ($page->post_parent != 0) { - $page = get_page($page->post_parent); - $uri = $page->post_name . "/" . $uri; - } + $mptt = new MPTT_Page; + $ancestors = $mptt->get_ancestors( $page_id ); + foreach ( $ancestors as $ancestor ) + $uri .= $ancestor->post_name; + $uri .= $page->post_name; return $uri; } @@ -2299,18 +2306,25 @@ global $wpdb; $defaults = array( - 'child_of' => 0, 'sort_order' => 'ASC', + 'child_of' => 0, 'ancestor_of' => 0, 'sort_order' => 'ASC', 'sort_column' => 'post_title', 'hierarchical' => 1, 'exclude' => '', 'include' => '', 'meta_key' => '', 'meta_value' => '', 'authors' => '', 'parent' => -1, 'exclude_tree' => '', - 'number' => '', 'offset' => 0 + 'number' => '', 'offset' => 0, 'page' => 0, 'per_page' => 20 ); $r = wp_parse_args( $args, $defaults ); extract( $r, EXTR_SKIP ); $number = (int) $number; $offset = (int) $offset; + if ( 0 < $page && $sort_column == 'lft' ) { + if ( 0 < $per_page ) + $number = $per_page; + else + $number = 20; + $offset = ($page - 1) * $number; + } $cache = array(); $key = md5( serialize( compact(array_keys($defaults)) ) ); @@ -2404,7 +2418,23 @@ if ( $parent >= 0 ) $where .= $wpdb->prepare(' AND post_parent = %d ', $parent); - $query = "SELECT * FROM $wpdb->posts $join WHERE (post_type = 'page' AND post_status = 'publish') $where "; + if ( $child_of > 0 ) { + $parent_bounds = $wpdb->get_row( "SELECT lft, rgt FROM $wpdb->posts WHERE ID = $child_of LIMIT 1" ); + $lft = $parent_bounds->lft; + $rgt = $parent_bounds->rgt; + $bounds = "AND lft > $lft AND rgt < $rgt "; + } else { + $bounds = ""; + } + + if ( $ancestor_of > 0 ) { + $child_bounds = $wpdb->get_row( "SELECT lft, rgt FROM $wpdb->posts WHERE ID = $ancestor_of LIMIT 1" ); + $lft = $child_bounds->lft; + $rgt = $child_bounds->rgt; + $bounds .= "AND lft <= $lft AND rgt >= $rgt "; + } + + $query = "SELECT * FROM $wpdb->posts $join WHERE (post_type = 'page' AND post_status = 'publish') $where $bounds"; $query .= $author_query; $query .= " ORDER BY " . $sort_column . " " . $sort_order ; @@ -2421,9 +2451,6 @@ // Update cache. update_page_cache($pages); - if ( $child_of || $hierarchical ) - $pages = & get_page_children($child_of, $pages); - if ( !empty($exclude_tree) ) { $exclude = array(); Index: wp-includes/version.php =================================================================== --- wp-includes/version.php (revision 11807) +++ wp-includes/version.php (working copy) @@ -15,7 +15,7 @@ * * @global int $wp_db_version */ -$wp_db_version = 11548; +$wp_db_version = 20001; // uber-high dummy value /** * Holds the TinyMCE version Index: wp-includes/mptt.php =================================================================== --- wp-includes/mptt.php (revision 0) +++ wp-includes/mptt.php (revision 0) @@ -0,0 +1,1044 @@ +table; + $table = $wpdb->$table_name; + $parent_field = $this->parent_field; + + wp_cache_flush (); + + $node = $this->get_node( $id ); + if ( ! $node ) + return false; + $parent_id = $node->$parent_field; + $parent = $this->get_node( $parent_id ); + + $lft = $this->get_lft( $id ); + $rgt = $lft + 1; + if ( isset( $parent->depth ) ) + $depth = $parent->depth + 1; + else + $depth = 0; + + if ( false === $wpdb->query( "UPDATE $table SET lft = lft + 2 WHERE lft >= $lft" ) ) + return false; + if ( false === $wpdb->query( "UPDATE $table SET rgt = rgt + 2 WHERE rgt >= $lft" ) ) + return false; + if ( false === $wpdb->query( "UPDATE $table SET lft = $lft, rgt = $rgt, depth = $depth WHERE $this->id_field = $id" ) ) + return false; + + wp_cache_flush (); + + return true; + } + + /** + * Removes a node from the tree + * + * @since MPTT-REPLACE + * @param int $id ID of node to remove from the structure + * @return bool True on success, false on failure + */ + function remove_node( $id ) { + global $wpdb; + $table = $this->table; + $node = $this->get_node( $id ); + if ( ! $node ) + return false; + if ( false === $wpdb->query( "UPDATE $wpdb->$table SET lft = lft - 1, rgt = rgt - 1, depth = depth - 1 WHERE lft > $node->lft AND rgt < $node->rgt" ) ) + return false; + if ( false === $wpdb->query( "UPDATE $wpdb->$table SET lft = lft - 2 WHERE lft > $node->rgt" ) ) + return false; + if ( false === $wpdb->query( "UPDATE $wpdb->$table SET rgt = rgt - 2 WHERE rgt > $node->rgt" ) ) + return false; + return true; + } + + /** + * Moves an existing node and its children to a new location in the tree + * + * @since MPTT-REPLACE + * @param int $id ID of node to move in the structure + * @return bool True on success, false on failure + */ + function move_subtree( $id ) { + global $wpdb; + $table = $this->table; + + $node = $this->get_node( $id ); + if ( ! $node ) + return false; + + $old_lft = $node->lft; + $old_rgt = $node->rgt; + $width = $old_rgt - $old_lft; + + $max = $wpdb->get_var( "SELECT MAX(rgt) FROM $wpdb->$this->table" ); + if ( false === $max ) + return false; + if ( false === $wpdb->query( "UPDATE $wpdb->$table SET lft = lft + $max, rgt = rgt + $max WHERE lft >= $old_lft AND rgt <= $old_rgt" ) ) + return false; + if ( false === $wpdb->query( "UPDATE $wpdb->$table SET lft = lft - $width WHERE lft > $old_rgt AND lft <= $max" ) ) + return false; + if ( false === $wpdb->query( "UPDATE $wpdb->$table SET rgt = rgt - $width WHERE rgt > $old_rgt AND rgt <= $max" ) ) + return false; + + $new_lft = $this->get_lft( $id ); + + if ( false === $wpdb->query( "UPDATE $wpdb->$table SET lft = lft + $width WHERE lft >= $new_lft AND lft <= $max" ) ) + return false; + if ( false === $wpdb->query( "UPDATE $wpdb->$table SET rgt = rgt + $width WHERE rgt >= $new_lft AND rgt <= $max" ) ) + return false; + + $delta = $new_lft - $old_lft - $max; + if ( false === $wpdb->query( "UPDATE $wpdb->$table SET lft = lft + $delta, rgt = rgt + $delta WHERE lft > $max" ) ) + return false; + + return true; + } + + /** + * Comparator between two nodes for use with usort() + * + * @since MPTT-REPLACE + * @param object $a First node + * @param object $b Second node + * @return int Comparison between $a and $b + * @abstract + */ + function comparator( $a, $b ) { + return ( $a->lft - $b->lft ); + } + + /** + * Retrieves node as object + * + * @since MPTT-REPLACE + * @param int $id ID of node to retrieve from the structure + * @return object Returns node + * @abstract + */ + function get_node( $id ) {} + + /** + * Retrieves children of a given parent + * + * @since MPTT-REPLACE + * @param object $element Node whose siblings to retrieve + * @return array Returns array of node objects + * @abstract + */ + function get_siblings( $element ) {} + + /** + * Retrieves ancestors of a given node + * + * @since MPTT-REPLACE + * @param int $id ID of node whose ancestors to retrieve + * @param bool $self Determines whether to include the specified node or not. + * @return array Returns array of node objects + * @abstract + */ + function get_ancestors( $id, $self ) {} + + /** + * Retrieves proper location for a node + * + * @since MPTT-REPLACE + * @param int $id ID of node to locate in the structure + * @return int lft value for node's new location + * @abstract + */ + function get_lft( $id ) {} + +} + +/** + * Manage MPTT data for categories + * + * @package WordPress + * @since MPTT-REPLACE + * @uses MPTT + */ +class MPTT_Category extends MPTT { + + /** + * Table in the database + * + * @since MPTT-REPLACE + * @var string + * @access private + */ + var $table = 'term_taxonomy'; + + /** + * ID field in the database. + * + * @since MPTT-REPLACE + * @var string + * @access private + */ + var $id_field = 'term_id'; + + /** + * Parent field in the database + * + * @since MPTT-REPLACE + * @var string + * @access private + */ + var $parent_field = 'parent'; + + /** + * Retrieves node as object + * + * @since MPTT-REPLACE + * @param int $id ID of node to retrieve from the structure + * @return object Returns node + */ + function get_node( $id ) { + return get_term( $id, 'category' ); + } + + /** + * Retrieves the siblings of a given element + * + * @since MPTT-REPLACE + * @param object $element Element whose siblings to retrieve + * @return array Returns array of node objects + */ + function get_siblings( $element ) { + $args = array( 'parent' => $element->parent, 'exclude' => $element->term_id, 'hide_empty' => false ); + $siblings = array(); + $siblings = get_terms( array( 'category' ), $args ); + return $siblings; + } + + /** + * Retrieves ancestors of a given node + * + * @since MPTT-REPLACE + * @param int $id ID of node whose ancestors to retrieve + * @param bool $self Determines whether to include the specified node or not. + * @return array Returns array of node objects + */ + function get_ancestors( $id, $self = false ) { + $args = array( 'ancestor_of' => $id, 'orderby' => 'lft', 'hide_empty' => false ); + if ( !$self ) + $args['exclude'] = $id; + return get_terms( array( 'category' ), $args ); + } + + /** + * Retrieves proper location for a node + * + * @since MPTT-REPLACE + * @param int $id ID of node to locate in the structure + * @return int lft value for node's new location + */ + function get_lft( $id ) { + $node = $this->get_node( $id ); + $siblings = $this->get_siblings( $node ); + if ( empty( $siblings ) ) { + if ( $node->parent ) { + $parent = $this->get_node( $node->parent ); + return $parent->rgt; + } else { + return 1; + } + } + $index = 0; + $total = count( $siblings ); + // Order by name. + while ( $index < $total && strcmp( $siblings[$index]->name, $node->name ) <= 0 ) + $index++; + if ( $total <= $index ) + $index = $total - 1; + if ( strcmp( $siblings[$index]->name, $node->name ) > 0 ) + return $siblings[$index]->lft; + else + return ( $siblings[$index]->rgt + 1 ); + } + +} + +/** + * Manage MPTT data for pages + * + * @package WordPress + * @since MPTT-REPLACE + * @uses MPTT + */ +class MPTT_Page extends MPTT { + + /** + * Table in the database + * + * @since MPTT-REPLACE + * @var string + * @access private + */ + var $table = 'posts'; + + /** + * ID field in the database. + * + * @since MPTT-REPLACE + * @var string + * @access private + */ + var $id_field = 'ID'; + + /** + * Parent field in the database + * + * @since MPTT-REPLACE + * @var string + * @access private + */ + var $parent_field = 'post_parent'; + + /** + * Retrieves node as object + * + * @since MPTT-REPLACE + * @param int $id ID of node to retrieve from the structure + * @return object Returns node + */ + function get_node( $id ) { + return get_post( $id ); + } + + /** + * Retrieves the siblings of a given element + * + * @since MPTT-REPLACE + * @param object $element Element whose siblings to retrieve + * @return array Returns array of node objects + */ + function get_siblings( $element ) { + $args = array( 'parent' => $element->post_parent, 'exclude' => $element->ID ); + $siblings = array(); + $siblings = get_pages( $args ); + return $siblings; + } + + /** + * Retrieves ancestors of a given node + * + * @since MPTT-REPLACE + * @param int $id ID of node whose ancestors to retrieve + * @param bool $self Determines whether to include the specified node or not. + * @return array Returns array of node objects + */ + function get_ancestors( $id, $self = false ) { + $args = array( 'ancestor_of' => $id, 'sort_column' => 'lft' ); + if ( !$self ) + $args['exclude'] = $id; + return get_pages( $args ); + } + + /** + * Retrieves proper location for a node + * + * @since MPTT-REPLACE + * @param int $id ID of node to locate in the structure + * @return int lft value for node's new location + */ + function get_lft( $id ) { + $node = $this->get_node( $id ); + $siblings = $this->get_siblings( $node ); + if ( empty( $siblings ) ) { + if ( $node->post_parent ) { + $parent = $this->get_node( $node->post_parent ); + return $parent->rgt; + } else { + return 1; + } + } + + $index = 0; + $total = count( $siblings ); + // Order by order, then by name. + while ( $index < $total ) { + if ( $siblings[$index]->menu_order < $node->menu_order ) + $index++; + else if ( $siblings[$index]->menu_order == $node->menu_order && strcmp( $siblings[$index]->post_title, $node->post_title ) < 0 ) + $index++; + else + break; + } + if ( $total <= $index) + $index = $total - 1; + if ( $siblings[$index]->menu_order > $node->menu_order ) + return $siblings[$index]->lft; + elseif ( $siblings[$index]->menu_order == $node->menu_order && strcmp( $siblings[$index]->post_title, $node->post_title ) > 0 ) + return $siblings[$index]->lft; + else + return ( $siblings[$index]->rgt + 1 ); + } + +} + +/** + * A class for displaying various tree-like structures. + * + * Extend the MPTT_Walker class to use it, see examples at the below. Child classes + * do not need to implement all of the abstract methods in the class. The child + * only needs to implement the methods that are needed. Also, the methods are + * not strictly abstract in that the parameter definition needs to be followed. + * The child classes can have additional parameters. + * + * @package WordPress + * @since mptt-REPLACE + * @abstract + */ +class MPTT_Walker { + /** + * What the class handles. + * + * @since MPTT-REPLACE + * @var string + * @access public + */ + var $tree_type; + + /** + * DB fields to use. + * + * @since MPTT-REPLACE + * @var array + * @access protected + */ + var $db_fields; + + /** + * Max number of pages walked by the paged walker + * + * @since MPTT-REPLACE + * @var int + * @access protected + */ + var $max_pages = 1; + + /** + * Starts the list before the elements are added. + * + * Additional parameters are used in child classes. The args parameter holds + * additional values that may be used with the child class methods. This + * method is called at the start of the output list. + * + * @since MPTT-REPLACE + * @abstract + * + * @param string $output Passed by reference. Used to append additional content. + */ + function start_lvl(&$output) {} + + /** + * Ends the list of after the elements are added. + * + * Additional parameters are used in child classes. The args parameter holds + * additional values that may be used with the child class methods. This + * method finishes the list at the end of output of the elements. + * + * @since MPTT-REPLACE + * @abstract + * + * @param string $output Passed by reference. Used to append additional content. + */ + function end_lvl(&$output) {} + + /** + * Start the element output. + * + * Additional parameters are used in child classes. The args parameter holds + * additional values that may be used with the child class methods. Includes + * the element output also. + * + * @since MPTT-REPLACE + * @abstract + * + * @param string $output Passed by reference. Used to append additional content. + */ + function start_el(&$output) {} + + /** + * Ends the element output, if needed. + * + * Additional parameters are used in child classes. The args parameter holds + * additional values that may be used with the child class methods. + * + * @since MPTT-REPLACE + * @abstract + * + * @param string $output Passed by reference. Used to append additional content. + */ + function end_el(&$output) {} + + /** + * Display array of elements hierarchically. + * + * It is a generic function which assumes the input is ordered by lft value. + * max_depth = -1 means flatly display every element. max_depth = + * 0 means display all levels. max_depth > 0 specifies the number of + * display levels. + * + * @since MPTT-REPLACE + * + * @param array $elements + * @param int $max_depth + * @return string + */ + function walk( $elements, $max_depth) { + + $args = array_slice(func_get_args(), 2); + $output = ''; + + if ($max_depth < -1) //invalid parameter + return $output; + + if (empty($elements)) //nothing to walk + return $output; + + $id_field = $this->db_fields['id']; + $parent_field = $this->db_fields['parent']; + + $max_i = count( $elements ); + + if ( -1 == $max_depth ) { + for ( $i = 0; $i < $max_i; $i++ ) { + $cb_args = array_merge( array( &$output, $elements[$i], 0 ), $args ); + call_user_func_array( array( &$this, 'start_el' ), $cb_args ); + call_user_func_array( array( &$this, 'end_el' ), $cb_args ); + } + return $output; + } + + $stack = array(); + $elements[$max_i] = (object) array( 'depth' => $elements[0]->depth ); + for ( $i = 0; $i < $max_i; $i++ ) { + + $current = $elements[$i]; + $depth = $current->depth; + if ( $max_depth != 0 && $depth >= $max_depth ) + continue; + $next_depth = $elements[$i + 1]->depth; + + $cb_args = array_merge( array( &$output, $current, $depth ), $args ); + call_user_func_array( array( &$this, 'start_el' ), $cb_args ); + + if ( $depth == $next_depth ) + call_user_func_array( array( &$this, 'end_el' ), $cb_args ); + else if ( $depth < $next_depth ) { + $cb_args[2]++; + call_user_func_array( array( &$this, 'start_lvl' ), $cb_args ); + array_push( $stack, $i ); + } + else { + for ( $lvl = ($depth - $next_depth); $lvl > 0; $lvl-- ) { + $finish = array_pop( $stack ); + $cb_lvl_args = array_merge( array( &$output, &$elements[$finish], $next_depth + $lvl ), $args ); + call_user_func_array( array( &$this, 'end_lvl' ), $cb_args ); + $cb_args = array_merge( array( &$output, &$elements[$finish], $elements[$finish]->depth ), $args ); + call_user_func_array( array( &$this, 'end_el' ), $cb_args ); + } + } + + } + + return $output; + } + +} + +/** + * Create HTML list of categories. + * + * @package WordPress + * @since MPTT-REPLACE + * @uses MPTT_Walker + */ +class MPTT_Walker_Category extends MPTT_Walker { + /** + * @see MPTT_Walker::$tree_type + * @since MPTT-REPLACE + * @var string + */ + var $tree_type = 'category'; + + /** + * @see MPTT_Walker::$db_fields + * @since MPTT-REPLACE + * @todo Decouple this + * @var array + */ + var $db_fields = array ('parent' => 'parent', 'id' => 'term_id'); + + /** + * @see MPTT_Walker::start_lvl() + * @since MPTT-REPLACE + * + * @param string $output Passed by reference. Used to append additional content. + * @param object $element Not used. + * @param int $depth Depth of category. Used for tab indentation. + * @param array $args Will only append content if style argument value is 'list'. + */ + function start_lvl(&$output, $element, $depth, $args) { + if ( 'list' != $args['style'] ) + return; + + $indent = str_repeat("\t", $depth); + $output .= "$indent