b2evolution PHP Cross Reference Blogging Systems

Source: /inc/_core/ui/results/_results.class.php - 2086 lines - 58853 bytes - Summary - Text - Print

Description: This file implements the Results class. This file is part of the evoCore framework - {@link http://evocore.net/} See also {@link http://sourceforge.net/projects/evocms/}.

   1  <?php
   2  /**
   3   * This file implements the Results class.
   4   *
   5   * This file is part of the evoCore framework - {@link http://evocore.net/}
   6   * See also {@link http://sourceforge.net/projects/evocms/}.
   7   *
   8   * @copyright (c)2003-2014 by Francois Planque - {@link http://fplanque.com/}
   9   * Parts of this file are copyright (c)2005-2006 by PROGIDISTRI - {@link http://progidistri.com/}.
  10   *
  11   * {@internal License choice
  12   * - If you have received this file as part of a package, please find the license.txt file in
  13   *   the same folder or the closest folder above for complete license terms.
  14   * - If you have received this file individually (e-g: from http://evocms.cvs.sourceforge.net/)
  15   *   then you must choose one of the following licenses before using the file:
  16   *   - GNU General Public License 2 (GPL) - http://www.opensource.org/licenses/gpl-license.php
  17   *   - Mozilla Public License 1.1 (MPL) - http://www.opensource.org/licenses/mozilla1.1.php
  18   * }}
  19   *
  20   * {@internal Open Source relicensing agreement:
  21   * PROGIDISTRI S.A.S. grants Francois PLANQUE the right to license
  22   * PROGIDISTRI S.A.S.'s contributions to this file and the b2evolution project
  23   * under any OSI approved OSS license (http://www.opensource.org/licenses/).
  24   * }}
  25   *
  26   * @package evocore
  27   *
  28   * {@internal Below is a list of authors who have contributed to design/coding of this file: }}
  29   * @author fplanque: Francois PLANQUE
  30   * @author fsaya: Fabrice SAYA-GASNIER / PROGIDISTRI
  31   *
  32   * @version $Id: _results.class.php 6158 2014-03-12 08:35:37Z manuel $
  33   */
  34  if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );
  35  
  36  
  37  // DEBUG: (Turn switch on or off to log debug info for specified category)
  38  $GLOBALS['debug_results'] = false;
  39  
  40  
  41  load_class( '_core/ui/_uiwidget.class.php', 'Table' );
  42  load_class( '_core/ui/_uiwidget.class.php', 'Widget' );
  43  
  44  /**
  45   * Results class
  46   *
  47   * @package evocore
  48   * @todo Support $cols[]['order_rows_callback'] / order_objects_callback also if there's a LIMIT?
  49   */
  50  class Results extends Table
  51  {
  52      /**
  53       * SQL query
  54       */
  55      var $sql;
  56  
  57      /**
  58       * SQL query to count total rows
  59       */
  60      var $count_sql;
  61  
  62      /**
  63       * Total number of rows (if > {@link $limit}, it will result in multiple pages)
  64       */
  65      var $total_rows;
  66  
  67      /**
  68       * Number of lines per page
  69       */
  70      var $limit;
  71  
  72      /**
  73       * Number of rows in result set for current page.
  74       */
  75      var $result_num_rows;
  76  
  77      /**
  78       * Current page
  79       */
  80      var $page;
  81  
  82      /**
  83       * Array of DB rows for current page.
  84       */
  85      var $rows;
  86  
  87      /**
  88       * List of IDs for current page.
  89       * @uses Results::$ID_col
  90       */
  91      var $page_ID_list;
  92  
  93      /**
  94       * Array of IDs for current page.
  95       * @uses Results::$ID_col
  96       */
  97      var $page_ID_array;
  98  
  99      /**
 100       * Current object idx in $rows array
 101       * @var integer
 102       */
 103      var $current_idx = 0;
 104  
 105      /**
 106       * idx relative to whole list (range: 0 to total_rows-1)
 107       * @var integer
 108       */
 109      var $global_idx;
 110  
 111      /**
 112       * Is this gobally the 1st item in the list? (NOT just the 1st in current page)
 113       */
 114      var $global_is_first;
 115  
 116      /**
 117       * Is this gobally the last item in the list? (NOT just the last in current page)
 118       */
 119      var $global_is_last;
 120  
 121  
 122      /**
 123       * Cache to use to instantiate an object and cache it for each line of results.
 124       *
 125       * For this to work, all columns of the related table must be selected in the query
 126       *
 127       * @var DataObjectCache
 128       */
 129      var $Cache;
 130  
 131      /**
 132       * This will hold the object instantiated by the Cache for the current line.
 133       */
 134      var $current_Obj;
 135  
 136  
 137      /**
 138       * Definitions for each column:
 139       * - th
 140       * - td
 141       * - order: SQL column name(s) to sort by (delimited by comma)
 142       * - order_objects_callback: a PHP callback function (can be array($Object, $method)).
 143       *     This gets three params: $a, $b, $desc.
 144       *     $a and $b are instantiated objects from {@link Results::$Cache}
 145       *     $desc is either 'ASC' or 'DESC'. The function has to return -1, 0 or 1,
 146       *     according to if the $a < $b, $a == $b or $a > $b.
 147       * - order_rows_callback: a PHP callback function (can be array($Object, $method)).
 148       *     This gets three params: $a, $b, $desc.
 149       *     $a and $b are DB row objects
 150       *     $desc is either 'ASC' or 'DESC'. The function has to return -1, 0 or 1,
 151       *     according to if the $a < $b, $a == $b or $a > $b.
 152       * - td_class
 153       *
 154       */
 155      var $cols;
 156  
 157      /**
 158       * Do we want to display column headers?
 159       * @var boolean
 160       */
 161      var $col_headers = true;
 162  
 163  
 164      /**
 165       * DB fieldname to group on.
 166       *
 167       * Leave empty if you don't want to group.
 168       *
 169       * NOTE: you have to use ORDER BY goup_column in your query for this to work correctly.
 170       *
 171       * @var mixed string or array
 172       */
 173      var $group_by = '';
 174  
 175      /**
 176       * Object property/properties to group on.
 177       *
 178       * Objects get instantiated and grouped by the given property/member value.
 179       *
 180       * NOTE: this requires {@link Result::$Cache} to be set and is probably only useful,
 181       *       if you do not use {@link Result::$limit}, because grouping appears after
 182       *       the relevant data has been pulled from DB.
 183       *
 184       * @var mixed string or array
 185       */
 186      var $group_by_obj_prop;
 187  
 188      /**
 189       * Current group identifier (by level/depth)
 190       * @var array
 191       */
 192      var $current_group_ID;
 193  
 194      /**
 195       * Definitions for each GROUP column:
 196       * -td
 197       * -td_start. A column with no def will de displayed using
 198       * the default defs from Results::$params, that is to say, one of these:
 199       *   - $this->params['grp_col_start_first'];
 200       *   - $this->params['grp_col_start_last'];
 201       *   - $this->params['grp_col_start'];
 202       */
 203      var $grp_cols = NULL;
 204  
 205      /**
 206       * Fieldname to detect empty data rows.
 207       *
 208       * Empty data rows can happen when left joining on groups.
 209       * Leave empty if you don't want to detect empty datarows.
 210       *
 211       * @var string
 212       */
 213      var $ID_col = '';
 214  
 215      /**
 216       * URL param names
 217       */
 218      var $page_param;
 219      var $order_param;
 220      var $limit_param;
 221  
 222      /**
 223       * List of sortable fields
 224       */
 225      var $order_field_list;
 226  
 227      /**
 228       * List of sortable columns by callback ("order_objects_callback" and "order_rows_callback")
 229       * @var array
 230       */
 231      var $order_callbacks;
 232  
 233  
 234      /**
 235       * Parameters for the functions area (to display functions at the end of results array):
 236       */
 237      var $functions_area;
 238  
 239  
 240      /**
 241       * Should there be nofollows on page navigation
 242       */
 243      var $nofollow_pagenav = false;
 244  
 245      /**
 246       * Constructor
 247       *
 248       * @todo we might not want to count total rows when not needed...
 249       * @todo fplanque: I am seriously considering putting $count_sql into 2nd or 3rd position. Any prefs?
 250       * @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!
 251       *
 252       * @param string SQL query
 253       * @param string prefix to differentiate page/order params when multiple Results appear one same page
 254       * @param string default ordering of columns (special syntax) if not specified in the URL params
 255       *               example: -A-- will sort in ascending order on 2nd column
 256       *               example: ---D will sort in descending order on 4th column
 257       * @param integer Default number of lines displayed on one page (0 to disable paging; null to use $UserSettings/results_per_page)
 258       * @param string SQL to get the total count of results
 259       * @param boolean
 260       * @param string|integer SQL query used to count the total # of rows
 261       *                                                 - if integer, we'll use that as the count
 262       *                                                 - if NULL, we'll try to COUNT(*) by ourselves
 263       */
 264  	function Results( $sql, $param_prefix = '', $default_order = '', $default_limit = NULL, $count_sql = NULL, $init_page = true )
 265      {
 266          parent::Table( NULL, $param_prefix );
 267  
 268          $this->sql = $sql;
 269          $this->count_sql = $count_sql;
 270  
 271          $this->init_limit_param( $default_limit );
 272  
 273          if( $init_page )
 274          {    // attribution of a page number
 275              $this->page_param = 'results_'.$this->param_prefix.'page';
 276              $this->page = param( $this->page_param, 'integer', 1, true );
 277          }
 278  
 279          $this->init_order_param( $default_order );
 280      }
 281  
 282  
 283      /**
 284       * Initialize the limit param
 285       *
 286       * @param integer Default number of lines displayed on one page (0 to disable paging; null to use $UserSettings/results_per_page)
 287       */
 288  	function init_limit_param( $default_limit )
 289      {
 290          global $UserSettings;
 291  
 292          if( empty( $UserSettings ) )
 293          {
 294              $UserSettings = new UserSettings();
 295          }
 296  
 297          // attribution of a limit number
 298          $this->limit_param = 'results_'.$this->param_prefix.'per_page';
 299          $this->limit = param( $this->limit_param, 'integer', -1, true );
 300          if( !empty( $this->param_prefix ) &&
 301              $this->limit > -1 &&
 302              $this->limit != (int)$UserSettings->get( $this->limit_param ) )
 303          {    // Change a limit number in DB for current user and current list
 304              if( $this->limit == $UserSettings->get( 'results_per_page' ) || $this->limit == 0 )
 305              {    // Delete a limit number for current list if it equals a default value
 306                  $UserSettings->delete( $this->limit_param );
 307                  $this->limit = $UserSettings->get( 'results_per_page' );
 308              }
 309              else
 310              {    // Set a new value of limit number current list
 311                  $UserSettings->set( $this->limit_param, $this->limit );
 312              }
 313              $UserSettings->dbupdate();
 314          }
 315  
 316          if( !empty( $this->param_prefix ) && $this->limit == -1 )
 317          {    // Set a limit number from DB
 318              if( $UserSettings->get( $this->limit_param ) > 0 )
 319              {    // Set a value for current list if it was already defined
 320                  $this->limit = $UserSettings->get( $this->limit_param );
 321              }
 322          }
 323  
 324          if( $this->limit == -1 || empty( $this->limit ) )
 325          {    // Set a default value
 326              $this->limit = is_null( $default_limit ) ? $UserSettings->get( 'results_per_page' ) : $default_limit;
 327          }
 328      }
 329  
 330  
 331      /**
 332       * Initialize the order param
 333       *
 334       * @param string default ordering of columns (special syntax) if not specified in the URL params
 335       *               example: -A-- will sort in ascending order on 2nd column
 336       *               example: ---D will sort in descending order on 4th column
 337       */
 338  	function init_order_param( $default_order )
 339      {
 340          global $UserSettings;
 341  
 342          if( empty( $UserSettings ) )
 343          {
 344              $UserSettings = new UserSettings();
 345          }
 346  
 347          // attribution of an order type
 348          $this->order_param = 'results_'.$this->param_prefix.'order';
 349          $this->order = param( $this->order_param, 'string', '', true );
 350          // remove symbols '-' from the end
 351          $this->order = preg_replace( '/(-*[AD]+)(-*)/i', '$1', $this->order );
 352  
 353          if( !empty( $this->param_prefix ) &&
 354              !empty( $this->order ) &&
 355              $this->order != $UserSettings->get( $this->order_param ) )
 356          {    // Change an order param in DB for current user and current list
 357              if( $this->order == $default_order )
 358              {    // Delete an order param for current list if it is a default value
 359                  $UserSettings->delete( $this->order_param );
 360              }
 361              else
 362              {    // Set a new value of an order param for current list
 363                  $UserSettings->set( $this->order_param, $this->order );
 364              }
 365              $UserSettings->dbupdate();
 366          }
 367  
 368          if( !empty( $this->param_prefix ) && empty( $this->order ) )
 369          {    // Set an order param from DB
 370              if( $UserSettings->get( $this->order_param ) != '' )
 371              {    // Set a value for current list if it was already defined
 372                  $this->order = $UserSettings->get( $this->order_param );
 373              }
 374          }
 375  
 376          if( empty( $this->order ) )
 377          {    // Set a default value
 378              $this->order = $default_order;
 379          }
 380      }
 381  
 382  
 383      /**
 384       * Reset the query -- EXPERIMENTAL
 385       *
 386       * Useful in derived classes such as ItemList to requery with a slighlty moidified filterset
 387       */
 388  	function reset()
 389      {
 390          $this->rows = NULL;
 391      }
 392  
 393  
 394      /**
 395       * Rewind resultset
 396       */
 397  	function restart()
 398      {
 399          if( !isset( $this->total_rows ) )
 400          { // Count total rows to be able to display pages correctly
 401              $this->count_total_rows( $this->count_sql );
 402          }
 403  
 404          // Make sure query has executed:
 405          $this->query( $this->sql );
 406  
 407          $this->current_idx = 0;
 408  
 409          $this->global_idx = (($this->page-1) * $this->limit) + $this->current_idx;
 410  
 411          $this->global_is_first = ( $this->global_idx <= 0 ) ? true : false;
 412  
 413          $this->global_is_last = ( $this->global_idx >= $this->total_rows-1 ) ? true : false;
 414  
 415          $this->current_group_ID = NULL;
 416      }
 417  
 418  
 419      /**
 420       * Increment and update all necessary counters before processing a new line in result set
 421       */
 422  	function next_idx()
 423      {
 424          $this->current_idx++;
 425  
 426          $this->global_idx = (($this->page-1) * $this->limit) + $this->current_idx;
 427  
 428          $this->global_is_first = ( $this->global_idx <= 0 ) ? true : false;
 429  
 430          $this->global_is_last = ( $this->global_idx >= $this->total_rows-1 ) ? true : false;
 431  
 432          return $this->current_idx;
 433      }
 434  
 435  
 436      /**
 437       * Run the query now!
 438       *
 439       * Will only run if it has not executed before.
 440       */
 441  	function query( $create_default_cols_if_needed = true, $append_limit = true, $append_order_by = true,
 442                                          $query_title = 'Results::Query()' )
 443      {
 444          global $DB, $Debuglog;
 445          if( !is_null( $this->rows ) )
 446          { // Query has already executed:
 447              return;
 448          }
 449  
 450          if( empty( $this->sql ) )
 451          { // the sql query is empty so we can't process it
 452              return;
 453          }
 454  
 455          // Make sure we have colum definitions:
 456          if( is_null( $this->cols ) && $create_default_cols_if_needed )
 457          { // Let's create default column definitions:
 458              $this->cols = array();
 459  
 460              if( !preg_match( '#^(SELECT.*?(\([^)]*?FROM[^)]*\).*)*)FROM#six', $this->sql, $matches ) )
 461              {
 462                  debug_die( 'Results->query() : No SELECT clause!' );
 463              }
 464              // Split requested columns by commata
 465              foreach( preg_split( '#\s*,\s*#', $matches[1] ) as $l_select )
 466              {
 467                  if( is_numeric( $l_select ) )
 468                  { // just a single value (would produce parse error as '$x$')
 469                      $this->cols[] = array( 'td' => $l_select );
 470                  }
 471                  elseif( preg_match( '#^(\w+)$#i', $l_select, $match ) )
 472                  { // regular column
 473                      $this->cols[] = array( 'td' => '$'.$match[1].'$' );
 474                  }
 475                  elseif( preg_match( '#^(.*?) AS (\w+)#i', $l_select, $match ) )
 476                  { // aliased column
 477                      $this->cols[] = array( 'td' => '$'.$match[2].'$' );
 478                  }
 479              }
 480  
 481              if( !isset($this->cols[0]) )
 482              {
 483                  debug_die( 'No columns selected!' );
 484              }
 485          }
 486  
 487  
 488          // Make a copy of the SQL, that we may change and that gets executed:
 489          $sql = $this->sql;
 490  
 491          // Append ORDER clause if necessary:
 492          if( $append_order_by && ($orders = $this->get_order_field_list()) )
 493          {    // We have orders to append
 494  
 495              if( strpos( $sql, 'ORDER BY') === false )
 496              { // there is no ORDER BY clause in the original SQL query
 497                  $sql .= ' ORDER BY '.$orders.' ';
 498              }
 499              else
 500              {    // try to insert the chosen order at an existing '*' point
 501                  $inserted_sql = preg_replace( '# \s ORDER \s+ BY (.+) \* #xi', ' ORDER BY $1 '.$orders, $sql );
 502  
 503                  if( $inserted_sql != $sql )
 504                  {    // Insertion ok:
 505                      $sql = $inserted_sql;
 506                  }
 507                  else
 508                  {    // No insert point found:
 509                      // the chosen order must be appended to an existing ORDER BY clause
 510                      $sql .= ', '.$orders;
 511                  }
 512              }
 513          }
 514          else
 515          {    // Make sure there is no * in order clause:
 516              $sql = preg_replace( '# \s ORDER \s+ BY (.+) \* #xi', ' ORDER BY $1 ', $sql );
 517          }
 518  
 519          $add_limit = $append_limit && ! empty( $this->limit );
 520  
 521          if( $add_limit && ! $this->order_callbacks )
 522          {    // No callbacks to be called, so we can limit the line range to the requested page:
 523              $Debuglog->add( 'LIMIT requested and no callbacks - adding LIMIT to query.', 'results' );
 524              $sql .= ' LIMIT '.max( 0, ( $this->page - 1 ) * $this->limit ).', '.$this->limit;
 525          }
 526  
 527          // Execute query and store results
 528          $this->rows = $DB->get_results( $sql, OBJECT, $query_title );
 529  
 530          if ( ! $this->order_callbacks || ! $add_limit )
 531          {
 532              $Debuglog->add( 'Storing row count (no LIMIT or no callbacks)', 'results' );
 533              $this->result_num_rows = $DB->num_rows;
 534          }
 535  
 536          // Sort with callbacks:
 537          if( $this->order_callbacks )
 538          {
 539              $Debuglog->add( 'Sorting with callbacks.', 'results' );
 540              foreach( $this->order_callbacks as $order_callback )
 541              {
 542                  #echo 'order_callback: '; var_dump($order_callback);
 543  
 544                  $this->order_callback_wrapper_data = $order_callback; // to pass ASC/DESC param and callback itself through the wrapper to the callback
 545  
 546                  if( empty($order_callback['use_rows']) )
 547                  { // default: instantiate objects for the callback:
 548                      usort( $this->rows, array( &$this, 'order_callback_wrapper_objects' ) );
 549                  }
 550                  else
 551                  {
 552                      usort( $this->rows, array( &$this, 'order_callback_wrapper_rows' ) );
 553                  }
 554              }
 555  
 556              if ( $add_limit )
 557              {
 558                  $Debuglog->add( 'Callback sorting: LIMIT needed, extracting slice from array', 'results' );
 559                  $this->rows = array_slice( $this->rows, max( 0, ( $this->page - 1 ) * $this->limit ), $this->limit );
 560                  $this->result_num_rows = count( $this->rows );
 561              }
 562          }
 563  
 564          // Group by object property:
 565          if( ! empty($this->group_by_obj_prop) )
 566          {
 567              if( ! is_array($this->group_by_obj_prop) )
 568              {
 569                  $this->group_by_obj_prop = array($this->group_by_obj_prop);
 570              }
 571  
 572              $this->mergesort( $this->rows, array( &$this, 'callback_group_by_obj_prop' ) );
 573          }
 574  
 575          // $Debuglog->add( 'rows on page='.$this->result_num_rows, 'results' );
 576      }
 577  
 578  
 579      /**
 580       * Merge sort. This is required to not re-order items when sorting for e.g. grouping at the end.
 581       *
 582       * @see http://de2.php.net/manual/en/function.usort.php#38827
 583       *
 584       * @param array List of items to sort
 585       * @param callback Sort function/method
 586       */
 587  	function mergesort(&$array, $cmp_function)
 588      {
 589          // Arrays of size < 2 require no action.
 590          if (count($array) < 2) return;
 591          // Split the array in half
 592          $halfway = count($array) / 2;
 593          $array1 = array_slice($array, 0, $halfway);
 594          $array2 = array_slice($array, $halfway);
 595          // Recurse to sort the two halves
 596          $this->mergesort($array1, $cmp_function);
 597          $this->mergesort($array2, $cmp_function);
 598          // If all of $array1 is <= all of $array2, just append them.
 599          if (call_user_func($cmp_function, end($array1), $array2[0]) < 1) {
 600                  $array = array_merge($array1, $array2);
 601                  return;
 602          }
 603          // Merge the two sorted arrays into a single sorted array
 604          $array = array();
 605          $ptr1 = $ptr2 = 0;
 606          while ($ptr1 < count($array1) && $ptr2 < count($array2)) {
 607                  if (call_user_func($cmp_function, $array1[$ptr1], $array2[$ptr2]) < 1) {
 608                          $array[] = $array1[$ptr1++];
 609                  }
 610                  else {
 611                          $array[] = $array2[$ptr2++];
 612                  }
 613          }
 614          // Merge the remainder
 615          while ($ptr1 < count($array1)) $array[] = $array1[$ptr1++];
 616          while ($ptr2 < count($array2)) $array[] = $array2[$ptr2++];
 617          return;
 618       }
 619  
 620  
 621      /**
 622       * Callback, to sort {@link Result::$rows} according to {@link Result::$group_by_obj_prop}.
 623       *
 624       * @param array DB row for object A
 625       * @param array DB row for object B
 626       * @param integer Depth, used internally (you can group on a list of member properties)
 627       * @return integer
 628       */
 629  	function callback_group_by_obj_prop( $row_a, $row_b, $depth = 0 )
 630      {
 631          $obj_prop = $this->group_by_obj_prop[$depth];
 632  
 633          $a = & $this->Cache->instantiate($row_a);
 634          $a_value = $a->$obj_prop;
 635          $b = & $this->Cache->instantiate($row_b);
 636          $b_value = $b->$obj_prop;
 637  
 638          if( $a_value == $b_value )
 639          {
 640              if( $depth+1 < count($this->group_by_obj_prop) )
 641              {
 642                  return $this->callback_group_by_obj_prop( $row_a, $row_b, ($depth + 1) );
 643              }
 644              else
 645              { // on the last level of grouping:
 646                  return 0;
 647              }
 648          }
 649  
 650          // Sort empty group_by-values to the bottom
 651          if( empty($a_value) )
 652              return 1;
 653          if( empty($b_value) )
 654              return -1;
 655  
 656          return strcasecmp( $a_value, $b_value );
 657      }
 658  
 659  
 660      /**
 661       * Wrapper method to {@link usort()}, which instantiates objects and passed them on to the
 662       * order callback.
 663       *
 664       * @return integer
 665       */
 666  	function order_callback_wrapper_objects( $row_a, $row_b )
 667      {
 668          $a = $this->Cache->instantiate($row_a);
 669          $b = $this->Cache->instantiate($row_b);
 670  
 671          return (int)call_user_func( $this->order_callback_wrapper_data['callback'],
 672                  $a, $b, $this->order_callback_wrapper_data['order'] );
 673      }
 674  
 675  
 676      /**
 677       * Wrapper method to {@link usort()}, which passes the rows to the order callback.
 678       *
 679       * @return integer
 680       */
 681  	function order_callback_wrapper_rows( $row_a, $row_b )
 682      {
 683          return (int)call_user_func( $this->order_callback_wrapper_data['callback'],
 684                  $row_a, $row_b, $this->order_callback_wrapper_data['order'] );
 685      }
 686  
 687  
 688      /**
 689       * Get a list of IDs for current page
 690       *
 691       * @uses Results::$ID_col
 692       */
 693  	function get_page_ID_list()
 694      {
 695          if( is_null( $this->page_ID_list ) )
 696          {
 697              $this->page_ID_list = implode( ',', $this->get_page_ID_array() );
 698              //echo '<br />'.$this->page_ID_list;
 699          }
 700  
 701          return $this->page_ID_list;
 702      }
 703  
 704  
 705      /**
 706       * Get an array of IDs for current page
 707       *
 708       * @uses Results::$ID_col
 709       */
 710  	function get_page_ID_array()
 711      {
 712          if( is_null( $this->page_ID_array ) )
 713          {
 714              $this->page_ID_array = array();
 715              // pre_dump( $this );
 716              if( $this->result_num_rows )
 717              {    // We have some rows to explore.
 718                  // Fp> note: I don't understand why we can sometimes have: $this->rows = array{ [0]=>  NULL }  (found in Manual skin intro post testing)
 719                  foreach( $this->rows as $row )
 720                  { // For each row/line:
 721                      $this->page_ID_array[] = $row->{$this->ID_col};
 722                  }
 723              }
 724          }
 725  
 726          return $this->page_ID_array;
 727      }
 728  
 729  
 730      /**
 731       * Count the total number of rows of the SQL result (all pages)
 732       *
 733       * This is done by dynamically modifying the SQL query and forging a COUNT() into it.
 734       *
 735       * @todo dh> This might get done using SQL_CALC_FOUND_ROWS (I noted this somewhere else already)
 736       * 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.
 737       * dh> We could just try it. Adding some assertion in there, leaving the old
 738       *     code in place. I'm quite certain that it is working correctly with
 739       *     recent MySQL versions.
 740       *
 741       * @todo allow overriding?
 742       * @todo handle problem of empty groups!
 743       */
 744  	function count_total_rows( $sql_count = NULL )
 745      {
 746          global $DB;
 747  
 748          if( is_integer( $sql_count ) )
 749          {    // we have a total already
 750              $this->total_rows = $sql_count;
 751          }
 752          else
 753          { // we need to query
 754              if( is_null( $sql_count ) )
 755              {
 756                  if( is_null($this->sql) )
 757                  { // We may want to remove this later...
 758                      $this->total_rows = 0;
 759                      $this->total_pages = 0;
 760                      return;
 761                  }
 762  
 763                  $sql_count = $this->sql;
 764                  // echo $sql_count;
 765  
 766                  /*
 767                   *
 768                   * On a un probl�me avec la recherche sur les soci�t�s
 769                   * si on fait un select count(*), �a sort un nombre de r�ponses �norme
 770                   * mais on ne sait pas pourquoi... la solution est de lister des champs dans le COUNT()
 771                   * MAIS malheureusement �a ne fonctionne pas pour d'autres requ�tes.
 772                   * L'id�al serait de r�ussir � isoler qu'est-ce qui, dans la requ�te SQL, provoque le comportement
 773                   * bizarre....
 774                   */
 775                  // Tentative 1:
 776                  // if( !preg_match( '#FROM(.*?)((WHERE|ORDER BY|GROUP BY) .*)?$#si', $sql_count, $matches ) )
 777                  //  debug_die( "Can't understand query..." );
 778                  // if( preg_match( '#(,|JOIN)#si', $matches[1] ) )
 779                  // { // there was a coma or a JOIN clause in the FROM clause of the original query,
 780                  // Tentative 2:
 781                  // fplanque: je pense que la diff�rence est sur la pr�sence de DISTINCT ou non.
 782                  // if( preg_match( '#\s DISTINCT \s#six', $sql_count, $matches ) )
 783                  if( preg_match( '#\s DISTINCT \s+ ([A-Za-z_]+)#six', $sql_count, $matches ) )
 784                  { //
 785                      // Get rid of any Aliases in colmun names:
 786                      // $sql_count = preg_replace( '#\s AS \s+ ([A-Za-z_]+) #six', ' ', $sql_count );
 787                      // ** We must use field names in the COUNT **
 788                      //$sql_count = preg_replace( '#SELECT \s+ (.+?) \s+ FROM#six', 'SELECT COUNT( $1 ) FROM', $sql_count );
 789  
 790                      //Tentative 3: we do a distinct on the first field only when counting:
 791                      $sql_count = preg_replace( '#^ \s* SELECT \s+ (.+?) \s+ FROM#six', 'SELECT COUNT( DISTINCT '.$matches[1].' ) FROM', $sql_count );
 792                  }
 793                  else
 794                  { // Single table request: we must NOT use field names in the count.
 795                      $sql_count = preg_replace( '#^ \s* SELECT \s+ (.+?) \s+ FROM#six', 'SELECT COUNT( * ) FROM', $sql_count );
 796                  }
 797  
 798  
 799                  // Make sure there is no ORDER BY clause at the end:
 800                  $sql_count = preg_replace( '# \s ORDER \s+ BY .* $#xi', '', $sql_count );
 801  
 802                  // echo $sql_count;
 803              }
 804  
 805              $this->total_rows = $DB->get_var( $sql_count, 0, 0, get_class($this).'::count_total_rows()' ); //count total rows
 806          }
 807  
 808          $this->total_pages = empty($this->limit) ? 1 : ceil($this->total_rows / $this->limit);
 809  
 810          // Make sure we're not requesting a page out of range:
 811          if( $this->page > $this->total_pages )
 812          {
 813              /*
 814                  sam2kb> Isn't it better to display "Page not found" error instead?
 815                  Current implementation is bad for SEO bacause it can potentially display an unlimited number of duplicate pages
 816                  fp> on what public (not admin) url do we have this problem for example?
 817                  sam2kb>fp Ideally there must be a list of options in Blog settings > SEO
 818                  - display error
 819                  - redirect to last page
 820                  - display last page (this is what current version does)
 821                  "paged" param: http://b2evolution.net/news/releases/?paged=99
 822                  "page" param: http://b2evolution.net/news/2011/09/09/b2evolution-4-1-0-release?page=99
 823                  fp> ok, the current implementation only has merit when the result list is not controlled by a plublic URL param.
 824                  Otherwise the only 2 options should be 404 and 301.
 825                  Also, teh detection code should probably NOT be here.
 826              */
 827              $this->page = $this->total_pages;
 828          }
 829      }
 830  
 831  
 832      /**
 833       * Note: this function might actually not be very useful.
 834       * If you define ->Cache before display, all rows will be instantiated on the fly.
 835       * No need to restart et go through the rows a second time here.
 836       *
 837       * @param DataObjectCache
 838       */
 839  	function instantiate_page_to_Cache( & $Cache )
 840      {
 841          $this->Cache = & $Cache;
 842  
 843          // Make sure query has executed and we're at the top of the resultset:
 844          $this->restart();
 845  
 846          foreach( $this->rows as $row )
 847          { // For each row/line:
 848  
 849              // Instantiate an object for the row and cache it:
 850              $this->Cache->instantiate( $row );
 851          }
 852  
 853      }
 854  
 855  
 856      /**
 857       * Display paged list/table based on object parameters
 858       *
 859       * This is the meat of this class!
 860       *
 861       * @param array|NULL
 862       * @param array Fadeout settings array( 'key column' => array of values ) or 'session'
 863       * @return int # of rows displayed
 864       */
 865  	function display( $display_params = NULL, $fadeout = NULL )
 866      {
 867          // Lazy fill $this->params:
 868          parent::display_init( $display_params, $fadeout );
 869  
 870          // -------------------------
 871          // Proceed with display:
 872          // -------------------------
 873          echo $this->params['before'];
 874  
 875          if( ! is_ajax_content() )
 876          { // Display TITLE/FILTERS only if NO AJAX request
 877  
 878              // DISPLAY TITLE:
 879              if( isset( $this->title ) )
 880              { // A title has been defined for this result set:
 881                  echo $this->replace_vars( $this->params['head_title'] );
 882              }
 883  
 884              // DISPLAY FILTERS:
 885              $this->display_filters();
 886          }
 887  
 888          // Flush in order to show the filters before slow SQL query will be executed below
 889          evo_flush();
 890  
 891          // Make sure query has executed and we're at the top of the resultset:
 892          $this->restart();
 893  
 894          if( ! is_ajax_content() )
 895          { // Display COL SELECTION only if NO AJAX request
 896              $this->display_colselect();
 897          }
 898  
 899          // START OF AJAX CONTENT:
 900          echo $this->replace_vars( $this->params['content_start'] );
 901  
 902              if( $this->total_pages == 0 )
 903              { // There are no results! Nothing to display!
 904  
 905                  // START OF LIST/TABLE:
 906                  $this->display_list_start();
 907  
 908                  // END OF LIST/TABLE:
 909                  $this->display_list_end();
 910              }
 911              else
 912              { // We have rows to display:
 913  
 914                  // GLOBAL (NAV) HEADER:
 915                  $this->display_nav( 'header' );
 916  
 917                  // START OF LIST/TABLE:
 918                  $this->display_list_start();
 919  
 920                      // DISPLAY COLUMN HEADERS:
 921                      $this->display_col_headers();
 922  
 923                      // GROUP & DATA ROWS:
 924                      $this->display_body();
 925  
 926                      // Totals line
 927                      $this->display_totals();
 928  
 929                      // Functions
 930                      $this->display_functions();
 931  
 932                  // END OF LIST/TABLE:
 933                  $this->display_list_end();
 934  
 935                  // GLOBAL (NAV) FOOTER:
 936                  $this->display_nav( 'footer' );
 937              }
 938  
 939          // END OF AJAX CONTENT:
 940          echo $this->params['content_end'];
 941  
 942          echo $this->params['after'];
 943  
 944          // Return number of rows displayed:
 945          return $this->current_idx;
 946      }
 947  
 948  
 949      /**
 950       * Initialize things in order to be ready for displaying.
 951       *
 952       * This is useful when manually displaying, i-e: not by using Results::display()
 953       *
 954       * @param array ***please document***
 955       * @param array Fadeout settings array( 'key column' => array of values ) or 'session'
 956        */
 957  	function display_init( $display_params = NULL, $fadeout = NULL )
 958      {
 959          // Lazy fill $this->params:
 960          parent::display_init( $display_params, $fadeout );
 961  
 962          // Make sure query has executed and we're at the top of the resultset:
 963          $this->restart();
 964      }
 965  
 966  
 967      /**
 968       * Display list/table body.
 969       *
 970       * This includes groups and data rows.
 971       */
 972  	function display_body()
 973      {
 974          // BODY START:
 975          $this->display_body_start();
 976  
 977          // Prepare data for grouping:
 978          $group_by_all = array();
 979          if( ! empty($this->group_by) )
 980          {
 981              $group_by_all['row'] = is_array($this->group_by) ? $this->group_by : array($this->group_by);
 982          }
 983          if( ! empty($this->group_by_obj_prop) )
 984          {
 985              $group_by_all['obj_prop'] = is_array($this->group_by_obj_prop) ? $this->group_by_obj_prop : array($this->group_by_obj_prop);
 986          }
 987  
 988          $this->current_group_count = array(); // useful in parse_col_content()
 989  
 990  
 991          foreach( $this->rows as $row )
 992          { // For each row/line:
 993  
 994              /*
 995               * GROUP ROW stuff:
 996               */
 997              if( ! empty($group_by_all) )
 998              {    // We are grouping (by SQL and/or object property)...
 999  
1000                  $group_depth = 0;
1001                  $group_changed = false;
1002                  foreach( $group_by_all as $type => $names )
1003                  {
1004                      foreach( $names as $name )
1005                      {
1006                          if( $type == 'row' )
1007                          {
1008                              $value = $row->$name;
1009                          }
1010                          elseif( $type == 'obj_prop' )
1011                          {
1012                              $this->current_Obj = $this->Cache->instantiate($row); // useful also for parse_col_content() below
1013                              $value = $this->current_Obj->$name;
1014                          }
1015                          else debug_die( 'Invalid Results-group_by-type: '.var_export( $type, true ) );
1016  
1017  
1018                          if( $this->current_group_ID[$group_depth] != $value )
1019                          { // Group changed here:
1020                              $this->current_group_ID[$group_depth] = $value;
1021  
1022                              if( ! isset($this->current_group_count[$group_depth]) )
1023                              {
1024                                  $this->current_group_count[$group_depth] = 0;
1025                              }
1026                              else
1027                              {
1028                                  $this->current_group_count[$group_depth]++;
1029                              }
1030  
1031                              // unset sub-group identifiers:
1032                              for( $i = $group_depth+1, $n = count($this->current_group_ID); $i < $n; $i++ )
1033                              {
1034                                  unset($this->current_group_ID[$i]);
1035                              }
1036  
1037                              $group_changed = true;
1038                              break 2;
1039                          }
1040  
1041                          $group_depth++;
1042                      }
1043                  }
1044  
1045                  if( $group_changed )
1046                  { // We have just entered a new group!
1047  
1048                      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
1049  
1050                      $col_count = 0;
1051                      foreach( $this->grp_cols as $grp_col )
1052                      { // For each column:
1053  
1054                          if( isset( $grp_col['td_class'] ) )
1055                          {    // We have a class for the total column
1056                              $class = $grp_col['td_class'];
1057                          }
1058                          else
1059                          {    // We have no class for the total column
1060                              $class = '';
1061                          }
1062  
1063                          if( ($col_count==0) && isset($this->params['grp_col_start_first']) )
1064                          { // Display first column column start:
1065                              $output = $this->params['grp_col_start_first'];
1066  
1067                              // Add the total column class in the grp col start first param class:
1068                              $output = str_replace( '$class$', $class, $output );
1069                          }
1070                          elseif( ($col_count==count($this->grp_cols)-1) && isset($this->params['grp_col_start_last']) )
1071                          { // Last column can get special formatting:
1072                              $output = $this->params['grp_col_start_last'];
1073  
1074                              // Add the total column class in the grp col start end param class:
1075                              $output = str_replace( '$class$', $class, $output );
1076                          }
1077                          else
1078                          { // Display regular column start:
1079                              $output = $this->params['grp_col_start'];
1080  
1081                              // Replace the "class_attrib" in the grp col start param by the td column class
1082                              $output = str_replace( '$class_attrib$', 'class="'.$class.'"', $output );
1083                          }
1084  
1085                          if( isset( $grp_col['td_colspan'] ) )
1086                          {
1087                              $colspan = $grp_col['td_colspan'];
1088                              if( $colspan < 0 )
1089                              { // We want to substract columns from the total count
1090                                  $colspan = $this->nb_cols + $colspan;
1091                              }
1092                              elseif( $colspan == 0 )
1093                              { // use $nb_cols
1094                                  $colspan = $this->nb_cols;
1095                              }
1096                              $output = str_replace( '$colspan_attrib$', 'colspan="'.$colspan.'"', $output );
1097                          }
1098                          else
1099                          { // remove non-HTML attrib:
1100                              $output = str_replace( '$colspan_attrib$', '', $output );
1101                          }
1102  
1103                          // Contents to output:
1104                          $output .= $this->parse_col_content( $grp_col['td'] );
1105                          //echo $output;
1106                          eval( "echo '$output';" );
1107  
1108                          echo '</td>';
1109                          $col_count++;
1110                      }
1111  
1112                      echo $this->params['grp_line_end'];
1113                  }
1114              }
1115  
1116  
1117              /*
1118               * DATA ROW stuff:
1119               */
1120              if( !empty($this->ID_col) && empty($row->{$this->ID_col}) )
1121              {    // We have detected an empty data row which we want to ignore... (happens with empty groups)
1122                  continue;
1123              }
1124  
1125  
1126              if( ! is_null( $this->Cache ) )
1127              { // We want to instantiate an object for the row and cache it:
1128                  // We also keep a local ref in case we want to use it for display:
1129                  $this->current_Obj = & $this->Cache->instantiate( $row );
1130              }
1131  
1132  
1133              // Check for fadeout
1134              $fadeout_line = false;
1135              if( !empty( $this->fadeout_array ) )
1136              {
1137                  foreach( $this->fadeout_array as $key => $crit )
1138                  {
1139                      // echo 'fadeout '.$key.'='.$crit;
1140                      if( isset( $row->$key ) && in_array( $row->$key, $crit ) )
1141                      { // Col is in the fadeout list
1142                          // TODO: CLEAN THIS UP!
1143                          $fadeout_line = true;
1144                          break;
1145                      }
1146                  }
1147              }
1148  
1149              // LINE START:
1150              $this->display_line_start( $this->current_idx == count($this->rows)-1, $fadeout_line );
1151  
1152              foreach( $this->cols as $col )
1153              { // For each column:
1154  
1155                  // COL START:
1156                  if ( ! empty($col['extra']) )
1157                  {
1158                      // array of extra params $col['extra']
1159                      $this->display_col_start( $col['extra'] );
1160                  }
1161                  else
1162                  {
1163                      $this->display_col_start();
1164                  }
1165  
1166  
1167                  // Contents to output:
1168                  $output = $this->parse_col_content( $col['td'] );
1169                  #pre_dump( '{'.$output.'}' );
1170  
1171                  $out = eval( "return '$output';" );
1172                  // fp> <input> is needed for checkboxes in the Blog User/Group permissions table > advanced
1173                  echo ( trim(strip_tags($out,'<img><input>')) === '' ? '&nbsp;' : $out );
1174  
1175                  // COL START:
1176                  $this->display_col_end();
1177              }
1178  
1179              // LINE END:
1180              $this->display_line_end();
1181  
1182              $this->next_idx();
1183          }
1184  
1185          // BODY END:
1186          $this->display_body_end();
1187      }
1188  
1189  
1190      /**
1191       * Display totals line if set.
1192       */
1193  	function display_totals()
1194      {
1195          $total_enable = false;
1196  
1197          // Search if we have totals line to display:
1198          foreach( $this->cols as $col )
1199          {
1200              if( isset( $col['total'] ) )
1201              {    // We have to display a totals line
1202                  $total_enable = true;
1203                  break;
1204              }
1205          }
1206  
1207          if( $total_enable )
1208          { // We have to dispaly a totals line
1209  
1210              echo $this->params['tfoot_start'];
1211              // <tr>
1212              echo $this->params['total_line_start'];
1213  
1214              $loop = 0;
1215  
1216              foreach( $this->cols as $col )
1217              {
1218                  if( isset( $col['total_class'] ) )
1219                  {    // We have a class for the total column
1220                      $class = $col['total_class'];
1221                  }
1222                  else
1223                  {    // We have no class for the total column
1224                      $class = '';
1225                  }
1226  
1227                  if( $loop == 0)
1228                  {    // The column is the first
1229                      $output = $this->params['total_col_start_first'];
1230                      // Add the total column class in the total col start first param class:
1231                      $output = str_replace( '$class$', $class, $output );
1232                   }
1233                  elseif( $loop ==( count( $this->cols ) -1 ) )
1234                  {    // The column is the last
1235                      $output = $this->params['total_col_start_last'];
1236                      // Add the total column class in the total col start end param class:
1237                      $output = str_replace( '$class$', $class, $output );
1238                  }
1239                  else
1240                  {
1241                      $output = $this->params['total_col_start'];
1242                      // Replace the "class_attrib" in the total col start param by the total column class
1243                      $output = str_replace( '$class_attrib$', 'class="'.$class.'"', $output );
1244                  }
1245  
1246                  // <td class="....">
1247                  echo $output;
1248  
1249                  if( isset( $col['total'] ) )
1250                  {    // The column has a total set, so display it:
1251                      $output = $col['total'];
1252                      $output = $this->parse_col_content( $output );
1253                      eval( "echo '$output';" );
1254                  }
1255                  else
1256                  {    // The column has no total
1257                      echo '&nbsp;';
1258                  }
1259                  // </td>
1260                  echo  $this->params['total_col_end'];
1261  
1262                  $loop++;
1263              }
1264              // </tr>
1265              echo $this->params['total_line_end'];
1266              echo $this->params['tfoot_end'];
1267          }
1268      }
1269  
1270  
1271      /**
1272     * Display the functions
1273     */
1274  	function display_functions()
1275      {
1276          if( empty( $this->functions_area ) )
1277          {    // We don't want to display a functions section:
1278              return;
1279          }
1280  
1281          echo $this->replace_vars( $this->params['functions_start'] );
1282  
1283          if( !empty( $this->functions_area['callback'] ) )
1284          {    // We want to display functions:
1285              if( is_array( $this->functions_area['callback'] ) )
1286              {    // The callback is an object function
1287                  $obj_name = $this->functions_area['callback'][0];
1288                  if( $obj_name != 'this' )
1289                  {    // We need the global object
1290                      global $$obj_name;
1291                  }
1292                  $func = $this->functions_area['callback'][1];
1293  
1294                  if( isset( $this->Form ) )
1295                  {    // There is a created form
1296                      $$obj_name->$func( $this->Form );
1297                  }
1298                  else
1299                  { // There is not a created form
1300                      $$obj_name->$func();
1301                  }
1302              }
1303              else
1304              {    // The callback is a function
1305                  $func = $this->functions_area['callback'];
1306  
1307                  if( isset( $this->Form ) )
1308                  {    // There is a created form
1309                      $func( $this->Form );
1310                  }
1311                  else
1312                  { // There is not a created form
1313                      $func();
1314                  }
1315              }
1316  
1317          }
1318  
1319          echo $this->params['functions_end'];
1320      }
1321  
1322  
1323      /**
1324       * Display navigation text, based on template.
1325       *
1326       * @param string template: 'header' or 'footer'
1327       */
1328  	function display_nav( $template )
1329      {
1330          if( empty($this->limit) && isset($this->params[$template.'_text_no_limit']) )
1331          {    // No LIMIT (there's always only one page)
1332              $navigation = $this->params[$template.'_text_no_limit'];
1333          }
1334          elseif( ( $this->total_pages <= 1 ) )
1335          {    // Single page (we probably don't want to show navigation in this case)
1336              $navigation = $this->replace_vars( $this->params[$template.'_text_single'] );
1337          }
1338          else
1339          {    // Several pages
1340              $navigation = $this->replace_vars( $this->params[$template.'_text'] );
1341          }
1342  
1343          if( !empty( $navigation ) )
1344          {    // Display navigation
1345              echo $this->params[$template.'_start'];
1346  
1347              echo $navigation;
1348  
1349              echo $this->params[$template.'_end'];
1350          }
1351      }
1352  
1353  
1354      /**
1355       * Returns values needed to make sort links for a given column
1356       *
1357       * Returns an array containing the following values:
1358       *  - current_order : 'ASC', 'DESC' or ''
1359       *  - order_asc : url to order in ascending order
1360       *  - order_desc
1361       *  - order_toggle : url to toggle sort order
1362       *
1363       * @param integer column to sort
1364       * @return array
1365       */
1366  	function get_col_sort_values( $col_idx )
1367      {
1368  
1369          // Current order:
1370          $order_char = substr( $this->order, $col_idx, 1 );
1371          if( $order_char == 'A' )
1372          {
1373              $col_sort_values['current_order'] = 'ASC';
1374          }
1375          elseif( $order_char == 'D' )
1376          {
1377              $col_sort_values['current_order'] = 'DESC';
1378          }
1379          else
1380          {
1381              $col_sort_values['current_order'] = '';
1382          }
1383  
1384  
1385          // Generate sort values to use for sorting on the current column:
1386          $order_asc = '';
1387          $order_desc = '';
1388          for( $i = 0; $i < $this->nb_cols; $i++ )
1389          {
1390              if(    $i == $col_idx )
1391              { // Link ordering the current column
1392                  $order_asc .= 'A';
1393                  $order_desc .= 'D';
1394              }
1395              else
1396              {
1397                  $order_asc .= '-';
1398                  $order_desc .= '-';
1399              }
1400          }
1401  
1402          $col_sort_values['order_asc'] = regenerate_url( $this->order_param, $this->order_param.'='.$order_asc, $this->params['page_url'] );
1403          $col_sort_values['order_desc'] = regenerate_url( $this->order_param, $this->order_param.'='.$order_desc, $this->params['page_url'] );
1404  
1405  
1406          if( !$col_sort_values['current_order'] && isset( $this->cols[$col_idx]['default_dir'] ) )
1407          {    // There is no current order on this column and a default order direction is set for it
1408              // So set a default order direction for it
1409  
1410              if( $this->cols[$col_idx]['default_dir'] == 'A' )
1411              {    // The default order direction is A, so set its toogle  order to the order_asc
1412                  $col_sort_values['order_toggle'] = $col_sort_values['order_asc'];
1413              }
1414              else
1415              { // The default order direction is A, so set its toogle order to the order_desc
1416                  $col_sort_values['order_toggle'] = $col_sort_values['order_desc'];
1417              }
1418          }
1419          elseif( $col_sort_values['current_order'] == 'ASC' )
1420          {    // There is an ASC current order on this column, so set its toogle order to the order_desc
1421              $col_sort_values['order_toggle'] = $col_sort_values['order_desc'];
1422          }
1423          else
1424          { // There is a DESC or NO current order on this column,  so set its toogle order to the order_asc
1425              $col_sort_values['order_toggle'] = $col_sort_values['order_asc'];
1426          }
1427  
1428          return $col_sort_values;
1429      }
1430  
1431  
1432      /**
1433       * Returns order field list add to SQL query:
1434       * @return string May be empty
1435       */
1436  	function get_order_field_list()
1437      {
1438          if( is_null( $this->order_field_list ) )
1439          { // Order list is not defined yet
1440              if( ( !empty( $this->order ) ) && ( !empty( $this->cols ) ) && ( substr( $this->order, 0, 1 ) == '/' ) )
1441              { // order is set in format '/order_field_name/A' or '/order_field_name/D'
1442                  $order_parts = explode( '/', $this->order );
1443                  $this->order = '';
1444                  if( ( count( $order_parts ) == 3 ) && ( ( $order_parts[2] == 'A' ) || ( $order_parts[2] == 'D' ) ) )
1445                  {
1446                      foreach( $this->cols as $col )
1447                      { // iterate thrugh columns and find matching column name
1448                          if( isset( $col['order'] ) && ( $col['order'] == $order_parts[1] ) )
1449                          { // we have found the requested orderable column
1450                              $this->order .= $order_parts[2];
1451                              break;
1452                          }
1453                          else
1454                          {
1455                              $this->order .= '-';
1456                          }
1457                      }
1458                  }
1459              }
1460  
1461              if( empty( $this->order ) )
1462              { // We have no user provided order:
1463                  if( empty( $this->cols ) )
1464                  {    // We have no columns to pick an automatic order from:
1465                      // echo 'Can\'t determine automatic order';
1466                      return '';
1467                  }
1468  
1469                  foreach( $this->cols as $col )
1470                  {
1471                      if( isset( $col['order'] ) || isset( $col['order_objects_callback'] ) || isset( $col['order_rows_callback'] ) )
1472                      { // We have found the first orderable column:
1473                          $this->order .= 'A';
1474                          break;
1475                      }
1476                      else
1477                      {
1478                          $this->order .= '-';
1479                      }
1480                  }
1481              }
1482  
1483              // echo ' order='.$this->order.' ';
1484  
1485              $orders = array();
1486              $this->order_callbacks = array();
1487  
1488              for( $i = 0; $i <= strlen( $this->order ); $i++ )
1489              {    // For each position in order string:
1490                  if( isset( $this->cols[$i]['order'] ) )
1491                  {    // if column is sortable:
1492                      # 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)
1493                      switch( substr( $this->order, $i, 1 ) )
1494                      {
1495                          case 'A':
1496                              $orders[] = preg_replace( '~(?<!asc|desc)\s*,~i', ' ASC,', $this->cols[$i]['order'] ).' ASC';
1497                              break;
1498  
1499                          case 'D':
1500                              $orders[] = preg_replace( '~(asc|desc)?\s*,~i', ' DESC,', $this->cols[$i]['order'] ).' DESC';
1501                              break;
1502                      }
1503                  }
1504  
1505                  if( isset( $this->cols[$i]['order_objects_callback'] ) )
1506                  {    // if column is sortable by object callback:
1507                      switch( substr( $this->order, $i, 1 ) )
1508                      {
1509                          case 'A':
1510                              $this->order_callbacks[] = array(
1511                                      'callback' => $this->cols[$i]['order_objects_callback'],
1512                                      'use_rows' => false,
1513                                      'order'=>'ASC' );
1514                              break;
1515  
1516                          case 'D':
1517                              $this->order_callbacks[] = array(
1518                                      'callback' => $this->cols[$i]['order_objects_callback'],
1519                                      'use_rows' => false,
1520                                      'order' => 'DESC' );
1521                              break;
1522                      }
1523                  }
1524  
1525                  if( isset( $this->cols[$i]['order_rows_callback'] ) )
1526                  {    // if column is sortable by callback:
1527                      switch( substr( $this->order, $i, 1 ) )
1528                      {
1529                          case 'A':
1530                              $this->order_callbacks[] = array(
1531                                      'callback' => $this->cols[$i]['order_rows_callback'],
1532                                      'use_rows' => true,
1533                                      'order'=>'ASC' );
1534                              break;
1535  
1536                          case 'D':
1537                              $this->order_callbacks[] = array(
1538                                      'callback' => $this->cols[$i]['order_rows_callback'],
1539                                      'use_rows' => true,
1540                                      'order' => 'DESC' );
1541                              break;
1542                      }
1543                  }
1544              }
1545              $this->order_field_list = implode( ',', $orders );
1546  
1547              #pre_dump( $this->order_field_list );
1548              #pre_dump( $this->order_callbacks );
1549          }
1550          return $this->order_field_list;    // May be empty
1551      }
1552  
1553  
1554      /**
1555       * Handle variable subtitutions for column contents.
1556       *
1557       * This is one of the key functions to look at when you want to use the Results class.
1558       * - $var$
1559       * - £var£ - replaced by its utf-8 hex character (c2 a3)
1560       * - ²var² - replaced by its utf-8 hex character (c2 b2)
1561       * - #var#
1562       * - {row}
1563       * - %func()%
1564       * - ~func()~
1565       * - ¤func()¤ - @deprecated by ~func()~ - replaced by its utf-8 hex character (c2 a4)
1566       */
1567  	function parse_col_content( $content )
1568      {
1569          // Make variable substitution for STRINGS:
1570          $content = preg_replace( '#\$ (\w+) \$#ix', "'.format_to_output(\$row->$1).'", $content );
1571          // Make variable substitution for URL STRINGS:
1572          $content = preg_replace( '#\x{c2}\x{a3} (\w+) \x{c2}\x{a3}#ix', "'.format_to_output(\$row->$1, 'urlencoded').'", $content );
1573          // Make variable substitution for escaped strings:
1574          $content = preg_replace( '#\x{c2}\x{b2} (\w+) \x{c2}\x{b2}#ix', "'.htmlentities(\$row->$1).'", $content );
1575          // Make variable substitution for RAWS:
1576          $content = preg_replace( '!\# (\w+) \#!ix', "\$row->$1", $content );
1577          // Make variable substitution for full ROW:
1578          $content = str_replace( '{row}', '$row', $content );
1579          // Make callback function substitution:
1580          $content = preg_replace( '#% (.+?) %#ix', "'.$1.'", $content );
1581          // Make variable substitution for intanciated Object:
1582          $content = str_replace( '{Obj}', "\$this->current_Obj", $content );
1583          // Make callback for Object method substitution:
1584          $content = preg_replace( '#@ (.+?) @#ix', "'.\$this->current_Obj->$1.'", $content );
1585          // Sometimes we need embedded function call, so we provide a second sign:
1586          $content = preg_replace( '#~ (.+?) ~#ix', "'.$1.'", $content );
1587  
1588          // @deprecated by ~func()~. Left here for backward compatibility only, to be removed in future versions.
1589          $content = preg_replace( '#\x{c2}\x{a4} (.+?) \x{c2}\x{a4}#ix', "'.$1.'", $content );
1590  
1591          // Make callback function move_icons for orderable lists // dh> what does it do?
1592          $content = str_replace( '{move}', "'.\$this->move_icons().'", $content );
1593  
1594          $content = str_replace( '{CUR_IDX}', $this->current_idx, $content );
1595          $content = str_replace( '{TOTAL_ROWS}', $this->total_rows, $content );
1596  
1597          return $content;
1598      }
1599  
1600  
1601      /**
1602       *
1603       * @todo Support {@link Results::$order_callbacks}
1604       */
1605  	function move_icons( )
1606      {
1607          $r = '';
1608  
1609          $reg = '#^'.$this->param_prefix.'order (ASC|DESC).*#';
1610  
1611          if( preg_match( $reg, $this->order_field_list, $res ) )
1612          {    // The table is sorted by the order column
1613              $sort = $res[1];
1614  
1615              // get the element ID
1616              $idname = $this->param_prefix . 'ID';
1617              $id = $this->rows[$this->current_idx]->$idname;
1618  
1619              // Move up arrow
1620              if( $this->global_is_first )
1621              {    // The element is the first so it can't move up, display a no move arrow
1622                  $r .= get_icon( 'nomove' ).' ';
1623              }
1624              else
1625              {
1626                  if(    $sort == 'ASC' )
1627                  {    // ASC sort, so move_up action for move up arrow
1628                      $action = 'move_up';
1629                      $alt = T_( 'Move up!' );
1630                      }
1631                  else
1632                  {    // Reverse sort, so action and alt are reverse too
1633                      $action = 'move_down';
1634                      $alt = T_('Move down! (reverse sort)');
1635                  }
1636                  $r .= action_icon( $alt, 'move_up', regenerate_url( 'action,'.$this->param_prefix.'ID' , $this->param_prefix.'ID='.$id.'&amp;action='.$action ) );
1637              }
1638  
1639              // Move down arrow
1640              if( $this->global_is_last )
1641              {    // The element is the last so it can't move up, display a no move arrow
1642                  $r .= get_icon( 'nomove' ).' ';
1643              }
1644              else
1645              {
1646                  if(    $sort == 'ASC' )
1647                  {    // ASC sort, so move_down action for move down arrow
1648                      $action = 'move_down';
1649                      $alt = T_( 'Move down!' );
1650                  }
1651                  else
1652                  { // Reverse sort, so action and alt are reverse too
1653                      $action = 'move_up';
1654                      $alt = T_('Move up! (reverse sort)');
1655                  }
1656                  $r .= action_icon( $alt, 'move_down', regenerate_url( 'action,'.$this->param_prefix.'ID', $this->param_prefix.'ID='.$id.'&amp;action='.$action ) );
1657              }
1658  
1659              return $r;
1660          }
1661          else
1662          {    // The table is not sorted by the order column, so we display no move arrows
1663  
1664              if( $this->global_is_first )
1665              {
1666                  // The element is the first so it can't move up, display a no move up arrow
1667                  $r = get_icon( 'nomove' ).' ';
1668              }
1669              else
1670              {    // Display no move up arrow
1671                  $r = action_icon( T_( 'Sort by order' ), 'nomove_up', regenerate_url( 'action', 'action=sort_by_order' ) );
1672              }
1673  
1674              if( $this->global_is_last )
1675              {
1676                  // The element is the last so it can't move down, display a no move down arrow
1677                  $r .= get_icon( 'nomove' ).' ';
1678              }
1679              else
1680              { // Display no move down arrow
1681                  $r .= action_icon( T_( 'Sort by order' ), 'nomove_down', regenerate_url( 'action','action=sort_by_order' ) );
1682              }
1683  
1684              return $r;
1685          }
1686      }
1687  
1688  
1689      /**
1690       * Widget callback for template vars.
1691       *
1692       * This allows to replace template vars, see {@link Widget::replace_callback()}.
1693       *
1694       * @return string
1695       */
1696  	function replace_callback( $matches )
1697      {
1698          // echo '['.$matches[1].']';
1699          switch( $matches[1] )
1700          {
1701              case 'start' :
1702                  return ( ($this->page-1)*$this->limit+1 );
1703  
1704              case 'end' :
1705                  return ( min( $this->total_rows, $this->page*$this->limit ) );
1706  
1707              case 'total_rows' :
1708                  //total number of rows in the sql query
1709                  return ( $this->total_rows );
1710  
1711              case 'page' :
1712                  //current page number
1713                  return ( $this->page );
1714  
1715              case 'total_pages' :
1716                  //total number of pages
1717                  return ( $this->total_pages );
1718  
1719              case 'prev' :
1720                  // inits the link to previous page
1721                  if ( $this->page <= 1 )
1722                  {
1723                      return $this->params['no_prev_text'];
1724                  }
1725                  $r = '<a href="'
1726                          .regenerate_url( $this->page_param, (($this->page > 2) ? $this->page_param.'='.($this->page-1) : ''), $this->params['page_url'] ).'"';
1727                  if( $this->nofollow_pagenav )
1728                  {    // We want to NOFOLLOW page navigation
1729                      $r .= ' rel="nofollow"';
1730                  }
1731                  $r .= '>'.$this->params['prev_text'].'</a>';
1732                  return $r;
1733  
1734              case 'next' :
1735                  // inits the link to next page
1736                  if( $this->page >= $this->total_pages )
1737                  {
1738                      return $this->params['no_next_text'];
1739                  }
1740                  $r = '<a href="'
1741                          .regenerate_url( $this->page_param, $this->page_param.'='.($this->page+1), $this->params['page_url'] ).'"';
1742                  if( $this->nofollow_pagenav )
1743                  {    // We want to NOFOLLOW page navigation
1744                      $r .= ' rel="nofollow"';
1745                  }
1746                  $r .= '>'.$this->params['next_text'].'</a>';
1747                  return $r;
1748  
1749              case 'list' :
1750                  //inits the page list
1751                  return $this->page_list( $this->first(), $this->last(), $this->params['page_url'] );
1752  
1753              case 'scroll_list' :
1754                  //inits the scrolling list of pages
1755                  return $this->page_scroll_list();
1756  
1757              case 'first' :
1758                  //inits the link to first page
1759                  return $this->display_first( $this->params['page_url'] );
1760  
1761              case 'last' :
1762                  //inits the link to last page
1763                  return $this->display_last( $this->params['page_url'] );
1764  
1765              case 'list_prev' :
1766                  //inits the link to previous page range
1767                  return $this->display_prev( $this->params['page_url'] );
1768  
1769              case 'list_next' :
1770                  //inits the link to next page range
1771                  return $this->display_next( $this->params['page_url'] );
1772  
1773              case 'page_size' :
1774                  //inits the list to select page size
1775                  return $this->display_page_size( $this->params['page_url'] );
1776  
1777              case 'prefix' :
1778                  //prefix
1779                  return $this->param_prefix;
1780  
1781              default :
1782                  return parent::replace_callback( $matches );
1783          }
1784      }
1785  
1786  
1787      /**
1788       * Returns the first page number to be displayed in the list
1789       */
1790  	function first()
1791      {
1792          if( $this->page <= intval( $this->params['list_span']/2 ))
1793          { // the current page number is small
1794              return 1;
1795          }
1796          elseif( $this->page > $this->total_pages-intval( $this->params['list_span']/2 ))
1797          { // the current page number is big
1798              return max( 1, $this->total_pages-$this->params['list_span']+1);
1799          }
1800          else
1801          { // the current page number can be centered
1802              return $this->page - intval($this->params['list_span']/2);
1803          }
1804      }
1805  
1806  
1807      /**
1808       * returns the last page number to be displayed in the list
1809       */
1810  	function last()
1811      {
1812          if( $this->page > $this->total_pages-intval( $this->params['list_span']/2 ))
1813          { //the current page number is big
1814              return $this->total_pages;
1815          }
1816          else
1817          {
1818              return min( $this->total_pages, $this->first()+$this->params['list_span']-1 );
1819          }
1820      }
1821  
1822  
1823      /**
1824       * returns the link to the first page, if necessary
1825       */
1826  	function display_first( $page_url = '' )
1827      {
1828          if( $this->first() > 1 )
1829          { //the list doesn't contain the first page
1830              return '<a href="'.regenerate_url( $this->page_param, '', $page_url ).'">1</a>';
1831          }
1832          else
1833          { //the list already contains the first page
1834              return NULL;
1835          }
1836      }
1837  
1838  
1839      /**
1840       * returns the link to the last page, if necessary
1841       */
1842  	function display_last( $page_url = '' )
1843      {
1844          if( $this->last() < $this->total_pages )
1845          { //the list doesn't contain the last page
1846              return '<a href="'.regenerate_url( $this->page_param, $this->page_param.'='.$this->total_pages, $page_url ).'">'.$this->total_pages.'</a>';
1847          }
1848          else
1849          { //the list already contains the last page
1850              return NULL;
1851          }
1852      }
1853  
1854  
1855      /**
1856       * returns a link to previous pages, if necessary
1857       */
1858  	function display_prev( $page_url = '' )
1859      {
1860          if( $this->first() > 2 )
1861          { //the list has to be displayed
1862              $page_no = ceil($this->first()/2);
1863              return '<a href="'.regenerate_url( $this->page_param, $this->page_param.'='.$page_no, $page_url ).'">'
1864                                  .$this->params['list_prev_text'].'</a>';
1865          }
1866  
1867      }
1868  
1869  
1870      /**
1871       * returns a link to next pages, if necessary
1872       */
1873  	function display_next( $page_url = '' )
1874      {
1875          if( $this->last() < $this->total_pages-1 )
1876          { //the list has to be displayed
1877              $page_no = $this->last() + floor(($this->total_pages-$this->last())/2);
1878              return '<a href="'.regenerate_url( $this->page_param,$this->page_param.'='.$page_no, $page_url ).'">'
1879                                  .$this->params['list_next_text'].'</a>';
1880          }
1881      }
1882  
1883  
1884      /**
1885       * returns a list to select page size
1886       */
1887  	function display_page_size( $page_url = '' )
1888      {
1889          // Don't allow to change a page size:
1890          if( $this->total_rows <= 10 || // if total number of rows is always less then min page size
1891              empty( $this->param_prefix ) ) // the lists without defined param_prefix
1892          {
1893              return;
1894          }
1895  
1896          $page_size_options = array(
1897                  '10' => sprintf( T_('%s lines'), '10' ),
1898                  '20' => sprintf( T_('%s lines'), '20' ),
1899                  '30' => sprintf( T_('%s lines'), '30' ),
1900                  '40' => sprintf( T_('%s lines'), '40' ),
1901                  '50' => sprintf( T_('%s lines'), '50' ),
1902                  '100' => sprintf( T_('%s lines'), '100' ),
1903                  '200' => sprintf( T_('%s lines'), '200' ),
1904                  '500' => sprintf( T_('%s lines'), '500' ),
1905              );
1906  
1907          $default_page_size_value = '0';
1908          if( is_logged_in() )
1909          {    // Get default page size for current user
1910              global $UserSettings;
1911  
1912              $default_page_size_value = $UserSettings->get( 'results_per_page' );
1913              $default_page_size_option = array( '0' => sprintf( T_('Default (%s lines)'), $default_page_size_value ) );
1914              $page_size_options = $default_page_size_option + $page_size_options;
1915          }
1916  
1917          $html = '<small>';
1918          $html .= T_('Lines per page:');
1919  
1920          $html .= ' <select name="'.$this->limit_param.'" onchange="location.href=\''.regenerate_url( $this->page_param.','.$this->limit_param, $this->limit_param, $page_url ).'=\'+this.value">';
1921          foreach( $page_size_options as $value => $name )
1922          {
1923              $selected = '';
1924              if( $this->limit == $value && $value != $default_page_size_value )
1925              {
1926                  $selected = ' selected="selected"';
1927              }
1928              $html .= '<option value="'.$value.'"'.$selected.'>'.$name.'</option>';
1929          }
1930          $html .= '</select>';
1931  
1932          $html .= '</small>';
1933  
1934          return $html;
1935      }
1936  
1937  
1938      /**
1939       * Returns the page link list under the table
1940       */
1941  	function page_list( $min, $max, $page_url = '' )
1942      {
1943          $i = 0;
1944          $list = '';
1945  
1946          for( $i=$min; $i<=$max; $i++)
1947          {
1948              if( $i == $this->page )
1949              { //no link for the current page
1950                  $list .= '<strong class="current_page">'.$i.'</strong> ';
1951              }
1952              else
1953              { //a link for non-current pages
1954                  $list .= '<a href="'
1955                      .regenerate_url( $this->page_param, ( $i>1 ? $this->page_param.'='.$i : '' ), $page_url ).'"';
1956                  if( $this->nofollow_pagenav )
1957                  {    // We want to NOFOLLOW page navigation
1958                      $list .=  ' rel="nofollow"';
1959                  }
1960                  $list .= '>'.$i.'</a> ';
1961              }
1962          }
1963          return $list;
1964      }
1965  
1966  
1967      /*
1968       * Returns a scrolling page list under the table
1969       */
1970  	function page_scroll_list()
1971      {
1972          $scroll = '';
1973          $i = 0;
1974          $range = $this->params['scroll_list_range'];
1975          $min = 1;
1976          $max = 1;
1977          $option = '';
1978          $selected = '';
1979          $range_display='';
1980  
1981          if( $range > $this->total_pages )
1982              { //the range is greater than the total number of pages, the list goes up to the number of pages
1983                  $max = $this->total_pages;
1984              }
1985              else
1986              { //initialisation of the range
1987                  $max = $range;
1988              }
1989  
1990          //initialization of the form
1991          $scroll ='<form class="inline" method="post" action="'.regenerate_url( $this->page_param ).'">
1992                              <select name="'.$this->page_param.'" onchange="parentNode.submit()">';//javascript to change page clicking in the scroll list
1993  
1994          while( $max <= $this->total_pages )
1995          { //construction loop
1996              if( $this->page <= $max && $this->page >= $min )
1997              { //display all the pages belonging to the range where the current page is located
1998                  for( $i = $min ; $i <= $max ; $i++)
1999                  { //construction of the <option> tags
2000                      $selected = ($i == $this->page) ? ' selected' : '';//the "selected" option is applied to the current page
2001                      $option = '<option'.$selected.' value="'.$i.'">'.$i.'</option>';
2002                      $scroll = $scroll.$option;
2003                  }
2004              }
2005              else
2006              { //inits the ranges inside the list
2007                  $range_display = '<option value="'.$min.'">'
2008                      .T_('Pages').' '.$min.' '. /* TRANS: Pages x _to_ y */ T_('to').' '.$max;
2009                  $scroll = $scroll.$range_display;
2010              }
2011  
2012              if( $max+$range > $this->total_pages && $max != $this->total_pages)
2013              { //$max has to be the total number of pages
2014                  $max = $this->total_pages;
2015              }
2016              else
2017              {
2018                  $max = $max+$range;//incrementation of the maximum value by the range
2019              }
2020  
2021              $min = $min+$range;//incrementation of the minimum value by the range
2022  
2023  
2024          }
2025          /*$input ='';
2026              $input = '<input type="submit" value="submit" />';*/
2027          $scroll = $scroll.'</select>'./*$input.*/'</form>';//end of the form*/
2028  
2029          return $scroll;
2030      }
2031  
2032  
2033      /**
2034       * Get number of rows available for display
2035       *
2036       * @return integer
2037       */
2038  	function get_num_rows()
2039      {
2040          return $this->result_num_rows;
2041      }
2042  
2043  
2044      /**
2045       * Template function: display message if list is empty
2046       *
2047       * @return boolean true if empty
2048       */
2049  	function display_if_empty( $params = array() )
2050      {
2051          if( $this->result_num_rows == 0 )
2052          {
2053              // Make sure we are not missing any param:
2054              $params = array_merge( array(
2055                      'before'      => '<p class="msg_nothing">',
2056                      'after'       => '</p>',
2057                      'msg_empty'   => T_('Sorry, there is nothing to display...'),
2058                  ), $params );
2059  
2060              echo $params['before'];
2061              echo $params['msg_empty'];
2062              echo $params['after'];
2063  
2064              return true;
2065          }
2066          return false;
2067      }
2068  
2069  }
2070  
2071  
2072  // _________________ Helper callback functions __________________
2073  
2074  function conditional( $condition, $on_true, $on_false = '' )
2075  {
2076      if( $condition )
2077      {
2078          return $on_true;
2079      }
2080      else
2081      {
2082          return $on_false;
2083      }
2084  }
2085  
2086  ?>

title

Description

title

Description

title

Description

title

title

Body