Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
/lib/ -> tablelib.php (source)

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

   1  <?php
   2  
   3  // This file is part of Moodle - http://moodle.org/
   4  //
   5  // Moodle is free software: you can redistribute it and/or modify
   6  // it under the terms of the GNU General Public License as published by
   7  // the Free Software Foundation, either version 3 of the License, or
   8  // (at your option) any later version.
   9  //
  10  // Moodle is distributed in the hope that it will be useful,
  11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13  // GNU General Public License for more details.
  14  //
  15  // You should have received a copy of the GNU General Public License
  16  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  17  
  18  /**
  19   * @package    core
  20   * @subpackage lib
  21   * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  /**#@+
  29   * These constants relate to the table's handling of URL parameters.
  30   */
  31  define('TABLE_VAR_SORT',   1);
  32  define('TABLE_VAR_HIDE',   2);
  33  define('TABLE_VAR_SHOW',   3);
  34  define('TABLE_VAR_IFIRST', 4);
  35  define('TABLE_VAR_ILAST',  5);
  36  define('TABLE_VAR_PAGE',   6);
  37  define('TABLE_VAR_RESET',  7);
  38  define('TABLE_VAR_DIR',    8);
  39  /**#@-*/
  40  
  41  /**#@+
  42   * Constants that indicate whether the paging bar for the table
  43   * appears above or below the table.
  44   */
  45  define('TABLE_P_TOP',    1);
  46  define('TABLE_P_BOTTOM', 2);
  47  /**#@-*/
  48  
  49  /**
  50   * Constant that defines the 'Show all' page size.
  51   */
  52  define('TABLE_SHOW_ALL_PAGE_SIZE', 5000);
  53  
  54  use core_table\local\filter\filterset;
  55  
  56  /**
  57   * @package   moodlecore
  58   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
  59   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  60   */
  61  class flexible_table {
  62  
  63      var $uniqueid        = NULL;
  64      var $attributes      = array();
  65      var $headers         = array();
  66  
  67      /**
  68       * @var string A column which should be considered as a header column.
  69       */
  70      protected $headercolumn = null;
  71  
  72      /**
  73       * @var string For create header with help icon.
  74       */
  75      private $helpforheaders = array();
  76      var $columns         = array();
  77      var $column_style    = array();
  78      var $column_class    = array();
  79      var $column_suppress = array();
  80      var $column_nosort   = array('userpic');
  81      private $column_textsort = array();
  82      /** @var boolean Stores if setup has already been called on this flixible table. */
  83      var $setup           = false;
  84      var $baseurl         = NULL;
  85      var $request         = array();
  86  
  87      /** @var string[] Columns that are expected to contain a users fullname.  */
  88      protected $userfullnamecolumns = ['fullname'];
  89  
  90      /** @var array[] Attributes for each column  */
  91      private $columnsattributes = [];
  92  
  93      /**
  94       * @var bool Whether or not to store table properties in the user_preferences table.
  95       */
  96      private $persistent = false;
  97      var $is_collapsible = false;
  98      var $is_sortable    = false;
  99  
 100      /**
 101       * @var array The fields to sort.
 102       */
 103      protected $sortdata;
 104  
 105      /** @var string The manually set first name initial preference */
 106      protected $ifirst;
 107  
 108      /** @var string The manually set last name initial preference */
 109      protected $ilast;
 110  
 111      var $use_pages      = false;
 112      var $use_initials   = false;
 113  
 114      var $maxsortkeys = 2;
 115      var $pagesize    = 30;
 116      var $currpage    = 0;
 117      var $totalrows   = 0;
 118      var $currentrow  = 0;
 119      var $sort_default_column = NULL;
 120      var $sort_default_order  = SORT_ASC;
 121  
 122      /** @var integer The defeult per page size for the table. */
 123      private $defaultperpage = 30;
 124  
 125      /**
 126       * Array of positions in which to display download controls.
 127       */
 128      var $showdownloadbuttonsat= array(TABLE_P_TOP);
 129  
 130      /**
 131       * @var string Key of field returned by db query that is the id field of the
 132       * user table or equivalent.
 133       */
 134      public $useridfield = 'id';
 135  
 136      /**
 137       * @var string which download plugin to use. Default '' means none - print
 138       * html table with paging. Property set by is_downloading which typically
 139       * passes in cleaned data from $
 140       */
 141      var $download  = '';
 142  
 143      /**
 144       * @var bool whether data is downloadable from table. Determines whether
 145       * to display download buttons. Set by method downloadable().
 146       */
 147      var $downloadable = false;
 148  
 149      /**
 150       * @var bool Has start output been called yet?
 151       */
 152      var $started_output = false;
 153  
 154      var $exportclass = null;
 155  
 156      /**
 157       * @var array For storing user-customised table properties in the user_preferences db table.
 158       */
 159      private $prefs = array();
 160  
 161      /** @var $sheettitle */
 162      protected $sheettitle;
 163  
 164      /** @var $filename */
 165      protected $filename;
 166  
 167      /** @var array $hiddencolumns List of hidden columns. */
 168      protected $hiddencolumns;
 169  
 170      /** @var $resetting bool Whether the table preferences is resetting. */
 171      protected $resetting;
 172  
 173      /**
 174       * @var string $caption The caption of table
 175       */
 176      public $caption;
 177  
 178      /**
 179       * @var array $captionattributes The caption attributes of table
 180       */
 181      public $captionattributes;
 182  
 183      /**
 184       * @var filterset The currently applied filerset
 185       * This is required for dynamic tables, but can be used by other tables too if desired.
 186       */
 187      protected $filterset = null;
 188  
 189      /**
 190       * Constructor
 191       * @param string $uniqueid all tables have to have a unique id, this is used
 192       *      as a key when storing table properties like sort order in the session.
 193       */
 194      function __construct($uniqueid) {
 195          $this->uniqueid = $uniqueid;
 196          $this->request  = array(
 197              TABLE_VAR_SORT   => 'tsort',
 198              TABLE_VAR_HIDE   => 'thide',
 199              TABLE_VAR_SHOW   => 'tshow',
 200              TABLE_VAR_IFIRST => 'tifirst',
 201              TABLE_VAR_ILAST  => 'tilast',
 202              TABLE_VAR_PAGE   => 'page',
 203              TABLE_VAR_RESET  => 'treset',
 204              TABLE_VAR_DIR    => 'tdir',
 205          );
 206      }
 207  
 208      /**
 209       * Call this to pass the download type. Use :
 210       *         $download = optional_param('download', '', PARAM_ALPHA);
 211       * To get the download type. We assume that if you call this function with
 212       * params that this table's data is downloadable, so we call is_downloadable
 213       * for you (even if the param is '', which means no download this time.
 214       * Also you can call this method with no params to get the current set
 215       * download type.
 216       * @param string $download dataformat type. One of csv, xhtml, ods, etc
 217       * @param string $filename filename for downloads without file extension.
 218       * @param string $sheettitle title for downloaded data.
 219       * @return string download dataformat type. One of csv, xhtml, ods, etc
 220       */
 221      function is_downloading($download = null, $filename='', $sheettitle='') {
 222          if ($download!==null) {
 223              $this->sheettitle = $sheettitle;
 224              $this->is_downloadable(true);
 225              $this->download = $download;
 226              $this->filename = clean_filename($filename);
 227              $this->export_class_instance();
 228          }
 229          return $this->download;
 230      }
 231  
 232      /**
 233       * Get, and optionally set, the export class.
 234       * @param $exportclass (optional) if passed, set the table to use this export class.
 235       * @return table_default_export_format_parent the export class in use (after any set).
 236       */
 237      function export_class_instance($exportclass = null) {
 238          if (!is_null($exportclass)) {
 239              $this->started_output = true;
 240              $this->exportclass = $exportclass;
 241              $this->exportclass->table = $this;
 242          } else if (is_null($this->exportclass) && !empty($this->download)) {
 243              $this->exportclass = new table_dataformat_export_format($this, $this->download);
 244              if (!$this->exportclass->document_started()) {
 245                  $this->exportclass->start_document($this->filename, $this->sheettitle);
 246              }
 247          }
 248          return $this->exportclass;
 249      }
 250  
 251      /**
 252       * Probably don't need to call this directly. Calling is_downloading with a
 253       * param automatically sets table as downloadable.
 254       *
 255       * @param bool $downloadable optional param to set whether data from
 256       * table is downloadable. If ommitted this function can be used to get
 257       * current state of table.
 258       * @return bool whether table data is set to be downloadable.
 259       */
 260      function is_downloadable($downloadable = null) {
 261          if ($downloadable !== null) {
 262              $this->downloadable = $downloadable;
 263          }
 264          return $this->downloadable;
 265      }
 266  
 267      /**
 268       * Call with boolean true to store table layout changes in the user_preferences table.
 269       * Note: user_preferences.value has a maximum length of 1333 characters.
 270       * Call with no parameter to get current state of table persistence.
 271       *
 272       * @param bool $persistent Optional parameter to set table layout persistence.
 273       * @return bool Whether or not the table layout preferences will persist.
 274       */
 275      public function is_persistent($persistent = null) {
 276          if ($persistent == true) {
 277              $this->persistent = true;
 278          }
 279          return $this->persistent;
 280      }
 281  
 282      /**
 283       * Where to show download buttons.
 284       * @param array $showat array of postions in which to show download buttons.
 285       * Containing TABLE_P_TOP and/or TABLE_P_BOTTOM
 286       */
 287      function show_download_buttons_at($showat) {
 288          $this->showdownloadbuttonsat = $showat;
 289      }
 290  
 291      /**
 292       * Sets the is_sortable variable to the given boolean, sort_default_column to
 293       * the given string, and the sort_default_order to the given integer.
 294       * @param bool $bool
 295       * @param string $defaultcolumn
 296       * @param int $defaultorder
 297       * @return void
 298       */
 299      function sortable($bool, $defaultcolumn = NULL, $defaultorder = SORT_ASC) {
 300          $this->is_sortable = $bool;
 301          $this->sort_default_column = $defaultcolumn;
 302          $this->sort_default_order  = $defaultorder;
 303      }
 304  
 305      /**
 306       * Use text sorting functions for this column (required for text columns with Oracle).
 307       * Be warned that you cannot use this with column aliases. You can only do this
 308       * with real columns. See MDL-40481 for an example.
 309       * @param string column name
 310       */
 311      function text_sorting($column) {
 312          $this->column_textsort[] = $column;
 313      }
 314  
 315      /**
 316       * Do not sort using this column
 317       * @param string column name
 318       */
 319      function no_sorting($column) {
 320          $this->column_nosort[] = $column;
 321      }
 322  
 323      /**
 324       * Is the column sortable?
 325       * @param string column name, null means table
 326       * @return bool
 327       */
 328      function is_sortable($column = null) {
 329          if (empty($column)) {
 330              return $this->is_sortable;
 331          }
 332          if (!$this->is_sortable) {
 333              return false;
 334          }
 335          return !in_array($column, $this->column_nosort);
 336      }
 337  
 338      /**
 339       * Sets the is_collapsible variable to the given boolean.
 340       * @param bool $bool
 341       * @return void
 342       */
 343      function collapsible($bool) {
 344          $this->is_collapsible = $bool;
 345      }
 346  
 347      /**
 348       * Sets the use_pages variable to the given boolean.
 349       * @param bool $bool
 350       * @return void
 351       */
 352      function pageable($bool) {
 353          $this->use_pages = $bool;
 354      }
 355  
 356      /**
 357       * Sets the use_initials variable to the given boolean.
 358       * @param bool $bool
 359       * @return void
 360       */
 361      function initialbars($bool) {
 362          $this->use_initials = $bool;
 363      }
 364  
 365      /**
 366       * Sets the pagesize variable to the given integer, the totalrows variable
 367       * to the given integer, and the use_pages variable to true.
 368       * @param int $perpage
 369       * @param int $total
 370       * @return void
 371       */
 372      function pagesize($perpage, $total) {
 373          $this->pagesize  = $perpage;
 374          $this->totalrows = $total;
 375          $this->use_pages = true;
 376      }
 377  
 378      /**
 379       * Assigns each given variable in the array to the corresponding index
 380       * in the request class variable.
 381       * @param array $variables
 382       * @return void
 383       */
 384      function set_control_variables($variables) {
 385          foreach ($variables as $what => $variable) {
 386              if (isset($this->request[$what])) {
 387                  $this->request[$what] = $variable;
 388              }
 389          }
 390      }
 391  
 392      /**
 393       * Gives the given $value to the $attribute index of $this->attributes.
 394       * @param string $attribute
 395       * @param mixed $value
 396       * @return void
 397       */
 398      function set_attribute($attribute, $value) {
 399          $this->attributes[$attribute] = $value;
 400      }
 401  
 402      /**
 403       * What this method does is set the column so that if the same data appears in
 404       * consecutive rows, then it is not repeated.
 405       *
 406       * For example, in the quiz overview report, the fullname column is set to be suppressed, so
 407       * that when one student has made multiple attempts, their name is only printed in the row
 408       * for their first attempt.
 409       * @param int $column the index of a column.
 410       */
 411      function column_suppress($column) {
 412          if (isset($this->column_suppress[$column])) {
 413              $this->column_suppress[$column] = true;
 414          }
 415      }
 416  
 417      /**
 418       * Sets the given $column index to the given $classname in $this->column_class.
 419       * @param int $column
 420       * @param string $classname
 421       * @return void
 422       */
 423      function column_class($column, $classname) {
 424          if (isset($this->column_class[$column])) {
 425              $this->column_class[$column] = ' '.$classname; // This space needed so that classnames don't run together in the HTML
 426          }
 427      }
 428  
 429      /**
 430       * Sets the given $column index and $property index to the given $value in $this->column_style.
 431       * @param int $column
 432       * @param string $property
 433       * @param mixed $value
 434       * @return void
 435       */
 436      function column_style($column, $property, $value) {
 437          if (isset($this->column_style[$column])) {
 438              $this->column_style[$column][$property] = $value;
 439          }
 440      }
 441  
 442      /**
 443       * Sets the given $attributes to $this->columnsattributes.
 444       * Column attributes will be added to every cell in the column.
 445       *
 446       * @param array[] $attributes e.g. ['c0_firstname' => ['data-foo' => 'bar']]
 447       */
 448      public function set_columnsattributes(array $attributes): void {
 449          $this->columnsattributes = $attributes;
 450      }
 451  
 452      /**
 453       * Sets all columns' $propertys to the given $value in $this->column_style.
 454       * @param int $property
 455       * @param string $value
 456       * @return void
 457       */
 458      function column_style_all($property, $value) {
 459          foreach (array_keys($this->columns) as $column) {
 460              $this->column_style[$column][$property] = $value;
 461          }
 462      }
 463  
 464      /**
 465       * Sets $this->baseurl.
 466       * @param moodle_url|string $url the url with params needed to call up this page
 467       */
 468      function define_baseurl($url) {
 469          $this->baseurl = new moodle_url($url);
 470      }
 471  
 472      /**
 473       * @param array $columns an array of identifying names for columns. If
 474       * columns are sorted then column names must correspond to a field in sql.
 475       */
 476      function define_columns($columns) {
 477          $this->columns = array();
 478          $this->column_style = array();
 479          $this->column_class = array();
 480          $this->columnsattributes = [];
 481          $colnum = 0;
 482  
 483          foreach ($columns as $column) {
 484              $this->columns[$column]         = $colnum++;
 485              $this->column_style[$column]    = array();
 486              $this->column_class[$column]    = '';
 487              $this->columnsattributes[$column] = [];
 488              $this->column_suppress[$column] = false;
 489          }
 490      }
 491  
 492      /**
 493       * @param array $headers numerical keyed array of displayed string titles
 494       * for each column.
 495       */
 496      function define_headers($headers) {
 497          $this->headers = $headers;
 498      }
 499  
 500      /**
 501       * Mark a specific column as being a table header using the column name defined in define_columns.
 502       *
 503       * Note: Only one column can be a header, and it will be rendered using a th tag.
 504       *
 505       * @param   string  $column
 506       */
 507      public function define_header_column(string $column) {
 508          $this->headercolumn = $column;
 509      }
 510  
 511      /**
 512       * Defines a help icon for the header
 513       *
 514       * Always use this function if you need to create header with sorting and help icon.
 515       *
 516       * @param renderable[] $helpicons An array of renderable objects to be used as help icons
 517       */
 518      public function define_help_for_headers($helpicons) {
 519          $this->helpforheaders = $helpicons;
 520      }
 521  
 522      /**
 523       * Mark the table preferences to be reset.
 524       */
 525      public function mark_table_to_reset(): void {
 526          $this->resetting = true;
 527      }
 528  
 529      /**
 530       * Is the table marked for reset preferences?
 531       *
 532       * @return bool True if the table is marked to reset, false otherwise.
 533       */
 534      protected function is_resetting_preferences(): bool {
 535          if ($this->resetting === null) {
 536              $this->resetting = optional_param($this->request[TABLE_VAR_RESET], false, PARAM_BOOL);
 537          }
 538  
 539          return $this->resetting;
 540  }
 541  
 542      /**
 543       * Must be called after table is defined. Use methods above first. Cannot
 544       * use functions below till after calling this method.
 545       * @return type?
 546       */
 547      function setup() {
 548  
 549          if (empty($this->columns) || empty($this->uniqueid)) {
 550              return false;
 551          }
 552  
 553          $this->initialise_table_preferences();
 554  
 555          if (empty($this->baseurl)) {
 556              debugging('You should set baseurl when using flexible_table.');
 557              global $PAGE;
 558              $this->baseurl = $PAGE->url;
 559          }
 560  
 561          if ($this->currpage == null) {
 562              $this->currpage = optional_param($this->request[TABLE_VAR_PAGE], 0, PARAM_INT);
 563          }
 564  
 565          $this->setup = true;
 566  
 567          // Always introduce the "flexible" class for the table if not specified
 568          if (empty($this->attributes)) {
 569              $this->attributes['class'] = 'flexible table table-striped table-hover';
 570          } else if (!isset($this->attributes['class'])) {
 571              $this->attributes['class'] = 'flexible table table-striped table-hover';
 572          } else if (!in_array('flexible', explode(' ', $this->attributes['class']))) {
 573              $this->attributes['class'] = trim('flexible table table-striped table-hover ' . $this->attributes['class']);
 574          }
 575      }
 576  
 577      /**
 578       * Get the order by clause from the session or user preferences, for the table with id $uniqueid.
 579       * @param string $uniqueid the identifier for a table.
 580       * @return SQL fragment that can be used in an ORDER BY clause.
 581       */
 582      public static function get_sort_for_table($uniqueid) {
 583          global $SESSION;
 584          if (isset($SESSION->flextable[$uniqueid])) {
 585              $prefs = $SESSION->flextable[$uniqueid];
 586          } else if (!$prefs = json_decode(get_user_preferences("flextable_{$uniqueid}", ''), true)) {
 587              return '';
 588          }
 589  
 590          if (empty($prefs['sortby'])) {
 591              return '';
 592          }
 593          if (empty($prefs['textsort'])) {
 594              $prefs['textsort'] = array();
 595          }
 596  
 597          return self::construct_order_by($prefs['sortby'], $prefs['textsort']);
 598      }
 599  
 600      /**
 601       * Prepare an an order by clause from the list of columns to be sorted.
 602       * @param array $cols column name => SORT_ASC or SORT_DESC
 603       * @return SQL fragment that can be used in an ORDER BY clause.
 604       */
 605      public static function construct_order_by($cols, $textsortcols=array()) {
 606          global $DB;
 607          $bits = array();
 608  
 609          foreach ($cols as $column => $order) {
 610              if (in_array($column, $textsortcols)) {
 611                  $column = $DB->sql_order_by_text($column);
 612              }
 613              if ($order == SORT_ASC) {
 614                  $bits[] = $DB->sql_order_by_null($column);
 615              } else {
 616                  $bits[] = $DB->sql_order_by_null($column, SORT_DESC);
 617              }
 618          }
 619  
 620          return implode(', ', $bits);
 621      }
 622  
 623      /**
 624       * @return SQL fragment that can be used in an ORDER BY clause.
 625       */
 626      public function get_sql_sort() {
 627          return self::construct_order_by($this->get_sort_columns(), $this->column_textsort);
 628      }
 629  
 630      /**
 631       * Whether the current table contains any fullname columns
 632       *
 633       * @return bool
 634       */
 635      private function contains_fullname_columns(): bool {
 636          $fullnamecolumns = array_intersect_key($this->columns, array_flip($this->userfullnamecolumns));
 637  
 638          return !empty($fullnamecolumns);
 639      }
 640  
 641      /**
 642       * Get the columns to sort by, in the form required by {@link construct_order_by()}.
 643       * @return array column name => SORT_... constant.
 644       */
 645      public function get_sort_columns() {
 646          if (!$this->setup) {
 647              throw new coding_exception('Cannot call get_sort_columns until you have called setup.');
 648          }
 649  
 650          if (empty($this->prefs['sortby'])) {
 651              return array();
 652          }
 653          foreach ($this->prefs['sortby'] as $column => $notused) {
 654              if (isset($this->columns[$column])) {
 655                  continue; // This column is OK.
 656              }
 657              if (in_array($column, \core_user\fields::get_name_fields()) && $this->contains_fullname_columns()) {
 658                  continue; // This column is OK.
 659              }
 660              // This column is not OK.
 661              unset($this->prefs['sortby'][$column]);
 662          }
 663  
 664          return $this->prefs['sortby'];
 665      }
 666  
 667      /**
 668       * @return int the offset for LIMIT clause of SQL
 669       */
 670      function get_page_start() {
 671          if (!$this->use_pages) {
 672              return '';
 673          }
 674          return $this->currpage * $this->pagesize;
 675      }
 676  
 677      /**
 678       * @return int the pagesize for LIMIT clause of SQL
 679       */
 680      function get_page_size() {
 681          if (!$this->use_pages) {
 682              return '';
 683          }
 684          return $this->pagesize;
 685      }
 686  
 687      /**
 688       * @return string sql to add to where statement.
 689       */
 690      function get_sql_where() {
 691          global $DB;
 692  
 693          $conditions = array();
 694          $params = array();
 695  
 696          if ($this->contains_fullname_columns()) {
 697              static $i = 0;
 698              $i++;
 699  
 700              if (!empty($this->prefs['i_first'])) {
 701                  $conditions[] = $DB->sql_like('firstname', ':ifirstc'.$i, false, false);
 702                  $params['ifirstc'.$i] = $this->prefs['i_first'].'%';
 703              }
 704              if (!empty($this->prefs['i_last'])) {
 705                  $conditions[] = $DB->sql_like('lastname', ':ilastc'.$i, false, false);
 706                  $params['ilastc'.$i] = $this->prefs['i_last'].'%';
 707              }
 708          }
 709  
 710          return array(implode(" AND ", $conditions), $params);
 711      }
 712  
 713      /**
 714       * Add a row of data to the table. This function takes an array or object with
 715       * column names as keys or property names.
 716       *
 717       * It ignores any elements with keys that are not defined as columns. It
 718       * puts in empty strings into the row when there is no element in the passed
 719       * array corresponding to a column in the table. It puts the row elements in
 720       * the proper order (internally row table data is stored by in arrays with
 721       * a numerical index corresponding to the column number).
 722       *
 723       * @param object|array $rowwithkeys array keys or object property names are column names,
 724       *                                      as defined in call to define_columns.
 725       * @param string $classname CSS class name to add to this row's tr tag.
 726       */
 727      function add_data_keyed($rowwithkeys, $classname = '') {
 728          $this->add_data($this->get_row_from_keyed($rowwithkeys), $classname);
 729      }
 730  
 731      /**
 732       * Add a number of rows to the table at once. And optionally finish output after they have been added.
 733       *
 734       * @param (object|array|null)[] $rowstoadd Array of rows to add to table, a null value in array adds a separator row. Or a
 735       *                                  object or array is added to table. We expect properties for the row array as would be
 736       *                                  passed to add_data_keyed.
 737       * @param bool     $finish
 738       */
 739      public function format_and_add_array_of_rows($rowstoadd, $finish = true) {
 740          foreach ($rowstoadd as $row) {
 741              if (is_null($row)) {
 742                  $this->add_separator();
 743              } else {
 744                  $this->add_data_keyed($this->format_row($row));
 745              }
 746          }
 747          if ($finish) {
 748              $this->finish_output(!$this->is_downloading());
 749          }
 750      }
 751  
 752      /**
 753       * Add a seperator line to table.
 754       */
 755      function add_separator() {
 756          if (!$this->setup) {
 757              return false;
 758          }
 759          $this->add_data(NULL);
 760      }
 761  
 762      /**
 763       * This method actually directly echoes the row passed to it now or adds it
 764       * to the download. If this is the first row and start_output has not
 765       * already been called this method also calls start_output to open the table
 766       * or send headers for the downloaded.
 767       * Can be used as before. print_html now calls finish_html to close table.
 768       *
 769       * @param array $row a numerically keyed row of data to add to the table.
 770       * @param string $classname CSS class name to add to this row's tr tag.
 771       * @return bool success.
 772       */
 773      function add_data($row, $classname = '') {
 774          if (!$this->setup) {
 775              return false;
 776          }
 777          if (!$this->started_output) {
 778              $this->start_output();
 779          }
 780          if ($this->exportclass!==null) {
 781              if ($row === null) {
 782                  $this->exportclass->add_seperator();
 783              } else {
 784                  $this->exportclass->add_data($row);
 785              }
 786          } else {
 787              $this->print_row($row, $classname);
 788          }
 789          return true;
 790      }
 791  
 792      /**
 793       * You should call this to finish outputting the table data after adding
 794       * data to the table with add_data or add_data_keyed.
 795       *
 796       */
 797      function finish_output($closeexportclassdoc = true) {
 798          if ($this->exportclass!==null) {
 799              $this->exportclass->finish_table();
 800              if ($closeexportclassdoc) {
 801                  $this->exportclass->finish_document();
 802              }
 803          } else {
 804              $this->finish_html();
 805          }
 806      }
 807  
 808      /**
 809       * Hook that can be overridden in child classes to wrap a table in a form
 810       * for example. Called only when there is data to display and not
 811       * downloading.
 812       */
 813      function wrap_html_start() {
 814      }
 815  
 816      /**
 817       * Hook that can be overridden in child classes to wrap a table in a form
 818       * for example. Called only when there is data to display and not
 819       * downloading.
 820       */
 821      function wrap_html_finish() {
 822      }
 823  
 824      /**
 825       * Call appropriate methods on this table class to perform any processing on values before displaying in table.
 826       * Takes raw data from the database and process it into human readable format, perhaps also adding html linking when
 827       * displaying table as html, adding a div wrap, etc.
 828       *
 829       * See for example col_fullname below which will be called for a column whose name is 'fullname'.
 830       *
 831       * @param array|object $row row of data from db used to make one row of the table.
 832       * @return array one row for the table, added using add_data_keyed method.
 833       */
 834      function format_row($row) {
 835          if (is_array($row)) {
 836              $row = (object)$row;
 837          }
 838          $formattedrow = array();
 839          foreach (array_keys($this->columns) as $column) {
 840              $colmethodname = 'col_'.$column;
 841              if (method_exists($this, $colmethodname)) {
 842                  $formattedcolumn = $this->$colmethodname($row);
 843              } else {
 844                  $formattedcolumn = $this->other_cols($column, $row);
 845                  if ($formattedcolumn===NULL) {
 846                      $formattedcolumn = $row->$column;
 847                  }
 848              }
 849              $formattedrow[$column] = $formattedcolumn;
 850          }
 851          return $formattedrow;
 852      }
 853  
 854      /**
 855       * Fullname is treated as a special columname in tablelib and should always
 856       * be treated the same as the fullname of a user.
 857       * @uses $this->useridfield if the userid field is not expected to be id
 858       * then you need to override $this->useridfield to point at the correct
 859       * field for the user id.
 860       *
 861       * @param object $row the data from the db containing all fields from the
 862       *                    users table necessary to construct the full name of the user in
 863       *                    current language.
 864       * @return string contents of cell in column 'fullname', for this row.
 865       */
 866      function col_fullname($row) {
 867          global $COURSE;
 868  
 869          $name = fullname($row, has_capability('moodle/site:viewfullnames', $this->get_context()));
 870          if ($this->download) {
 871              return $name;
 872          }
 873  
 874          $userid = $row->{$this->useridfield};
 875          if ($COURSE->id == SITEID) {
 876              $profileurl = new moodle_url('/user/profile.php', array('id' => $userid));
 877          } else {
 878              $profileurl = new moodle_url('/user/view.php',
 879                      array('id' => $userid, 'course' => $COURSE->id));
 880          }
 881          return html_writer::link($profileurl, $name);
 882      }
 883  
 884      /**
 885       * You can override this method in a child class. See the description of
 886       * build_table which calls this method.
 887       */
 888      function other_cols($column, $row) {
 889          if (isset($row->$column) && ($column === 'email' || $column === 'idnumber') &&
 890                  (!$this->is_downloading() || $this->export_class_instance()->supports_html())) {
 891              // Columns email and idnumber may potentially contain malicious characters, escape them by default.
 892              // This function will not be executed if the child class implements col_email() or col_idnumber().
 893              return s($row->$column);
 894          }
 895          return NULL;
 896      }
 897  
 898      /**
 899       * Used from col_* functions when text is to be displayed. Does the
 900       * right thing - either converts text to html or strips any html tags
 901       * depending on if we are downloading and what is the download type. Params
 902       * are the same as format_text function in weblib.php but some default
 903       * options are changed.
 904       */
 905      function format_text($text, $format=FORMAT_MOODLE, $options=NULL, $courseid=NULL) {
 906          if (!$this->is_downloading()) {
 907              if (is_null($options)) {
 908                  $options = new stdClass;
 909              }
 910              //some sensible defaults
 911              if (!isset($options->para)) {
 912                  $options->para = false;
 913              }
 914              if (!isset($options->newlines)) {
 915                  $options->newlines = false;
 916              }
 917              if (!isset($options->smiley)) {
 918                  $options->smiley = false;
 919              }
 920              if (!isset($options->filter)) {
 921                  $options->filter = false;
 922              }
 923              return format_text($text, $format, $options);
 924          } else {
 925              $eci = $this->export_class_instance();
 926              return $eci->format_text($text, $format, $options, $courseid);
 927          }
 928      }
 929      /**
 930       * This method is deprecated although the old api is still supported.
 931       * @deprecated 1.9.2 - Jun 2, 2008
 932       */
 933      function print_html() {
 934          if (!$this->setup) {
 935              return false;
 936          }
 937          $this->finish_html();
 938      }
 939  
 940      /**
 941       * This function is not part of the public api.
 942       * @return string initial of first name we are currently filtering by
 943       */
 944      function get_initial_first() {
 945          if (!$this->use_initials) {
 946              return NULL;
 947          }
 948  
 949          return $this->prefs['i_first'];
 950      }
 951  
 952      /**
 953       * This function is not part of the public api.
 954       * @return string initial of last name we are currently filtering by
 955       */
 956      function get_initial_last() {
 957          if (!$this->use_initials) {
 958              return NULL;
 959          }
 960  
 961          return $this->prefs['i_last'];
 962      }
 963  
 964      /**
 965       * Helper function, used by {@link print_initials_bar()} to output one initial bar.
 966       * @param array $alpha of letters in the alphabet.
 967       * @param string $current the currently selected letter.
 968       * @param string $class class name to add to this initial bar.
 969       * @param string $title the name to put in front of this initial bar.
 970       * @param string $urlvar URL parameter name for this initial.
 971       *
 972       * @deprecated since Moodle 3.3
 973       */
 974      protected function print_one_initials_bar($alpha, $current, $class, $title, $urlvar) {
 975  
 976          debugging('Method print_one_initials_bar() is no longer used and has been deprecated, ' .
 977              'to print initials bar call print_initials_bar()', DEBUG_DEVELOPER);
 978  
 979          echo html_writer::start_tag('div', array('class' => 'initialbar ' . $class)) .
 980              $title . ' : ';
 981          if ($current) {
 982              echo html_writer::link($this->baseurl->out(false, array($urlvar => '')), get_string('all'));
 983          } else {
 984              echo html_writer::tag('strong', get_string('all'));
 985          }
 986  
 987          foreach ($alpha as $letter) {
 988              if ($letter === $current) {
 989                  echo html_writer::tag('strong', $letter);
 990              } else {
 991                  echo html_writer::link($this->baseurl->out(false, array($urlvar => $letter)), $letter);
 992              }
 993          }
 994  
 995          echo html_writer::end_tag('div');
 996      }
 997  
 998      /**
 999       * This function is not part of the public api.
1000       */
1001      function print_initials_bar() {
1002          global $OUTPUT;
1003  
1004          $ifirst = $this->get_initial_first();
1005          $ilast = $this->get_initial_last();
1006          if (is_null($ifirst)) {
1007              $ifirst = '';
1008          }
1009          if (is_null($ilast)) {
1010              $ilast = '';
1011          }
1012  
1013          if ((!empty($ifirst) || !empty($ilast) || $this->use_initials) && $this->contains_fullname_columns()) {
1014              $prefixfirst = $this->request[TABLE_VAR_IFIRST];
1015              $prefixlast = $this->request[TABLE_VAR_ILAST];
1016              echo $OUTPUT->initials_bar($ifirst, 'firstinitial', get_string('firstname'), $prefixfirst, $this->baseurl);
1017              echo $OUTPUT->initials_bar($ilast, 'lastinitial', get_string('lastname'), $prefixlast, $this->baseurl);
1018          }
1019  
1020      }
1021  
1022      /**
1023       * This function is not part of the public api.
1024       */
1025      function print_nothing_to_display() {
1026          global $OUTPUT;
1027  
1028          // Render the dynamic table header.
1029          echo $this->get_dynamic_table_html_start();
1030  
1031          // Render button to allow user to reset table preferences.
1032          echo $this->render_reset_button();
1033  
1034          $this->print_initials_bar();
1035  
1036          echo $OUTPUT->heading(get_string('nothingtodisplay'));
1037  
1038          // Render the dynamic table footer.
1039          echo $this->get_dynamic_table_html_end();
1040      }
1041  
1042      /**
1043       * This function is not part of the public api.
1044       */
1045      function get_row_from_keyed($rowwithkeys) {
1046          if (is_object($rowwithkeys)) {
1047              $rowwithkeys = (array)$rowwithkeys;
1048          }
1049          $row = array();
1050          foreach (array_keys($this->columns) as $column) {
1051              if (isset($rowwithkeys[$column])) {
1052                  $row [] = $rowwithkeys[$column];
1053              } else {
1054                  $row[] ='';
1055              }
1056          }
1057          return $row;
1058      }
1059  
1060      /**
1061       * Get the html for the download buttons
1062       *
1063       * Usually only use internally
1064       */
1065      public function download_buttons() {
1066          global $OUTPUT;
1067  
1068          if ($this->is_downloadable() && !$this->is_downloading()) {
1069              return $OUTPUT->download_dataformat_selector(get_string('downloadas', 'table'),
1070                      $this->baseurl->out_omit_querystring(), 'download', $this->baseurl->params());
1071          } else {
1072              return '';
1073          }
1074      }
1075  
1076      /**
1077       * This function is not part of the public api.
1078       * You don't normally need to call this. It is called automatically when
1079       * needed when you start adding data to the table.
1080       *
1081       */
1082      function start_output() {
1083          $this->started_output = true;
1084          if ($this->exportclass!==null) {
1085              $this->exportclass->start_table($this->sheettitle);
1086              $this->exportclass->output_headers($this->headers);
1087          } else {
1088              $this->start_html();
1089              $this->print_headers();
1090              echo html_writer::start_tag('tbody');
1091          }
1092      }
1093  
1094      /**
1095       * This function is not part of the public api.
1096       */
1097      function print_row($row, $classname = '') {
1098          echo $this->get_row_html($row, $classname);
1099      }
1100  
1101      /**
1102       * Generate html code for the passed row.
1103       *
1104       * @param array $row Row data.
1105       * @param string $classname classes to add.
1106       *
1107       * @return string $html html code for the row passed.
1108       */
1109      public function get_row_html($row, $classname = '') {
1110          static $suppress_lastrow = NULL;
1111          $rowclasses = array();
1112  
1113          if ($classname) {
1114              $rowclasses[] = $classname;
1115          }
1116  
1117          $rowid = $this->uniqueid . '_r' . $this->currentrow;
1118          $html = '';
1119  
1120          $html .= html_writer::start_tag('tr', array('class' => implode(' ', $rowclasses), 'id' => $rowid));
1121  
1122          // If we have a separator, print it
1123          if ($row === NULL) {
1124              $colcount = count($this->columns);
1125              $html .= html_writer::tag('td', html_writer::tag('div', '',
1126                      array('class' => 'tabledivider')), array('colspan' => $colcount));
1127  
1128          } else {
1129              $html .= $this->get_row_cells_html($rowid, $row, $suppress_lastrow);
1130          }
1131  
1132          $html .= html_writer::end_tag('tr');
1133  
1134          $suppress_enabled = array_sum($this->column_suppress);
1135          if ($suppress_enabled) {
1136              $suppress_lastrow = $row;
1137          }
1138          $this->currentrow++;
1139          return $html;
1140      }
1141  
1142      /**
1143       * Generate html code for the row cells.
1144       *
1145       * @param string $rowid
1146       * @param array $row
1147       * @param array|null $suppresslastrow
1148       * @return string
1149       */
1150      public function get_row_cells_html(string $rowid, array $row, ?array $suppresslastrow): string {
1151          $html = '';
1152          $colbyindex = array_flip($this->columns);
1153          foreach ($row as $index => $data) {
1154              $column = $colbyindex[$index];
1155  
1156              $columnattributes = $this->columnsattributes[$column] ?? [];
1157              if (isset($columnattributes['class'])) {
1158                  $this->column_class($column, $columnattributes['class']);
1159                  unset($columnattributes['class']);
1160              }
1161  
1162              $attributes = [
1163                  'class' => "cell c{$index}" . $this->column_class[$column],
1164                  'id' => "{$rowid}_c{$index}",
1165                  'style' => $this->make_styles_string($this->column_style[$column]),
1166              ];
1167  
1168              $celltype = 'td';
1169              if ($this->headercolumn && $column == $this->headercolumn) {
1170                  $celltype = 'th';
1171                  $attributes['scope'] = 'row';
1172              }
1173  
1174              $attributes += $columnattributes;
1175  
1176              if (empty($this->prefs['collapse'][$column])) {
1177                  if ($this->column_suppress[$column] && $suppresslastrow !== null && $suppresslastrow[$index] === $data) {
1178                      $content = '&nbsp;';
1179                  } else {
1180                      $content = $data;
1181                  }
1182              } else {
1183                  $content = '&nbsp;';
1184              }
1185  
1186              $html .= html_writer::tag($celltype, $content, $attributes);
1187          }
1188          return $html;
1189      }
1190  
1191      /**
1192       * This function is not part of the public api.
1193       */
1194      function finish_html() {
1195          global $OUTPUT, $PAGE;
1196  
1197          if (!$this->started_output) {
1198              //no data has been added to the table.
1199              $this->print_nothing_to_display();
1200  
1201          } else {
1202              // Print empty rows to fill the table to the current pagesize.
1203              // This is done so the header aria-controls attributes do not point to
1204              // non existant elements.
1205              $emptyrow = array_fill(0, count($this->columns), '');
1206              while ($this->currentrow < $this->pagesize) {
1207                  $this->print_row($emptyrow, 'emptyrow');
1208              }
1209  
1210              echo html_writer::end_tag('tbody');
1211              echo html_writer::end_tag('table');
1212              echo html_writer::end_tag('div');
1213              $this->wrap_html_finish();
1214  
1215              // Paging bar
1216              if(in_array(TABLE_P_BOTTOM, $this->showdownloadbuttonsat)) {
1217                  echo $this->download_buttons();
1218              }
1219  
1220              if($this->use_pages) {
1221                  $pagingbar = new paging_bar($this->totalrows, $this->currpage, $this->pagesize, $this->baseurl);
1222                  $pagingbar->pagevar = $this->request[TABLE_VAR_PAGE];
1223                  echo $OUTPUT->render($pagingbar);
1224              }
1225  
1226              // Render the dynamic table footer.
1227              echo $this->get_dynamic_table_html_end();
1228          }
1229      }
1230  
1231      /**
1232       * Generate the HTML for the collapse/uncollapse icon. This is a helper method
1233       * used by {@link print_headers()}.
1234       * @param string $column the column name, index into various names.
1235       * @param int $index numerical index of the column.
1236       * @return string HTML fragment.
1237       */
1238      protected function show_hide_link($column, $index) {
1239          global $OUTPUT;
1240          // Some headers contain <br /> tags, do not include in title, hence the
1241          // strip tags.
1242  
1243          $ariacontrols = '';
1244          for ($i = 0; $i < $this->pagesize; $i++) {
1245              $ariacontrols .= $this->uniqueid . '_r' . $i . '_c' . $index . ' ';
1246          }
1247  
1248          $ariacontrols = trim($ariacontrols);
1249  
1250          if (!empty($this->prefs['collapse'][$column])) {
1251              $linkattributes = [
1252                  'title' => get_string('show') . ' ' . strip_tags($this->headers[$index]),
1253                  'aria-expanded' => 'false',
1254                  'aria-controls' => $ariacontrols,
1255                  'data-action' => 'show',
1256                  'data-column' => $column,
1257                  'role' => 'button',
1258              ];
1259              return html_writer::link($this->baseurl->out(false, array($this->request[TABLE_VAR_SHOW] => $column)),
1260                      $OUTPUT->pix_icon('t/switch_plus', null), $linkattributes);
1261  
1262          } else if ($this->headers[$index] !== NULL) {
1263              $linkattributes = [
1264                  'title' => get_string('hide') . ' ' . strip_tags($this->headers[$index]),
1265                  'aria-expanded' => 'true',
1266                  'aria-controls' => $ariacontrols,
1267                  'data-action' => 'hide',
1268                  'data-column' => $column,
1269                  'role' => 'button',
1270              ];
1271              return html_writer::link($this->baseurl->out(false, array($this->request[TABLE_VAR_HIDE] => $column)),
1272                      $OUTPUT->pix_icon('t/switch_minus', null), $linkattributes);
1273          }
1274      }
1275  
1276      /**
1277       * This function is not part of the public api.
1278       */
1279      function print_headers() {
1280          global $CFG, $OUTPUT;
1281  
1282          // Set the primary sort column/order where possible, so that sort links/icons are correct.
1283          [
1284              'sortby' => $primarysortcolumn,
1285              'sortorder' => $primarysortorder,
1286          ] = $this->get_primary_sort_order();
1287  
1288          echo html_writer::start_tag('thead');
1289          echo html_writer::start_tag('tr');
1290          foreach ($this->columns as $column => $index) {
1291  
1292              $icon_hide = '';
1293              if ($this->is_collapsible) {
1294                  $icon_hide = $this->show_hide_link($column, $index);
1295              }
1296              switch ($column) {
1297  
1298                  case 'userpic':
1299                      // do nothing, do not display sortable links
1300                      break;
1301  
1302                  default:
1303  
1304                      if (array_search($column, $this->userfullnamecolumns) !== false) {
1305                          // Check the full name display for sortable fields.
1306                          if (has_capability('moodle/site:viewfullnames', $this->get_context())) {
1307                              $nameformat = $CFG->alternativefullnameformat;
1308                          } else {
1309                              $nameformat = $CFG->fullnamedisplay;
1310                          }
1311  
1312                          if ($nameformat == 'language') {
1313                              $nameformat = get_string('fullnamedisplay');
1314                          }
1315  
1316                          $requirednames = order_in_string(\core_user\fields::get_name_fields(), $nameformat);
1317  
1318                          if (!empty($requirednames)) {
1319                              if ($this->is_sortable($column)) {
1320                                  // Done this way for the possibility of more than two sortable full name display fields.
1321                                  $this->headers[$index] = '';
1322                                  foreach ($requirednames as $name) {
1323                                      $sortname = $this->sort_link(get_string($name),
1324                                          $name, $primarysortcolumn === $name, $primarysortorder);
1325                                      $this->headers[$index] .= $sortname . ' / ';
1326                                  }
1327                                  $helpicon = '';
1328                                  if (isset($this->helpforheaders[$index])) {
1329                                      $helpicon = $OUTPUT->render($this->helpforheaders[$index]);
1330                                  }
1331                                  $this->headers[$index] = substr($this->headers[$index], 0, -3) . $helpicon;
1332                              }
1333                          }
1334                      } else if ($this->is_sortable($column)) {
1335                          $helpicon = '';
1336                          if (isset($this->helpforheaders[$index])) {
1337                              $helpicon = $OUTPUT->render($this->helpforheaders[$index]);
1338                          }
1339                          $this->headers[$index] = $this->sort_link($this->headers[$index],
1340                                  $column, $primarysortcolumn == $column, $primarysortorder) . $helpicon;
1341                      }
1342              }
1343  
1344              $attributes = array(
1345                  'class' => 'header c' . $index . $this->column_class[$column],
1346                  'scope' => 'col',
1347              );
1348              if ($this->headers[$index] === NULL) {
1349                  $content = '&nbsp;';
1350              } else if (!empty($this->prefs['collapse'][$column])) {
1351                  $content = $icon_hide;
1352              } else {
1353                  if (is_array($this->column_style[$column])) {
1354                      $attributes['style'] = $this->make_styles_string($this->column_style[$column]);
1355                  }
1356                  $helpicon = '';
1357                  if (isset($this->helpforheaders[$index]) && !$this->is_sortable($column)) {
1358                      $helpicon  = $OUTPUT->render($this->helpforheaders[$index]);
1359                  }
1360                  $content = $this->headers[$index] . $helpicon . html_writer::tag('div',
1361                          $icon_hide, array('class' => 'commands'));
1362              }
1363              echo html_writer::tag('th', $content, $attributes);
1364          }
1365  
1366          echo html_writer::end_tag('tr');
1367          echo html_writer::end_tag('thead');
1368      }
1369  
1370      /**
1371       * Calculate the preferences for sort order based on user-supplied values and get params.
1372       */
1373      protected function set_sorting_preferences(): void {
1374          $sortdata = $this->sortdata;
1375  
1376          if ($sortdata === null) {
1377              $sortdata = $this->prefs['sortby'];
1378  
1379              $sortorder = optional_param($this->request[TABLE_VAR_DIR], $this->sort_default_order, PARAM_INT);
1380              $sortby = optional_param($this->request[TABLE_VAR_SORT], '', PARAM_ALPHANUMEXT);
1381  
1382              if (array_key_exists($sortby, $sortdata)) {
1383                  // This key already exists somewhere. Change its sortorder and bring it to the top.
1384                  unset($sortdata[$sortby]);
1385              }
1386              $sortdata = array_merge([$sortby => $sortorder], $sortdata);
1387          }
1388  
1389          $usernamefields = \core_user\fields::get_name_fields();
1390          $sortdata = array_filter($sortdata, function($sortby) use ($usernamefields) {
1391              $isvalidsort = $sortby && $this->is_sortable($sortby);
1392              $isvalidsort = $isvalidsort && empty($this->prefs['collapse'][$sortby]);
1393              $isrealcolumn = isset($this->columns[$sortby]);
1394              $isfullnamefield = $this->contains_fullname_columns() && in_array($sortby, $usernamefields);
1395  
1396              return $isvalidsort && ($isrealcolumn || $isfullnamefield);
1397          }, ARRAY_FILTER_USE_KEY);
1398  
1399          // Finally, make sure that no more than $this->maxsortkeys are present into the array.
1400          $sortdata = array_slice($sortdata, 0, $this->maxsortkeys);
1401  
1402          // If a default order is defined and it is not in the current list of order by columns, add it at the end.
1403          // This prevents results from being returned in a random order if the only order by column contains equal values.
1404          if (!empty($this->sort_default_column) && !array_key_exists($this->sort_default_column, $sortdata)) {
1405              $sortdata = array_merge($sortdata, [$this->sort_default_column => $this->sort_default_order]);
1406          }
1407  
1408          // Apply the sortdata to the preference.
1409          $this->prefs['sortby'] = $sortdata;
1410      }
1411  
1412      /**
1413       * Fill in the preferences for the initials bar.
1414       */
1415      protected function set_initials_preferences(): void {
1416          $ifirst = $this->ifirst;
1417          $ilast = $this->ilast;
1418  
1419          if ($ifirst === null) {
1420              $ifirst = optional_param($this->request[TABLE_VAR_IFIRST], null, PARAM_RAW);
1421          }
1422  
1423          if ($ilast === null) {
1424              $ilast = optional_param($this->request[TABLE_VAR_ILAST], null, PARAM_RAW);
1425          }
1426  
1427          if (!is_null($ifirst) && ($ifirst === '' || strpos(get_string('alphabet', 'langconfig'), $ifirst) !== false)) {
1428              $this->prefs['i_first'] = $ifirst;
1429          }
1430  
1431          if (!is_null($ilast) && ($ilast === '' || strpos(get_string('alphabet', 'langconfig'), $ilast) !== false)) {
1432              $this->prefs['i_last'] = $ilast;
1433          }
1434  
1435      }
1436  
1437      /**
1438       * Set hide and show preferences.
1439       */
1440      protected function set_hide_show_preferences(): void {
1441  
1442          if ($this->hiddencolumns !== null) {
1443              $this->prefs['collapse'] = array_fill_keys(array_filter($this->hiddencolumns, function($column) {
1444                  return array_key_exists($column, $this->columns);
1445              }), true);
1446          } else {
1447              if ($column = optional_param($this->request[TABLE_VAR_HIDE], '', PARAM_ALPHANUMEXT)) {
1448                  if (isset($this->columns[$column])) {
1449                      $this->prefs['collapse'][$column] = true;
1450                  }
1451              }
1452          }
1453  
1454          if ($column = optional_param($this->request[TABLE_VAR_SHOW], '', PARAM_ALPHANUMEXT)) {
1455              unset($this->prefs['collapse'][$column]);
1456          }
1457  
1458          foreach (array_keys($this->prefs['collapse']) as $column) {
1459              if (array_key_exists($column, $this->prefs['sortby'])) {
1460                  unset($this->prefs['sortby'][$column]);
1461              }
1462          }
1463      }
1464  
1465      /**
1466       * Set the list of hidden columns.
1467       *
1468       * @param array $columns The list of hidden columns.
1469       */
1470      public function set_hidden_columns(array $columns): void {
1471          $this->hiddencolumns = $columns;
1472      }
1473  
1474      /**
1475       * Initialise table preferences.
1476       */
1477      protected function initialise_table_preferences(): void {
1478          global $SESSION;
1479  
1480          // Load any existing user preferences.
1481          if ($this->persistent) {
1482              $this->prefs = json_decode(get_user_preferences("flextable_{$this->uniqueid}", ''), true);
1483              $oldprefs = $this->prefs;
1484          } else if (isset($SESSION->flextable[$this->uniqueid])) {
1485              $this->prefs = $SESSION->flextable[$this->uniqueid];
1486              $oldprefs = $this->prefs;
1487          }
1488  
1489          // Set up default preferences if needed.
1490          if (!$this->prefs || $this->is_resetting_preferences()) {
1491              $this->prefs = [
1492                  'collapse' => [],
1493                  'sortby'   => [],
1494                  'i_first'  => '',
1495                  'i_last'   => '',
1496                  'textsort' => $this->column_textsort,
1497              ];
1498          }
1499  
1500          if (!isset($oldprefs)) {
1501              $oldprefs = $this->prefs;
1502          }
1503  
1504          // Save user preferences if they have changed.
1505          if ($this->is_resetting_preferences()) {
1506              $this->sortdata = null;
1507              $this->ifirst = null;
1508              $this->ilast = null;
1509          }
1510  
1511          if (($showcol = optional_param($this->request[TABLE_VAR_SHOW], '', PARAM_ALPHANUMEXT)) &&
1512              isset($this->columns[$showcol])) {
1513              $this->prefs['collapse'][$showcol] = false;
1514          } else if (($hidecol = optional_param($this->request[TABLE_VAR_HIDE], '', PARAM_ALPHANUMEXT)) &&
1515              isset($this->columns[$hidecol])) {
1516              $this->prefs['collapse'][$hidecol] = true;
1517              if (array_key_exists($hidecol, $this->prefs['sortby'])) {
1518                  unset($this->prefs['sortby'][$hidecol]);
1519              }
1520          }
1521  
1522          $this->set_hide_show_preferences();
1523          $this->set_sorting_preferences();
1524          $this->set_initials_preferences();
1525  
1526          // Now, reduce the width of collapsed columns and remove the width from columns that should be expanded.
1527          foreach (array_keys($this->columns) as $column) {
1528              if (!empty($this->prefs['collapse'][$column])) {
1529                  $this->column_style[$column]['width'] = '10px';
1530              } else {
1531                  unset($this->column_style[$column]['width']);
1532              }
1533          }
1534  
1535          if (empty($this->baseurl)) {
1536              debugging('You should set baseurl when using flexible_table.');
1537              global $PAGE;
1538              $this->baseurl = $PAGE->url;
1539          }
1540  
1541          if ($this->currpage == null) {
1542              $this->currpage = optional_param($this->request[TABLE_VAR_PAGE], 0, PARAM_INT);
1543          }
1544  
1545          $this->save_preferences($oldprefs);
1546      }
1547  
1548      /**
1549       * Save preferences.
1550       *
1551       * @param array $oldprefs Old preferences to compare against.
1552       */
1553      protected function save_preferences($oldprefs): void {
1554          global $SESSION;
1555  
1556          if ($this->prefs != $oldprefs) {
1557              if ($this->persistent) {
1558                  set_user_preference('flextable_' . $this->uniqueid, json_encode($this->prefs));
1559              } else {
1560                  $SESSION->flextable[$this->uniqueid] = $this->prefs;
1561              }
1562          }
1563          unset($oldprefs);
1564      }
1565  
1566      /**
1567       * Set the preferred table sorting attributes.
1568       *
1569       * @param string $sortby The field to sort by.
1570       * @param int $sortorder The sort order.
1571       */
1572      public function set_sortdata(array $sortdata): void {
1573          $this->sortdata = [];
1574          foreach ($sortdata as $sortitem) {
1575              if (!array_key_exists($sortitem['sortby'], $this->sortdata)) {
1576                  $this->sortdata[$sortitem['sortby']] = (int) $sortitem['sortorder'];
1577              }
1578          }
1579      }
1580  
1581      /**
1582       * Get the default per page.
1583       *
1584       * @return int
1585       */
1586      public function get_default_per_page(): int {
1587          return $this->defaultperpage;
1588      }
1589  
1590      /**
1591       * Set the default per page.
1592       *
1593       * @param int $defaultperpage
1594       */
1595      public function set_default_per_page(int $defaultperpage): void {
1596          $this->defaultperpage = $defaultperpage;
1597      }
1598  
1599      /**
1600       * Set the preferred first name initial in an initials bar.
1601       *
1602       * @param string $initial The character to set
1603       */
1604      public function set_first_initial(string $initial): void {
1605          $this->ifirst = $initial;
1606      }
1607  
1608      /**
1609       * Set the preferred last name initial in an initials bar.
1610       *
1611       * @param string $initial The character to set
1612       */
1613      public function set_last_initial(string $initial): void {
1614          $this->ilast = $initial;
1615      }
1616  
1617      /**
1618       * Set the page number.
1619       *
1620       * @param int $pagenumber The page number.
1621       */
1622      public function set_page_number(int $pagenumber): void {
1623          $this->currpage = $pagenumber - 1;
1624      }
1625  
1626      /**
1627       * Generate the HTML for the sort icon. This is a helper method used by {@link sort_link()}.
1628       * @param bool $isprimary whether an icon is needed (it is only needed for the primary sort column.)
1629       * @param int $order SORT_ASC or SORT_DESC
1630       * @return string HTML fragment.
1631       */
1632      protected function sort_icon($isprimary, $order) {
1633          global $OUTPUT;
1634  
1635          if (!$isprimary) {
1636              return '';
1637          }
1638  
1639          if ($order == SORT_ASC) {
1640              return $OUTPUT->pix_icon('t/sort_asc', get_string('asc'));
1641          } else {
1642              return $OUTPUT->pix_icon('t/sort_desc', get_string('desc'));
1643          }
1644      }
1645  
1646      /**
1647       * Generate the correct tool tip for changing the sort order. This is a
1648       * helper method used by {@link sort_link()}.
1649       * @param bool $isprimary whether the is column is the current primary sort column.
1650       * @param int $order SORT_ASC or SORT_DESC
1651       * @return string the correct title.
1652       */
1653      protected function sort_order_name($isprimary, $order) {
1654          if ($isprimary && $order != SORT_ASC) {
1655              return get_string('desc');
1656          } else {
1657              return get_string('asc');
1658          }
1659      }
1660  
1661      /**
1662       * Generate the HTML for the sort link. This is a helper method used by {@link print_headers()}.
1663       * @param string $text the text for the link.
1664       * @param string $column the column name, may be a fake column like 'firstname' or a real one.
1665       * @param bool $isprimary whether the is column is the current primary sort column.
1666       * @param int $order SORT_ASC or SORT_DESC
1667       * @return string HTML fragment.
1668       */
1669      protected function sort_link($text, $column, $isprimary, $order) {
1670          // If we are already sorting by this column, switch direction.
1671          if (array_key_exists($column, $this->prefs['sortby'])) {
1672              $sortorder = $this->prefs['sortby'][$column] == SORT_ASC ? SORT_DESC : SORT_ASC;
1673          } else {
1674              $sortorder = $order;
1675          }
1676  
1677          $params = [
1678              $this->request[TABLE_VAR_SORT] => $column,
1679              $this->request[TABLE_VAR_DIR] => $sortorder,
1680          ];
1681  
1682          return html_writer::link($this->baseurl->out(false, $params),
1683                  $text . get_accesshide(get_string('sortby') . ' ' .
1684                  $text . ' ' . $this->sort_order_name($isprimary, $order)),
1685                  [
1686                      'data-sortable' => $this->is_sortable($column),
1687                      'data-sortby' => $column,
1688                      'data-sortorder' => $sortorder,
1689                      'role' => 'button',
1690                  ]) . ' ' . $this->sort_icon($isprimary, $order);
1691      }
1692  
1693      /**
1694       * Return primary sorting column/order, either the first preferred "sortby" value or defaults defined for the table
1695       *
1696       * @return array
1697       */
1698      protected function get_primary_sort_order(): array {
1699          if (reset($this->prefs['sortby'])) {
1700              return $this->get_sort_order();
1701          }
1702  
1703          return [
1704              'sortby' => $this->sort_default_column,
1705              'sortorder' => $this->sort_default_order,
1706          ];
1707      }
1708  
1709      /**
1710       * Return sorting attributes values.
1711       *
1712       * @return array
1713       */
1714      protected function get_sort_order(): array {
1715          $sortbys = $this->prefs['sortby'];
1716          $sortby = key($sortbys);
1717  
1718          return [
1719              'sortby' => $sortby,
1720              'sortorder' => $sortbys[$sortby],
1721          ];
1722      }
1723  
1724      /**
1725       * Get dynamic class component.
1726       *
1727       * @return string
1728       */
1729      protected function get_component() {
1730          $tableclass = explode("\\", get_class($this));
1731          return reset($tableclass);
1732      }
1733  
1734      /**
1735       * Get dynamic class handler.
1736       *
1737       * @return string
1738       */
1739      protected function get_handler() {
1740          $tableclass = explode("\\", get_class($this));
1741          return end($tableclass);
1742      }
1743  
1744      /**
1745       * Get the dynamic table start wrapper.
1746       * If this is not a dynamic table, then an empty string is returned making this safe to blindly call.
1747       *
1748       * @return string
1749       */
1750      protected function get_dynamic_table_html_start(): string {
1751          if (is_a($this, \core_table\dynamic::class)) {
1752              $sortdata = array_map(function($sortby, $sortorder) {
1753                  return [
1754                      'sortby' => $sortby,
1755                      'sortorder' => $sortorder,
1756                  ];
1757              }, array_keys($this->prefs['sortby']), array_values($this->prefs['sortby']));;
1758  
1759              return html_writer::start_tag('div', [
1760                  'class' => 'table-dynamic position-relative',
1761                  'data-region' => 'core_table/dynamic',
1762                  'data-table-handler' => $this->get_handler(),
1763                  'data-table-component' => $this->get_component(),
1764                  'data-table-uniqueid' => $this->uniqueid,
1765                  'data-table-filters' => json_encode($this->get_filterset()),
1766                  'data-table-sort-data' => json_encode($sortdata),
1767                  'data-table-first-initial' => $this->prefs['i_first'],
1768                  'data-table-last-initial' => $this->prefs['i_last'],
1769                  'data-table-page-number' => $this->currpage + 1,
1770                  'data-table-page-size' => $this->pagesize,
1771                  'data-table-default-per-page' => $this->get_default_per_page(),
1772                  'data-table-hidden-columns' => json_encode(array_keys($this->prefs['collapse'])),
1773                  'data-table-total-rows' => $this->totalrows,
1774              ]);
1775          }
1776  
1777          return '';
1778      }
1779  
1780      /**
1781       * Get the dynamic table end wrapper.
1782       * If this is not a dynamic table, then an empty string is returned making this safe to blindly call.
1783       *
1784       * @return string
1785       */
1786      protected function get_dynamic_table_html_end(): string {
1787          global $PAGE;
1788  
1789          if (is_a($this, \core_table\dynamic::class)) {
1790              $output = '';
1791  
1792              $perpageurl = new moodle_url($PAGE->url);
1793  
1794              // Generate "Show all/Show per page" link.
1795              if ($this->pagesize == TABLE_SHOW_ALL_PAGE_SIZE && $this->totalrows > $this->get_default_per_page()) {
1796                  $perpagesize = $this->get_default_per_page();
1797                  $perpagestring = get_string('showperpage', '', $this->get_default_per_page());
1798              } else if ($this->pagesize < $this->totalrows) {
1799                  $perpagesize = TABLE_SHOW_ALL_PAGE_SIZE;
1800                  $perpagestring = get_string('showall', '', $this->totalrows);
1801              }
1802              if (isset($perpagesize) && isset($perpagestring)) {
1803                  $perpageurl->param('perpage', $perpagesize);
1804                  $output .= html_writer::link(
1805                      $perpageurl,
1806                      $perpagestring,
1807                      [
1808                          'data-action' => 'showcount',
1809                          'data-target-page-size' => $perpagesize,
1810                      ]
1811                  );
1812              }
1813  
1814              $PAGE->requires->js_call_amd('core_table/dynamic', 'init');
1815              $output .= html_writer::end_tag('div');
1816              return $output;
1817          }
1818  
1819          return '';
1820      }
1821  
1822      /**
1823       * This function is not part of the public api.
1824       */
1825      function start_html() {
1826          global $OUTPUT;
1827  
1828          // Render the dynamic table header.
1829          echo $this->get_dynamic_table_html_start();
1830  
1831          // Render button to allow user to reset table preferences.
1832          echo $this->render_reset_button();
1833  
1834          // Do we need to print initial bars?
1835          $this->print_initials_bar();
1836  
1837          // Paging bar
1838          if ($this->use_pages) {
1839              $pagingbar = new paging_bar($this->totalrows, $this->currpage, $this->pagesize, $this->baseurl);
1840              $pagingbar->pagevar = $this->request[TABLE_VAR_PAGE];
1841              echo $OUTPUT->render($pagingbar);
1842          }
1843  
1844          if (in_array(TABLE_P_TOP, $this->showdownloadbuttonsat)) {
1845              echo $this->download_buttons();
1846          }
1847  
1848          $this->wrap_html_start();
1849          // Start of main data table
1850  
1851          echo html_writer::start_tag('div', array('class' => 'no-overflow'));
1852          echo html_writer::start_tag('table', $this->attributes) . $this->render_caption();
1853      }
1854  
1855      /**
1856       * This function set caption for table.
1857       *
1858       * @param string $caption Caption of table.
1859       * @param array|null $captionattributes Caption attributes of table.
1860       */
1861      public function set_caption(string $caption, ?array $captionattributes): void {
1862          $this->caption = $caption;
1863          $this->captionattributes = $captionattributes;
1864      }
1865  
1866      /**
1867       * This function renders a table caption.
1868       *
1869       * @return string $output Caption of table.
1870       */
1871      public function render_caption(): string {
1872          if ($this->caption === null) {
1873              return '';
1874          }
1875  
1876          return html_writer::tag(
1877              'caption',
1878              $this->caption,
1879              $this->captionattributes,
1880          );
1881      }
1882  
1883      /**
1884       * This function is not part of the public api.
1885       * @param array $styles CSS-property => value
1886       * @return string values suitably to go in a style="" attribute in HTML.
1887       */
1888      function make_styles_string($styles) {
1889          if (empty($styles)) {
1890              return null;
1891          }
1892  
1893          $string = '';
1894          foreach($styles as $property => $value) {
1895              $string .= $property . ':' . $value . ';';
1896          }
1897          return $string;
1898      }
1899  
1900      /**
1901       * Generate the HTML for the table preferences reset button.
1902       *
1903       * @return string HTML fragment, empty string if no need to reset
1904       */
1905      protected function render_reset_button() {
1906  
1907          if (!$this->can_be_reset()) {
1908              return '';
1909          }
1910  
1911          $url = $this->baseurl->out(false, array($this->request[TABLE_VAR_RESET] => 1));
1912  
1913          $html  = html_writer::start_div('resettable mdl-right');
1914          $html .= html_writer::link($url, get_string('resettable'), ['role' => 'button']);
1915          $html .= html_writer::end_div();
1916  
1917          return $html;
1918      }
1919  
1920      /**
1921       * Are there some table preferences that can be reset?
1922       *
1923       * If true, then the "reset table preferences" widget should be displayed.
1924       *
1925       * @return bool
1926       */
1927      protected function can_be_reset() {
1928          // Loop through preferences and make sure they are empty or set to the default value.
1929          foreach ($this->prefs as $prefname => $prefval) {
1930              if ($prefname === 'sortby' and !empty($this->sort_default_column)) {
1931                  // Check if the actual sorting differs from the default one.
1932                  if (empty($prefval) or $prefval !== array($this->sort_default_column => $this->sort_default_order)) {
1933                      return true;
1934                  }
1935  
1936              } else if ($prefname === 'collapse' and !empty($prefval)) {
1937                  // Check if there are some collapsed columns (all are expanded by default).
1938                  foreach ($prefval as $columnname => $iscollapsed) {
1939                      if ($iscollapsed) {
1940                          return true;
1941                      }
1942                  }
1943  
1944              } else if (!empty($prefval)) {
1945                  // For all other cases, we just check if some preference is set.
1946                  return true;
1947              }
1948          }
1949  
1950          return false;
1951      }
1952  
1953      /**
1954       * Get the context for the table.
1955       *
1956       * Note: This function _must_ be overridden by dynamic tables to ensure that the context is correctly determined
1957       * from the filterset parameters.
1958       *
1959       * @return context
1960       */
1961      public function get_context(): context {
1962          global $PAGE;
1963  
1964          if (is_a($this, \core_table\dynamic::class)) {
1965              throw new coding_exception('The get_context function must be defined for a dynamic table');
1966          }
1967  
1968          return $PAGE->context;
1969      }
1970  
1971      /**
1972       * Set the filterset in the table class.
1973       *
1974       * The use of filtersets is a requirement for dynamic tables, but can be used by other tables too if desired.
1975       *
1976       * @param filterset $filterset The filterset object to get filters and table parameters from
1977       */
1978      public function set_filterset(filterset $filterset): void {
1979          $this->filterset = $filterset;
1980  
1981          $this->guess_base_url();
1982      }
1983  
1984      /**
1985       * Get the currently defined filterset.
1986       *
1987       * @return filterset
1988       */
1989      public function get_filterset(): ?filterset {
1990          return $this->filterset;
1991      }
1992  
1993      /**
1994       * Attempt to guess the base URL.
1995       */
1996      public function guess_base_url(): void {
1997          if (is_a($this, \core_table\dynamic::class)) {
1998              throw new coding_exception('The guess_base_url function must be defined for a dynamic table');
1999          }
2000      }
2001  }
2002  
2003  
2004  /**
2005   * @package   moodlecore
2006   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
2007   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2008   */
2009  class table_sql extends flexible_table {
2010  
2011      public $countsql = NULL;
2012      public $countparams = NULL;
2013      /**
2014       * @var object sql for querying db. Has fields 'fields', 'from', 'where', 'params'.
2015       */
2016      public $sql = NULL;
2017      /**
2018       * @var array|\Traversable Data fetched from the db.
2019       */
2020      public $rawdata = NULL;
2021  
2022      /**
2023       * @var bool Overriding default for this.
2024       */
2025      public $is_sortable    = true;
2026      /**
2027       * @var bool Overriding default for this.
2028       */
2029      public $is_collapsible = true;
2030  
2031      /**
2032       * @param string $uniqueid a string identifying this table.Used as a key in
2033       *                          session  vars.
2034       */
2035      function __construct($uniqueid) {
2036          parent::__construct($uniqueid);
2037          // some sensible defaults
2038          $this->set_attribute('class', 'generaltable generalbox');
2039      }
2040  
2041      /**
2042       * Take the data returned from the db_query and go through all the rows
2043       * processing each col using either col_{columnname} method or other_cols
2044       * method or if other_cols returns NULL then put the data straight into the
2045       * table.
2046       *
2047       * After calling this function, don't forget to call close_recordset.
2048       */
2049      public function build_table() {
2050  
2051          if ($this->rawdata instanceof \Traversable && !$this->rawdata->valid()) {
2052              return;
2053          }
2054          if (!$this->rawdata) {
2055              return;
2056          }
2057  
2058          foreach ($this->rawdata as $row) {
2059              $formattedrow = $this->format_row($row);
2060              $this->add_data_keyed($formattedrow,
2061                  $this->get_row_class($row));
2062          }
2063      }
2064  
2065      /**
2066       * Closes recordset (for use after building the table).
2067       */
2068      public function close_recordset() {
2069          if ($this->rawdata && ($this->rawdata instanceof \core\dml\recordset_walk ||
2070                  $this->rawdata instanceof moodle_recordset)) {
2071              $this->rawdata->close();
2072              $this->rawdata = null;
2073          }
2074      }
2075  
2076      /**
2077       * Get any extra classes names to add to this row in the HTML.
2078       * @param $row array the data for this row.
2079       * @return string added to the class="" attribute of the tr.
2080       */
2081      function get_row_class($row) {
2082          return '';
2083      }
2084  
2085      /**
2086       * This is only needed if you want to use different sql to count rows.
2087       * Used for example when perhaps all db JOINS are not needed when counting
2088       * records. You don't need to call this function the count_sql
2089       * will be generated automatically.
2090       *
2091       * We need to count rows returned by the db seperately to the query itself
2092       * as we need to know how many pages of data we have to display.
2093       */
2094      function set_count_sql($sql, array $params = NULL) {
2095          $this->countsql = $sql;
2096          $this->countparams = $params;
2097      }
2098  
2099      /**
2100       * Set the sql to query the db. Query will be :
2101       *      SELECT $fields FROM $from WHERE $where
2102       * Of course you can use sub-queries, JOINS etc. by putting them in the
2103       * appropriate clause of the query.
2104       */
2105      function set_sql($fields, $from, $where, array $params = array()) {
2106          $this->sql = new stdClass();
2107          $this->sql->fields = $fields;
2108          $this->sql->from = $from;
2109          $this->sql->where = $where;
2110          $this->sql->params = $params;
2111      }
2112  
2113      /**
2114       * Query the db. Store results in the table object for use by build_table.
2115       *
2116       * @param int $pagesize size of page for paginated displayed table.
2117       * @param bool $useinitialsbar do you want to use the initials bar. Bar
2118       * will only be used if there is a fullname column defined for the table.
2119       */
2120      function query_db($pagesize, $useinitialsbar=true) {
2121          global $DB;
2122          if (!$this->is_downloading()) {
2123              if ($this->countsql === NULL) {
2124                  $this->countsql = 'SELECT COUNT(1) FROM '.$this->sql->from.' WHERE '.$this->sql->where;
2125                  $this->countparams = $this->sql->params;
2126              }
2127              $grandtotal = $DB->count_records_sql($this->countsql, $this->countparams);
2128              if ($useinitialsbar && !$this->is_downloading()) {
2129                  $this->initialbars(true);
2130              }
2131  
2132              list($wsql, $wparams) = $this->get_sql_where();
2133              if ($wsql) {
2134                  $this->countsql .= ' AND '.$wsql;
2135                  $this->countparams = array_merge($this->countparams, $wparams);
2136  
2137                  $this->sql->where .= ' AND '.$wsql;
2138                  $this->sql->params = array_merge($this->sql->params, $wparams);
2139  
2140                  $total  = $DB->count_records_sql($this->countsql, $this->countparams);
2141              } else {
2142                  $total = $grandtotal;
2143              }
2144  
2145              $this->pagesize($pagesize, $total);
2146          }
2147  
2148          // Fetch the attempts
2149          $sort = $this->get_sql_sort();
2150          if ($sort) {
2151              $sort = "ORDER BY $sort";
2152          }
2153          $sql = "SELECT
2154                  {$this->sql->fields}
2155                  FROM {$this->sql->from}
2156                  WHERE {$this->sql->where}
2157                  {$sort}";
2158  
2159          if (!$this->is_downloading()) {
2160              $this->rawdata = $DB->get_records_sql($sql, $this->sql->params, $this->get_page_start(), $this->get_page_size());
2161          } else {
2162              $this->rawdata = $DB->get_records_sql($sql, $this->sql->params);
2163          }
2164      }
2165  
2166      /**
2167       * Convenience method to call a number of methods for you to display the
2168       * table.
2169       */
2170      function out($pagesize, $useinitialsbar, $downloadhelpbutton='') {
2171          global $DB;
2172          if (!$this->columns) {
2173              $onerow = $DB->get_record_sql("SELECT {$this->sql->fields} FROM {$this->sql->from} WHERE {$this->sql->where}",
2174                  $this->sql->params, IGNORE_MULTIPLE);
2175              //if columns is not set then define columns as the keys of the rows returned
2176              //from the db.
2177              $this->define_columns(array_keys((array)$onerow));
2178              $this->define_headers(array_keys((array)$onerow));
2179          }
2180          $this->pagesize = $pagesize;
2181          $this->setup();
2182          $this->query_db($pagesize, $useinitialsbar);
2183          $this->build_table();
2184          $this->close_recordset();
2185          $this->finish_output();
2186      }
2187  }
2188  
2189  
2190  /**
2191   * @package   moodlecore
2192   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
2193   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2194   */
2195  class table_default_export_format_parent {
2196      /**
2197       * @var flexible_table or child class reference pointing to table class
2198       * object from which to export data.
2199       */
2200      var $table;
2201  
2202      /**
2203       * @var bool output started. Keeps track of whether any output has been
2204       * started yet.
2205       */
2206      var $documentstarted = false;
2207  
2208      /**
2209       * Constructor
2210       *
2211       * @param flexible_table $table
2212       */
2213      public function __construct(&$table) {
2214          $this->table =& $table;
2215      }
2216  
2217      /**
2218       * Old syntax of class constructor. Deprecated in PHP7.
2219       *
2220       * @deprecated since Moodle 3.1
2221       */
2222      public function table_default_export_format_parent(&$table) {
2223          debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
2224          self::__construct($table);
2225      }
2226  
2227      function set_table(&$table) {
2228          $this->table =& $table;
2229      }
2230  
2231      function add_data($row) {
2232          return false;
2233      }
2234  
2235      function add_seperator() {
2236          return false;
2237      }
2238  
2239      function document_started() {
2240          return $this->documentstarted;
2241      }
2242      /**
2243       * Given text in a variety of format codings, this function returns
2244       * the text as safe HTML or as plain text dependent on what is appropriate
2245       * for the download format. The default removes all tags.
2246       */
2247      function format_text($text, $format=FORMAT_MOODLE, $options=NULL, $courseid=NULL) {
2248          //use some whitespace to indicate where there was some line spacing.
2249          $text = str_replace(array('</p>', "\n", "\r"), '   ', $text);
2250          return html_entity_decode(strip_tags($text), ENT_COMPAT);
2251      }
2252  
2253      /**
2254       * Format a row of data, removing HTML tags and entities from each of the cells
2255       *
2256       * @param array $row
2257       * @return array
2258       */
2259      public function format_data(array $row): array {
2260          return array_map([$this, 'format_text'], $row);
2261      }
2262  }
2263  
2264  /**
2265   * Dataformat exporter
2266   *
2267   * @package    core
2268   * @subpackage tablelib
2269   * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
2270   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2271   */
2272  class table_dataformat_export_format extends table_default_export_format_parent {
2273  
2274      /** @var \core\dataformat\base $dataformat */
2275      protected $dataformat;
2276  
2277      /** @var $rownum */
2278      protected $rownum = 0;
2279  
2280      /** @var $columns */
2281      protected $columns;
2282  
2283      /**
2284       * Constructor
2285       *
2286       * @param string $table An sql table
2287       * @param string $dataformat type of dataformat for export
2288       */
2289      public function __construct(&$table, $dataformat) {
2290          parent::__construct($table);
2291  
2292          if (ob_get_length()) {
2293              throw new coding_exception("Output can not be buffered before instantiating table_dataformat_export_format");
2294          }
2295  
2296          $classname = 'dataformat_' . $dataformat . '\writer';
2297          if (!class_exists($classname)) {
2298              throw new coding_exception("Unable to locate dataformat/$dataformat/classes/writer.php");
2299          }
2300          $this->dataformat = new $classname;
2301  
2302          // The dataformat export time to first byte could take a while to generate...
2303          set_time_limit(0);
2304  
2305          // Close the session so that the users other tabs in the same session are not blocked.
2306          \core\session\manager::write_close();
2307      }
2308  
2309      /**
2310       * Whether the current dataformat supports export of HTML
2311       *
2312       * @return bool
2313       */
2314      public function supports_html(): bool {
2315          return $this->dataformat->supports_html();
2316      }
2317  
2318      /**
2319       * Start document
2320       *
2321       * @param string $filename
2322       * @param string $sheettitle
2323       */
2324      public function start_document($filename, $sheettitle) {
2325          $this->documentstarted = true;
2326          $this->dataformat->set_filename($filename);
2327          $this->dataformat->send_http_headers();
2328          $this->dataformat->set_sheettitle($sheettitle);
2329          $this->dataformat->start_output();
2330      }
2331  
2332      /**
2333       * Start export
2334       *
2335       * @param string $sheettitle optional spreadsheet worksheet title
2336       */
2337      public function start_table($sheettitle) {
2338          $this->dataformat->set_sheettitle($sheettitle);
2339      }
2340  
2341      /**
2342       * Output headers
2343       *
2344       * @param array $headers
2345       */
2346      public function output_headers($headers) {
2347          $this->columns = $this->format_data($headers);
2348          if (method_exists($this->dataformat, 'write_header')) {
2349              error_log('The function write_header() does not support multiple sheets. In order to support multiple sheets you ' .
2350                  'must implement start_output() and start_sheet() and remove write_header() in your dataformat.');
2351              $this->dataformat->write_header($this->columns);
2352          } else {
2353              $this->dataformat->start_sheet($this->columns);
2354          }
2355      }
2356  
2357      /**
2358       * Add a row of data
2359       *
2360       * @param array $row One record of data
2361       */
2362      public function add_data($row) {
2363          if (!$this->supports_html()) {
2364              $row = $this->format_data($row);
2365          }
2366  
2367          $this->dataformat->write_record($row, $this->rownum++);
2368          return true;
2369      }
2370  
2371      /**
2372       * Finish export
2373       */
2374      public function finish_table() {
2375          if (method_exists($this->dataformat, 'write_footer')) {
2376              error_log('The function write_footer() does not support multiple sheets. In order to support multiple sheets you ' .
2377                  'must implement close_sheet() and close_output() and remove write_footer() in your dataformat.');
2378              $this->dataformat->write_footer($this->columns);
2379          } else {
2380              $this->dataformat->close_sheet($this->columns);
2381          }
2382      }
2383  
2384      /**
2385       * Finish download
2386       */
2387      public function finish_document() {
2388          $this->dataformat->close_output();
2389          exit();
2390      }
2391  }