<?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>')) === '' ? ' ' : $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 ' ';
}
// </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.'&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.'&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;
}
}
?>