Seditio Source
Root |
./othercms/b2evolution_7.2.3/inc/chapters/model/_chaptercache.class.php
<?php
/**
 * This file implements the ChapterCache class.
 *
 * This file is part of the evoCore framework - {@link http://evocore.net/}
 * See also {@link https://github.com/b2evolution/b2evolution}.
 *
 * @license GNU GPL v2 - {@link http://b2evolution.net/about/gnu-gpl-license}
 *
 * @copyright (c)2003-2020 by Francois Planque - {@link http://fplanque.com/}
 *
 * @package evocore
 *
 * @author fplanque: Francois PLANQUE
 */
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );

load_class( 'chapters/model/_chapter.class.php', 'Chapter' );

/**
 * ChapterCache Class
 *
 * @package evocore
 */
class ChapterCache extends DataObjectCache
{
    var
$subset_cache = array();

   
/**
     * Which property of the objects defines the subset
     */
   
var $subset_property;

   
/**
     * Which subsets have been loaded
     */
   
var $loaded_subsets = array();

   
/**
     * These are the level 0 categories (which have no parent)
     */
   
var $root_cats = array();

   
/**
     * These are the level 0 categories (which have no parent) for each subset
     */
   
var $subset_root_cats = array();

   
/**
     * Have the children been revealed for all subsets yet?
     */
   
var $revealed_all_children = false;
   
/**
     * Have the children been revealed for all subsets yet?
     */
   
var $revealed_subsets = array();

   
/**
     * Flags about which arrays are already sorted
     */
   
var $sorted_flags = array();

   
/**
     * Lazy filled index of url titles
     */
   
var $urlname_index = array();


   
/**
     * Constructor
     */
   
function __construct()
    {
       
parent::__construct( 'Chapter', false, 'T_categories', 'cat_', 'cat_ID', 'cat_name' );

       
// This is the property by which we will filter out subsets, for exampel 'blog_ID' if we want to only load a specific collection:
       
$this->subset_property = 'blog_ID';
    }


   
/**
     * Empty/reset the cache
     */
   
function clear( $keep_shadow = false )
    {
       
$this->subset_cache = array();
       
$this->loaded_subsets = array();
       
$this->root_cats = array();
       
$this->subset_root_cats = array();
       
$this->revealed_all_children = false;
       
$this->revealed_subsets = array();
       
$this->sorted_flags = array();
       
$this->urlname_index = array();
       
parent::clear( $keep_shadow );
    }


   
/**
     * Add a dataobject to the cache
     *
     * @param object Chapter object to add in cache
     * @return boolean TRUE on adding, FALSE on wrong object or if it is already in cache
     */
   
function add( $Chapter )
    {
        if(
parent::add( $Chapter ) )
        {    
// Successfully added

           
if( !empty( $this->subset_property ) )
            {    
// Also add to subset cache:
               
$this->subset_cache[ $Chapter->{$this->subset_property} ][ $Chapter->ID ] = $Chapter;
            }
            return
true;
        }

        return
false;
    }


   
/**
     * Get an object from cache by ID
     *
     * Load the cache if necessary (all at once if allowed).
     *
     * @param integer ID of object to load
     * @param boolean true if function should die on error
     * @param boolean true if function should die on empty/null
      * @param integer|NULL NULL for all subsets
     * @return Chapter Reference on cached object or false.
     */
   
function & get_by_ID( $req_ID, $halt_on_error = true, $halt_on_empty = true, $subset_ID = NULL )
    {
        global
$DB, $Debuglog;

        if( empty(
$req_ID) )
        {
            if(
$halt_on_empty)
            {
               
debug_die( "Requested Chapter from $this->dbtablename without ID!" );
            }
           
$r = NULL;
            return
$r;
        }

        if( !empty(
$this->cache[ $req_ID ] ) )
        {
// Already in cache
           
$Debuglog->add( "Accessing Chapter($req_ID) from cache", 'dataobjects' );
            return
$this->cache[ $req_ID ];
        }
        elseif( !
$this->all_loaded )
        {
// Not in cache, but not everything is loaded yet
           
if( $this->load_all || is_null($subset_ID) )
            {
// It's ok to just load everything:
               
$this->load_all();
            }
            else
            {
// Load just the requested object:
               
$Debuglog->add( "Loading <strong>$this->objtype($req_ID)</strong> into cache", 'dataobjects' );
               
// Note: $req_ID MUST be an unsigned integer. This is how DataObject works.
               
$sql = "SELECT *
                          FROM T_categories
                         WHERE cat_ID =
$req_ID
                           AND cat_blog_ID = "
.$subset_ID;

                if(
$row = $DB->get_row( $sql, OBJECT, 0, 'ChapterCache::get_by_ID()' ) )
                {
                    if( !
$this->instantiate( $row ) )
                    {
                       
$Debuglog->add( 'Could not add() object to cache!', 'dataobjects' );
                    }
                }
                else
                {
                   
$Debuglog->add( 'Could not get DataObject by ID. Query: '.$sql, 'dataobjects' );
                }
            }
        }

        if( empty(
$this->cache[ $req_ID ] ) )
        {
// Requested object does not exist
            // $Debuglog->add( 'failure', 'dataobjects' );
           
if( $halt_on_error )
            {
               
debug_die( "Requested $this->objtype does not exist!" );
            }
           
$r = false;
            return
$r;
        }

        return
$this->cache[ $req_ID ];
    }


   
/**
     * Get an array with chapters ID that are located in the given blog from root to the given chapter
     *
     * @param integer Blog ID
     * @param integer Chapter ID
     * @return array Chapters ID
     */
   
function get_chapter_path( $blog_ID, $chapter_ID )
    {
       
$this->load_subset( $blog_ID );

       
$chapter_path = array( $chapter_ID );
        if( isset(
$this->subset_cache[ $blog_ID ] ) )
        {
           
$chapters = $this->subset_cache[ $blog_ID ];
            if( isset(
$chapters[ $chapter_ID ] ) )
            {
               
$Chapter = $chapters[ $chapter_ID ];
                while(
$Chapter->get( 'parent_ID' ) > 0 )
                {
                   
$chapter_path[] = $Chapter->get( 'parent_ID' );
                   
// Select a parent chapter
                   
$Chapter = $chapters[ $Chapter->get( 'parent_ID' ) ];
                }
            }
        }

        return
$chapter_path;
    }


   
/**
     * Get an object from cache by urlname
     *
     * Load the cache if necessary (all at once if allowed).
     *
     * @param string ID of object to load
     * @param boolean true if function should die on error
     * @return reference on cached object
     */
   
function & get_by_urlname( $req_urlname, $halt_on_error = true )
    {
        global
$DB, $Debuglog;

        if( !isset(
$this->urlname_index[$req_urlname] ) )
        {
// not yet in cache:
            // Load just the requested object:
           
$Debuglog->add( "Loading <strong>$this->objtype($req_urlname)</strong> into cache", 'dataobjects' );
           
$sql = "SELECT *
                      FROM
$this->dbtablename
                     WHERE cat_urlname = "
.$DB->quote($req_urlname);
           
$row = $DB->get_row( $sql );
            if( empty(
$row ) )
            {    
// Requested object does not exist
               
if( $halt_on_error ) debug_die( "Requested $this->objtype does not exist!" );
               
// put into index:
               
$this->urlname_index[$req_urlname] = false;

                return
$this->urlname_index[$req_urlname];
            }

           
$this->instantiate( $row );

           
// put into index:
           
$this->urlname_index[$req_urlname] = & $this->cache[ $row->cat_ID ];
        }
        else
        {
           
$Debuglog->add( "Retrieving <strong>$this->objtype($req_urlname)</strong> from cache" );
        }

        return
$this->urlname_index[$req_urlname];
    }


   
/**
     * Load a list of chapter referenced by their urlname into the cache
     *
     * @param array of urlnames of Chapters to load
     */
   
function load_urlname_array( $req_array )
    {
        global
$DB, $Debuglog;

       
$req_list = $DB->quote( $req_array );
       
$Debuglog->add( "Loading <strong>$this->objtype($req_list)</strong> into cache", 'dataobjects' );
       
$sql = "SELECT * FROM $this->dbtablename WHERE cat_urlname IN ( $req_list )";
       
$dbIDname = $this->dbIDname;
       
$objtype = $this->objtype;
        foreach(
$DB->get_results( $sql ) as $row )
        {
           
$this->cache[ $row->$dbIDname ] = new $objtype( $row ); // COPY!

            // put into index:
           
$this->urlname_index[$row->cat_urlname] = & $this->cache[ $row->$dbIDname ];

           
$Debuglog->add( "Cached <strong>$this->objtype($row->cat_urlname)</strong>" );
        }
    }



   
/**
     * Load a keyed subset of the cache
     *
      * @param integer|NULL NULL for all subsets
      * @param string Force 'order by' setting ('manual' = 'ORDER BY cat_order')
     */
   
function load_subset( $subset_ID, $force_order_by = '' )
    {
        global
$DB, $Debuglog, $Settings;

        if(
$this->all_loaded || isset( $this->loaded_subsets[$subset_ID] ) )
        {
// Already loaded
           
return false;
        }

       
// fp> TODO: This kills other subsets. BAD if we want to handle multiple subsets independently
       
$this->clear( true );

       
$Debuglog->add( 'ChapterCache - Loading <strong>chapters('.$subset_ID.')</strong> into cache', 'dataobjects' );
       
$sql = 'SELECT *
                    FROM T_categories
                    WHERE cat_blog_ID = '
.$subset_ID;

        foreach(
$DB->get_results( $sql, OBJECT, 'Loading chapters('.$subset_ID.') into cache' ) as $row )
        {
           
// Instantiate a custom object
           
$this->instantiate( $row );
        }

       
$this->loaded_subsets[$subset_ID] = true;

        return
true;
    }


   
/**
     * Get chapters in the given subset
     * Note: This is not ordered and it returns all of chpaters and subchapters
     *
     * @param integer subset ID
     * @return array of Chapters
     */
   
function get_chapters_by_subset( $subset_ID )
    {
        if( ! isset(
$this->loaded_subsets[$subset_ID] ) )
        {
// Load subset
           
$this->load_subset( $subset_ID );
        }

        return
$this->subset_cache[$subset_ID];
    }


   
/**
     * Get chapters by collection ID and parent ID
     *
     * @param integer collection ID
     * @param integer parent chapter ID
     * @param boolean set to true to sort the result, leave it on false otherwise
     *
     * @return array set of chapters
     */
   
function get_chapters( $coll_ID, $parent_ID = 0, $sorted = false )
    {
       
$this->reveal_children( $coll_ID, $sorted );

        if(
$parent_ID == 0 )
        {
            return
$this->subset_root_cats[$coll_ID];
        }

       
$parent_Chapter = & $this->get_by_ID( $parent_ID );
        if(
$sorted )
        {
// Sort chapter
           
$parent_Chapter->sort_children();
        }

        return
$parent_Chapter->children;
    }


   
/**
     * Check if there are categories in the given subset
     *
     * @param integer subset ID
     * @return boolean true if subset has chapters, false otherwise
     */
   
function has_chapters_in_subset( $subset_ID )
    {
       
$this->load_subset( $subset_ID );
        return ! empty(
$this->subset_cache[$subset_ID] );
    }


   
/**
     * Move a chapter and its descendants to a different collection
     *
     * @param integer
     * @param integer
     * @param integer
     */
   
function move_Chapter_subtree( $chapter_ID, $src_collection_ID, $dest_collection_ID )
    {
       
/**
         * @var DB
         */
       
global $DB;

       
// Make sure children have been revealed for specific subset:
       
$this->reveal_children( $src_collection_ID );

       
// fp>We get the Chapter AFTER reveal_children, because something is wrong with reveal_children or get_by_ID
        // I don't know what right now, but if we get Chapter earlier, we'll be stuck with an old copy of it that does NOT have the children
        // TODO: find out what's freakin wrong
       
$Chapter = $this->get_by_ID($chapter_ID);

       
$chapters_to_move = array();
       
// Get $chapters_to_move:
       
$this->recurse_move_subtree( $Chapter, $chapters_to_move );
       
// pre_dump( $chapters_to_move );

       
$DB->begin();

       
// Move to root:
       
if( $parent_Chapter = $Chapter->get_parent_Chapter() )
        {    
// Was not already at root, cut it and move it:
            // echo 'Move to root';
           
$Chapter->set( 'parent_ID', NULL );
           
$Chapter->dbupdate();
        }

       
// Move Chapters to new Blog:
       
$sql = 'UPDATE T_categories
                             SET cat_blog_ID = '
.$dest_collection_ID.'
                         WHERE cat_blog_ID = '
.$src_collection_ID /* extra security */ .'
                              AND cat_ID IN ('
.implode( ',', $chapters_to_move ).')';
       
$DB->query( $sql );

       
$DB->commit();

       
// Now the cache is badly screwed. Reseting it is fair enough, because this won't happen very often.
       
$this->clear();
    }


   
/**
     * Support function for move_Chapter_subtree
     *
     * @param Chapter
     * @param array
     */
   
function recurse_move_subtree( & $Chapter, & $list_array )
    {
       
// Add this to the list:
       
$list_array[] = $Chapter->ID;

        foreach(
$Chapter->children as $child_Chapter )
        {
           
$this->recurse_move_subtree( $child_Chapter, $list_array );
        }
    }


   
/**
     * Instanciate a new object within this cache
      *
      * @param object|NULL
      * @param integer|NULL subset to use for new object
     */
   
function & new_obj( $row = NULL, $subset_ID = NULL )
    {
       
// Instantiate a custom object
       
$Chapter = new Chapter( $row, $subset_ID ); // Copy

       
return $Chapter;
    }


   
/**
     * Reveal children
     *
     * After executing this, each Chapter will have an array pointing to its direct children
     *
     * @param integer|NULL NULL for all subsets
     * @param boolean Set true to sort root categories or leave it on false otherwise
     */
   
function reveal_children( $subset_ID = NULL, $sort_root_cats = false )
    {
        global
$Debuglog;
        global
$Timer;

        if(
$this->revealed_all_children )
        {    
// All children have already been revealed: (can happen even if we require a subset *now*)
           
if( $sort_root_cats )
            {
               
$this->sort_root_cats( $subset_ID );
            }
            return;
        }

        if( empty(
$this->subset_property) )
        {    
// We are not handling a subset but everything instead:

           
echo 'DEBUG - PARTIAL IMPLEMENTATION - Revealing all children -- this is not yet handling all edge cases that the subset version can handle';

           
// Make sure everything has been loaded:
           
$this->load_all();

           
// Reveal children:
           
if( !empty( $this->cache ) )
            {    
// There are loaded categories, so loop on all loaded categories to set their children list if it has:
               
foreach( $this->cache as $cat_ID => $Chapter )
                {
                   
// echo $Chapter->name;
                   
if( ! is_null( $Chapter->parent_ID ) )
                    {    
// This category has a parent, so add it to its parent children list:
                       
$this->cache[$Chapter->parent_ID]->add_child_category( $this->cache[$cat_ID] );
                    }
                    else
                    {    
// This category has no parent, so add it to the parent categories list
                       
$this->root_cats[] = & $this->cache[$cat_ID];
                    }
                }
            }

            if(
$sort_root_cats )
            {
// Sort root categories as it was requested
               
$this->sort_root_cats( NULL );
            }

           
$this->revealed_all_children = true;
        }
        else
        {    
// We are handling a subset (for example Chapers of a specific blog_ID only):

           
if( is_null( $subset_ID ) )
            {    
// No specific subset requested, we are going to reveal all subsets in a row:

                // Make sure everything has been loaded:
               
$this->load_all();

               
// Commented out, because it is needed in case of cross posting through blogs
                // echo 'DEBUG - PARTIAL IMPLEMENTATION - REVEALING ALL SUBSETS in a row. Is this needed?';

               
foreach( $this->subset_cache as $subset_ID => $dummy )
                {
                   
$this->reveal_children( $subset_ID, $sort_root_cats );
                }

               
$this->revealed_all_children = true;
            }
            else
            {    
// We're interested in a specific subset:
                // *** THIS IS THE CASE USED IN B2EVOLUTION ***

               
if( !empty( $this->revealed_subsets[$subset_ID] ) )
                {    
// Children have already been revealed:
                   
if( $sort_root_cats )
                    {
                       
$this->sort_root_cats( $subset_ID );
                    }
                    return;
                }

               
$Timer->resume('reveal_children', false );

               
$Debuglog->add( 'Revealing subset of children', 'CategoryCache' );

               
// Make sure the requested subset has been loaded:
               
$this->load_subset($subset_ID);

               
// Reveal children:
               
if( !empty( $this->subset_cache[$subset_ID] ) )
                {    
// There are loaded categories

                    // Now check that each category has a path to the root:
                   
foreach( $this->subset_cache[$subset_ID] as $cat_ID => $dummy )    // "as" would give a freakin COPY of the object :(((
                   
{
                       
$this->check_path_to_root( $subset_ID, $cat_ID );
                    }

                   
// loop on all loaded categories to set their children list if they have some:
                   
foreach( $this->subset_cache[$subset_ID] as $cat_ID => $dummy )    // "as" would give a freakin COPY of the object :(((
                   
{
                       
$Chapter = & $this->subset_cache[$subset_ID][$cat_ID];
                       
// echo '<br>'.$cat_ID;
                        // echo $Chapter->name;
                       
if( ! is_null( $Chapter->parent_ID ) )
                        {    
// This category has a parent, so add it to its parent children list:
                           
$Debuglog->add( 'adding child with ID='.$cat_ID.' to parent ID='.$Chapter->parent_ID, 'CategoryCache' );
                           
$this->cache[$Chapter->parent_ID]->add_child_category( $this->cache[$cat_ID] );
                        }
                        else
                        {    
// This category has no parent, so add it to the parent categories list
                           
$Debuglog->add( 'adding parent with ID='.$cat_ID, 'CategoryCache' );
                           
$this->root_cats[] = & $Chapter; // $this->cache[$cat_ID];
                           
$this->subset_root_cats[$this->cache[$cat_ID]->{$this->subset_property}][] = & $Chapter; // $this->cache[$cat_ID];
                       
}
                    }

                    if(
$sort_root_cats )
                    {
// Sort subset root categories as it was requested
                       
$this->sort_root_cats( $subset_ID );
                    }

                   
$Timer->pause('reveal_children', false );

                }

               
// Children have been revealed.
               
$this->revealed_subsets[$subset_ID] = true;
            }
        }

    }


   
/**
     * Support function for reveal_children()
     *
     * @param integer
     * @param integer
     * @return true if root parent
     */
   
function check_path_to_root( $subset_ID, $cat_ID, $path_array = array() )
    {
        global
$Debuglog;
       
$padding = str_pad('',count($path_array),'*');
       
$Debuglog->add( $padding.'Checking path to root for cat ID='.$cat_ID.' with path:'.implode(',',$path_array), 'CategoryCache' );
       
$Chapter = & $this->subset_cache[$subset_ID][$cat_ID];
        if(
is_null( $Chapter->parent_ID ) )
        {    
// Found root parent
           
$Debuglog->add( $padding.'*OK', 'CategoryCache' );
            return
true;
        }
       
// This is not the last parent...
        // Record path...
       
$path_array[] = $Chapter->ID;

        if(
in_array( $Chapter->parent_ID, $path_array ) )
        {    
// We are about to enter an infinite loop
           
$Debuglog->add( $padding.'*LOOP! -> assign to root', 'CategoryCache' );
           
$Chapter->parent_ID = NULL;
            return
false;
        }
        elseif( ! isset(
$this->cache[$Chapter->parent_ID] ) )
        {
           
$Debuglog->add( $padding.'*Missing parent ID('.$Chapter->parent_ID.')! -> assign to root', 'CategoryCache' );
           
$Chapter->parent_ID = NULL;
            return
false;
        }

       
// Recurse!
       
return $this->check_path_to_root( $subset_ID, $Chapter->parent_ID, $path_array );
    }


   
/**
     * Compare an Item with a Chapter based on the given order type
     * TODO: asimo> Move this function to a different class ( E.g. to Chapter or to ChapterCache )
     *
     * @param Object Item
     * @param Object Chapter
     * @param string order type: 'alpha' or 'manual'
     * @return @return number -1 if Item < Chapter, 1 if Item > Chapter, 0 if Item == Chapter
     */
   
function compare_item_with_chapter( $Item, $Chapter, $order_type )
    {
        if(
$order_type == 'alpha' )
        {
            return
strcmp( $Item->title, $Chapter->name );
        }

       
// Get item order depending on category:
       
$item_order = $Item->get_order( $Chapter->get( 'parent_ID' ) );

        if(
$item_order == NULL )
        {
            return
$Chapter->order == NULL ? 0 : 1;
        }
        elseif(
$Chapter->order == NULL )
        {
            return -
1;
        }

        return (
$item_order < $Chapter->order ) ? -1 : ( ( $item_order > $Chapter->order ) ? 1 : 0 );
    }


   
/**
     * Sort root categories or root categories in a subset if they are not sorted yet
     *
     * @param integer subset_ID, leave it on null to sort in all subsets
     */
   
function sort_root_cats( $subset_ID = NULL )
    {
        if(
$subset_ID == NULL && empty($this->subset_property) )
        {
            if( !
$this->sorted_flags['root_cats'] )
            {
// Root cats were not sorted yet
               
usort( $this->root_cats, array( 'Chapter','compare_chapters' ) );
               
$this->sorted_flags['root_cats'] = true;
            }
            return;
        }

        if(
$subset_ID == NULL )
        {
            foreach(
array_keys( $this->subset_root_cats ) as $key )
            {
                if(
$this->sorted_flags[$key] )
                {
// This subset was already sorted
                   
continue;
                }

               
usort( $this->subset_root_cats[$key], array( 'Chapter','compare_chapters' ) );
               
$this->sorted_flags[$key] = true;
            }
            return;
        }

        if( empty(
$this->sorted_flags[$subset_ID]) )
        {
// This subset was not sorted yet
           
usort( $this->subset_root_cats[$subset_ID], array( 'Chapter','compare_chapters' ) );
           
$this->sorted_flags[$subset_ID] = true;
        }
    }


   
/**
     * Return recursive display of loaded categories
     *
     * @param array callback functions (to format the display)
     * @param integer|NULL NULL for all subsets
     * @param array categories list to display
     * @param integer depth of categories list
     * @param integer Max depth of categories list, 0/empty - is infinite
     * @param array Other params regarding the recursive display:
     *   boolean sorted, sort categories or not - default value is false
     *   string chapter_path, array of chapter ids from root category to the selected category
     *   boolean expand_all, set true to expand all categories and not only the one in the chapter path
     *   etc.
     *
     * @return string recursive list of all loaded categories
     */
   
function recurse( $callbacks, $subset_ID = NULL, $cat_array = NULL, $level = 0, $max_level = 0, $params = array() )
    {
       
$params = array_merge( array(
               
'highlight_current' => true,
               
'sorted' => false,
               
'chapter_path' => array(),
               
'expand_all' => true,
               
'posts_are_loaded' => false,
               
'subset_ID' => $subset_ID,
               
'max_level' => $max_level
           
), $params );
       
$sorted = $params['sorted'];
       
$display_posts = ! empty( $callbacks['posts'] );

       
// Make sure children have been (loaded and) revealed for specific subset:
       
$this->reveal_children( $subset_ID, $sorted );

        if(
$display_posts && !$params['posts_are_loaded'] && ( $params['expand_all'] || !empty( $params['chapter_path'] ) ) )
        {
// Posts needs to be loaded
           
$ItemCache = & get_ItemCache();
           
$ItemCache->load_by_categories( $params['chapter_path'], $subset_ID );
           
$params['posts_are_loaded'] = true;
        }

        if(
is_null( $cat_array ) )
        {    
// Get all parent categories:
           
if( is_null( $subset_ID ) )
            {
               
$cat_array = $this->root_cats;
            }
            elseif( isset(
$this->subset_root_cats[$subset_ID] ) )
            {
// We have root cats for the requested subset:
               
$cat_array = $this->subset_root_cats[$subset_ID];
            }
            else
            {
               
$cat_array = array();
            }
        }

       
$r = '';

       
// Go through all categories at this level:
       
foreach( $cat_array as $cat )
        {
            if( isset(
$params['excluded_cat_IDs']) && in_array(  $cat->ID, $params['excluded_cat_IDs'] ) )
            {    
// Category is excluded, Skip it:
               
continue;
            }
           
           
// Check if category is expended
           
$params['is_selected'] = $params['highlight_current'] && in_array( $cat->ID, $params['chapter_path'] );
           
$params['is_opened'] = $params['expand_all'] || $params['is_selected'];

           
// Display a category:
           
if( is_array( $callbacks['line'] ) )
            {
// object callback:
               
$r .= $callbacks['line'][0]->{$callbacks['line'][1]}( $cat, $level, $params ); // <li> Category  - or - <tr><td>Category</td></tr> ...
           
}
            else
            {
               
$r .= $callbacks['line']( $cat, $level, $params ); // <li> Category  - or - <tr><td>Category</td></tr> ...
           
}

            if(
$params['is_opened'] || ( $max_level > $level + 1 ) )
            {    
// Iterate through sub categories recursively:
               
$params['level'] = $level;
               
$r .= $this->iterate_through_category_children( $cat, $callbacks, true, $params );
            }
        }

        if( ! empty(
$r ) )
        {
// We have something to display at this level, wrap in in before/after:
           
$r_before = '';
            if(
is_array( $callbacks['before_level'] ) )
            {
// object callback:
               
$r_before .= $callbacks['before_level'][0]->{$callbacks['before_level'][1]}( $level ); // <ul>
           
}
            elseif( isset(
$callbacks['before_level'] ) )
            {
               
$r_before .= $callbacks['before_level']( $level ); // <ul>
           
}

           
$r = $r_before.$r;

            if(
is_array( $callbacks['after_level'] ) )
            {
// object callback:
               
$r .= $callbacks['after_level'][0]->{$callbacks['after_level'][1]}( $level ); // </ul>
           
}
            elseif( isset(
$callbacks['after_level'] ) )
            {
               
$r .= $callbacks['after_level']( $level ); // </ul>
           
}
        }

        return
$r;
    }


   
/**
     * Iterate through the given Chapter sub cats and items
     *
     * @param Object Chapter
     * @param array Callback functions to display a Chapter and to display an Item
     * @param boolean Set true to iterate sub categories recursively, false otherwise
     * @param array Any additional params ( e.g. 'sorted', 'level', 'list_subs_start', etc. )
     * @return string the concatenated callbacks result
     */
   
function iterate_through_category_children( $Chapter, $callbacks, $recurse = false, $params = array() )
    {
       
$r = "";
       
$cat_items = array();
       
$has_sub_cats = ! empty( $Chapter->children );
       
$params = array_merge( array(
               
'sorted'    => false,
               
'level'     => 0,
               
'max_level' => 0,
               
'subset_ID' => $Chapter->blog_ID,
               
'items_order_alpha_func' => 'compare_items_by_title',
               
'exclude_cats' => array(),
            ),
$params );

        if(
$params['sorted'] && $has_sub_cats )
        {
           
$Chapter->sort_children();
        }
        if( ! empty(
$callbacks['posts'] ) )
        {
           
$ItemCache = & get_ItemCache();
           
$cat_items = $ItemCache->get_by_cat_ID( $Chapter->ID, $params['items_order_alpha_func'] );
        }

       
// Does this category have content to display?
       
if( $has_sub_cats || !empty( $cat_items ) )
        {
// Display category or posts
           
$cat_index = 0;
           
$item_index = 0;
           
$subcats_to_display = array();
           
$chapter_children_ids = array_keys( $Chapter->children );
           
$has_more_children = isset( $chapter_children_ids[$cat_index] );
           
$has_more_items = isset( $cat_items[$item_index] );
           
// alpha or manual order?
           
$cat_order = $Chapter->get_subcat_ordering();
           
// Set post params for post display
           
$params['chapter_ID'] = $Chapter->ID;
           
$params['cat_order'] = $cat_order;

            if( (
$has_more_children || $has_more_items ) && isset( $params['list_subs_start'] ) )
            {
               
$r .= $params['list_subs_start'];
            }

           
// Display all sub Chapters and items of current Chapter:
           
while( $has_more_children || $has_more_items )
            {
               
$current_sub_Chapter = $has_more_children ? $Chapter->children[$chapter_children_ids[$cat_index]] : NULL;
               
$current_Item = $has_more_items ? $cat_items[$item_index] : NULL;

               
// Determine if the next thing to display is a Chapter or an Item:
               
if( $current_Item != NULL
                   
&& ( $current_sub_Chapter == NULL
                       
|| ( $this->compare_item_with_chapter( $current_Item, $current_sub_Chapter, $cat_order ) <= 0 ) ) )
                {    
// Next Item should display before Next Chapter/subcat

                   
if( ! empty( $subcats_to_display ) )
                    {
                        if(
$recurse )
                        {    
// Display sub-contents:
                           
$r .= $this->recurse( $callbacks, $params['subset_ID'], $subcats_to_display, $params['level'] + 1, $params['max_level'], $params );
                        }
                        else
                        {    
// Display each category without recursion:
                           
foreach( $subcats_to_display as $sub_Chapter )
                            {
// Display each category:
                               
if( in_array( $sub_Chapter->ID, $params['exclude_cats'] ) )
                                {    
// Exclude category:
                                   
continue;
                                }
                                if(
is_array( $callbacks['line'] ) )
                                {
// object callback:
                                   
$r .= $callbacks['line'][0]->{$callbacks['line'][1]}( $sub_Chapter, 0, $params ); // <li> Category  - or - <tr><td>Category</td></tr> ...
                               
}
                                else
                                {
                                   
$r .= $callbacks['line']( $sub_Chapter, 0, $params ); // <li> Category  - or - <tr><td>Category</td></tr> ...
                               
}
                            }
                        }
                       
$subcats_to_display = array();
                    }

                   
// Display an Item:
                   
if( is_array( $callbacks['posts'] ) )
                    {
// object callback:
                       
$r .= $callbacks['posts'][0]->{$callbacks['posts'][1]}( $current_Item, $params['level'] + 1, $params );
                    }
                    else
                    {
                       
$r .= $callbacks['posts']( $current_Item, $params['level'] + 1, $params );
                    }
                   
$has_more_items = isset( $cat_items[++$item_index] );
                }
                elseif(
$current_sub_Chapter != NULL )
                {    
// Next Chapter should display before next Item:

                   
$subcats_to_display[] = $current_sub_Chapter;
                   
$has_more_children = isset( $chapter_children_ids[++$cat_index] );
                }
            }

            if( ! empty(
$subcats_to_display ) )
            {
// Display all subcats which were not displayed yet:
               
if( $recurse )
                {
                   
$r .= $this->recurse( $callbacks, $params['subset_ID'], $subcats_to_display, $params['level'] + 1, $params['max_level'], $params );
                }
                else
                {
// Display each category without recursion:
                   
foreach( $subcats_to_display as $sub_Chapter )
                    {
// Display each category:
                       
if( in_array( $sub_Chapter->ID, $params['exclude_cats'] ) )
                        {    
// Exclude category:
                           
continue;
                        }
                        if(
is_array( $callbacks['line'] ) )
                        {
// object callback:
                           
$r .= $callbacks['line'][0]->{$callbacks['line'][1]}( $sub_Chapter, 0, $params ); // <li> Category  - or - <tr><td>Category</td></tr> ...
                       
}
                        else
                        {
                           
$r .= $callbacks['line']( $sub_Chapter, 0, $params ); // <li> Category  - or - <tr><td>Category</td></tr> ...
                       
}
                    }
                }
            }

            if( (
$cat_index > 0 || $item_index > 0 ) && isset( $params['list_subs_end'] ) )
            {
               
$r .= $params['list_subs_end'];
            }
        }
        elseif( isset(
$callbacks['no_children'] ) )
        {
// Display message when no children:
           
           
if( is_array( $callbacks['no_children'] ) )
            {
// object callback:
               
$r .= $callbacks['no_children'][0]->{$callbacks['no_children'][1]}( $Chapter, $params['level'] + 1 ); // </li>
           
} else {
               
$r .= $callbacks['no_children']( $Chapter, $params['level'] + 1 ); // </li>
           
}
        }

        return
$r;
    }


   
/**
     * Return array of recursive select options list of all loaded categories
     *
     * @param integer NULL for all subsets
     * @param boolean Include the root element?
     * @param array GenercCategory objects to display (will recurse from those starting points)
     * @param integer depth of categories list
     * @param array IDs of categories to exclude (their children will be ignored to)
     *
     * @return array select options list of all loaded categories
     */
   
function recurse_select_options( $subset_ID = NULL, $include_root = false, $Cat_array = NULL,
                           
$level = 0, $exclude_array = array() )
    {
       
// Make sure children have been revealed for specific subset:
       
$this->reveal_children( $subset_ID );

        if(
is_null( $Cat_array ) )
        {    
// Get all parent categorie:
           
$Cat_array = $this->root_cats;
        }

       
// Sort categories alphabetically or manually depending on settings:
       
usort( $Cat_array, array( 'Chapter', 'compare_chapters' ) );

       
$options = array();

        if(
$include_root )
        {
           
$options[] = T_('Root');
           
$level++;
        }

        foreach(
$Cat_array as $Chapter )
        {
            if(
in_array( $Chapter->ID, $exclude_array ) )
            {    
// We want to exclude that cat.
               
continue;
            }

           
// Set category indentation in the select:
           
$indent = '';
            for(
$i = 0; $i < $level; $i++)
            {
               
$indent .='&nbsp;&nbsp;';
            }
           
// Set category option:
           
$options[ $Chapter->ID ] = $indent.$Chapter->name;

            if( ! empty(
$Chapter->children ) )
            {    
// Add children categories:
               
$options += $this->recurse_select_options( $subset_ID, false, $Chapter->children, $level+1, $exclude_array );
            }
        }

        return
$options;
    }


   
/**
     * Return string of recursive select options list of all loaded categories
     *
     * @param integer selected category in the select input
     * @param integer NULL for all subsets
     * @param boolean Include the root element?
     * @param array GenercCategory objects to display (will recurse from those starting points)
     * @param integer depth of categories list
     * @param array IDs of categories to exclude (their children will be ignored to)
     *
     * @return string select options list of all loaded categories
     */
   
function recurse_select( $selected = NULL, $subset_ID = NULL, $include_root = false, $Cat_array = NULL,
                           
$level = 0, $exclude_array = array() )
    {
       
$r = '';

       
$options = $this->recurse_select_options( $subset_ID, $include_root, $Cat_array, $level, $exclude_array );

        foreach(
$options as $chapter_ID => $option_title )
        {
           
$r .= '<option value="'.( $chapter_ID ? $chapter_ID : '' ).'"'.( $chapter_ID == $selected ? ' selected="selected"' : '' ).'>'.$option_title.'</option>';
        }

        return
$r;
    }
}

?>