Seditio Source
Root |
./othercms/b2evolution_7.2.3/inc/_core/ui/results/_results.class.php
<?php
/**
 * This file implements the Results 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/}
 * Parts of this file are copyright (c)2005-2006 by PROGIDISTRI - {@link http://progidistri.com/}.
 *
 * @package evocore
 */
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );


// DEBUG: (Turn switch on or off to log debug info for specified category)
$GLOBALS['debug_results'] = false;


load_class( '_core/ui/_table.class.php', 'Table' );

/**
 * Results class
 *
 * @package evocore
 * @todo Support $cols[]['order_rows_callback'] / order_objects_callback also if there's a LIMIT?
 */
class Results extends Table
{
   
/**
     * SQL query
     */
   
var $sql;

   
/**
     * SQL query to count total rows
     */
   
var $count_sql;

   
/**
     * Total number of rows (if > {@link $limit}, it will result in multiple pages)
     *
     * Note: Do not use directly outside of the results class, use get_total_rows() function instead!
     */
   
var $total_rows;

   
/**
     * Number of lines per page
     */
   
var $limit;

   
/**
     * Number of rows in result set for current page.
     */
   
var $result_num_rows;

   
/**
     * Current page
     */
   
var $page;

   
/**
     * Array of DB rows for current page.
     */
   
var $rows;

   
/**
     * List of IDs for current page.
     * @uses Results::$ID_col
     */
   
var $page_ID_list;

   
/**
     * Array of IDs for current page.
     * @uses Results::$ID_col
     */
   
var $page_ID_array;

   
/**
     * Current object idx in $rows array
     * @var integer
     */
   
var $current_idx = 0;

   
/**
     * idx relative to whole list (range: 0 to total_rows-1)
     * @var integer
     */
   
var $global_idx;

   
/**
     * Is this gobally the 1st item in the list? (NOT just the 1st in current page)
     */
   
var $global_is_first;

   
/**
     * Is this gobally the last item in the list? (NOT just the last in current page)
     */
   
var $global_is_last;


   
/**
     * Cache to use to instantiate an object and cache it for each line of results.
     *
     * For this to work, all columns of the related table must be selected in the query
     *
     * @var DataObjectCache
     */
   
var $Cache;

   
/**
     * This will hold the object instantiated by the Cache for the current line.
     */
   
var $current_Obj;


   
/**
     * Definitions for each column:
     * - th
     * - td
     * - order: SQL column name(s) to sort by (delimited by comma)
     * - order_objects_callback: a PHP callback function (can be array($Object, $method)).
     *     This gets three params: $a, $b, $desc.
     *     $a and $b are instantiated objects from {@link Results::$Cache}
     *     $desc is either 'ASC' or 'DESC'. The function has to return -1, 0 or 1,
     *     according to if the $a < $b, $a == $b or $a > $b.
     * - order_rows_callback: a PHP callback function (can be array($Object, $method)).
     *     This gets three params: $a, $b, $desc.
     *     $a and $b are DB row objects
     *     $desc is either 'ASC' or 'DESC'. The function has to return -1, 0 or 1,
     *     according to if the $a < $b, $a == $b or $a > $b.
     * - td_class
     *
     */
   
var $cols;

   
/**
     * Do we want to display column headers?
     * @var boolean
     */
   
var $col_headers = true;


   
/**
     * DB fieldname to group on.
     *
     * Leave empty if you don't want to group.
     *
     * NOTE: you have to use ORDER BY goup_column in your query for this to work correctly.
     *
     * @var mixed string or array
     */
   
var $group_by = '';

   
/**
     * Object property/properties to group on.
     *
     * Objects get instantiated and grouped by the given property/member value.
     *
     * NOTE: this requires {@link Result::$Cache} to be set and is probably only useful,
     *       if you do not use {@link Result::$limit}, because grouping appears after
     *       the relevant data has been pulled from DB.
     *
     * @var mixed string or array
     */
   
var $group_by_obj_prop;

   
/**
     * Current group identifier (by level/depth)
     * @var array
     */
   
var $current_group_ID;

   
/**
     * Definitions for each GROUP column:
     * -td
     * -td_start. A column with no def will de displayed using
     * the default defs from Results::$params, that is to say, one of these:
     *   - $this->params['grp_col_start_first'];
     *   - $this->params['grp_col_start_last'];
     *   - $this->params['grp_col_start'];
     */
   
var $grp_cols = NULL;

   
/**
     * Fieldname to detect empty data rows.
     *
     * Empty data rows can happen when left joining on groups.
     * Leave empty if you don't want to detect empty datarows.
     *
     * @var string
     */
   
var $ID_col = '';

   
/**
     * URL param names
     */
   
var $page_param;
    var
$order_param;
    var
$limit_param;

   
/**
     * List of sortable fields
     */
   
var $order_field_list;

   
/**
     * List of sortable columns by callback ("order_objects_callback" and "order_rows_callback")
     * @var array
     */
   
var $order_callbacks;

   
/**
     * Default ordering of columns (special syntax) if not specified in the URL params
     *    example: -A-- will sort in ascending order on 2nd column
     *    example: ---D will sort in descending order on 4th column
     * @var string
     */
   
var $default_order;

   
/**
     * Used to force an order param to default value when total rows count > this var;
     * NULL - to don't force
     * @var integer|NULL
     */
   
var $force_order_by_count;

   
/**
     * The internal messages that are displayed under filters block
     * @var array
     */
   
var $messages;

   
/**
     * Parameters for the functions area (to display functions at the end of results array):
     */
   
var $functions_area;


   
/**
     * Should there be nofollows on page navigation
     */
   
var $nofollow_pagenav = false;

   
/**
     * Additional debug info prefix for each SQL query of this class.
     * Useful to detect what widget, plugin and etc. calls the SQL queries
     */
   
var $query_title_prefix = '';

   
/**
     * List of checkbox toggle selectors to display checkbox toggle panel.
     * Can be a single selector, e.g., 'input[name^=prefix_]:checkbox',
     * or an array of selectors for toggling multiple set of checkboxes,
     * e.g., array( 'Checkbox set 1 label' => 'input[^=set1_]:checkbox', 'Checkbox set 2 label' => 'input[^=set2_]:checkbox' )
     */
   
var $checkbox_toggle_selectors;

   
/**
     * Constructor
     *
     * @todo we might not want to count total rows when not needed...
     * @todo fplanque: I am seriously considering putting $count_sql into 2nd or 3rd position. Any prefs?
     * @todo dh> We might just use "SELECT SQL_CALC_FOUND_ROWS ..." and "FOUND_ROWS()"..! - available since MySQL 4 - would save one query just for counting!
     *
     * @param string SQL query
     * @param string prefix to differentiate page/order params when multiple Results appear one same page
     * @param string default ordering of columns (special syntax) if not specified in the URL params
     *               example: -A-- will sort in ascending order on 2nd column
     *               example: ---D will sort in descending order on 4th column
     * @param integer Default number of lines displayed on one page (0 to disable paging; null to use $UserSettings/results_per_page)
     * @param string|integer SQL query used to count the total # of rows
     *                                                 - if integer, we'll use that as the count
     *                                                 - if NULL, we'll try to COUNT(*) by ourselves
     * @param boolean TRUE to initialize page params
     * @param integer|NULL Total rows count that allows to use an order of any column, NULL - don't restrict
     */
   
function __construct( $sql, $param_prefix = '', $default_order = '', $default_limit = NULL, $count_sql = NULL, $init_page = true, $force_order_by_count = NULL )
    {
       
parent::__construct( NULL, $param_prefix );

       
$this->sql = $sql;
       
$this->count_sql = $count_sql;

       
$this->init_limit_param( $default_limit );

        if(
$init_page )
        {
// attribution of a page number
           
$this->page_param = 'results_'.$this->param_prefix.'page';
           
$this->page = param( $this->page_param, 'integer', 1, true );
        }

       
$this->default_order = $default_order;
       
$this->force_order_by_count = $force_order_by_count;
    }


   
/**
     * Initialize the limit param
     *
     * @param integer Default number of lines displayed on one page (0 to disable paging; null to use $UserSettings/results_per_page)
     */
   
function init_limit_param( $default_limit )
    {
        global
$UserSettings;

        if( empty(
$UserSettings ) )
        {
           
load_class( 'users/model/_usersettings.class.php', 'UserSettings' );
           
$UserSettings = new UserSettings();
        }

       
// attribution of a limit number
       
$this->limit_param = 'results_'.$this->param_prefix.'per_page';
       
$this->limit = param( $this->limit_param, 'integer', -1, true );
        if( !empty(
$this->param_prefix ) &&
           
$this->limit > -1 &&
           
$this->limit != (int)$UserSettings->get( $this->limit_param ) )
        {    
// Change a limit number in DB for current user and current list
           
if( $this->limit == $UserSettings->get( 'results_per_page' ) || $this->limit == 0 )
            {    
// Delete a limit number for current list if it equals a default value
               
$UserSettings->delete( $this->limit_param );
               
$this->limit = $UserSettings->get( 'results_per_page' );
            }
            else
            {    
// Set a new value of limit number current list
               
$UserSettings->set( $this->limit_param, $this->limit );
            }
           
$UserSettings->dbupdate();
        }

        if( !empty(
$this->param_prefix ) && $this->limit == -1 )
        {    
// Set a limit number from DB
           
if( $UserSettings->get( $this->limit_param ) > 0 )
            {    
// Set a value for current list if it was already defined
               
$this->limit = $UserSettings->get( $this->limit_param );
            }
        }

        if(
$this->limit == -1 || empty( $this->limit ) )
        {    
// Set a default value
           
$this->limit = is_null( $default_limit ) ? $UserSettings->get( 'results_per_page' ) : intval( $default_limit );
        }
    }


   
/**
     * Initialize the order param
     */
   
function init_order_param()
    {
        global
$UserSettings;

        if( empty(
$UserSettings ) )
        {
           
$UserSettings = new UserSettings();
        }

       
// attribution of an order type
       
$this->order_param = 'results_'.$this->param_prefix.'order';
       
$order_request = param( $this->order_param, 'string', '', true );
       
// remove symbols '-' from the end
       
$order_request = rtrim( $order_request, '-' );

        if(
$this->force_order_by_count !== NULL && ! empty( $order_request ) )
        {
// Check if we should force an order filed to default value
           
if( $this->get_total_rows() > $this->force_order_by_count )
            {
// This table has very much records we should force an order to default
               
$reverse_default_order = str_replace( 'D', 'A', $this->default_order );
               
$reverse_default_order = ( $reverse_default_order == $this->default_order ) ? str_replace( 'A', 'D', $this->default_order ) : $reverse_default_order;
                if(
$order_request != $this->default_order && $order_request != $reverse_default_order )
                {
// If an order from request is not default then we must change it to default
                   
$this->order = $this->default_order;
                   
$order_request_title = $order_request;
                    if( isset(
$this->cols ) )
                    {
// Try to find a title of the ordered field to display it in warning message
                       
$order_index = strpos( $order_request, 'A' );
                       
$order_index = ( $order_index === FALSE ) ? strpos( $order_request, 'D' ) : $order_index;
                        if( isset(
$this->cols[ $order_index ] ) && isset( $this->cols[ $order_index ]['th'] ) )
                        {
                           
$order_request_title = $this->cols[ $order_index ]['th'];
                        }
                    }
                   
// Add a message to inform user about this order type is not allowed in this case
                   
$this->add_message( sprintf( T_( 'In order to maintain good performance, you cannot sort by %s when there are more than %s results.' ), $order_request_title, number_format( $this->force_order_by_count, 0, '', ' ' ) ) );
                }
            }
        }

        if( empty(
$this->order ) )
        {
// Set an order from GET request
           
$this->order = $order_request;
        }

        if( !empty(
$this->param_prefix ) &&
            !empty(
$this->order ) &&
           
$this->order != $UserSettings->get( $this->order_param ) )
        {    
// Change an order param in DB for current user and current list
           
if( $this->order == $this->default_order )
            {    
// Delete an order param for current list if it is a default value
               
$UserSettings->delete( $this->order_param );
            }
            else
            {    
// Set a new value of an order param for current list
               
$UserSettings->set( $this->order_param, $this->order );
            }
           
$UserSettings->dbupdate();
        }

        if( !empty(
$this->param_prefix ) && empty( $this->order ) )
        {    
// Set an order param from DB
           
if( $UserSettings->get( $this->order_param ) != '' )
            {    
// Set a value for current list if it was already defined
               
$this->order = $UserSettings->get( $this->order_param );
            }
        }

        if( empty(
$this->order ) )
        {    
// Set a default value
           
$this->order = $this->default_order;
        }
    }


   
/**
     * Reset the query -- EXPERIMENTAL
     *
     * Useful in derived classes such as ItemList to requery with a slighlty moidified filterset
     */
   
function reset()
    {
       
$this->rows = NULL;
       
$this->total_rows = NULL;
    }


   
/**
     * Rewind resultset
     */
   
function restart()
    {
        if( !isset(
$this->total_rows ) )
        {
// Count total rows to be able to display pages correctly
           
$this->count_total_rows( $this->count_sql );
        }

       
// Make sure query has executed:
       
$this->run_query();

       
$this->current_idx = 0;

       
$this->global_idx = (($this->page-1) * $this->limit) + $this->current_idx;

       
$this->global_is_first = ( $this->global_idx <= 0 ) ? true : false;

       
$this->global_is_last = ( $this->global_idx >= $this->total_rows-1 ) ? true : false;

       
$this->current_group_ID = NULL;
    }


   
/**
     * Increment and update all necessary counters before processing a new line in result set
     */
   
function next_idx()
    {
       
$this->current_idx++;

       
$this->global_idx = (($this->page-1) * $this->limit) + $this->current_idx;

       
$this->global_is_first = ( $this->global_idx <= 0 ) ? true : false;

       
$this->global_is_last = ( $this->global_idx >= $this->total_rows-1 ) ? true : false;

        return
$this->current_idx;
    }


   
/**
     * Run the query now!
     *
     * Will only run if it has not executed before.
     *
     * We need this query() stub in order to call it from restart() and still
     * let derivative classes override it (e-g: CommentList2)
     *
     * @deprecated Use new function run_query()
     */
   
function query( $create_default_cols_if_needed = true, $append_limit = true, $append_order_by = true )
    {
       
$this->run_query( $create_default_cols_if_needed, $append_limit, $append_order_by );
    }


   
/**
     * Run the query now!
     *
     * Will only run if it has not executed before.
     */
   
function run_query( $create_default_cols_if_needed = true, $append_limit = true, $append_order_by = true,
                                       
$query_title = 'Results::run_query()' )
    {
        global
$DB, $Debuglog;
        if( !
is_null( $this->rows ) )
        {
// Query has already executed:
           
return;
        }

        if( empty(
$this->sql ) )
        {
// the sql query is empty so we can't process it
           
return;
        }

        if( isset(
$this->total_rows ) && intval( $this->total_rows ) === 0 )
        {
// The number of total rows was counted and the sql query has no results
           
return;
        }

       
// Make sure we have colum definitions:
       
if( is_null( $this->cols ) && $create_default_cols_if_needed )
        {
// Let's create default column definitions:
           
$this->cols = array();

            if( !
preg_match( '#^(SELECT.*?(\([^)]*?FROM[^)]*\).*)*)FROM#six', $this->sql, $matches ) )
            {
               
debug_die( 'Results->run_query() : No SELECT clause!' );
            }
           
// Split requested columns by commata
           
foreach( preg_split( '#\s*,\s*#', $matches[1] ) as $l_select )
            {
                if(
is_numeric( $l_select ) )
                {
// just a single value (would produce parse error as '$x$')
                   
$this->cols[] = array( 'td' => $l_select );
                }
                elseif(
preg_match( '#^(\w+)$#i', $l_select, $match ) )
                {
// regular column
                   
$this->cols[] = array( 'td' => '$'.$match[1].'$' );
                }
                elseif(
preg_match( '#^(.*?) AS (\w+)#i', $l_select, $match ) )
                {
// aliased column
                   
$this->cols[] = array( 'td' => '$'.$match[2].'$' );
                }
            }

            if( !isset(
$this->cols[0]) )
            {
               
debug_die( 'No columns selected!' );
            }
        }


       
// Make a copy of the SQL, that we may change and that gets executed:
       
$sql = $this->sql;

       
// Append ORDER clause if necessary:
       
if( $append_order_by && ($orders = $this->get_order_field_list()) )
        {    
// We have orders to append

           
if( ! preg_match( '# \s ORDER \s+ BY \s+ [^\)]+$#xi', $sql ) )
            {
// there is no ORDER BY clause in the original SQL query at the end(excluding order clause in sub queries)
               
$sql .= ' ORDER BY '.$orders.' ';
            }
            else
            {
// try to insert the chosen order at an existing '*' point
               
if( preg_match( '#[^\s]+\s(DESC|ASC)#i', $orders, $match_orders ) )
                {
// Set an order direction for additional order field to the same value as it has first order field
                    // For example, this sql clause "ORDER BY *, hit_ID" should be changed to:
                    //    1) "ORDER BY *, hit_ID ASC"  if $orders == "hit_type ASC, hit_response_code DESC"
                    //    2) "ORDER BY *, hit_ID DESC" if $orders == "hit_type DESC, hit_response_code ASC"
                   
$sql = preg_replace( '# \s ORDER \s+ BY .+ \*, \s+ ([a-z0-9\-_]+)$#xi', ' ORDER BY *, $1 '.$match_orders[1], $sql );
                }
               
$inserted_sql = preg_replace( '# \s ORDER \s+ BY (.+) \* #xi', ' ORDER BY $1 '.$orders, $sql );

                if(
$inserted_sql != $sql )
                {
// Insertion ok:
                   
$sql = $inserted_sql;
                }
                else
                {
// No insert point found:
                    // the chosen order must be appended to an existing ORDER BY clause
                   
$sql .= ', '.$orders;
                }
            }
        }
        else
        {    
// Make sure there is no * in order clause:
           
$sql = preg_replace( '# \s ORDER \s+ BY (.+) \* #xi', ' ORDER BY $1 ', $sql );
        }

       
$add_limit = $append_limit && ! empty( $this->limit );

        if(
$add_limit && ! $this->order_callbacks )
        {    
// No callbacks to be called, so we can limit the line range to the requested page:
           
$Debuglog->add( 'LIMIT requested and no callbacks - adding LIMIT to query.', 'results' );
           
$sql .= ' LIMIT '.max( 0, ( $this->page - 1 ) * $this->limit ).', '.$this->limit;
        }

       
// Execute query and store results
       
$this->rows = $DB->get_results( $sql, OBJECT, ( empty( $this->query_title_prefix ) ? '' : $this->query_title_prefix.' - ' ).$query_title );

        if ( !
$this->order_callbacks || ! $add_limit )
        {
           
$Debuglog->add( 'Storing row count (no LIMIT or no callbacks)', 'results' );
           
$this->result_num_rows = $DB->num_rows;
        }

       
// Sort with callbacks:
       
if( $this->order_callbacks )
        {
           
$Debuglog->add( 'Sorting with callbacks.', 'results' );
            foreach(
$this->order_callbacks as $order_callback )
            {
               
#echo 'order_callback: '; var_dump($order_callback);

               
$this->order_callback_wrapper_data = $order_callback; // to pass ASC/DESC param and callback itself through the wrapper to the callback

               
if( empty($order_callback['use_rows']) )
                {
// default: instantiate objects for the callback:
                   
usort( $this->rows, array( &$this, 'order_callback_wrapper_objects' ) );
                }
                else
                {
                   
usort( $this->rows, array( &$this, 'order_callback_wrapper_rows' ) );
                }
            }

            if (
$add_limit )
            {
               
$Debuglog->add( 'Callback sorting: LIMIT needed, extracting slice from array', 'results' );
               
$this->rows = array_slice( $this->rows, max( 0, ( $this->page - 1 ) * $this->limit ), $this->limit );
               
$this->result_num_rows = count( $this->rows );
            }
        }

       
// Group by object property:
       
if( ! empty($this->group_by_obj_prop) )
        {
            if( !
is_array($this->group_by_obj_prop) )
            {
               
$this->group_by_obj_prop = array($this->group_by_obj_prop);
            }

           
$this->mergesort( $this->rows, array( &$this, 'callback_group_by_obj_prop' ) );
        }

       
// $Debuglog->add( 'rows on page='.$this->result_num_rows, 'results' );
   
}


   
/**
     * Merge sort. This is required to not re-order items when sorting for e.g. grouping at the end.
     *
     * @see http://de2.php.net/manual/en/function.usort.php#38827
     *
     * @param array List of items to sort
     * @param callback Sort function/method
     */
   
function mergesort(&$array, $cmp_function)
    {
       
// Arrays of size < 2 require no action.
       
if (count($array) < 2) return;
       
// Split the array in half
       
$halfway = count($array) / 2;
       
$array1 = array_slice($array, 0, $halfway);
       
$array2 = array_slice($array, $halfway);
       
// Recurse to sort the two halves
       
$this->mergesort($array1, $cmp_function);
       
$this->mergesort($array2, $cmp_function);
       
// If all of $array1 is <= all of $array2, just append them.
       
if (call_user_func($cmp_function, end($array1), $array2[0]) < 1) {
               
$array = array_merge($array1, $array2);
                return;
        }
       
// Merge the two sorted arrays into a single sorted array
       
$array = array();
       
$ptr1 = $ptr2 = 0;
        while (
$ptr1 < count($array1) && $ptr2 < count($array2)) {
                if (
call_user_func($cmp_function, $array1[$ptr1], $array2[$ptr2]) < 1) {
                       
$array[] = $array1[$ptr1++];
                }
                else {
                       
$array[] = $array2[$ptr2++];
                }
        }
       
// Merge the remainder
       
while ($ptr1 < count($array1)) $array[] = $array1[$ptr1++];
        while (
$ptr2 < count($array2)) $array[] = $array2[$ptr2++];
        return;
     }


   
/**
     * Callback, to sort {@link Result::$rows} according to {@link Result::$group_by_obj_prop}.
     *
     * @param array DB row for object A
     * @param array DB row for object B
     * @param integer Depth, used internally (you can group on a list of member properties)
     * @return integer
     */
   
function callback_group_by_obj_prop( $row_a, $row_b, $depth = 0 )
    {
       
$obj_prop = $this->group_by_obj_prop[$depth];

       
$a = & $this->Cache->instantiate($row_a);
       
$a_value = $a->$obj_prop;
       
$b = & $this->Cache->instantiate($row_b);
       
$b_value = $b->$obj_prop;

        if(
$a_value == $b_value )
        {
            if(
$depth+1 < count($this->group_by_obj_prop) )
            {
                return
$this->callback_group_by_obj_prop( $row_a, $row_b, ($depth + 1) );
            }
            else
            {
// on the last level of grouping:
               
return 0;
            }
        }

       
// Sort empty group_by-values to the bottom
       
if( empty($a_value) )
            return
1;
        if( empty(
$b_value) )
            return -
1;

        return
strcasecmp( $a_value, $b_value );
    }


   
/**
     * Wrapper method to {@link usort()}, which instantiates objects and passed them on to the
     * order callback.
     *
     * @return integer
     */
   
function order_callback_wrapper_objects( $row_a, $row_b )
    {
       
$a = $this->Cache->instantiate($row_a);
       
$b = $this->Cache->instantiate($row_b);

        return (int)
call_user_func( $this->order_callback_wrapper_data['callback'],
               
$a, $b, $this->order_callback_wrapper_data['order'] );
    }


   
/**
     * Wrapper method to {@link usort()}, which passes the rows to the order callback.
     *
     * @return integer
     */
   
function order_callback_wrapper_rows( $row_a, $row_b )
    {
        return (int)
call_user_func( $this->order_callback_wrapper_data['callback'],
               
$row_a, $row_b, $this->order_callback_wrapper_data['order'] );
    }


   
/**
     * Get a list of IDs for current page
     *
     * @uses Results::$ID_col
     */
   
function get_page_ID_list()
    {
        if(
is_null( $this->page_ID_list ) )
        {
           
$this->page_ID_list = implode( ',', $this->get_page_ID_array() );
           
//echo '<br />'.$this->page_ID_list;
       
}

        return
$this->page_ID_list;
    }


   
/**
     * Get an array of IDs for current page
     *
     * @uses Results::$ID_col
     */
   
function get_page_ID_array()
    {
        if(
is_null( $this->page_ID_array ) )
        {
           
$this->page_ID_array = array();
           
// pre_dump( $this );
           
if( $this->result_num_rows )
            {    
// We have some rows to explore.
                // Fp> note: I don't understand why we can sometimes have: $this->rows = array{ [0]=>  NULL }  (found in Manual skin intro post testing)
               
foreach( $this->rows as $row )
                {
// For each row/line:
                   
$this->page_ID_array[] = $row->{$this->ID_col};
                }
            }
        }

        return
$this->page_ID_array;
    }


   
/**
     * Count the total number of rows of the SQL result (all pages)
     *
     * This is done by dynamically modifying the SQL query and forging a COUNT() into it.
     *
     * @todo dh> This might get done using SQL_CALC_FOUND_ROWS (I noted this somewhere else already)
     * fp> I have a vague memory about issues with SQL_CALC_FOUND_ROWS. Maybe it was not returned accurate counts. Or maybe it didn't work with GROUP BY. Sth like that.
     * dh> We could just try it. Adding some assertion in there, leaving the old
     *     code in place. I'm quite certain that it is working correctly with
     *     recent MySQL versions.
     *
     * @todo allow overriding?
     * @todo handle problem of empty groups!
     */
   
function count_total_rows( $sql_count = NULL )
    {
        global
$DB;

        if(
is_integer( $sql_count ) )
        {    
// we have a total already
           
$this->total_rows = $sql_count;
        }
        else
        {
// we need to query
           
if( is_null( $sql_count ) )
            {
                if(
is_null($this->sql) )
                {
// We may want to remove this later...
                   
$this->total_rows = 0;
                   
$this->total_pages = 0;
                    return;
                }

               
$sql_count = $this->sql;
               
// echo $sql_count;

                /*
                 *
                 * On a un probl�me avec la recherche sur les soci�t�s
                 * si on fait un select count(*), �a sort un nombre de r�ponses �norme
                 * mais on ne sait pas pourquoi... la solution est de lister des champs dans le COUNT()
                 * MAIS malheureusement �a ne fonctionne pas pour d'autres requ�tes.
                 * L'id�al serait de r�ussir � isoler qu'est-ce qui, dans la requ�te SQL, provoque le comportement
                 * bizarre....
                 */
                // Tentative 1:
                // if( !preg_match( '#FROM(.*?)((WHERE|ORDER BY|GROUP BY) .*)?$#si', $sql_count, $matches ) )
                //  debug_die( "Can't understand query..." );
                // if( preg_match( '#(,|JOIN)#si', $matches[1] ) )
                // { // there was a coma or a JOIN clause in the FROM clause of the original query,
                // Tentative 2:
                // fplanque: je pense que la diff�rence est sur la pr�sence de DISTINCT ou non.
                // if( preg_match( '#\s DISTINCT \s#six', $sql_count, $matches ) )
               
if( preg_match( '#\s DISTINCT \s+ ([A-Za-z_]+)#six', $sql_count, $matches ) )
                {
//
                    // Get rid of any Aliases in colmun names:
                    // $sql_count = preg_replace( '#\s AS \s+ ([A-Za-z_]+) #six', ' ', $sql_count );
                    // ** We must use field names in the COUNT **
                    //$sql_count = preg_replace( '#SELECT \s+ (.+?) \s+ FROM#six', 'SELECT COUNT( $1 ) FROM', $sql_count );

                    //Tentative 3: we do a distinct on the first field only when counting:
                   
$sql_count = preg_replace( '#^ \s* SELECT \s+ (.+?) \s+ FROM#six', 'SELECT COUNT( DISTINCT '.$matches[1].' ) FROM', $sql_count );
                }
                else
                {
// Single table request: we must NOT use field names in the count.
                   
$sql_count = preg_replace( '#^ \s* SELECT \s+ (.+?) \s+ FROM#six', 'SELECT COUNT( * ) FROM', $sql_count );
                }


               
// Make sure there is no ORDER BY clause at the end:
               
$sql_count = preg_replace( '# \s ORDER \s+ BY .* $#xi', '', $sql_count );

               
// echo $sql_count;
           
}

           
// Calculate the sum of values because the results may be grouped, so we must know a count of rows in all groups and not only in first group:
           
$this->total_rows = array_sum( $DB->get_col( $sql_count, 0, ( empty( $this->query_title_prefix ) ? '' : $this->query_title_prefix.' - ' ).get_class( $this ).'::count_total_rows()' ) ); //count total rows
       
}

       
// Calculate total pages depending on total rows and page size:
       
if( empty( $this->limit ) )
        {    
// If no page limiting, Display all results on single page:
           
$this->total_pages = $this->total_rows > 0 ? 1 : 0;
        }
        else
        {    
// If the results should be limited by page size:
           
$this->total_pages = ceil( $this->total_rows / $this->limit );
        }

       
// Make sure we're not requesting a page out of range:
       
if( $this->page > $this->total_pages )
        {
           
/*
                sam2kb> Isn't it better to display "Page not found" error instead?
                Current implementation is bad for SEO bacause it can potentially display an unlimited number of duplicate pages
                fp> on what public (not admin) url do we have this problem for example?
                sam2kb>fp Ideally there must be a list of options in Blog settings > SEO
                - display error
                - redirect to last page
                - display last page (this is what current version does)
                "paged" param: http://b2evolution.net/news/releases/?paged=99
                "page" param: http://b2evolution.net/news/2011/09/09/b2evolution-4-1-0-release?page=99
                fp> ok, the current implementation only has merit when the result list is not controlled by a plublic URL param.
                Otherwise the only 2 options should be 404 and 301.
                Also, teh detection code should probably NOT be here.
            */
           
$this->page = $this->total_pages;
        }
    }


   
/**
     * Get the number of total rows in the result
     *
     * @return integer
     */
   
function get_total_rows()
    {
        if( !isset(
$this->total_rows ) )
        {
// Count total rows to be able to display pages correctly
           
$this->count_total_rows( $this->count_sql );
        }

        return
$this->total_rows;
    }


   
/**
     * Note: this function might actually not be very useful.
     * If you define ->Cache before display, all rows will be instantiated on the fly.
     * No need to restart et go through the rows a second time here.
     *
     * @param DataObjectCache
     */
   
function instantiate_page_to_Cache( & $Cache )
    {
       
$this->Cache = & $Cache;

       
// Make sure query has executed and we're at the top of the resultset:
       
$this->restart();

        foreach(
$this->rows as $row )
        {
// For each row/line:

            // Instantiate an object for the row and cache it:
           
$this->Cache->instantiate( $row );
        }

    }


   
/**
     * Display paged list/table based on object parameters
     *
     * This is the meat of this class!
     *
     * @param array|NULL
     * @param array Fadeout settings array( 'key column' => array of values ) or 'session'
     * @return int # of rows displayed
     */
   
function display( $display_params = NULL, $fadeout = NULL )
    {
       
// Lazy fill $this->params:
       
parent::display_init( $display_params, $fadeout );

       
// -------------------------
        // Proceed with display:
        // -------------------------
       
echo $this->params['before'];

        if( !
is_ajax_content() )
        {
// Display TITLE/FILTERS only if NO AJAX request

            // DISPLAY TITLE:
           
if( isset( $this->title ) )
            {
// A title has been defined for this result set:
               
echo $this->replace_vars( $this->params['head_title'] );
            }

           
// Set this to TRUE in order to display all checkboxes before labels
           
$this->force_checkboxes_to_inline = true;

           
// DISPLAY FILTERS:
           
$this->display_filters();
        }

        if( ! isset(
$this->params['disable_evo_flush'] ) || ! $this->params['disable_evo_flush'] )
        {    
// Flush in order to show the filters before slow SQL query will be executed below
           
evo_flush();
        }

       
// Initialize the order param
       
$this->init_order_param();

       
// Make sure query has executed and we're at the top of the resultset:
       
$this->restart();

        if( !
is_ajax_content() )
        {
// Display COL SELECTION only if NO AJAX request
           
$this->display_colselect();
        }

       
// START OF AJAX CONTENT:
       
echo $this->replace_vars( $this->params['content_start'] );

            if(
$this->total_pages == 0 )
            {
// There are NO RESULTS! Nothing to display!

                // START OF LIST/TABLE:
               
$this->display_list_start();

               
// END OF LIST/TABLE:
               
$this->display_list_end();
            }
            else
            {
// We have rows to display:

                // Display internal messages
               
$this->display_messages();

               
// GLOBAL (NAV) HEADER:
               
$this->display_nav( 'header' );

               
// START OF LIST/TABLE:
               
$this->display_list_start();

                   
// DISPLAY COLUMN HEADERS:
                   
$this->display_col_headers();

                   
// GROUP & DATA ROWS:
                   
$this->display_body();

                   
// Totals line
                   
$this->display_totals();

                   
// Functions
                   
$this->display_functions();

               
// END OF LIST/TABLE:
               
$this->display_list_end();

               
// GLOBAL (NAV) FOOTER:
               
$this->display_nav( 'footer' );
            }

       
// END OF AJAX CONTENT:
       
echo $this->params['content_end'];

        echo
$this->params['after'];

       
// Return number of rows displayed:
       
return $this->current_idx;
    }


   
/**
     * Initialize things in order to be ready for displaying.
     *
     * This is useful when manually displaying, i-e: not by using Results::display()
      *
     * @param array ***please document***
     * @param array Fadeout settings array( 'key column' => array of values ) or 'session'
      */
   
function display_init( $display_params = NULL, $fadeout = NULL )
    {
       
// Lazy fill $this->params:
       
parent::display_init( $display_params, $fadeout );

       
// Make sure query has executed and we're at the top of the resultset:
       
if( ( ! isset( $this->total_rows ) ) || ( ( intval( $this->total_rows ) > 0 ) && ( ! isset( $this->rows ) ) ) )
        {
// The result is not empty but the rows are not set yet, the query needs to be executed
           
$this->restart();
        }
    }


   
/**
     * Display list/table body.
     *
     * This includes groups and data rows.
     */
   
function display_body()
    {
       
// BODY START:
       
$this->display_body_start();

       
// Prepare data for grouping:
       
$group_by_all = array();
        if( ! empty(
$this->group_by) )
        {
           
$group_by_all['row'] = is_array($this->group_by) ? $this->group_by : array($this->group_by);
        }
        if( ! empty(
$this->group_by_obj_prop) )
        {
           
$group_by_all['obj_prop'] = is_array($this->group_by_obj_prop) ? $this->group_by_obj_prop : array($this->group_by_obj_prop);
        }

       
$this->current_group_count = array(); // useful in parse_col_content()


       
foreach( $this->rows as $row )
        {
// For each row/line:

            /*
             * GROUP ROW stuff:
             */
           
if( ! empty($group_by_all) )
            {    
// We are grouping (by SQL and/or object property)...

               
$group_depth = 0;
               
$group_changed = false;
                foreach(
$group_by_all as $type => $names )
                {
                    foreach(
$names as $name )
                    {
                        if(
$type == 'row' )
                        {
                           
$value = $row->$name;
                        }
                        elseif(
$type == 'obj_prop' )
                        {
                           
$this->current_Obj = $this->Cache->instantiate($row); // useful also for parse_col_content() below
                           
$value = $this->current_Obj->$name;
                        }
                        else
debug_die( 'Invalid Results-group_by-type: '.var_export( $type, true ) );


                        if( ! isset(
$this->current_group_ID[$group_depth] ) || // Condition to start first group
                           
$this->current_group_ID[$group_depth] != $value ) // Condition to start all next groups
                       
{ // Group changed here:
                           
$this->current_group_ID[$group_depth] = $value;

                            if( ! isset(
$this->current_group_count[$group_depth]) )
                            {
                               
$this->current_group_count[$group_depth] = 0;
                            }
                            else
                            {
                               
$this->current_group_count[$group_depth]++;
                            }

                           
// unset sub-group identifiers:
                           
for( $i = $group_depth+1, $n = count($this->current_group_ID); $i < $n; $i++ )
                            {
                                unset(
$this->current_group_ID[$i]);
                            }

                           
$group_changed = true;
                            break
2;
                        }

                       
$group_depth++;
                    }
                }

                if(
$group_changed )
                {
// We have just entered a new group!

                   
echo $this->params['grp_line_start']; // TODO: dh> support grp_line_start_odd, grp_line_start_last, grp_line_start_odd_last - as defined in _adminUI_general.class.php

                   
$col_count = 0;
                    foreach(
$this->grp_cols as $grp_col )
                    {
// For each column:

                       
if( isset( $grp_col['td_class'] ) )
                        {    
// We have a class for the total column
                           
$class = $grp_col['td_class'];
                        }
                        else
                        {    
// We have no class for the total column
                           
$class = '';
                        }

                        if( (
$col_count==0) && isset($this->params['grp_col_start_first']) )
                        {
// Display first column column start:
                           
$output = $this->params['grp_col_start_first'];

                           
// Add the total column class in the grp col start first param class:
                           
$output = str_replace( '$class$', $class, $output );
                        }
                        elseif( (
$col_count==count($this->grp_cols)-1) && isset($this->params['grp_col_start_last']) )
                        {
// Last column can get special formatting:
                           
$output = $this->params['grp_col_start_last'];

                           
// Add the total column class in the grp col start end param class:
                           
$output = str_replace( '$class$', $class, $output );
                        }
                        else
                        {
// Display regular column start:
                           
$output = $this->params['grp_col_start'];

                           
// Replace the "class_attrib" in the grp col start param by the td column class
                           
$output = str_replace( '$class_attrib$', 'class="'.$class.'"', $output );
                        }

                        if( isset(
$grp_col['td_colspan'] ) )
                        {
                           
$colspan = $grp_col['td_colspan'];
                            if(
$colspan < 0 )
                            {
// We want to substract columns from the total count
                               
$colspan = $this->nb_cols + $colspan;
                            }
                            elseif(
$colspan == 0 )
                            {
// use $nb_cols
                               
$colspan = $this->nb_cols;
                            }
                           
$output = str_replace( '$colspan_attrib$', 'colspan="'.$colspan.'"', $output );
                        }
                        else
                        {
// remove non-HTML attrib:
                           
$output = str_replace( '$colspan_attrib$', '', $output );
                        }

                       
// Contents to output:
                       
$output .= $this->parse_col_content( $grp_col['td'] );
                       
//echo $output;
                       
eval( "echo '$output';" );

                        echo
'</td>';
                       
$col_count++;
                    }

                    echo
$this->params['grp_line_end'];
                }
            }


           
/*
             * DATA ROW stuff:
             */
           
if( !empty($this->ID_col) && empty($row->{$this->ID_col}) )
            {    
// We have detected an empty data row which we want to ignore... (happens with empty groups)
               
continue;
            }


            if( !
is_null( $this->Cache ) )
            {
// We want to instantiate an object for the row and cache it:
                // We also keep a local ref in case we want to use it for display:
               
$this->current_Obj = & $this->Cache->instantiate( $row );
            }


           
// Check for fadeout
           
$fadeout_line = false;
            if( !empty(
$this->fadeout_array ) )
            {
                foreach(
$this->fadeout_array as $key => $crit )
                {
                   
// echo 'fadeout '.$key.'='.$crit;
                   
if( isset( $row->$key ) && in_array( $row->$key, $crit ) )
                    {
// Col is in the fadeout list
                        // TODO: CLEAN THIS UP!
                       
$fadeout_line = true;
                        break;
                    }
                }
            }

           
// LINE START:
           
$this->display_line_start( $this->current_idx == count($this->rows)-1, $fadeout_line );

            foreach(
$this->cols as $col )
            {
// For each column:

               
if( isset( $this->current_colspan ) && $this->current_colspan > 1 )
                {    
// Skip this column because previous column has a colspan:
                   
$this->current_colspan--;
                    continue;
                }

               
// COL START:
               
if ( ! empty($col['extra']) )
                {
                   
// array of extra params $col['extra']
                   
$this->display_col_start( $col['extra'], $row );
                }
                else
                {
                   
$this->display_col_start( array(), $row );
                }


               
// Contents to output:
               
$output = $this->parse_col_content( $col['td'] );
               
#pre_dump( '{'.$output.'}' );

               
$out = eval( "return '$output';" );
               
// fp> <input> is needed for checkboxes in the Blog User/Group permissions table > advanced
               
echo ( trim(strip_tags($out,'<img><input><span>')) === '' ? '&nbsp;' : $out );

               
// COL START:
               
$this->display_col_end();
            }

           
// LINE END:
           
$this->display_line_end();

           
$this->next_idx();
        }

       
// BODY END:
       
$this->display_body_end();
    }


   
/**
     * Display totals line if set.
     */
   
function display_totals()
    {
       
$total_enable = false;

       
// Search if we have totals line to display:
       
foreach( $this->cols as $col )
        {
            if( isset(
$col['total'] ) )
            {    
// We have to display a totals line
               
$total_enable = true;
                break;
            }
        }

        if(
$total_enable )
        {
// We have to dispaly a totals line

           
echo $this->params['tfoot_start'];
           
// <tr>
           
echo $this->params['total_line_start'];

           
$loop = 0;

            foreach(
$this->cols as $col )
            {
                if( isset(
$col['total_class'] ) )
                {    
// We have a class for the total column
                   
$class = $col['total_class'];
                }
                else
                {    
// We have no class for the total column
                   
$class = '';
                }

                if(
$loop == 0)
                {    
// The column is the first
                   
$output = $this->params['total_col_start_first'];
                   
// Add the total column class in the total col start first param class:
                   
$output = str_replace( '$class$', $class, $output );
                 }
                elseif(
$loop ==( count( $this->cols ) -1 ) )
                {    
// The column is the last
                   
$output = $this->params['total_col_start_last'];
                   
// Add the total column class in the total col start end param class:
                   
$output = str_replace( '$class$', $class, $output );
                }
                else
                {
                   
$output = $this->params['total_col_start'];
                   
// Replace the "class_attrib" in the total col start param by the total column class
                   
$output = str_replace( '$class_attrib$', 'class="'.$class.'"', $output );
                }

               
// <td class="....">
               
echo $output;

                if( isset(
$col['total'] ) )
                {    
// The column has a total set, so display it:
                   
$output = $col['total'];
                   
$output = $this->parse_col_content( $output );
                    eval(
"echo '$output';" );
                }
                else
                {    
// The column has no total
                   
echo '&nbsp;';
                }
               
// </td>
               
echo  $this->params['total_col_end'];

               
$loop++;
            }
           
// </tr>
           
echo $this->params['total_line_end'];
            echo
$this->params['tfoot_end'];
        }
    }


   
/**
   * Display the functions
   */
   
function display_functions()
    {
        if( empty(
$this->functions_area ) )
        {    
// We don't want to display a functions section:
           
return;
        }

        echo
$this->replace_vars( $this->params['functions_start'] );

        if( !empty(
$this->functions_area['callback'] ) )
        {    
// We want to display functions:
           
if( is_array( $this->functions_area['callback'] ) )
            {    
// The callback is an object function
               
$obj_name = $this->functions_area['callback'][0];
                if(
$obj_name != 'this' )
                {    
// We need the global object
                   
global $$obj_name;
                }
               
$func = $this->functions_area['callback'][1];

                if( isset(
$this->Form ) )
                {    
// There is a created form
                   
$$obj_name->$func( $this->Form );
                }
                else
                {
// There is not a created form
                   
$$obj_name->$func();
                }
            }
            else
            {    
// The callback is a function
               
$func = $this->functions_area['callback'];

                if( isset(
$this->Form ) )
                {    
// There is a created form
                   
$func( $this->Form );
                }
                else
                {
// There is not a created form
                   
$func();
                }
            }

        }

        echo
$this->params['functions_end'];
    }


   
/**
     * Display navigation text, based on template.
     *
     * @param string template: 'header' or 'footer'
     */
   
function display_nav( $template )
    {
        if( empty(
$this->limit ) && isset( $this->params[$template.'_text_no_limit'] ) )
        {
// No LIMIT (there's always only one page)
           
$navigation = $this->params[$template.'_text_no_limit'];
        }
        elseif( (
$this->total_pages <= 1 ) )
        {
// Single page (we probably don't want to show navigation in this case)
           
$navigation = $this->replace_vars( $this->params[$template.'_text_single'] );
        }
        else
        {
// Several pages
            // e-g: 'header_text' or 'footer_text'
           
$navigation = $this->replace_vars( $this->params[$template.'_text'] );
        }

       
$navigation_text = trim( strip_tags( $navigation ) );
        if( ! empty(
$navigation_text ) )
        {
// Display navigation only when it is really filled with some text
            // e-g: 'header_start' or 'footer_start'
           
echo $this->params[$template.'_start'];

            echo
$navigation;

            echo
$this->params[$template.'_end'];
        }
    }


   
/**
     * Display list/table end.
     *
     * Typically outputs </ul> or </table>
     */
   
function display_list_end()
    {
        if(
$this->total_pages == 0 )
        {
// There are no results! Nothing to display!
           
echo $this->replace_vars( $this->params['no_results_end'] );
        }
        else
        {    
// We have rows to display:
           
$r = '';
            if( ! empty(
$this->checkbox_toggle_selectors ) )
            {
               
$r .= $this->params['footer_start'];
               
$r .= '<div class="form-inline">';
                if(
is_array( $this->checkbox_toggle_selectors ) && count( $this->checkbox_toggle_selectors ) > 1 )
                {
                   
$r .= '<select class="form-control input-sm">';
                    foreach(
$this->checkbox_toggle_selectors as $label => $selector )
                    {
                       
$r .= '<option value="'.format_to_output( $selector, 'formvalue' ).'">'.$label.'</option>';
                    }
                   
$r .= '</select> ';
                   
$r .= '<span class="btn-group">';
                   
$r .= '<input type="button" class="btn btn-default btn-xs" value="'.format_to_output( T_('Check all'), 'htmlattr' ).'" onclick="jQuery( jQuery( this ).prevAll( \'select\' ).val(), jQuery( this ).closest( \'.results\' ) ).prop( \'checked\', true );" /> '.
                           
'<input type="button" class="btn btn-default btn-xs" value="'.format_to_output( T_('Uncheck all'), 'htmlattr' ).'" onclick="jQuery( jQuery( this ).prevAll( \'select\' ).val(), jQuery( this ).closest( \'.results\' ) ).prop( \'checked\', false );" /> '.
                           
'<input type="button" class="btn btn-default btn-xs" value="'.format_to_output( T_('Reverse'), 'htmlattr' ).'" onclick="jQuery( jQuery( this ).prevAll( \'select\' ).val(), jQuery( this ).closest( \'.results\' ) ).each( function() { this.checked = !this.checked } );"  />';
                   
$r .= '</span>';
                }
                else
                {
                    if(
is_array( $this->checkbox_toggle_selectors ) )
                    {
                       
$selector = current( $this->checkbox_toggle_selectors );
                    }
                    else
                    {
                       
$selector = $this->checkbox_toggle_selectors;
                    }
                   
$r .= '<span class="btn-group">';
                   
$r .= '<input type="button" class="btn btn-default btn-xs" value="'.format_to_output( T_('Check all'), 'htmlattr' ).'" onclick="jQuery( \''.format_to_js( $selector ).'\', jQuery( this ).closest( \'.results\' ) ).prop( \'checked\', true );" /> '.
                           
'<input type="button" class="btn btn-default btn-xs" value="'.format_to_output( T_('Uncheck all'), 'htmlattr' ).'" onclick="jQuery( \''.format_to_js( $selector ).'\', jQuery( this ).closest( \'.results\' ) ).prop( \'checked\', false );" /> '.
                           
'<input type="button" class="btn btn-default btn-xs" value="'.format_to_output( T_('Reverse'), 'htmlattr' ).'" onclick="jQuery( \''.format_to_js( $selector ).'\', jQuery( this ).closest( \'.results\' ) ).each( function() { this.checked = !this.checked } );"  />';
                   
$r .= '</span>';
                }

                if( ! empty(
$this->list_mass_actions ) )
                {    
// Additional actions:
                   
foreach( $this->list_mass_actions as $toggle_action_key => $toggle_action )
                    {
                       
$r .= ' ';
                        switch(
$toggle_action['type'] )
                        {
                            case
'text':
                               
$r .= $toggle_action['text'];
                                break;
                            case
'button':
                            case
'submit':
                               
$r .= '<input type="'.$toggle_action['type'].'" name="actionArray['.$toggle_action_key.']" id="'.$toggle_action_key.'"'
                                   
.' value="'.format_to_output( $toggle_action['text'] ).'"'
                                   
.' class="btn btn-xs '.( empty( $toggle_action['class'] ) ? 'btn-default' : ''.$toggle_action['class'] ).'" />';
                                break;
                        }
                    }
                }
               
$r .= '</div>';
               
$r .= $this->params['footer_end'];
            }
            echo
$this->params['list_end'].$r;

            if( ! empty(
$this->list_mass_actions ) && isset( $this->Form ) )
            {    
// Start form for list with mass actions:
               
$this->Form->end_form();
            }
        }
    }


   
/**
     * Returns values needed to make sort links for a given column
     *
     * Returns an array containing the following values:
     *  - current_order : 'ASC', 'DESC' or ''
     *  - order_asc : url to order in ascending order
     *  - order_desc
     *  - order_toggle : url to toggle sort order
     *
     * @param integer column to sort
     * @return array
     */
   
function get_col_sort_values( $col_idx )
    {

       
// Current order:
       
$order_char = substr( $this->order, $col_idx, 1 );
        if(
$order_char == 'A' )
        {
           
$col_sort_values['current_order'] = 'ASC';
        }
        elseif(
$order_char == 'D' )
        {
           
$col_sort_values['current_order'] = 'DESC';
        }
        else
        {
           
$col_sort_values['current_order'] = '';
        }


       
// Generate sort values to use for sorting on the current column:
       
$order_asc = '';
       
$order_desc = '';
        for(
$i = 0; $i < $this->nb_cols; $i++ )
        {
            if(    
$i == $col_idx )
            {
// Link ordering the current column
               
$order_asc .= 'A';
               
$order_desc .= 'D';
            }
            else
            {
               
$order_asc .= '-';
               
$order_desc .= '-';
            }
        }

       
$col_sort_values['order_asc'] = regenerate_url( $this->order_param, $this->order_param.'='.$order_asc, $this->params['page_url'] );
       
$col_sort_values['order_desc'] = regenerate_url( $this->order_param, $this->order_param.'='.$order_desc, $this->params['page_url'] );


        if( !
$col_sort_values['current_order'] && isset( $this->cols[$col_idx]['default_dir'] ) )
        {    
// There is no current order on this column and a default order direction is set for it
            // So set a default order direction for it

           
if( $this->cols[$col_idx]['default_dir'] == 'A' )
            {    
// The default order direction is A, so set its toogle  order to the order_asc
               
$col_sort_values['order_toggle'] = $col_sort_values['order_asc'];
            }
            else
            {
// The default order direction is A, so set its toogle order to the order_desc
               
$col_sort_values['order_toggle'] = $col_sort_values['order_desc'];
            }
        }
        elseif(
$col_sort_values['current_order'] == 'ASC' )
        {    
// There is an ASC current order on this column, so set its toogle order to the order_desc
           
$col_sort_values['order_toggle'] = $col_sort_values['order_desc'];
        }
        else
        {
// There is a DESC or NO current order on this column,  so set its toogle order to the order_asc
           
$col_sort_values['order_toggle'] = $col_sort_values['order_asc'];
        }

        return
$col_sort_values;
    }


   
/**
     * Returns order field list add to SQL query:
     * @return string May be empty
     */
   
function get_order_field_list()
    {
        if(
is_null( $this->order_field_list ) )
        {
// Order list is not defined yet
           
if( ( !empty( $this->order ) ) && ( !empty( $this->cols ) ) && ( substr( $this->order, 0, 1 ) == '/' ) )
            {
// order is set in format '/order_field_name/A' or '/order_field_name/D'
               
$order_parts = explode( '/', $this->order );
               
$this->order = '';
                if( (
count( $order_parts ) == 3 ) && ( ( $order_parts[2] == 'A' ) || ( $order_parts[2] == 'D' ) ) )
                {
                    foreach(
$this->cols as $col )
                    {
// iterate thrugh columns and find matching column name
                       
if( isset( $col['order'] ) && ( $col['order'] == $order_parts[1] ) )
                        {
// we have found the requested orderable column
                           
$this->order .= $order_parts[2];
                            break;
                        }
                        else
                        {
                           
$this->order .= '-';
                        }
                    }
                }
            }

            if( empty(
$this->order ) )
            {
// We have no user provided order:
               
if( empty( $this->cols ) )
                {    
// We have no columns to pick an automatic order from:
                    // echo 'Can\'t determine automatic order';
                   
return '';
                }

               
$this->order = '';
                foreach(
$this->cols as $col )
                {
                    if( isset(
$col['order'] ) || isset( $col['order_objects_callback'] ) || isset( $col['order_rows_callback'] ) )
                    {
// We have found the first orderable column:
                       
$this->order .= 'A';
                        break;
                    }
                    else
                    {
                       
$this->order .= '-';
                    }
                }
            }

           
// echo ' order='.$this->order.' ';

           
$orders = array();
           
$this->order_callbacks = array();

            for(
$i = 0; $i <= strlen( $this->order ); $i++ )
            {    
// For each position in order string:
               
if( isset( $this->cols[$i]['order'] ) )
                {    
// if column is sortable:
                    # Add ASC/DESC to any order cols (except if there is ASC/DESC given already, which is used to order NULL values always at the end)
                   
switch( substr( $this->order, $i, 1 ) )
                    {
                        case
'A':
                           
$orders[] = preg_replace( '~(?<!asc|desc)\s*,~i', ' ASC,', $this->cols[$i]['order'] ).' ASC';
                            break;

                        case
'D':
                           
$orders[] = preg_replace( '~(asc|desc)?\s*,~i', ' DESC,', $this->cols[$i]['order'] ).' DESC';
                            break;
                    }
                }

                if( isset(
$this->cols[$i]['order_objects_callback'] ) )
                {    
// if column is sortable by object callback:
                   
switch( substr( $this->order, $i, 1 ) )
                    {
                        case
'A':
                           
$this->order_callbacks[] = array(
                                   
'callback' => $this->cols[$i]['order_objects_callback'],
                                   
'use_rows' => false,
                                   
'order'=>'ASC' );
                            break;

                        case
'D':
                           
$this->order_callbacks[] = array(
                                   
'callback' => $this->cols[$i]['order_objects_callback'],
                                   
'use_rows' => false,
                                   
'order' => 'DESC' );
                            break;
                    }
                }

                if( isset(
$this->cols[$i]['order_rows_callback'] ) )
                {    
// if column is sortable by callback:
                   
switch( substr( $this->order, $i, 1 ) )
                    {
                        case
'A':
                           
$this->order_callbacks[] = array(
                                   
'callback' => $this->cols[$i]['order_rows_callback'],
                                   
'use_rows' => true,
                                   
'order'=>'ASC' );
                            break;

                        case
'D':
                           
$this->order_callbacks[] = array(
                                   
'callback' => $this->cols[$i]['order_rows_callback'],
                                   
'use_rows' => true,
                                   
'order' => 'DESC' );
                            break;
                    }
                }
            }
           
$this->order_field_list = implode( ',', $orders );
        }
        return
$this->order_field_list;    // May be empty
   
}


   
/**
     * Handle variable subtitutions for column contents.
     *
     * This is one of the key functions to look at when you want to use the Results class.
     * - $var$
     * - £var£ - replaced by its utf-8 hex character (c2 a3)
     * - ²var² - replaced by its utf-8 hex character (c2 b2)
     * - #var#
     * - {row}
     * - %func()%
     * - ~func()~
     * - ¤func()¤ - @deprecated by ~func()~ - replaced by its utf-8 hex character (c2 a4)
     */
   
function parse_col_content( $content )
    {
       
// Make variable substitution for STRINGS:
       
$content = preg_replace( '#\$ (\w+) \$#ix', "'.format_to_output(\$row->$1).'", $content );
       
// Make variable substitution for URL STRINGS:
       
$content = preg_replace( '#\x{c2}\x{a3} (\w+) \x{c2}\x{a3}#ix', "'.format_to_output(\$row->$1, 'urlencoded').'", $content );
       
// Make variable substitution for escaped strings:
       
$content = preg_replace( '#\x{c2}\x{b2} (\w+) \x{c2}\x{b2}#ix', "'.htmlentities(\$row->$1).'", $content );
       
// Make variable substitution for RAWS:
       
$content = preg_replace( '!\# (\w+) \#!ix', "\$row->$1", $content );
       
// Make variable substitution for full ROW:
       
$content = str_replace( '{row}', '$row', $content );
       
// Make callback function substitution:
       
$content = preg_replace( '#% (.+?) %#ix', "'.$1.'", $content );
       
// Make variable substitution for intanciated Object:
       
$content = str_replace( '{Obj}', "\$this->current_Obj", $content );
       
// Make callback for Object method substitution:
       
$content = preg_replace( '#@ (.+?) @#ix', "'.\$this->current_Obj->$1.'", $content );
       
// Sometimes we need embedded function call, so we provide a second sign:
       
$content = preg_replace( '#~ (.+?) ~#ix', "'.$1.'", $content );

       
// @deprecated by ~func()~. Left here for backward compatibility only, to be removed in future versions.
       
$content = preg_replace( '#\x{c2}\x{a4} (.+?) \x{c2}\x{a4}#ix', "'.$1.'", $content );

       
// Make callback function move_icons for orderable lists // dh> what does it do?
       
$content = str_replace( '{move}', "'.\$this->move_icons().'", $content );

       
$content = str_replace( array( '{CUR_IDX}', '{TOTAL_ROWS}', '{ROW_IDX_TYPE}' ),
            array(
$this->current_idx, $this->total_rows, "'".$this->get_row_order_type()."'" ),
           
$content );

        return
$content;
    }


   
/**
     *
     * @todo Support {@link Results::$order_callbacks}
     */
   
function move_icons( )
    {
       
$r = '';

       
$reg = '#^'.$this->param_prefix.'order (ASC|DESC).*#';

        if(
preg_match( $reg, $this->order_field_list, $res ) )
        {    
// The table is sorted by the order column
           
$sort = $res[1];

           
// get the element ID
           
$idname = $this->param_prefix . 'ID';
           
$id = $this->rows[$this->current_idx]->$idname;

           
// Move up arrow
           
if( $this->global_is_first )
            {    
// The element is the first so it can't move up, display a no move arrow
               
$r .= get_icon( 'nomove' ).' ';
            }
            else
            {
                if(    
$sort == 'ASC' )
                {    
// ASC sort, so move_up action for move up arrow
                   
$action = 'move_up';
                   
$alt = T_( 'Move up' ).'!';
                    }
                else
                {    
// Reverse sort, so action and alt are reverse too
                   
$action = 'move_down';
                   
$alt = T_('Move down! (reverse sort)');
                }
               
$r .= action_icon( $alt, 'move_up', regenerate_url( 'action,'.$this->param_prefix.'ID' , $this->param_prefix.'ID='.$id.'&amp;action='.$action ) );
            }

           
// Move down arrow
           
if( $this->global_is_last )
            {    
// The element is the last so it can't move up, display a no move arrow
               
$r .= get_icon( 'nomove' ).' ';
            }
            else
            {
                if(    
$sort == 'ASC' )
                {    
// ASC sort, so move_down action for move down arrow
                   
$action = 'move_down';
                   
$alt = T_( 'Move down' ).'!';
                }
                else
                {
// Reverse sort, so action and alt are reverse too
                   
$action = 'move_up';
                   
$alt = T_('Move up! (reverse sort)');
                }
               
$r .= action_icon( $alt, 'move_down', regenerate_url( 'action,'.$this->param_prefix.'ID', $this->param_prefix.'ID='.$id.'&amp;action='.$action ) );
            }

            return
$r;
        }
        else
        {    
// The table is not sorted by the order column, so we display no move arrows

           
if( $this->global_is_first )
            {
               
// The element is the first so it can't move up, display a no move up arrow
               
$r = get_icon( 'nomove' ).' ';
            }
            else
            {    
// Display no move up arrow
               
$r = action_icon( T_( 'Sort by order' ), 'nomove_up', regenerate_url( 'action', 'action=sort_by_order' ) );
            }

            if(
$this->global_is_last )
            {
               
// The element is the last so it can't move down, display a no move down arrow
               
$r .= get_icon( 'nomove' ).' ';
            }
            else
            {
// Display no move down arrow
               
$r .= action_icon( T_( 'Sort by order' ), 'nomove_down', regenerate_url( 'action','action=sort_by_order' ) );
            }

            return
$r;
        }
    }


   
/**
     * Widget callback for template vars.
     *
     * This allows to replace template vars, see {@link Widget::replace_callback()}.
     *
     * @return string
     */
   
function replace_callback( $matches )
    {
       
// echo '['.$matches[1].']';
       
switch( $matches[1] )
        {
            case
'nb_results':
            case
'total_rows':
                return
$this->total_rows;

            case
'start' :
                return ( (
$this->page-1)*$this->limit+1 );

            case
'end' :
                return (
min( $this->total_rows, $this->page*$this->limit ) );

            case
'total_rows' :
               
//total number of rows in the sql query
               
return ( $this->total_rows );

            case
'page' :
               
//current page number
               
return ( $this->page );

            case
'total_pages' :
               
//total number of pages
               
return ( $this->total_pages );

            case
'prev' :
               
// inits the link to previous page
               
if ( $this->page <= 1 )
                {
                    return
$this->params['no_prev_text'];
                }
               
$r = '';
                if( isset(
$this->params['page_item_before'] ) )
                {
                   
$r .= add_tag_class( $this->params['page_item_before'], 'listnav_prev' );
                }
               
$r .= '<a href="'
                       
.regenerate_url( $this->page_param, (($this->page > 2) ? $this->page_param.'='.($this->page-1) : ''), $this->params['page_url'] ).'"';
               
$attr_rel = array();
                if(
$this->nofollow_pagenav )
                {
// We want to NOFOLLOW page navigation
                   
$attr_rel[] = 'nofollow';
                }
               
// Add attribute rel="prev" for previous page
               
$attr_rel[] = 'prev';
                if( ! empty(
$attr_rel ) )
                {
                   
$r .= ' rel="'.implode( ' ', $attr_rel ).'"';
                }
                if( ! empty(
$this->params['prev_class'] ) )
                {
// Add "class" for prev link
                   
$r .= ' class="'.$this->params['prev_class'].'"';
                }
               
$r .= '>'.$this->params['prev_text'].'</a>';
                if( isset(
$this->params['page_item_after'] ) )
                {
                   
$r .= $this->params['page_item_after'];
                }
                return
$r;

            case
'next' :
               
// inits the link to next page
               
if( $this->page >= $this->total_pages )
                {
                    return
$this->params['no_next_text'];
                }
               
$r = '';
                if( isset(
$this->params['page_item_before'] ) )
                {
                   
$r .= add_tag_class( $this->params['page_item_before'], 'listnav_next' );
                }
               
$r .= '<a href="'
                       
.regenerate_url( $this->page_param, $this->page_param.'='.($this->page+1), $this->params['page_url'] ).'"';
               
$attr_rel = array();
                if(
$this->nofollow_pagenav )
                {
// We want to NOFOLLOW page navigation
                   
$attr_rel[] = 'nofollow';
                }
               
// Add attribute rel="next" for next page
               
$attr_rel[] = 'next';
                if( ! empty(
$attr_rel ) )
                {
                   
$r .= ' rel="'.implode( ' ', $attr_rel ).'"';
                }
                if( ! empty(
$this->params['next_class'] ) )
                {
// Add "class" for prev link
                   
$r .= ' class="'.$this->params['next_class'].'"';
                }
               
$r .= '>'.$this->params['next_text'].'</a>';
                if( isset(
$this->params['page_item_after'] ) )
                {
                   
$r .= $this->params['page_item_after'];
                }
                return
$r;

            case
'list' :
               
//inits the page list
               
return $this->page_list( $this->first(), $this->last(), $this->params['page_url'] );

            case
'scroll_list' :
               
//inits the scrolling list of pages
               
return $this->page_scroll_list();

            case
'first' :
               
//inits the link to first page
               
return $this->display_first( $this->params['page_url'] );

            case
'last' :
               
//inits the link to last page
               
return $this->display_last( $this->params['page_url'] );

            case
'list_prev' :
               
//inits the link to previous page range
               
return $this->display_prev( $this->params['page_url'] );

            case
'list_next' :
               
//inits the link to next page range
               
return $this->display_next( $this->params['page_url'] );

            case
'page_size' :
               
//inits the list to select page size
               
return $this->display_page_size( $this->params['page_url'] );

            case
'prefix' :
               
//prefix
               
return $this->param_prefix;

            default :
                return
parent::replace_callback( $matches );
        }
    }


   
/**
     * Returns the first page number to be displayed in the list
     */
   
function first()
    {
        if(
$this->page <= intval( $this->params['list_span']/2 ))
        {
// the current page number is small
           
return 1;
        }
        elseif(
$this->page > $this->total_pages-intval( $this->params['list_span']/2 ))
        {
// the current page number is big
           
return max( 1, $this->total_pages-$this->params['list_span']+1);
        }
        else
        {
// the current page number can be centered
           
return $this->page - intval($this->params['list_span']/2);
        }
    }


   
/**
     * returns the last page number to be displayed in the list
     */
   
function last()
    {
        if(
$this->page > $this->total_pages-intval( $this->params['list_span']/2 ))
        {
//the current page number is big
           
return $this->total_pages;
        }
        else
        {
            return
min( $this->total_pages, $this->first()+$this->params['list_span']-1 );
        }
    }


   
/**
     * returns the link to the first page, if necessary
     */
   
function display_first( $page_url = '' )
    {
       
$r = '';
        if(
$this->page == 1 && isset( $this->params['page_item_current_before'] ) )
        {
           
$r .= add_tag_class( $this->params['page_item_current_before'], 'listnav_first' );
        }
        elseif( isset(
$this->params['page_item_before'] ) )
        {
           
$r .= add_tag_class( $this->params['page_item_before'], 'listnav_first' );
        }

        if(
$this->page == 1 )
        {    
// no link for the current page
           
if( ! isset( $this->params['page_current_template'] ) )
            {
               
$this->params['page_current_template'] = '<strong class="current_page">$page_num$</strong>';
            }
           
$r .= str_replace( '$page_num$', 1, $this->params['page_current_template'] );
        }
        else
        {    
// a link for non-current pages
           
$r .= '<a href="'.regenerate_url( $this->page_param, '', $page_url ).'">1</a>';
        }

        if(
$this->page == 1 && isset( $this->params['page_item_current_after'] ) )
        {
           
$r .= $this->params['page_item_current_after'];
        }
        elseif( isset(
$this->params['page_item_after'] ) )
        {
           
$r .= $this->params['page_item_after'];
        }

        return
$r;
    }


   
/**
     * returns the link to the last page, if necessary
     */
   
function display_last( $page_url = '' )
    {
       
$r = '';
        if(
$this->page == $this->total_pages && isset( $this->params['page_item_current_before'] ) )
        {
           
$r .= add_tag_class( $this->params['page_item_current_before'], 'listnav_last' );
        }
        elseif( isset(
$this->params['page_item_before'] ) )
        {
           
$r .= add_tag_class( $this->params['page_item_before'], 'listnav_last' );
        }

        if(
$this->page == $this->total_pages )
        {    
// no link for the current page
           
if( ! isset( $this->params['page_current_template'] ) )
            {
               
$this->params['page_current_template'] = '<strong class="current_page">$page_num$</strong>';
            }
           
$r .= str_replace( '$page_num$', $this->total_pages, $this->params['page_current_template'] );
        }
        else
        {    
// a link for non-current pages
           
$r .= '<a href="'.regenerate_url( $this->page_param, $this->page_param.'='.$this->total_pages, $page_url ).'">'.$this->total_pages.'</a>';
        }

        if(
$this->page == $this->total_pages && isset( $this->params['page_item_current_after'] ) )
        {
           
$r .= $this->params['page_item_current_after'];
        }
        elseif( isset(
$this->params['page_item_after'] ) )
        {
           
$r .= $this->params['page_item_after'];
        }

        return
$r;
    }


   
/**
     * returns a link to previous pages, if necessary
     */
   
function display_prev( $page_url = '' )
    {
        if(
$this->first() > 2 )
        {
//the list has to be displayed
           
$page_no = ceil($this->first()/2);

           
$r = '';
            if( isset(
$this->params['page_item_before'] ) )
            {
               
$r .= add_tag_class( $this->params['page_item_before'], 'listnav_prev_list' );
            }

           
$r .= '<a href="'.regenerate_url( $this->page_param, $this->page_param.'='.$page_no, $page_url ).'">'
                               
.$this->params['list_prev_text'].'</a>';

            if( isset(
$this->params['page_item_after'] ) )
            {
               
$r .= $this->params['page_item_after'];
            }

            return
$r;
        }

    }


   
/**
     * returns a link to next pages, if necessary
     */
   
function display_next( $page_url = '' )
    {
        if(
$this->last() < $this->total_pages-1 )
        {
//the list has to be displayed
           
$page_no = $this->last() + floor( ( $this->total_pages - $this->last() ) / 2 );

           
$r = '';
            if( isset(
$this->params['page_item_before'] ) )
            {
               
$r .= add_tag_class( $this->params['page_item_before'], 'listnav_next_list' );;
            }

           
$r .= '<a href="'.regenerate_url( $this->page_param,$this->page_param.'='.$page_no, $page_url ).'">'
                               
.$this->params['list_next_text'].'</a>';

            if( isset(
$this->params['page_item_after'] ) )
            {
               
$r .= $this->params['page_item_after'];
            }

            return
$r;
        }
    }


   
/**
     * returns a list to select page size
     */
   
function display_page_size( $page_url = '' )
    {
       
// Don't allow to change a page size:
       
if( $this->total_rows <= 10 || // if total number of rows is always less then min page size
           
empty( $this->param_prefix ) ) // the lists without defined param_prefix
       
{
            return;
        }

       
$page_size_options = array(
               
'10' => sprintf( T_('%s lines'), '10' ),
               
'20' => sprintf( T_('%s lines'), '20' ),
               
'30' => sprintf( T_('%s lines'), '30' ),
               
'40' => sprintf( T_('%s lines'), '40' ),
               
'50' => sprintf( T_('%s lines'), '50' ),
               
'100' => sprintf( T_('%s lines'), '100' ),
               
'200' => sprintf( T_('%s lines'), '200' ),
               
'500' => sprintf( T_('%s lines'), '500' ),
            );

       
$default_page_size_value = '0';
        if(
is_logged_in() )
        {    
// Get default page size for current user
           
global $UserSettings;

           
$default_page_size_value = $UserSettings->get( 'results_per_page' );
           
$default_page_size_option = array( '0' => sprintf( T_('Default (%s lines)'), $default_page_size_value ) );
           
$page_size_options = $default_page_size_option + $page_size_options;
        }

       
$html = '<small class="page_size_selector">';
       
$html .= T_('Lines per page:');

       
$html .= ' <select name="'.$this->limit_param.'" onchange="location.href=\''.regenerate_url( $this->page_param.','.$this->limit_param, $this->limit_param, $page_url ).'=\'+this.value" class="form-control input-sm">';
        foreach(
$page_size_options as $value => $name )
        {
           
$selected = '';
            if(
$this->limit == $value && $value != $default_page_size_value )
            {
               
$selected = ' selected="selected"';
            }
           
$html .= '<option value="'.$value.'"'.$selected.'>'.$name.'</option>';
        }
       
$html .= '</select>';

       
$html .= '</small>';

        return
$html;
    }


   
/**
     * Returns the page link list under the table
     */
   
function page_list( $min, $max, $page_url = '' )
    {
       
$hidden_active_distances = array( 1, 2 ) ;

       
$i = 0;
       
$list = '';

       
// Do not include first page in the page list range
       
if( $this->first() == 1 )
        {
           
$min++;
            if( (
$this->last() + 1 ) < $this->total_pages )
            {
               
$max++;
            }
        }

       
// Also, do not include last page in the page list range
       
if( $this->last() == $this->total_pages )
        {
           
$max--;
            if(
$min > 2 )
            {
               
$min--;
            }
        }

       
$page_prev_i = $this->page - 1;
       
$page_next_i = $this->page + 1;
       
$pib = isset( $this->params['page_item_before'] ) ? add_tag_class( $this->params['page_item_before'], '**active_distance_**' ) : '';

        for(
$i=$min; $i<=$max; $i++)
        {
            if(
$this->page <= 4 )
            {
               
$a = $i - 4;
               
$active_dist = $a > 0 ? $a : null;
            }
            elseif(
$this->page > ( $this->total_pages - 3 ) )
            {
                if(
$i > ( $this->total_pages - 3 ) )
                {
                   
$active_dist = null;
                }
                else
                {
                   
$active_dist = ( $this->total_pages - 3 ) - $i;
                }
               
//$active_dist = null;
           
}
            else
            {
               
$active_dist = abs( $this->page - $i );
            }

            if(
in_array( $active_dist, $hidden_active_distances ) && ( $i < $this->page ) && ( $i > 2 ) && ( $this->page > 4 ) )
            {
// show pseudo prev_list
               
$page_no = ceil($this->first()/2);
                if(
$page_no == 1 )
                {
                   
$page_no++;
                }
                if( isset(
$this->params['page_item_before'] ) && trim( $this->params['page_item_before'] ) )
                {
                   
$list .= add_tag_class( $this->params['page_item_before'], 'listnav_distance_'.$active_dist );
                   
$list .= '<a href="'.regenerate_url( $this->page_param, $this->page_param.'='.$page_no, $page_url ).'">'
                                   
.$this->params['list_next_text'].'</a>';
                }
                else
                {
                   
$list_link = '<a href="'.regenerate_url( $this->page_param, $this->page_param.'='.$page_no, $page_url ).'">'
                                   
.$this->params['list_next_text'].'</a>';
                   
$list_link = add_tag_class( $list_link, 'listnav_distance_'.$active_dist );
                   
$list .= $list_link;
                }

                if( isset(
$this->params['page_item_after'] ) )
                {
                   
$list .= $this->params['page_item_after'];
                }
            }

            if(
$i == $this->page && isset( $this->params['page_item_current_before'] ) )
            {
// current page
               
$list .= $this->params['page_item_current_before'];
            }
            elseif( isset(
$this->params['page_item_before'] ) )
            {
// other page
               
if( $active_dist )
                {
                   
$list .= str_replace( '**active_distance_**', 'active_distance_'.$active_dist, $pib );
                }
                else
                {
                   
$list .= str_replace( '**active_distance_**', '', $pib );
                }
            }

            if(
$i == $this->page )
            {
// no link for the current page
               
if( ! isset( $this->params['page_current_template'] ) )
                {
                   
$this->params['page_current_template'] = '<strong class="current_page">$page_num$</strong>';
                }
               
$list .= str_replace( '$page_num$', $i, $this->params['page_current_template'] );
            }
            else
            {
// a link for non-current pages
               
$list .= '<a href="'
                   
.regenerate_url( $this->page_param, ( $i>1 ? $this->page_param.'='.$i : '' ), $page_url ).'"';
               
$attr_rel = array();
                if(
$this->nofollow_pagenav )
                {
// We want to NOFOLLOW page navigation
                   
$attr_rel[] = 'nofollow';
                }
                if(
$page_prev_i == $i )
                {
// Add attribute rel="prev" for previous page
                   
$attr_rel[] = 'prev';
                }
                elseif(
$page_next_i == $i )
                {
// Add attribute rel="next" for next page
                   
$attr_rel[] = 'next';
                }
                if( ! empty(
$attr_rel ) )
                {
                   
$list .= ' rel="'.implode( ' ', $attr_rel ).'"';
                }
               
$list .= '>'.$i.'</a>';
            }

            if(
$i == $this->page && isset( $this->params['page_item_current_after'] ) )
            {
               
$list .= $this->params['page_item_current_after'];
            }
            elseif( isset(
$this->params['page_item_after'] ) )
            {
               
$list .= $this->params['page_item_after'];
            }

            if(
$i != $max )
            {
// Separator between page numbers
               
if( isset( $this->params['page_list_separator'] ) )
                {
// Use the customized separator from skin
                   
$list .= $this->params['page_list_separator'];
                }
                else
                {
// Use a space as default separator
                   
$list .= ' ';
                }
            }

            if(
in_array( $active_dist, $hidden_active_distances ) && ( $i > $this->page ) && ( $i < ( $this->total_pages - 1 ) ) )
            {
// show pseudo next_list
               
$page_no = $this->last() + floor( ( $this->total_pages - $this->last() ) / 2 );
                if(
$page_no == $this->total_pages )
                {
                   
$page_no--;
                }
                if( isset(
$this->params['page_item_before'] ) && trim( $this->params['page_item_before'] ) )
                {
                   
$list .= add_tag_class( $this->params['page_item_before'], 'listnav_distance_'.$active_dist );
                   
$list .= '<a href="'.regenerate_url( $this->page_param, $this->page_param.'='.$page_no, $page_url ).'">'
                                   
.$this->params['list_next_text'].'</a>';
                }
                else
                {
                   
$list_link = '<a href="'.regenerate_url( $this->page_param, $this->page_param.'='.$page_no, $page_url ).'">'
                                   
.$this->params['list_next_text'].'</a>';
                   
$list .= add_tag_class( $list_link, 'listnav_distance_'.$active_dist );
                }

                if( isset(
$this->params['page_item_after'] ) )
                {
                   
$list .= $this->params['page_item_after'];
                }
            }
        }
        return
$list;
    }


   
/*
     * Returns a scrolling page list under the table
     */
   
function page_scroll_list()
    {
       
$scroll = '';
       
$i = 0;
       
$range = $this->params['scroll_list_range'];
       
$min = 1;
       
$max = 1;
       
$option = '';
       
$selected = '';
       
$range_display='';

        if(
$range > $this->total_pages )
            {
//the range is greater than the total number of pages, the list goes up to the number of pages
               
$max = $this->total_pages;
            }
            else
            {
//initialisation of the range
               
$max = $range;
            }

       
//initialization of the form
       
$scroll ='<form class="inline" method="post" action="'.regenerate_url( $this->page_param ).'">
                            <select name="'
.$this->page_param.'" onchange="parentNode.submit()">';//javascript to change page clicking in the scroll list

       
while( $max <= $this->total_pages )
        {
//construction loop
           
if( $this->page <= $max && $this->page >= $min )
            {
//display all the pages belonging to the range where the current page is located
               
for( $i = $min ; $i <= $max ; $i++)
                {
//construction of the <option> tags
                   
$selected = ($i == $this->page) ? ' selected' : '';//the "selected" option is applied to the current page
                   
$option = '<option'.$selected.' value="'.$i.'">'.$i.'</option>';
                   
$scroll = $scroll.$option;
                }
            }
            else
            {
//inits the ranges inside the list
               
$range_display = '<option value="'.$min.'">'
                   
.T_('Pages').' '.$min.' '. /* TRANS: Pages x _to_ y */ T_('to').' '.$max;
               
$scroll = $scroll.$range_display;
            }

            if(
$max+$range > $this->total_pages && $max != $this->total_pages)
            {
//$max has to be the total number of pages
               
$max = $this->total_pages;
            }
            else
            {
               
$max = $max+$range;//incrementation of the maximum value by the range
           
}

           
$min = $min+$range;//incrementation of the minimum value by the range


       
}
       
/*$input ='';
            $input = '<input type="submit" value="submit" />';*/
       
$scroll = $scroll.'</select>'./*$input.*/'</form>';//end of the form*/

       
return $scroll;
    }


   
/**
     * Get number of rows available for display
     *
     * @return integer
     */
   
function get_num_rows()
    {
        return
$this->result_num_rows;
    }


   
/**
     * Template function: display message if list is empty
     *
     * @return boolean true if empty
     */
   
function display_if_empty( $params = array() )
    {
        if(
$this->result_num_rows == 0 )
        {
           
// Make sure we are not missing any param:
           
$params = array_merge( array(
                   
'before'      => '<p class="msg_nothing">',
                   
'after'       => '</p>',
                   
'msg_empty'   => T_('Sorry, there is nothing to display...'),
                ),
$params );

            echo
$params['before'];
            echo
$params['msg_empty'];
            echo
$params['after'];

            return
true;
        }
        return
false;
    }


   
/**
     * Add message to array
     *
     * @param string Message string
     */
   
function add_message( $message )
    {
        if( !
is_array( $this->messages ) )
        {
// Initialize array only first time
           
$this->messages = array();
        }

       
$this->messages[] = $message;
    }


   
/**
     * Print out the messages on screen
     */
   
function display_messages()
    {
        if( empty(
$this->messages ) )
        {
// No messages, Don't print
           
return;
        }

        echo
$this->replace_vars( $this->params['messages_start'] );

        echo
implode( $this->params['messages_separator'], $this->messages );

        echo
$this->params['messages_end'];
    }


   
/**
     * Get an order type of the current row
     *
     * @return string 'single' - when only one row in list
     *                'first'  - Current row is first in whole list
     *                'last'   - Current row is last in whole list
     *                'middle' - Current row is not first and not last
     */
   
function get_row_order_type()
    {
        if(
$this->total_rows == 1 )
        {
// Only one row in list
           
return 'single';
        }
        elseif(
$this->page == 1 && $this->current_idx == 0 )
        {
// First row
           
return 'first';
        }
        elseif( (
$this->page == $this->total_pages ) && ( $this->current_idx == $this->total_rows - ( ( $this->total_pages - 1 )* $this->limit ) - 1 ) )
        {
// Last row
           
return 'last';
        }
        else
        {
// Middle row
           
return 'middle';
        }
    }
}


// _________________ Helper callback functions __________________

function conditional( $condition, $on_true, $on_false = '' )
{
    if(
$condition )
    {
        return
$on_true;
    }
    else
    {
        return
$on_false;
    }
}

?>