Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Classes representing HTML elements, used by $OUTPUT methods
  19   *
  20   * Please see http://docs.moodle.org/en/Developement:How_Moodle_outputs_HTML
  21   * for an overview.
  22   *
  23   * @package core
  24   * @category output
  25   * @copyright 2009 Tim Hunt
  26   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  27   */
  28  
  29  defined('MOODLE_INTERNAL') || die();
  30  
  31  /**
  32   * Interface marking other classes as suitable for renderer_base::render()
  33   *
  34   * @copyright 2010 Petr Skoda (skodak) info@skodak.org
  35   * @package core
  36   * @category output
  37   */
  38  interface renderable {
  39      // intentionally empty
  40  }
  41  
  42  /**
  43   * Interface marking other classes having the ability to export their data for use by templates.
  44   *
  45   * @copyright 2015 Damyon Wiese
  46   * @package core
  47   * @category output
  48   * @since 2.9
  49   */
  50  interface templatable {
  51  
  52      /**
  53       * Function to export the renderer data in a format that is suitable for a
  54       * mustache template. This means:
  55       * 1. No complex types - only stdClass, array, int, string, float, bool
  56       * 2. Any additional info that is required for the template is pre-calculated (e.g. capability checks).
  57       *
  58       * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
  59       * @return stdClass|array
  60       */
  61      public function export_for_template(renderer_base $output);
  62  }
  63  
  64  /**
  65   * Data structure representing a file picker.
  66   *
  67   * @copyright 2010 Dongsheng Cai
  68   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  69   * @since Moodle 2.0
  70   * @package core
  71   * @category output
  72   */
  73  class file_picker implements renderable {
  74  
  75      /**
  76       * @var stdClass An object containing options for the file picker
  77       */
  78      public $options;
  79  
  80      /**
  81       * Constructs a file picker object.
  82       *
  83       * The following are possible options for the filepicker:
  84       *    - accepted_types  (*)
  85       *    - return_types    (FILE_INTERNAL)
  86       *    - env             (filepicker)
  87       *    - client_id       (uniqid)
  88       *    - itemid          (0)
  89       *    - maxbytes        (-1)
  90       *    - maxfiles        (1)
  91       *    - buttonname      (false)
  92       *
  93       * @param stdClass $options An object containing options for the file picker.
  94       */
  95      public function __construct(stdClass $options) {
  96          global $CFG, $USER, $PAGE;
  97          require_once($CFG->dirroot. '/repository/lib.php');
  98          $defaults = array(
  99              'accepted_types'=>'*',
 100              'return_types'=>FILE_INTERNAL,
 101              'env' => 'filepicker',
 102              'client_id' => uniqid(),
 103              'itemid' => 0,
 104              'maxbytes'=>-1,
 105              'maxfiles'=>1,
 106              'buttonname'=>false
 107          );
 108          foreach ($defaults as $key=>$value) {
 109              if (empty($options->$key)) {
 110                  $options->$key = $value;
 111              }
 112          }
 113  
 114          $options->currentfile = '';
 115          if (!empty($options->itemid)) {
 116              $fs = get_file_storage();
 117              $usercontext = context_user::instance($USER->id);
 118              if (empty($options->filename)) {
 119                  if ($files = $fs->get_area_files($usercontext->id, 'user', 'draft', $options->itemid, 'id DESC', false)) {
 120                      $file = reset($files);
 121                  }
 122              } else {
 123                  $file = $fs->get_file($usercontext->id, 'user', 'draft', $options->itemid, $options->filepath, $options->filename);
 124              }
 125              if (!empty($file)) {
 126                  $options->currentfile = html_writer::link(moodle_url::make_draftfile_url($file->get_itemid(), $file->get_filepath(), $file->get_filename()), $file->get_filename());
 127              }
 128          }
 129  
 130          // initilise options, getting files in root path
 131          $this->options = initialise_filepicker($options);
 132  
 133          // copying other options
 134          foreach ($options as $name=>$value) {
 135              if (!isset($this->options->$name)) {
 136                  $this->options->$name = $value;
 137              }
 138          }
 139      }
 140  }
 141  
 142  /**
 143   * Data structure representing a user picture.
 144   *
 145   * @copyright 2009 Nicolas Connault, 2010 Petr Skoda
 146   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 147   * @since Modle 2.0
 148   * @package core
 149   * @category output
 150   */
 151  class user_picture implements renderable {
 152      /**
 153       * @var array List of mandatory fields in user record here. (do not include
 154       * TEXT columns because it would break SELECT DISTINCT in MSSQL and ORACLE)
 155       */
 156      protected static $fields = array('id', 'picture', 'firstname', 'lastname', 'firstnamephonetic', 'lastnamephonetic',
 157              'middlename', 'alternatename', 'imagealt', 'email');
 158  
 159      /**
 160       * @var stdClass A user object with at least fields all columns specified
 161       * in $fields array constant set.
 162       */
 163      public $user;
 164  
 165      /**
 166       * @var int The course id. Used when constructing the link to the user's
 167       * profile, page course id used if not specified.
 168       */
 169      public $courseid;
 170  
 171      /**
 172       * @var bool Add course profile link to image
 173       */
 174      public $link = true;
 175  
 176      /**
 177       * @var int Size in pixels. Special values are (true/1 = 100px) and
 178       * (false/0 = 35px)
 179       * for backward compatibility.
 180       */
 181      public $size = 35;
 182  
 183      /**
 184       * @var bool Add non-blank alt-text to the image.
 185       * Default true, set to false when image alt just duplicates text in screenreaders.
 186       */
 187      public $alttext = true;
 188  
 189      /**
 190       * @var bool Whether or not to open the link in a popup window.
 191       */
 192      public $popup = false;
 193  
 194      /**
 195       * @var string Image class attribute
 196       */
 197      public $class = 'userpicture';
 198  
 199      /**
 200       * @var bool Whether to be visible to screen readers.
 201       */
 202      public $visibletoscreenreaders = true;
 203  
 204      /**
 205       * @var bool Whether to include the fullname in the user picture link.
 206       */
 207      public $includefullname = false;
 208  
 209      /**
 210       * @var mixed Include user authentication token. True indicates to generate a token for current user, and integer value
 211       * indicates to generate a token for the user whose id is the value indicated.
 212       */
 213      public $includetoken = false;
 214  
 215      /**
 216       * User picture constructor.
 217       *
 218       * @param stdClass $user user record with at least id, picture, imagealt, firstname and lastname set.
 219       *                 It is recommended to add also contextid of the user for performance reasons.
 220       */
 221      public function __construct(stdClass $user) {
 222          global $DB;
 223  
 224          if (empty($user->id)) {
 225              throw new coding_exception('User id is required when printing user avatar image.');
 226          }
 227  
 228          // only touch the DB if we are missing data and complain loudly...
 229          $needrec = false;
 230          foreach (self::$fields as $field) {
 231              if (!property_exists($user, $field)) {
 232                  $needrec = true;
 233                  debugging('Missing '.$field.' property in $user object, this is a performance problem that needs to be fixed by a developer. '
 234                            .'Please use user_picture::fields() to get the full list of required fields.', DEBUG_DEVELOPER);
 235                  break;
 236              }
 237          }
 238  
 239          if ($needrec) {
 240              $this->user = $DB->get_record('user', array('id'=>$user->id), self::fields(), MUST_EXIST);
 241          } else {
 242              $this->user = clone($user);
 243          }
 244      }
 245  
 246      /**
 247       * Returns a list of required user fields, useful when fetching required user info from db.
 248       *
 249       * In some cases we have to fetch the user data together with some other information,
 250       * the idalias is useful there because the id would otherwise override the main
 251       * id of the result record. Please note it has to be converted back to id before rendering.
 252       *
 253       * @param string $tableprefix name of database table prefix in query
 254       * @param array $extrafields extra fields to be included in result (do not include TEXT columns because it would break SELECT DISTINCT in MSSQL and ORACLE)
 255       * @param string $idalias alias of id field
 256       * @param string $fieldprefix prefix to add to all columns in their aliases, does not apply to 'id'
 257       * @return string
 258       */
 259      public static function fields($tableprefix = '', array $extrafields = NULL, $idalias = 'id', $fieldprefix = '') {
 260          if (!$tableprefix and !$extrafields and !$idalias) {
 261              return implode(',', self::$fields);
 262          }
 263          if ($tableprefix) {
 264              $tableprefix .= '.';
 265          }
 266          foreach (self::$fields as $field) {
 267              if ($field === 'id' and $idalias and $idalias !== 'id') {
 268                  $fields[$field] = "$tableprefix$field AS $idalias";
 269              } else {
 270                  if ($fieldprefix and $field !== 'id') {
 271                      $fields[$field] = "$tableprefix$field AS $fieldprefix$field";
 272                  } else {
 273                      $fields[$field] = "$tableprefix$field";
 274                  }
 275              }
 276          }
 277          // add extra fields if not already there
 278          if ($extrafields) {
 279              foreach ($extrafields as $e) {
 280                  if ($e === 'id' or isset($fields[$e])) {
 281                      continue;
 282                  }
 283                  if ($fieldprefix) {
 284                      $fields[$e] = "$tableprefix$e AS $fieldprefix$e";
 285                  } else {
 286                      $fields[$e] = "$tableprefix$e";
 287                  }
 288              }
 289          }
 290          return implode(',', $fields);
 291      }
 292  
 293      /**
 294       * Extract the aliased user fields from a given record
 295       *
 296       * Given a record that was previously obtained using {@link self::fields()} with aliases,
 297       * this method extracts user related unaliased fields.
 298       *
 299       * @param stdClass $record containing user picture fields
 300       * @param array $extrafields extra fields included in the $record
 301       * @param string $idalias alias of the id field
 302       * @param string $fieldprefix prefix added to all columns in their aliases, does not apply to 'id'
 303       * @return stdClass object with unaliased user fields
 304       */
 305      public static function unalias(stdClass $record, array $extrafields = null, $idalias = 'id', $fieldprefix = '') {
 306  
 307          if (empty($idalias)) {
 308              $idalias = 'id';
 309          }
 310  
 311          $return = new stdClass();
 312  
 313          foreach (self::$fields as $field) {
 314              if ($field === 'id') {
 315                  if (property_exists($record, $idalias)) {
 316                      $return->id = $record->{$idalias};
 317                  }
 318              } else {
 319                  if (property_exists($record, $fieldprefix.$field)) {
 320                      $return->{$field} = $record->{$fieldprefix.$field};
 321                  }
 322              }
 323          }
 324          // add extra fields if not already there
 325          if ($extrafields) {
 326              foreach ($extrafields as $e) {
 327                  if ($e === 'id' or property_exists($return, $e)) {
 328                      continue;
 329                  }
 330                  $return->{$e} = $record->{$fieldprefix.$e};
 331              }
 332          }
 333  
 334          return $return;
 335      }
 336  
 337      /**
 338       * Works out the URL for the users picture.
 339       *
 340       * This method is recommended as it avoids costly redirects of user pictures
 341       * if requests are made for non-existent files etc.
 342       *
 343       * @param moodle_page $page
 344       * @param renderer_base $renderer
 345       * @return moodle_url
 346       */
 347      public function get_url(moodle_page $page, renderer_base $renderer = null) {
 348          global $CFG;
 349  
 350          if (is_null($renderer)) {
 351              $renderer = $page->get_renderer('core');
 352          }
 353  
 354          // Sort out the filename and size. Size is only required for the gravatar
 355          // implementation presently.
 356          if (empty($this->size)) {
 357              $filename = 'f2';
 358              $size = 35;
 359          } else if ($this->size === true or $this->size == 1) {
 360              $filename = 'f1';
 361              $size = 100;
 362          } else if ($this->size > 100) {
 363              $filename = 'f3';
 364              $size = (int)$this->size;
 365          } else if ($this->size >= 50) {
 366              $filename = 'f1';
 367              $size = (int)$this->size;
 368          } else {
 369              $filename = 'f2';
 370              $size = (int)$this->size;
 371          }
 372  
 373          $defaulturl = $renderer->image_url('u/'.$filename); // default image
 374  
 375          if ((!empty($CFG->forcelogin) and !isloggedin()) ||
 376              (!empty($CFG->forceloginforprofileimage) && (!isloggedin() || isguestuser()))) {
 377              // Protect images if login required and not logged in;
 378              // also if login is required for profile images and is not logged in or guest
 379              // do not use require_login() because it is expensive and not suitable here anyway.
 380              return $defaulturl;
 381          }
 382  
 383          // First try to detect deleted users - but do not read from database for performance reasons!
 384          if (!empty($this->user->deleted) or strpos($this->user->email, '@') === false) {
 385              // All deleted users should have email replaced by md5 hash,
 386              // all active users are expected to have valid email.
 387              return $defaulturl;
 388          }
 389  
 390          // Did the user upload a picture?
 391          if ($this->user->picture > 0) {
 392              if (!empty($this->user->contextid)) {
 393                  $contextid = $this->user->contextid;
 394              } else {
 395                  $context = context_user::instance($this->user->id, IGNORE_MISSING);
 396                  if (!$context) {
 397                      // This must be an incorrectly deleted user, all other users have context.
 398                      return $defaulturl;
 399                  }
 400                  $contextid = $context->id;
 401              }
 402  
 403              $path = '/';
 404              if (clean_param($page->theme->name, PARAM_THEME) == $page->theme->name) {
 405                  // We append the theme name to the file path if we have it so that
 406                  // in the circumstance that the profile picture is not available
 407                  // when the user actually requests it they still get the profile
 408                  // picture for the correct theme.
 409                  $path .= $page->theme->name.'/';
 410              }
 411              // Set the image URL to the URL for the uploaded file and return.
 412              $url = moodle_url::make_pluginfile_url(
 413                      $contextid, 'user', 'icon', null, $path, $filename, false, $this->includetoken);
 414              $url->param('rev', $this->user->picture);
 415              return $url;
 416          }
 417  
 418          if ($this->user->picture == 0 and !empty($CFG->enablegravatar)) {
 419              // Normalise the size variable to acceptable bounds
 420              if ($size < 1 || $size > 512) {
 421                  $size = 35;
 422              }
 423              // Hash the users email address
 424              $md5 = md5(strtolower(trim($this->user->email)));
 425              // Build a gravatar URL with what we know.
 426  
 427              // Find the best default image URL we can (MDL-35669)
 428              if (empty($CFG->gravatardefaulturl)) {
 429                  $absoluteimagepath = $page->theme->resolve_image_location('u/'.$filename, 'core');
 430                  if (strpos($absoluteimagepath, $CFG->dirroot) === 0) {
 431                      $gravatardefault = $CFG->wwwroot . substr($absoluteimagepath, strlen($CFG->dirroot));
 432                  } else {
 433                      $gravatardefault = $CFG->wwwroot . '/pix/u/' . $filename . '.png';
 434                  }
 435              } else {
 436                  $gravatardefault = $CFG->gravatardefaulturl;
 437              }
 438  
 439              // If the currently requested page is https then we'll return an
 440              // https gravatar page.
 441              if (is_https()) {
 442                  return new moodle_url("https://secure.gravatar.com/avatar/{$md5}", array('s' => $size, 'd' => $gravatardefault));
 443              } else {
 444                  return new moodle_url("http://www.gravatar.com/avatar/{$md5}", array('s' => $size, 'd' => $gravatardefault));
 445              }
 446          }
 447  
 448          return $defaulturl;
 449      }
 450  }
 451  
 452  /**
 453   * Data structure representing a help icon.
 454   *
 455   * @copyright 2010 Petr Skoda (info@skodak.org)
 456   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 457   * @since Moodle 2.0
 458   * @package core
 459   * @category output
 460   */
 461  class help_icon implements renderable, templatable {
 462  
 463      /**
 464       * @var string lang pack identifier (without the "_help" suffix),
 465       * both get_string($identifier, $component) and get_string($identifier.'_help', $component)
 466       * must exist.
 467       */
 468      public $identifier;
 469  
 470      /**
 471       * @var string Component name, the same as in get_string()
 472       */
 473      public $component;
 474  
 475      /**
 476       * @var string Extra descriptive text next to the icon
 477       */
 478      public $linktext = null;
 479  
 480      /**
 481       * Constructor
 482       *
 483       * @param string $identifier string for help page title,
 484       *  string with _help suffix is used for the actual help text.
 485       *  string with _link suffix is used to create a link to further info (if it exists)
 486       * @param string $component
 487       */
 488      public function __construct($identifier, $component) {
 489          $this->identifier = $identifier;
 490          $this->component  = $component;
 491      }
 492  
 493      /**
 494       * Verifies that both help strings exists, shows debug warnings if not
 495       */
 496      public function diag_strings() {
 497          $sm = get_string_manager();
 498          if (!$sm->string_exists($this->identifier, $this->component)) {
 499              debugging("Help title string does not exist: [$this->identifier, $this->component]");
 500          }
 501          if (!$sm->string_exists($this->identifier.'_help', $this->component)) {
 502              debugging("Help contents string does not exist: [{$this->identifier}_help, $this->component]");
 503          }
 504      }
 505  
 506      /**
 507       * Export this data so it can be used as the context for a mustache template.
 508       *
 509       * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
 510       * @return array
 511       */
 512      public function export_for_template(renderer_base $output) {
 513          global $CFG;
 514  
 515          $title = get_string($this->identifier, $this->component);
 516  
 517          if (empty($this->linktext)) {
 518              $alt = get_string('helpprefix2', '', trim($title, ". \t"));
 519          } else {
 520              $alt = get_string('helpwiththis');
 521          }
 522  
 523          $data = get_formatted_help_string($this->identifier, $this->component, false);
 524  
 525          $data->alt = $alt;
 526          $data->icon = (new pix_icon('help', $alt, 'core', ['class' => 'iconhelp']))->export_for_template($output);
 527          $data->linktext = $this->linktext;
 528          $data->title = get_string('helpprefix2', '', trim($title, ". \t"));
 529  
 530          $options = [
 531              'component' => $this->component,
 532              'identifier' => $this->identifier,
 533              'lang' => current_language()
 534          ];
 535  
 536          // Debugging feature lets you display string identifier and component.
 537          if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
 538              $options['strings'] = 1;
 539          }
 540  
 541          $data->url = (new moodle_url('/help.php', $options))->out(false);
 542          $data->ltr = !right_to_left();
 543          return $data;
 544      }
 545  }
 546  
 547  
 548  /**
 549   * Data structure representing an icon font.
 550   *
 551   * @copyright 2016 Damyon Wiese
 552   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 553   * @package core
 554   * @category output
 555   */
 556  class pix_icon_font implements templatable {
 557  
 558      /**
 559       * @var pix_icon $pixicon The original icon.
 560       */
 561      private $pixicon = null;
 562  
 563      /**
 564       * @var string $key The mapped key.
 565       */
 566      private $key;
 567  
 568      /**
 569       * @var bool $mapped The icon could not be mapped.
 570       */
 571      private $mapped;
 572  
 573      /**
 574       * Constructor
 575       *
 576       * @param pix_icon $pixicon The original icon
 577       */
 578      public function __construct(pix_icon $pixicon) {
 579          global $PAGE;
 580  
 581          $this->pixicon = $pixicon;
 582          $this->mapped = false;
 583          $iconsystem = \core\output\icon_system::instance();
 584  
 585          $this->key = $iconsystem->remap_icon_name($pixicon->pix, $pixicon->component);
 586          if (!empty($this->key)) {
 587              $this->mapped = true;
 588          }
 589      }
 590  
 591      /**
 592       * Return true if this pix_icon was successfully mapped to an icon font.
 593       *
 594       * @return bool
 595       */
 596      public function is_mapped() {
 597          return $this->mapped;
 598      }
 599  
 600      /**
 601       * Export this data so it can be used as the context for a mustache template.
 602       *
 603       * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
 604       * @return array
 605       */
 606      public function export_for_template(renderer_base $output) {
 607  
 608          $pixdata = $this->pixicon->export_for_template($output);
 609  
 610          $title = isset($this->pixicon->attributes['title']) ? $this->pixicon->attributes['title'] : '';
 611          $alt = isset($this->pixicon->attributes['alt']) ? $this->pixicon->attributes['alt'] : '';
 612          if (empty($title)) {
 613              $title = $alt;
 614          }
 615          $data = array(
 616              'extraclasses' => $pixdata['extraclasses'],
 617              'title' => $title,
 618              'alt' => $alt,
 619              'key' => $this->key
 620          );
 621  
 622          return $data;
 623      }
 624  }
 625  
 626  /**
 627   * Data structure representing an icon subtype.
 628   *
 629   * @copyright 2016 Damyon Wiese
 630   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 631   * @package core
 632   * @category output
 633   */
 634  class pix_icon_fontawesome extends pix_icon_font {
 635  
 636  }
 637  
 638  /**
 639   * Data structure representing an icon.
 640   *
 641   * @copyright 2010 Petr Skoda
 642   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 643   * @since Moodle 2.0
 644   * @package core
 645   * @category output
 646   */
 647  class pix_icon implements renderable, templatable {
 648  
 649      /**
 650       * @var string The icon name
 651       */
 652      var $pix;
 653  
 654      /**
 655       * @var string The component the icon belongs to.
 656       */
 657      var $component;
 658  
 659      /**
 660       * @var array An array of attributes to use on the icon
 661       */
 662      var $attributes = array();
 663  
 664      /**
 665       * Constructor
 666       *
 667       * @param string $pix short icon name
 668       * @param string $alt The alt text to use for the icon
 669       * @param string $component component name
 670       * @param array $attributes html attributes
 671       */
 672      public function __construct($pix, $alt, $component='moodle', array $attributes = null) {
 673          global $PAGE;
 674  
 675          $this->pix = $pix;
 676          $this->component  = $component;
 677          $this->attributes = (array)$attributes;
 678  
 679          if (empty($this->attributes['class'])) {
 680              $this->attributes['class'] = '';
 681          }
 682  
 683          // Set an additional class for big icons so that they can be styled properly.
 684          if (substr($pix, 0, 2) === 'b/') {
 685              $this->attributes['class'] .= ' iconsize-big';
 686          }
 687  
 688          // If the alt is empty, don't place it in the attributes, otherwise it will override parent alt text.
 689          if (!is_null($alt)) {
 690              $this->attributes['alt'] = $alt;
 691  
 692              // If there is no title, set it to the attribute.
 693              if (!isset($this->attributes['title'])) {
 694                  $this->attributes['title'] = $this->attributes['alt'];
 695              }
 696          } else {
 697              unset($this->attributes['alt']);
 698          }
 699  
 700          if (empty($this->attributes['title'])) {
 701              // Remove the title attribute if empty, we probably want to use the parent node's title
 702              // and some browsers might overwrite it with an empty title.
 703              unset($this->attributes['title']);
 704          }
 705  
 706          // Hide icons from screen readers that have no alt.
 707          if (empty($this->attributes['alt'])) {
 708              $this->attributes['aria-hidden'] = 'true';
 709          }
 710      }
 711  
 712      /**
 713       * Export this data so it can be used as the context for a mustache template.
 714       *
 715       * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
 716       * @return array
 717       */
 718      public function export_for_template(renderer_base $output) {
 719          $attributes = $this->attributes;
 720          $extraclasses = '';
 721  
 722          foreach ($attributes as $key => $item) {
 723              if ($key == 'class') {
 724                  $extraclasses = $item;
 725                  unset($attributes[$key]);
 726                  break;
 727              }
 728          }
 729  
 730          $attributes['src'] = $output->image_url($this->pix, $this->component)->out(false);
 731          $templatecontext = array();
 732          foreach ($attributes as $name => $value) {
 733              $templatecontext[] = array('name' => $name, 'value' => $value);
 734          }
 735          $title = isset($attributes['title']) ? $attributes['title'] : '';
 736          if (empty($title)) {
 737              $title = isset($attributes['alt']) ? $attributes['alt'] : '';
 738          }
 739          $data = array(
 740              'attributes' => $templatecontext,
 741              'extraclasses' => $extraclasses
 742          );
 743  
 744          return $data;
 745      }
 746  
 747      /**
 748       * Much simpler version of export that will produce the data required to render this pix with the
 749       * pix helper in a mustache tag.
 750       *
 751       * @return array
 752       */
 753      public function export_for_pix() {
 754          $title = isset($this->attributes['title']) ? $this->attributes['title'] : '';
 755          if (empty($title)) {
 756              $title = isset($this->attributes['alt']) ? $this->attributes['alt'] : '';
 757          }
 758          return [
 759              'key' => $this->pix,
 760              'component' => $this->component,
 761              'title' => $title
 762          ];
 763      }
 764  }
 765  
 766  /**
 767   * Data structure representing an activity icon.
 768   *
 769   * The difference is that activity icons will always render with the standard icon system (no font icons).
 770   *
 771   * @copyright 2017 Damyon Wiese
 772   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 773   * @package core
 774   */
 775  class image_icon extends pix_icon {
 776  }
 777  
 778  /**
 779   * Data structure representing an emoticon image
 780   *
 781   * @copyright 2010 David Mudrak
 782   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 783   * @since Moodle 2.0
 784   * @package core
 785   * @category output
 786   */
 787  class pix_emoticon extends pix_icon implements renderable {
 788  
 789      /**
 790       * Constructor
 791       * @param string $pix short icon name
 792       * @param string $alt alternative text
 793       * @param string $component emoticon image provider
 794       * @param array $attributes explicit HTML attributes
 795       */
 796      public function __construct($pix, $alt, $component = 'moodle', array $attributes = array()) {
 797          if (empty($attributes['class'])) {
 798              $attributes['class'] = 'emoticon';
 799          }
 800          parent::__construct($pix, $alt, $component, $attributes);
 801      }
 802  }
 803  
 804  /**
 805   * Data structure representing a simple form with only one button.
 806   *
 807   * @copyright 2009 Petr Skoda
 808   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 809   * @since Moodle 2.0
 810   * @package core
 811   * @category output
 812   */
 813  class single_button implements renderable {
 814  
 815      /**
 816       * @var moodle_url Target url
 817       */
 818      public $url;
 819  
 820      /**
 821       * @var string Button label
 822       */
 823      public $label;
 824  
 825      /**
 826       * @var string Form submit method post or get
 827       */
 828      public $method = 'post';
 829  
 830      /**
 831       * @var string Wrapping div class
 832       */
 833      public $class = 'singlebutton';
 834  
 835      /**
 836       * @var bool True if button is primary button. Used for styling.
 837       */
 838      public $primary = false;
 839  
 840      /**
 841       * @var bool True if button disabled, false if normal
 842       */
 843      public $disabled = false;
 844  
 845      /**
 846       * @var string Button tooltip
 847       */
 848      public $tooltip = null;
 849  
 850      /**
 851       * @var string Form id
 852       */
 853      public $formid;
 854  
 855      /**
 856       * @var array List of attached actions
 857       */
 858      public $actions = array();
 859  
 860      /**
 861       * @var array $params URL Params
 862       */
 863      public $params;
 864  
 865      /**
 866       * @var string Action id
 867       */
 868      public $actionid;
 869  
 870      /**
 871       * @var array
 872       */
 873      protected $attributes = [];
 874  
 875      /**
 876       * Constructor
 877       * @param moodle_url $url
 878       * @param string $label button text
 879       * @param string $method get or post submit method
 880       * @param array $attributes Attributes for the HTML button tag
 881       */
 882      public function __construct(moodle_url $url, $label, $method='post', $primary=false, $attributes = []) {
 883          $this->url    = clone($url);
 884          $this->label  = $label;
 885          $this->method = $method;
 886          $this->primary = $primary;
 887          $this->attributes = $attributes;
 888      }
 889  
 890      /**
 891       * Shortcut for adding a JS confirm dialog when the button is clicked.
 892       * The message must be a yes/no question.
 893       *
 894       * @param string $confirmmessage The yes/no confirmation question. If "Yes" is clicked, the original action will occur.
 895       */
 896      public function add_confirm_action($confirmmessage) {
 897          $this->add_action(new confirm_action($confirmmessage));
 898      }
 899  
 900      /**
 901       * Add action to the button.
 902       * @param component_action $action
 903       */
 904      public function add_action(component_action $action) {
 905          $this->actions[] = $action;
 906      }
 907  
 908      /**
 909       * Sets an attribute for the HTML button tag.
 910       *
 911       * @param  string $name  The attribute name
 912       * @param  mixed  $value The value
 913       * @return null
 914       */
 915      public function set_attribute($name, $value) {
 916          $this->attributes[$name] = $value;
 917      }
 918  
 919      /**
 920       * Export data.
 921       *
 922       * @param renderer_base $output Renderer.
 923       * @return stdClass
 924       */
 925      public function export_for_template(renderer_base $output) {
 926          $url = $this->method === 'get' ? $this->url->out_omit_querystring(true) : $this->url->out_omit_querystring();
 927  
 928          $data = new stdClass();
 929          $data->id = html_writer::random_id('single_button');
 930          $data->formid = $this->formid;
 931          $data->method = $this->method;
 932          $data->url = $url === '' ? '#' : $url;
 933          $data->label = $this->label;
 934          $data->classes = $this->class;
 935          $data->disabled = $this->disabled;
 936          $data->tooltip = $this->tooltip;
 937          $data->primary = $this->primary;
 938  
 939          $data->attributes = [];
 940          foreach ($this->attributes as $key => $value) {
 941              $data->attributes[] = ['name' => $key, 'value' => $value];
 942          }
 943  
 944          // Form parameters.
 945          $params = $this->url->params();
 946          if ($this->method === 'post') {
 947              $params['sesskey'] = sesskey();
 948          }
 949          $data->params = array_map(function($key) use ($params) {
 950              return ['name' => $key, 'value' => $params[$key]];
 951          }, array_keys($params));
 952  
 953          // Button actions.
 954          $actions = $this->actions;
 955          $data->actions = array_map(function($action) use ($output) {
 956              return $action->export_for_template($output);
 957          }, $actions);
 958          $data->hasactions = !empty($data->actions);
 959  
 960          return $data;
 961      }
 962  }
 963  
 964  
 965  /**
 966   * Simple form with just one select field that gets submitted automatically.
 967   *
 968   * If JS not enabled small go button is printed too.
 969   *
 970   * @copyright 2009 Petr Skoda
 971   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 972   * @since Moodle 2.0
 973   * @package core
 974   * @category output
 975   */
 976  class single_select implements renderable, templatable {
 977  
 978      /**
 979       * @var moodle_url Target url - includes hidden fields
 980       */
 981      var $url;
 982  
 983      /**
 984       * @var string Name of the select element.
 985       */
 986      var $name;
 987  
 988      /**
 989       * @var array $options associative array value=>label ex.: array(1=>'One, 2=>Two)
 990       *     it is also possible to specify optgroup as complex label array ex.:
 991       *         array(array('Odd'=>array(1=>'One', 3=>'Three)), array('Even'=>array(2=>'Two')))
 992       *         array(1=>'One', '--1uniquekey'=>array('More'=>array(2=>'Two', 3=>'Three')))
 993       */
 994      var $options;
 995  
 996      /**
 997       * @var string Selected option
 998       */
 999      var $selected;
1000  
1001      /**
1002       * @var array Nothing selected
1003       */
1004      var $nothing;
1005  
1006      /**
1007       * @var array Extra select field attributes
1008       */
1009      var $attributes = array();
1010  
1011      /**
1012       * @var string Button label
1013       */
1014      var $label = '';
1015  
1016      /**
1017       * @var array Button label's attributes
1018       */
1019      var $labelattributes = array();
1020  
1021      /**
1022       * @var string Form submit method post or get
1023       */
1024      var $method = 'get';
1025  
1026      /**
1027       * @var string Wrapping div class
1028       */
1029      var $class = 'singleselect';
1030  
1031      /**
1032       * @var bool True if button disabled, false if normal
1033       */
1034      var $disabled = false;
1035  
1036      /**
1037       * @var string Button tooltip
1038       */
1039      var $tooltip = null;
1040  
1041      /**
1042       * @var string Form id
1043       */
1044      var $formid = null;
1045  
1046      /**
1047       * @var help_icon The help icon for this element.
1048       */
1049      var $helpicon = null;
1050  
1051      /**
1052       * Constructor
1053       * @param moodle_url $url form action target, includes hidden fields
1054       * @param string $name name of selection field - the changing parameter in url
1055       * @param array $options list of options
1056       * @param string $selected selected element
1057       * @param array $nothing
1058       * @param string $formid
1059       */
1060      public function __construct(moodle_url $url, $name, array $options, $selected = '', $nothing = array('' => 'choosedots'), $formid = null) {
1061          $this->url      = $url;
1062          $this->name     = $name;
1063          $this->options  = $options;
1064          $this->selected = $selected;
1065          $this->nothing  = $nothing;
1066          $this->formid   = $formid;
1067      }
1068  
1069      /**
1070       * Shortcut for adding a JS confirm dialog when the button is clicked.
1071       * The message must be a yes/no question.
1072       *
1073       * @param string $confirmmessage The yes/no confirmation question. If "Yes" is clicked, the original action will occur.
1074       */
1075      public function add_confirm_action($confirmmessage) {
1076          $this->add_action(new component_action('submit', 'M.util.show_confirm_dialog', array('message' => $confirmmessage)));
1077      }
1078  
1079      /**
1080       * Add action to the button.
1081       *
1082       * @param component_action $action
1083       */
1084      public function add_action(component_action $action) {
1085          $this->actions[] = $action;
1086      }
1087  
1088      /**
1089       * Adds help icon.
1090       *
1091       * @deprecated since Moodle 2.0
1092       */
1093      public function set_old_help_icon($helppage, $title, $component = 'moodle') {
1094          throw new coding_exception('set_old_help_icon() can not be used any more, please see set_help_icon().');
1095      }
1096  
1097      /**
1098       * Adds help icon.
1099       *
1100       * @param string $identifier The keyword that defines a help page
1101       * @param string $component
1102       */
1103      public function set_help_icon($identifier, $component = 'moodle') {
1104          $this->helpicon = new help_icon($identifier, $component);
1105      }
1106  
1107      /**
1108       * Sets select's label
1109       *
1110       * @param string $label
1111       * @param array $attributes (optional)
1112       */
1113      public function set_label($label, $attributes = array()) {
1114          $this->label = $label;
1115          $this->labelattributes = $attributes;
1116  
1117      }
1118  
1119      /**
1120       * Export data.
1121       *
1122       * @param renderer_base $output Renderer.
1123       * @return stdClass
1124       */
1125      public function export_for_template(renderer_base $output) {
1126          $attributes = $this->attributes;
1127  
1128          $data = new stdClass();
1129          $data->name = $this->name;
1130          $data->method = $this->method;
1131          $data->action = $this->method === 'get' ? $this->url->out_omit_querystring(true) : $this->url->out_omit_querystring();
1132          $data->classes = $this->class;
1133          $data->label = $this->label;
1134          $data->disabled = $this->disabled;
1135          $data->title = $this->tooltip;
1136          $data->formid = !empty($this->formid) ? $this->formid : html_writer::random_id('single_select_f');
1137          $data->id = !empty($attributes['id']) ? $attributes['id'] : html_writer::random_id('single_select');
1138  
1139          // Select element attributes.
1140          // Unset attributes that are already predefined in the template.
1141          unset($attributes['id']);
1142          unset($attributes['class']);
1143          unset($attributes['name']);
1144          unset($attributes['title']);
1145          unset($attributes['disabled']);
1146  
1147          // Map the attributes.
1148          $data->attributes = array_map(function($key) use ($attributes) {
1149              return ['name' => $key, 'value' => $attributes[$key]];
1150          }, array_keys($attributes));
1151  
1152          // Form parameters.
1153          $params = $this->url->params();
1154          if ($this->method === 'post') {
1155              $params['sesskey'] = sesskey();
1156          }
1157          $data->params = array_map(function($key) use ($params) {
1158              return ['name' => $key, 'value' => $params[$key]];
1159          }, array_keys($params));
1160  
1161          // Select options.
1162          $hasnothing = false;
1163          if (is_string($this->nothing) && $this->nothing !== '') {
1164              $nothing = ['' => $this->nothing];
1165              $hasnothing = true;
1166              $nothingkey = '';
1167          } else if (is_array($this->nothing)) {
1168              $nothingvalue = reset($this->nothing);
1169              if ($nothingvalue === 'choose' || $nothingvalue === 'choosedots') {
1170                  $nothing = [key($this->nothing) => get_string('choosedots')];
1171              } else {
1172                  $nothing = $this->nothing;
1173              }
1174              $hasnothing = true;
1175              $nothingkey = key($this->nothing);
1176          }
1177          if ($hasnothing) {
1178              $options = $nothing + $this->options;
1179          } else {
1180              $options = $this->options;
1181          }
1182  
1183          foreach ($options as $value => $name) {
1184              if (is_array($options[$value])) {
1185                  foreach ($options[$value] as $optgroupname => $optgroupvalues) {
1186                      $sublist = [];
1187                      foreach ($optgroupvalues as $optvalue => $optname) {
1188                          $option = [
1189                              'value' => $optvalue,
1190                              'name' => $optname,
1191                              'selected' => strval($this->selected) === strval($optvalue),
1192                          ];
1193  
1194                          if ($hasnothing && $nothingkey === $optvalue) {
1195                              $option['ignore'] = 'data-ignore';
1196                          }
1197  
1198                          $sublist[] = $option;
1199                      }
1200                      $data->options[] = [
1201                          'name' => $optgroupname,
1202                          'optgroup' => true,
1203                          'options' => $sublist
1204                      ];
1205                  }
1206              } else {
1207                  $option = [
1208                      'value' => $value,
1209                      'name' => $options[$value],
1210                      'selected' => strval($this->selected) === strval($value),
1211                      'optgroup' => false
1212                  ];
1213  
1214                  if ($hasnothing && $nothingkey === $value) {
1215                      $option['ignore'] = 'data-ignore';
1216                  }
1217  
1218                  $data->options[] = $option;
1219              }
1220          }
1221  
1222          // Label attributes.
1223          $data->labelattributes = [];
1224          // Unset label attributes that are already in the template.
1225          unset($this->labelattributes['for']);
1226          // Map the label attributes.
1227          foreach ($this->labelattributes as $key => $value) {
1228              $data->labelattributes[] = ['name' => $key, 'value' => $value];
1229          }
1230  
1231          // Help icon.
1232          $data->helpicon = !empty($this->helpicon) ? $this->helpicon->export_for_template($output) : false;
1233  
1234          return $data;
1235      }
1236  }
1237  
1238  /**
1239   * Simple URL selection widget description.
1240   *
1241   * @copyright 2009 Petr Skoda
1242   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1243   * @since Moodle 2.0
1244   * @package core
1245   * @category output
1246   */
1247  class url_select implements renderable, templatable {
1248      /**
1249       * @var array $urls associative array value=>label ex.: array(1=>'One, 2=>Two)
1250       *     it is also possible to specify optgroup as complex label array ex.:
1251       *         array(array('Odd'=>array(1=>'One', 3=>'Three)), array('Even'=>array(2=>'Two')))
1252       *         array(1=>'One', '--1uniquekey'=>array('More'=>array(2=>'Two', 3=>'Three')))
1253       */
1254      var $urls;
1255  
1256      /**
1257       * @var string Selected option
1258       */
1259      var $selected;
1260  
1261      /**
1262       * @var array Nothing selected
1263       */
1264      var $nothing;
1265  
1266      /**
1267       * @var array Extra select field attributes
1268       */
1269      var $attributes = array();
1270  
1271      /**
1272       * @var string Button label
1273       */
1274      var $label = '';
1275  
1276      /**
1277       * @var array Button label's attributes
1278       */
1279      var $labelattributes = array();
1280  
1281      /**
1282       * @var string Wrapping div class
1283       */
1284      var $class = 'urlselect';
1285  
1286      /**
1287       * @var bool True if button disabled, false if normal
1288       */
1289      var $disabled = false;
1290  
1291      /**
1292       * @var string Button tooltip
1293       */
1294      var $tooltip = null;
1295  
1296      /**
1297       * @var string Form id
1298       */
1299      var $formid = null;
1300  
1301      /**
1302       * @var help_icon The help icon for this element.
1303       */
1304      var $helpicon = null;
1305  
1306      /**
1307       * @var string If set, makes button visible with given name for button
1308       */
1309      var $showbutton = null;
1310  
1311      /**
1312       * Constructor
1313       * @param array $urls list of options
1314       * @param string $selected selected element
1315       * @param array $nothing
1316       * @param string $formid
1317       * @param string $showbutton Set to text of button if it should be visible
1318       *   or null if it should be hidden (hidden version always has text 'go')
1319       */
1320      public function __construct(array $urls, $selected = '', $nothing = array('' => 'choosedots'), $formid = null, $showbutton = null) {
1321          $this->urls       = $urls;
1322          $this->selected   = $selected;
1323          $this->nothing    = $nothing;
1324          $this->formid     = $formid;
1325          $this->showbutton = $showbutton;
1326      }
1327  
1328      /**
1329       * Adds help icon.
1330       *
1331       * @deprecated since Moodle 2.0
1332       */
1333      public function set_old_help_icon($helppage, $title, $component = 'moodle') {
1334          throw new coding_exception('set_old_help_icon() can not be used any more, please see set_help_icon().');
1335      }
1336  
1337      /**
1338       * Adds help icon.
1339       *
1340       * @param string $identifier The keyword that defines a help page
1341       * @param string $component
1342       */
1343      public function set_help_icon($identifier, $component = 'moodle') {
1344          $this->helpicon = new help_icon($identifier, $component);
1345      }
1346  
1347      /**
1348       * Sets select's label
1349       *
1350       * @param string $label
1351       * @param array $attributes (optional)
1352       */
1353      public function set_label($label, $attributes = array()) {
1354          $this->label = $label;
1355          $this->labelattributes = $attributes;
1356      }
1357  
1358      /**
1359       * Clean a URL.
1360       *
1361       * @param string $value The URL.
1362       * @return The cleaned URL.
1363       */
1364      protected function clean_url($value) {
1365          global $CFG;
1366  
1367          if (empty($value)) {
1368              // Nothing.
1369  
1370          } else if (strpos($value, $CFG->wwwroot . '/') === 0) {
1371              $value = str_replace($CFG->wwwroot, '', $value);
1372  
1373          } else if (strpos($value, '/') !== 0) {
1374              debugging("Invalid url_select urls parameter: url '$value' is not local relative url!", DEBUG_DEVELOPER);
1375          }
1376  
1377          return $value;
1378      }
1379  
1380      /**
1381       * Flatten the options for Mustache.
1382       *
1383       * This also cleans the URLs.
1384       *
1385       * @param array $options The options.
1386       * @param array $nothing The nothing option.
1387       * @return array
1388       */
1389      protected function flatten_options($options, $nothing) {
1390          $flattened = [];
1391  
1392          foreach ($options as $value => $option) {
1393              if (is_array($option)) {
1394                  foreach ($option as $groupname => $optoptions) {
1395                      if (!isset($flattened[$groupname])) {
1396                          $flattened[$groupname] = [
1397                              'name' => $groupname,
1398                              'isgroup' => true,
1399                              'options' => []
1400                          ];
1401                      }
1402                      foreach ($optoptions as $optvalue => $optoption) {
1403                          $cleanedvalue = $this->clean_url($optvalue);
1404                          $flattened[$groupname]['options'][$cleanedvalue] = [
1405                              'name' => $optoption,
1406                              'value' => $cleanedvalue,
1407                              'selected' => $this->selected == $optvalue,
1408                          ];
1409                      }
1410                  }
1411  
1412              } else {
1413                  $cleanedvalue = $this->clean_url($value);
1414                  $flattened[$cleanedvalue] = [
1415                      'name' => $option,
1416                      'value' => $cleanedvalue,
1417                      'selected' => $this->selected == $value,
1418                  ];
1419              }
1420          }
1421  
1422          if (!empty($nothing)) {
1423              $value = key($nothing);
1424              $name = reset($nothing);
1425              $flattened = [
1426                  $value => ['name' => $name, 'value' => $value, 'selected' => $this->selected == $value]
1427              ] + $flattened;
1428          }
1429  
1430          // Make non-associative array.
1431          foreach ($flattened as $key => $value) {
1432              if (!empty($value['options'])) {
1433                  $flattened[$key]['options'] = array_values($value['options']);
1434              }
1435          }
1436          $flattened = array_values($flattened);
1437  
1438          return $flattened;
1439      }
1440  
1441      /**
1442       * Export for template.
1443       *
1444       * @param renderer_base $output Renderer.
1445       * @return stdClass
1446       */
1447      public function export_for_template(renderer_base $output) {
1448          $attributes = $this->attributes;
1449  
1450          $data = new stdClass();
1451          $data->formid = !empty($this->formid) ? $this->formid : html_writer::random_id('url_select_f');
1452          $data->classes = $this->class;
1453          $data->label = $this->label;
1454          $data->disabled = $this->disabled;
1455          $data->title = $this->tooltip;
1456          $data->id = !empty($attributes['id']) ? $attributes['id'] : html_writer::random_id('url_select');
1457          $data->sesskey = sesskey();
1458          $data->action = (new moodle_url('/course/jumpto.php'))->out(false);
1459  
1460          // Remove attributes passed as property directly.
1461          unset($attributes['class']);
1462          unset($attributes['id']);
1463          unset($attributes['name']);
1464          unset($attributes['title']);
1465          unset($attributes['disabled']);
1466  
1467          $data->showbutton = $this->showbutton;
1468  
1469          // Select options.
1470          $nothing = false;
1471          if (is_string($this->nothing) && $this->nothing !== '') {
1472              $nothing = ['' => $this->nothing];
1473          } else if (is_array($this->nothing)) {
1474              $nothingvalue = reset($this->nothing);
1475              if ($nothingvalue === 'choose' || $nothingvalue === 'choosedots') {
1476                  $nothing = [key($this->nothing) => get_string('choosedots')];
1477              } else {
1478                  $nothing = $this->nothing;
1479              }
1480          }
1481          $data->options = $this->flatten_options($this->urls, $nothing);
1482  
1483          // Label attributes.
1484          $data->labelattributes = [];
1485          // Unset label attributes that are already in the template.
1486          unset($this->labelattributes['for']);
1487          // Map the label attributes.
1488          foreach ($this->labelattributes as $key => $value) {
1489              $data->labelattributes[] = ['name' => $key, 'value' => $value];
1490          }
1491  
1492          // Help icon.
1493          $data->helpicon = !empty($this->helpicon) ? $this->helpicon->export_for_template($output) : false;
1494  
1495          // Finally all the remaining attributes.
1496          $data->attributes = [];
1497          foreach ($attributes as $key => $value) {
1498              $data->attributes[] = ['name' => $key, 'value' => $value];
1499          }
1500  
1501          return $data;
1502      }
1503  }
1504  
1505  /**
1506   * Data structure describing html link with special action attached.
1507   *
1508   * @copyright 2010 Petr Skoda
1509   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1510   * @since Moodle 2.0
1511   * @package core
1512   * @category output
1513   */
1514  class action_link implements renderable {
1515  
1516      /**
1517       * @var moodle_url Href url
1518       */
1519      public $url;
1520  
1521      /**
1522       * @var string Link text HTML fragment
1523       */
1524      public $text;
1525  
1526      /**
1527       * @var array HTML attributes
1528       */
1529      public $attributes;
1530  
1531      /**
1532       * @var array List of actions attached to link
1533       */
1534      public $actions;
1535  
1536      /**
1537       * @var pix_icon Optional pix icon to render with the link
1538       */
1539      public $icon;
1540  
1541      /**
1542       * Constructor
1543       * @param moodle_url $url
1544       * @param string $text HTML fragment
1545       * @param component_action $action
1546       * @param array $attributes associative array of html link attributes + disabled
1547       * @param pix_icon $icon optional pix_icon to render with the link text
1548       */
1549      public function __construct(moodle_url $url,
1550                                  $text,
1551                                  component_action $action=null,
1552                                  array $attributes=null,
1553                                  pix_icon $icon=null) {
1554          $this->url = clone($url);
1555          $this->text = $text;
1556          $this->attributes = (array)$attributes;
1557          if ($action) {
1558              $this->add_action($action);
1559          }
1560          $this->icon = $icon;
1561      }
1562  
1563      /**
1564       * Add action to the link.
1565       *
1566       * @param component_action $action
1567       */
1568      public function add_action(component_action $action) {
1569          $this->actions[] = $action;
1570      }
1571  
1572      /**
1573       * Adds a CSS class to this action link object
1574       * @param string $class
1575       */
1576      public function add_class($class) {
1577          if (empty($this->attributes['class'])) {
1578              $this->attributes['class'] = $class;
1579          } else {
1580              $this->attributes['class'] .= ' ' . $class;
1581          }
1582      }
1583  
1584      /**
1585       * Returns true if the specified class has been added to this link.
1586       * @param string $class
1587       * @return bool
1588       */
1589      public function has_class($class) {
1590          return strpos(' ' . $this->attributes['class'] . ' ', ' ' . $class . ' ') !== false;
1591      }
1592  
1593      /**
1594       * Return the rendered HTML for the icon. Useful for rendering action links in a template.
1595       * @return string
1596       */
1597      public function get_icon_html() {
1598          global $OUTPUT;
1599          if (!$this->icon) {
1600              return '';
1601          }
1602          return $OUTPUT->render($this->icon);
1603      }
1604  
1605      /**
1606       * Export for template.
1607       *
1608       * @param renderer_base $output The renderer.
1609       * @return stdClass
1610       */
1611      public function export_for_template(renderer_base $output) {
1612          $data = new stdClass();
1613          $attributes = $this->attributes;
1614  
1615          if (empty($attributes['id'])) {
1616              $attributes['id'] = html_writer::random_id('action_link');
1617          }
1618          $data->id = $attributes['id'];
1619          unset($attributes['id']);
1620  
1621          $data->disabled = !empty($attributes['disabled']);
1622          unset($attributes['disabled']);
1623  
1624          $data->text = $this->text instanceof renderable ? $output->render($this->text) : (string) $this->text;
1625          $data->url = $this->url ? $this->url->out(false) : '';
1626          $data->icon = $this->icon ? $this->icon->export_for_pix() : null;
1627          $data->classes = isset($attributes['class']) ? $attributes['class'] : '';
1628          unset($attributes['class']);
1629  
1630          $data->attributes = array_map(function($key, $value) {
1631              return [
1632                  'name' => $key,
1633                  'value' => $value
1634              ];
1635          }, array_keys($attributes), $attributes);
1636  
1637          $data->actions = array_map(function($action) use ($output) {
1638              return $action->export_for_template($output);
1639          }, !empty($this->actions) ? $this->actions : []);
1640          $data->hasactions = !empty($this->actions);
1641  
1642          return $data;
1643      }
1644  }
1645  
1646  /**
1647   * Simple html output class
1648   *
1649   * @copyright 2009 Tim Hunt, 2010 Petr Skoda
1650   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1651   * @since Moodle 2.0
1652   * @package core
1653   * @category output
1654   */
1655  class html_writer {
1656  
1657      /**
1658       * Outputs a tag with attributes and contents
1659       *
1660       * @param string $tagname The name of tag ('a', 'img', 'span' etc.)
1661       * @param string $contents What goes between the opening and closing tags
1662       * @param array $attributes The tag attributes (array('src' => $url, 'class' => 'class1') etc.)
1663       * @return string HTML fragment
1664       */
1665      public static function tag($tagname, $contents, array $attributes = null) {
1666          return self::start_tag($tagname, $attributes) . $contents . self::end_tag($tagname);
1667      }
1668  
1669      /**
1670       * Outputs an opening tag with attributes
1671       *
1672       * @param string $tagname The name of tag ('a', 'img', 'span' etc.)
1673       * @param array $attributes The tag attributes (array('src' => $url, 'class' => 'class1') etc.)
1674       * @return string HTML fragment
1675       */
1676      public static function start_tag($tagname, array $attributes = null) {
1677          return '<' . $tagname . self::attributes($attributes) . '>';
1678      }
1679  
1680      /**
1681       * Outputs a closing tag
1682       *
1683       * @param string $tagname The name of tag ('a', 'img', 'span' etc.)
1684       * @return string HTML fragment
1685       */
1686      public static function end_tag($tagname) {
1687          return '</' . $tagname . '>';
1688      }
1689  
1690      /**
1691       * Outputs an empty tag with attributes
1692       *
1693       * @param string $tagname The name of tag ('input', 'img', 'br' etc.)
1694       * @param array $attributes The tag attributes (array('src' => $url, 'class' => 'class1') etc.)
1695       * @return string HTML fragment
1696       */
1697      public static function empty_tag($tagname, array $attributes = null) {
1698          return '<' . $tagname . self::attributes($attributes) . ' />';
1699      }
1700  
1701      /**
1702       * Outputs a tag, but only if the contents are not empty
1703       *
1704       * @param string $tagname The name of tag ('a', 'img', 'span' etc.)
1705       * @param string $contents What goes between the opening and closing tags
1706       * @param array $attributes The tag attributes (array('src' => $url, 'class' => 'class1') etc.)
1707       * @return string HTML fragment
1708       */
1709      public static function nonempty_tag($tagname, $contents, array $attributes = null) {
1710          if ($contents === '' || is_null($contents)) {
1711              return '';
1712          }
1713          return self::tag($tagname, $contents, $attributes);
1714      }
1715  
1716      /**
1717       * Outputs a HTML attribute and value
1718       *
1719       * @param string $name The name of the attribute ('src', 'href', 'class' etc.)
1720       * @param string $value The value of the attribute. The value will be escaped with {@link s()}
1721       * @return string HTML fragment
1722       */
1723      public static function attribute($name, $value) {
1724          if ($value instanceof moodle_url) {
1725              return ' ' . $name . '="' . $value->out() . '"';
1726          }
1727  
1728          // special case, we do not want these in output
1729          if ($value === null) {
1730              return '';
1731          }
1732  
1733          // no sloppy trimming here!
1734          return ' ' . $name . '="' . s($value) . '"';
1735      }
1736  
1737      /**
1738       * Outputs a list of HTML attributes and values
1739       *
1740       * @param array $attributes The tag attributes (array('src' => $url, 'class' => 'class1') etc.)
1741       *       The values will be escaped with {@link s()}
1742       * @return string HTML fragment
1743       */
1744      public static function attributes(array $attributes = null) {
1745          $attributes = (array)$attributes;
1746          $output = '';
1747          foreach ($attributes as $name => $value) {
1748              $output .= self::attribute($name, $value);
1749          }
1750          return $output;
1751      }
1752  
1753      /**
1754       * Generates a simple image tag with attributes.
1755       *
1756       * @param string $src The source of image
1757       * @param string $alt The alternate text for image
1758       * @param array $attributes The tag attributes (array('height' => $max_height, 'class' => 'class1') etc.)
1759       * @return string HTML fragment
1760       */
1761      public static function img($src, $alt, array $attributes = null) {
1762          $attributes = (array)$attributes;
1763          $attributes['src'] = $src;
1764          $attributes['alt'] = $alt;
1765  
1766          return self::empty_tag('img', $attributes);
1767      }
1768  
1769      /**
1770       * Generates random html element id.
1771       *
1772       * @staticvar int $counter
1773       * @staticvar type $uniq
1774       * @param string $base A string fragment that will be included in the random ID.
1775       * @return string A unique ID
1776       */
1777      public static function random_id($base='random') {
1778          static $counter = 0;
1779          static $uniq;
1780  
1781          if (!isset($uniq)) {
1782              $uniq = uniqid();
1783          }
1784  
1785          $counter++;
1786          return $base.$uniq.$counter;
1787      }
1788  
1789      /**
1790       * Generates a simple html link
1791       *
1792       * @param string|moodle_url $url The URL
1793       * @param string $text The text
1794       * @param array $attributes HTML attributes
1795       * @return string HTML fragment
1796       */
1797      public static function link($url, $text, array $attributes = null) {
1798          $attributes = (array)$attributes;
1799          $attributes['href']  = $url;
1800          return self::tag('a', $text, $attributes);
1801      }
1802  
1803      /**
1804       * Generates a simple checkbox with optional label
1805       *
1806       * @param string $name The name of the checkbox
1807       * @param string $value The value of the checkbox
1808       * @param bool $checked Whether the checkbox is checked
1809       * @param string $label The label for the checkbox
1810       * @param array $attributes Any attributes to apply to the checkbox
1811       * @param array $labelattributes Any attributes to apply to the label, if present
1812       * @return string html fragment
1813       */
1814      public static function checkbox($name, $value, $checked = true, $label = '',
1815              array $attributes = null, array $labelattributes = null) {
1816          $attributes = (array) $attributes;
1817          $output = '';
1818  
1819          if ($label !== '' and !is_null($label)) {
1820              if (empty($attributes['id'])) {
1821                  $attributes['id'] = self::random_id('checkbox_');
1822              }
1823          }
1824          $attributes['type']    = 'checkbox';
1825          $attributes['value']   = $value;
1826          $attributes['name']    = $name;
1827          $attributes['checked'] = $checked ? 'checked' : null;
1828  
1829          $output .= self::empty_tag('input', $attributes);
1830  
1831          if ($label !== '' and !is_null($label)) {
1832              $labelattributes = (array) $labelattributes;
1833              $labelattributes['for'] = $attributes['id'];
1834              $output .= self::tag('label', $label, $labelattributes);
1835          }
1836  
1837          return $output;
1838      }
1839  
1840      /**
1841       * Generates a simple select yes/no form field
1842       *
1843       * @param string $name name of select element
1844       * @param bool $selected
1845       * @param array $attributes - html select element attributes
1846       * @return string HTML fragment
1847       */
1848      public static function select_yes_no($name, $selected=true, array $attributes = null) {
1849          $options = array('1'=>get_string('yes'), '0'=>get_string('no'));
1850          return self::select($options, $name, $selected, null, $attributes);
1851      }
1852  
1853      /**
1854       * Generates a simple select form field
1855       *
1856       * Note this function does HTML escaping on the optgroup labels, but not on the choice labels.
1857       *
1858       * @param array $options associative array value=>label ex.:
1859       *                array(1=>'One, 2=>Two)
1860       *              it is also possible to specify optgroup as complex label array ex.:
1861       *                array(array('Odd'=>array(1=>'One', 3=>'Three)), array('Even'=>array(2=>'Two')))
1862       *                array(1=>'One', '--1uniquekey'=>array('More'=>array(2=>'Two', 3=>'Three')))
1863       * @param string $name name of select element
1864       * @param string|array $selected value or array of values depending on multiple attribute
1865       * @param array|bool $nothing add nothing selected option, or false of not added
1866       * @param array $attributes html select element attributes
1867       * @return string HTML fragment
1868       */
1869      public static function select(array $options, $name, $selected = '', $nothing = array('' => 'choosedots'), array $attributes = null) {
1870          $attributes = (array)$attributes;
1871          if (is_array($nothing)) {
1872              foreach ($nothing as $k=>$v) {
1873                  if ($v === 'choose' or $v === 'choosedots') {
1874                      $nothing[$k] = get_string('choosedots');
1875                  }
1876              }
1877              $options = $nothing + $options; // keep keys, do not override
1878  
1879          } else if (is_string($nothing) and $nothing !== '') {
1880              // BC
1881              $options = array(''=>$nothing) + $options;
1882          }
1883  
1884          // we may accept more values if multiple attribute specified
1885          $selected = (array)$selected;
1886          foreach ($selected as $k=>$v) {
1887              $selected[$k] = (string)$v;
1888          }
1889  
1890          if (!isset($attributes['id'])) {
1891              $id = 'menu'.$name;
1892              // name may contaion [], which would make an invalid id. e.g. numeric question type editing form, assignment quickgrading
1893              $id = str_replace('[', '', $id);
1894              $id = str_replace(']', '', $id);
1895              $attributes['id'] = $id;
1896          }
1897  
1898          if (!isset($attributes['class'])) {
1899              $class = 'menu'.$name;
1900              // name may contaion [], which would make an invalid class. e.g. numeric question type editing form, assignment quickgrading
1901              $class = str_replace('[', '', $class);
1902              $class = str_replace(']', '', $class);
1903              $attributes['class'] = $class;
1904          }
1905          $attributes['class'] = 'select custom-select ' . $attributes['class']; // Add 'select' selector always.
1906  
1907          $attributes['name'] = $name;
1908  
1909          if (!empty($attributes['disabled'])) {
1910              $attributes['disabled'] = 'disabled';
1911          } else {
1912              unset($attributes['disabled']);
1913          }
1914  
1915          $output = '';
1916          foreach ($options as $value=>$label) {
1917              if (is_array($label)) {
1918                  // ignore key, it just has to be unique
1919                  $output .= self::select_optgroup(key($label), current($label), $selected);
1920              } else {
1921                  $output .= self::select_option($label, $value, $selected);
1922              }
1923          }
1924          return self::tag('select', $output, $attributes);
1925      }
1926  
1927      /**
1928       * Returns HTML to display a select box option.
1929       *
1930       * @param string $label The label to display as the option.
1931       * @param string|int $value The value the option represents
1932       * @param array $selected An array of selected options
1933       * @return string HTML fragment
1934       */
1935      private static function select_option($label, $value, array $selected) {
1936          $attributes = array();
1937          $value = (string)$value;
1938          if (in_array($value, $selected, true)) {
1939              $attributes['selected'] = 'selected';
1940          }
1941          $attributes['value'] = $value;
1942          return self::tag('option', $label, $attributes);
1943      }
1944  
1945      /**
1946       * Returns HTML to display a select box option group.
1947       *
1948       * @param string $groupname The label to use for the group
1949       * @param array $options The options in the group
1950       * @param array $selected An array of selected values.
1951       * @return string HTML fragment.
1952       */
1953      private static function select_optgroup($groupname, $options, array $selected) {
1954          if (empty($options)) {
1955              return '';
1956          }
1957          $attributes = array('label'=>$groupname);
1958          $output = '';
1959          foreach ($options as $value=>$label) {
1960              $output .= self::select_option($label, $value, $selected);
1961          }
1962          return self::tag('optgroup', $output, $attributes);
1963      }
1964  
1965      /**
1966       * This is a shortcut for making an hour selector menu.
1967       *
1968       * @param string $type The type of selector (years, months, days, hours, minutes)
1969       * @param string $name fieldname
1970       * @param int $currenttime A default timestamp in GMT
1971       * @param int $step minute spacing
1972       * @param array $attributes - html select element attributes
1973       * @return HTML fragment
1974       */
1975      public static function select_time($type, $name, $currenttime = 0, $step = 5, array $attributes = null) {
1976          global $OUTPUT;
1977  
1978          if (!$currenttime) {
1979              $currenttime = time();
1980          }
1981          $calendartype = \core_calendar\type_factory::get_calendar_instance();
1982          $currentdate = $calendartype->timestamp_to_date_array($currenttime);
1983          $userdatetype = $type;
1984          $timeunits = array();
1985  
1986          switch ($type) {
1987              case 'years':
1988                  $timeunits = $calendartype->get_years();
1989                  $userdatetype = 'year';
1990                  break;
1991              case 'months':
1992                  $timeunits = $calendartype->get_months();
1993                  $userdatetype = 'month';
1994                  $currentdate['month'] = (int)$currentdate['mon'];
1995                  break;
1996              case 'days':
1997                  $timeunits = $calendartype->get_days();
1998                  $userdatetype = 'mday';
1999                  break;
2000              case 'hours':
2001                  for ($i=0; $i<=23; $i++) {
2002                      $timeunits[$i] = sprintf("%02d",$i);
2003                  }
2004                  break;
2005              case 'minutes':
2006                  if ($step != 1) {
2007                      $currentdate['minutes'] = ceil($currentdate['minutes']/$step)*$step;
2008                  }
2009  
2010                  for ($i=0; $i<=59; $i+=$step) {
2011                      $timeunits[$i] = sprintf("%02d",$i);
2012                  }
2013                  break;
2014              default:
2015                  throw new coding_exception("Time type $type is not supported by html_writer::select_time().");
2016          }
2017  
2018          $attributes = (array) $attributes;
2019          $data = (object) [
2020              'name' => $name,
2021              'id' => !empty($attributes['id']) ? $attributes['id'] : self::random_id('ts_'),
2022              'label' => get_string(substr($type, 0, -1), 'form'),
2023              'options' => array_map(function($value) use ($timeunits, $currentdate, $userdatetype) {
2024                  return [
2025                      'name' => $timeunits[$value],
2026                      'value' => $value,
2027                      'selected' => $currentdate[$userdatetype] == $value
2028                  ];
2029              }, array_keys($timeunits)),
2030          ];
2031  
2032          unset($attributes['id']);
2033          unset($attributes['name']);
2034          $data->attributes = array_map(function($name) use ($attributes) {
2035              return [
2036                  'name' => $name,
2037                  'value' => $attributes[$name]
2038              ];
2039          }, array_keys($attributes));
2040  
2041          return $OUTPUT->render_from_template('core/select_time', $data);
2042      }
2043  
2044      /**
2045       * Shortcut for quick making of lists
2046       *
2047       * Note: 'list' is a reserved keyword ;-)
2048       *
2049       * @param array $items
2050       * @param array $attributes
2051       * @param string $tag ul or ol
2052       * @return string
2053       */
2054      public static function alist(array $items, array $attributes = null, $tag = 'ul') {
2055          $output = html_writer::start_tag($tag, $attributes)."\n";
2056          foreach ($items as $item) {
2057              $output .= html_writer::tag('li', $item)."\n";
2058          }
2059          $output .= html_writer::end_tag($tag);
2060          return $output;
2061      }
2062  
2063      /**
2064       * Returns hidden input fields created from url parameters.
2065       *
2066       * @param moodle_url $url
2067       * @param array $exclude list of excluded parameters
2068       * @return string HTML fragment
2069       */
2070      public static function input_hidden_params(moodle_url $url, array $exclude = null) {
2071          $exclude = (array)$exclude;
2072          $params = $url->params();
2073          foreach ($exclude as $key) {
2074              unset($params[$key]);
2075          }
2076  
2077          $output = '';
2078          foreach ($params as $key => $value) {
2079              $attributes = array('type'=>'hidden', 'name'=>$key, 'value'=>$value);
2080              $output .= self::empty_tag('input', $attributes)."\n";
2081          }
2082          return $output;
2083      }
2084  
2085      /**
2086       * Generate a script tag containing the the specified code.
2087       *
2088       * @param string $jscode the JavaScript code
2089       * @param moodle_url|string $url optional url of the external script, $code ignored if specified
2090       * @return string HTML, the code wrapped in <script> tags.
2091       */
2092      public static function script($jscode, $url=null) {
2093          if ($jscode) {
2094              return self::tag('script', "\n//<![CDATA[\n$jscode\n//]]>\n") . "\n";
2095  
2096          } else if ($url) {
2097              return self::tag('script', '', ['src' => $url]) . "\n";
2098  
2099          } else {
2100              return '';
2101          }
2102      }
2103  
2104      /**
2105       * Renders HTML table
2106       *
2107       * This method may modify the passed instance by adding some default properties if they are not set yet.
2108       * If this is not what you want, you should make a full clone of your data before passing them to this
2109       * method. In most cases this is not an issue at all so we do not clone by default for performance
2110       * and memory consumption reasons.
2111       *
2112       * @param html_table $table data to be rendered
2113       * @return string HTML code
2114       */
2115      public static function table(html_table $table) {
2116          // prepare table data and populate missing properties with reasonable defaults
2117          if (!empty($table->align)) {
2118              foreach ($table->align as $key => $aa) {
2119                  if ($aa) {
2120                      $table->align[$key] = 'text-align:'. fix_align_rtl($aa) .';';  // Fix for RTL languages
2121                  } else {
2122                      $table->align[$key] = null;
2123                  }
2124              }
2125          }
2126          if (!empty($table->size)) {
2127              foreach ($table->size as $key => $ss) {
2128                  if ($ss) {
2129                      $table->size[$key] = 'width:'. $ss .';';
2130                  } else {
2131                      $table->size[$key] = null;
2132                  }
2133              }
2134          }
2135          if (!empty($table->wrap)) {
2136              foreach ($table->wrap as $key => $ww) {
2137                  if ($ww) {
2138                      $table->wrap[$key] = 'white-space:nowrap;';
2139                  } else {
2140                      $table->wrap[$key] = '';
2141                  }
2142              }
2143          }
2144          if (!empty($table->head)) {
2145              foreach ($table->head as $key => $val) {
2146                  if (!isset($table->align[$key])) {
2147                      $table->align[$key] = null;
2148                  }
2149                  if (!isset($table->size[$key])) {
2150                      $table->size[$key] = null;
2151                  }
2152                  if (!isset($table->wrap[$key])) {
2153                      $table->wrap[$key] = null;
2154                  }
2155  
2156              }
2157          }
2158          if (empty($table->attributes['class'])) {
2159              $table->attributes['class'] = 'generaltable';
2160          }
2161          if (!empty($table->tablealign)) {
2162              $table->attributes['class'] .= ' boxalign' . $table->tablealign;
2163          }
2164  
2165          // explicitly assigned properties override those defined via $table->attributes
2166          $table->attributes['class'] = trim($table->attributes['class']);
2167          $attributes = array_merge($table->attributes, array(
2168                  'id'            => $table->id,
2169                  'width'         => $table->width,
2170                  'summary'       => $table->summary,
2171                  'cellpadding'   => $table->cellpadding,
2172                  'cellspacing'   => $table->cellspacing,
2173              ));
2174          $output = html_writer::start_tag('table', $attributes) . "\n";
2175  
2176          $countcols = 0;
2177  
2178          // Output a caption if present.
2179          if (!empty($table->caption)) {
2180              $captionattributes = array();
2181              if ($table->captionhide) {
2182                  $captionattributes['class'] = 'accesshide';
2183              }
2184              $output .= html_writer::tag(
2185                  'caption',
2186                  $table->caption,
2187                  $captionattributes
2188              );
2189          }
2190  
2191          if (!empty($table->head)) {
2192              $countcols = count($table->head);
2193  
2194              $output .= html_writer::start_tag('thead', array()) . "\n";
2195              $output .= html_writer::start_tag('tr', array()) . "\n";
2196              $keys = array_keys($table->head);
2197              $lastkey = end($keys);
2198  
2199              foreach ($table->head as $key => $heading) {
2200                  // Convert plain string headings into html_table_cell objects
2201                  if (!($heading instanceof html_table_cell)) {
2202                      $headingtext = $heading;
2203                      $heading = new html_table_cell();
2204                      $heading->text = $headingtext;
2205                      $heading->header = true;
2206                  }
2207  
2208                  if ($heading->header !== false) {
2209                      $heading->header = true;
2210                  }
2211  
2212                  $tagtype = 'td';
2213                  if ($heading->header && (string)$heading->text != '') {
2214                      $tagtype = 'th';
2215                  }
2216  
2217                  $heading->attributes['class'] .= ' header c' . $key;
2218                  if (isset($table->headspan[$key]) && $table->headspan[$key] > 1) {
2219                      $heading->colspan = $table->headspan[$key];
2220                      $countcols += $table->headspan[$key] - 1;
2221                  }
2222  
2223                  if ($key == $lastkey) {
2224                      $heading->attributes['class'] .= ' lastcol';
2225                  }
2226                  if (isset($table->colclasses[$key])) {
2227                      $heading->attributes['class'] .= ' ' . $table->colclasses[$key];
2228                  }
2229                  $heading->attributes['class'] = trim($heading->attributes['class']);
2230                  $attributes = array_merge($heading->attributes, [
2231                      'style'     => $table->align[$key] . $table->size[$key] . $heading->style,
2232                      'colspan'   => $heading->colspan,
2233                  ]);
2234  
2235                  if ($tagtype == 'th') {
2236                      $attributes['scope'] = !empty($heading->scope) ? $heading->scope : 'col';
2237                  }
2238  
2239                  $output .= html_writer::tag($tagtype, $heading->text, $attributes) . "\n";
2240              }
2241              $output .= html_writer::end_tag('tr') . "\n";
2242              $output .= html_writer::end_tag('thead') . "\n";
2243  
2244              if (empty($table->data)) {
2245                  // For valid XHTML strict every table must contain either a valid tr
2246                  // or a valid tbody... both of which must contain a valid td
2247                  $output .= html_writer::start_tag('tbody', array('class' => 'empty'));
2248                  $output .= html_writer::tag('tr', html_writer::tag('td', '', array('colspan'=>count($table->head))));
2249                  $output .= html_writer::end_tag('tbody');
2250              }
2251          }
2252  
2253          if (!empty($table->data)) {
2254              $keys       = array_keys($table->data);
2255              $lastrowkey = end($keys);
2256              $output .= html_writer::start_tag('tbody', array());
2257  
2258              foreach ($table->data as $key => $row) {
2259                  if (($row === 'hr') && ($countcols)) {
2260                      $output .= html_writer::tag('td', html_writer::tag('div', '', array('class' => 'tabledivider')), array('colspan' => $countcols));
2261                  } else {
2262                      // Convert array rows to html_table_rows and cell strings to html_table_cell objects
2263                      if (!($row instanceof html_table_row)) {
2264                          $newrow = new html_table_row();
2265  
2266                          foreach ($row as $cell) {
2267                              if (!($cell instanceof html_table_cell)) {
2268                                  $cell = new html_table_cell($cell);
2269                              }
2270                              $newrow->cells[] = $cell;
2271                          }
2272                          $row = $newrow;
2273                      }
2274  
2275                      if (isset($table->rowclasses[$key])) {
2276                          $row->attributes['class'] .= ' ' . $table->rowclasses[$key];
2277                      }
2278  
2279                      if ($key == $lastrowkey) {
2280                          $row->attributes['class'] .= ' lastrow';
2281                      }
2282  
2283                      // Explicitly assigned properties should override those defined in the attributes.
2284                      $row->attributes['class'] = trim($row->attributes['class']);
2285                      $trattributes = array_merge($row->attributes, array(
2286                              'id'            => $row->id,
2287                              'style'         => $row->style,
2288                          ));
2289                      $output .= html_writer::start_tag('tr', $trattributes) . "\n";
2290                      $keys2 = array_keys($row->cells);
2291                      $lastkey = end($keys2);
2292  
2293                      $gotlastkey = false; //flag for sanity checking
2294                      foreach ($row->cells as $key => $cell) {
2295                          if ($gotlastkey) {
2296                              //This should never happen. Why do we have a cell after the last cell?
2297                              mtrace("A cell with key ($key) was found after the last key ($lastkey)");
2298                          }
2299  
2300                          if (!($cell instanceof html_table_cell)) {
2301                              $mycell = new html_table_cell();
2302                              $mycell->text = $cell;
2303                              $cell = $mycell;
2304                          }
2305  
2306                          if (($cell->header === true) && empty($cell->scope)) {
2307                              $cell->scope = 'row';
2308                          }
2309  
2310                          if (isset($table->colclasses[$key])) {
2311                              $cell->attributes['class'] .= ' ' . $table->colclasses[$key];
2312                          }
2313  
2314                          $cell->attributes['class'] .= ' cell c' . $key;
2315                          if ($key == $lastkey) {
2316                              $cell->attributes['class'] .= ' lastcol';
2317                              $gotlastkey = true;
2318                          }
2319                          $tdstyle = '';
2320                          $tdstyle .= isset($table->align[$key]) ? $table->align[$key] : '';
2321                          $tdstyle .= isset($table->size[$key]) ? $table->size[$key] : '';
2322                          $tdstyle .= isset($table->wrap[$key]) ? $table->wrap[$key] : '';
2323                          $cell->attributes['class'] = trim($cell->attributes['class']);
2324                          $tdattributes = array_merge($cell->attributes, array(
2325                                  'style' => $tdstyle . $cell->style,
2326                                  'colspan' => $cell->colspan,
2327                                  'rowspan' => $cell->rowspan,
2328                                  'id' => $cell->id,
2329                                  'abbr' => $cell->abbr,
2330                                  'scope' => $cell->scope,
2331                              ));
2332                          $tagtype = 'td';
2333                          if ($cell->header === true) {
2334                              $tagtype = 'th';
2335                          }
2336                          $output .= html_writer::tag($tagtype, $cell->text, $tdattributes) . "\n";
2337                      }
2338                  }
2339                  $output .= html_writer::end_tag('tr') . "\n";
2340              }
2341              $output .= html_writer::end_tag('tbody') . "\n";
2342          }
2343          $output .= html_writer::end_tag('table') . "\n";
2344  
2345          return $output;
2346      }
2347  
2348      /**
2349       * Renders form element label
2350       *
2351       * By default, the label is suffixed with a label separator defined in the
2352       * current language pack (colon by default in the English lang pack).
2353       * Adding the colon can be explicitly disabled if needed. Label separators
2354       * are put outside the label tag itself so they are not read by
2355       * screenreaders (accessibility).
2356       *
2357       * Parameter $for explicitly associates the label with a form control. When
2358       * set, the value of this attribute must be the same as the value of
2359       * the id attribute of the form control in the same document. When null,
2360       * the label being defined is associated with the control inside the label
2361       * element.
2362       *
2363       * @param string $text content of the label tag
2364       * @param string|null $for id of the element this label is associated with, null for no association
2365       * @param bool $colonize add label separator (colon) to the label text, if it is not there yet
2366       * @param array $attributes to be inserted in the tab, for example array('accesskey' => 'a')
2367       * @return string HTML of the label element
2368       */
2369      public static function label($text, $for, $colonize = true, array $attributes=array()) {
2370          if (!is_null($for)) {
2371              $attributes = array_merge($attributes, array('for' => $for));
2372          }
2373          $text = trim($text);
2374          $label = self::tag('label', $text, $attributes);
2375  
2376          // TODO MDL-12192 $colonize disabled for now yet
2377          // if (!empty($text) and $colonize) {
2378          //     // the $text may end with the colon already, though it is bad string definition style
2379          //     $colon = get_string('labelsep', 'langconfig');
2380          //     if (!empty($colon)) {
2381          //         $trimmed = trim($colon);
2382          //         if ((substr($text, -strlen($trimmed)) == $trimmed) or (substr($text, -1) == ':')) {
2383          //             //debugging('The label text should not end with colon or other label separator,
2384          //             //           please fix the string definition.', DEBUG_DEVELOPER);
2385          //         } else {
2386          //             $label .= $colon;
2387          //         }
2388          //     }
2389          // }
2390  
2391          return $label;
2392      }
2393  
2394      /**
2395       * Combines a class parameter with other attributes. Aids in code reduction
2396       * because the class parameter is very frequently used.
2397       *
2398       * If the class attribute is specified both in the attributes and in the
2399       * class parameter, the two values are combined with a space between.
2400       *
2401       * @param string $class Optional CSS class (or classes as space-separated list)
2402       * @param array $attributes Optional other attributes as array
2403       * @return array Attributes (or null if still none)
2404       */
2405      private static function add_class($class = '', array $attributes = null) {
2406          if ($class !== '') {
2407              $classattribute = array('class' => $class);
2408              if ($attributes) {
2409                  if (array_key_exists('class', $attributes)) {
2410                      $attributes['class'] = trim($attributes['class'] . ' ' . $class);
2411                  } else {
2412                      $attributes = $classattribute + $attributes;
2413                  }
2414              } else {
2415                  $attributes = $classattribute;
2416              }
2417          }
2418          return $attributes;
2419      }
2420  
2421      /**
2422       * Creates a <div> tag. (Shortcut function.)
2423       *
2424       * @param string $content HTML content of tag
2425       * @param string $class Optional CSS class (or classes as space-separated list)
2426       * @param array $attributes Optional other attributes as array
2427       * @return string HTML code for div
2428       */
2429      public static function div($content, $class = '', array $attributes = null) {
2430          return self::tag('div', $content, self::add_class($class, $attributes));
2431      }
2432  
2433      /**
2434       * Starts a <div> tag. (Shortcut function.)
2435       *
2436       * @param string $class Optional CSS class (or classes as space-separated list)
2437       * @param array $attributes Optional other attributes as array
2438       * @return string HTML code for open div tag
2439       */
2440      public static function start_div($class = '', array $attributes = null) {
2441          return self::start_tag('div', self::add_class($class, $attributes));
2442      }
2443  
2444      /**
2445       * Ends a <div> tag. (Shortcut function.)
2446       *
2447       * @return string HTML code for close div tag
2448       */
2449      public static function end_div() {
2450          return self::end_tag('div');
2451      }
2452  
2453      /**
2454       * Creates a <span> tag. (Shortcut function.)
2455       *
2456       * @param string $content HTML content of tag
2457       * @param string $class Optional CSS class (or classes as space-separated list)
2458       * @param array $attributes Optional other attributes as array
2459       * @return string HTML code for span
2460       */
2461      public static function span($content, $class = '', array $attributes = null) {
2462          return self::tag('span', $content, self::add_class($class, $attributes));
2463      }
2464  
2465      /**
2466       * Starts a <span> tag. (Shortcut function.)
2467       *
2468       * @param string $class Optional CSS class (or classes as space-separated list)
2469       * @param array $attributes Optional other attributes as array
2470       * @return string HTML code for open span tag
2471       */
2472      public static function start_span($class = '', array $attributes = null) {
2473          return self::start_tag('span', self::add_class($class, $attributes));
2474      }
2475  
2476      /**
2477       * Ends a <span> tag. (Shortcut function.)
2478       *
2479       * @return string HTML code for close span tag
2480       */
2481      public static function end_span() {
2482          return self::end_tag('span');
2483      }
2484  }
2485  
2486  /**
2487   * Simple javascript output class
2488   *
2489   * @copyright 2010 Petr Skoda
2490   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2491   * @since Moodle 2.0
2492   * @package core
2493   * @category output
2494   */
2495  class js_writer {
2496  
2497      /**
2498       * Returns javascript code calling the function
2499       *
2500       * @param string $function function name, can be complex like Y.Event.purgeElement
2501       * @param array $arguments parameters
2502       * @param int $delay execution delay in seconds
2503       * @return string JS code fragment
2504       */
2505      public static function function_call($function, array $arguments = null, $delay=0) {
2506          if ($arguments) {
2507              $arguments = array_map('json_encode', convert_to_array($arguments));
2508              $arguments = implode(', ', $arguments);
2509          } else {
2510              $arguments = '';
2511          }
2512          $js = "$function($arguments);";
2513  
2514          if ($delay) {
2515              $delay = $delay * 1000; // in miliseconds
2516              $js = "setTimeout(function() { $js }, $delay);";
2517          }
2518          return $js . "\n";
2519      }
2520  
2521      /**
2522       * Special function which adds Y as first argument of function call.
2523       *
2524       * @param string $function The function to call
2525       * @param array $extraarguments Any arguments to pass to it
2526       * @return string Some JS code
2527       */
2528      public static function function_call_with_Y($function, array $extraarguments = null) {
2529          if ($extraarguments) {
2530              $extraarguments = array_map('json_encode', convert_to_array($extraarguments));
2531              $arguments = 'Y, ' . implode(', ', $extraarguments);
2532          } else {
2533              $arguments = 'Y';
2534          }
2535          return "$function($arguments);\n";
2536      }
2537  
2538      /**
2539       * Returns JavaScript code to initialise a new object
2540       *
2541       * @param string $var If it is null then no var is assigned the new object.
2542       * @param string $class The class to initialise an object for.
2543       * @param array $arguments An array of args to pass to the init method.
2544       * @param array $requirements Any modules required for this class.
2545       * @param int $delay The delay before initialisation. 0 = no delay.
2546       * @return string Some JS code
2547       */
2548      public static function object_init($var, $class, array $arguments = null, array $requirements = null, $delay=0) {
2549          if (is_array($arguments)) {
2550              $arguments = array_map('json_encode', convert_to_array($arguments));
2551              $arguments = implode(', ', $arguments);
2552          }
2553  
2554          if ($var === null) {
2555              $js = "new $class(Y, $arguments);";
2556          } else if (strpos($var, '.')!==false) {
2557              $js = "$var = new $class(Y, $arguments);";
2558          } else {
2559              $js = "var $var = new $class(Y, $arguments);";
2560          }
2561  
2562          if ($delay) {
2563              $delay = $delay * 1000; // in miliseconds
2564              $js = "setTimeout(function() { $js }, $delay);";
2565          }
2566  
2567          if (count($requirements) > 0) {
2568              $requirements = implode("', '", $requirements);
2569              $js = "Y.use('$requirements', function(Y){ $js });";
2570          }
2571          return $js."\n";
2572      }
2573  
2574      /**
2575       * Returns code setting value to variable
2576       *
2577       * @param string $name
2578       * @param mixed $value json serialised value
2579       * @param bool $usevar add var definition, ignored for nested properties
2580       * @return string JS code fragment
2581       */
2582      public static function set_variable($name, $value, $usevar = true) {
2583          $output = '';
2584  
2585          if ($usevar) {
2586              if (strpos($name, '.')) {
2587                  $output .= '';
2588              } else {
2589                  $output .= 'var ';
2590              }
2591          }
2592  
2593          $output .= "$name = ".json_encode($value).";";
2594  
2595          return $output;
2596      }
2597  
2598      /**
2599       * Writes event handler attaching code
2600       *
2601       * @param array|string $selector standard YUI selector for elements, may be
2602       *     array or string, element id is in the form "#idvalue"
2603       * @param string $event A valid DOM event (click, mousedown, change etc.)
2604       * @param string $function The name of the function to call
2605       * @param array $arguments An optional array of argument parameters to pass to the function
2606       * @return string JS code fragment
2607       */
2608      public static function event_handler($selector, $event, $function, array $arguments = null) {
2609          $selector = json_encode($selector);
2610          $output = "Y.on('$event', $function, $selector, null";
2611          if (!empty($arguments)) {
2612              $output .= ', ' . json_encode($arguments);
2613          }
2614          return $output . ");\n";
2615      }
2616  }
2617  
2618  /**
2619   * Holds all the information required to render a <table> by {@link core_renderer::table()}
2620   *
2621   * Example of usage:
2622   * $t = new html_table();
2623   * ... // set various properties of the object $t as described below
2624   * echo html_writer::table($t);
2625   *
2626   * @copyright 2009 David Mudrak <david.mudrak@gmail.com>
2627   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2628   * @since Moodle 2.0
2629   * @package core
2630   * @category output
2631   */
2632  class html_table {
2633  
2634      /**
2635       * @var string Value to use for the id attribute of the table
2636       */
2637      public $id = null;
2638  
2639      /**
2640       * @var array Attributes of HTML attributes for the <table> element
2641       */
2642      public $attributes = array();
2643  
2644      /**
2645       * @var array An array of headings. The n-th array item is used as a heading of the n-th column.
2646       * For more control over the rendering of the headers, an array of html_table_cell objects
2647       * can be passed instead of an array of strings.
2648       *
2649       * Example of usage:
2650       * $t->head = array('Student', 'Grade');
2651       */
2652      public $head;
2653  
2654      /**
2655       * @var array An array that can be used to make a heading span multiple columns.
2656       * In this example, {@link html_table:$data} is supposed to have three columns. For the first two columns,
2657       * the same heading is used. Therefore, {@link html_table::$head} should consist of two items.
2658       *
2659       * Example of usage:
2660       * $t->headspan = array(2,1);
2661       */
2662      public $headspan;
2663  
2664      /**
2665       * @var array An array of column alignments.
2666       * The value is used as CSS 'text-align' property. Therefore, possible
2667       * values are 'left', 'right', 'center' and 'justify'. Specify 'right' or 'left' from the perspective
2668       * of a left-to-right (LTR) language. For RTL, the values are flipped automatically.
2669       *
2670       * Examples of usage:
2671       * $t->align = array(null, 'right');
2672       * or
2673       * $t->align[1] = 'right';
2674       */
2675      public $align;
2676  
2677      /**
2678       * @var array The value is used as CSS 'size' property.
2679       *
2680       * Examples of usage:
2681       * $t->size = array('50%', '50%');
2682       * or
2683       * $t->size[1] = '120px';
2684       */
2685      public $size;
2686  
2687      /**
2688       * @var array An array of wrapping information.
2689       * The only possible value is 'nowrap' that sets the
2690       * CSS property 'white-space' to the value 'nowrap' in the given column.
2691       *
2692       * Example of usage:
2693       * $t->wrap = array(null, 'nowrap');
2694       */
2695      public $wrap;
2696  
2697      /**
2698       * @var array Array of arrays or html_table_row objects containing the data. Alternatively, if you have
2699       * $head specified, the string 'hr' (for horizontal ruler) can be used
2700       * instead of an array of cells data resulting in a divider rendered.
2701       *
2702       * Example of usage with array of arrays:
2703       * $row1 = array('Harry Potter', '76 %');
2704       * $row2 = array('Hermione Granger', '100 %');
2705       * $t->data = array($row1, $row2);
2706       *
2707       * Example with array of html_table_row objects: (used for more fine-grained control)
2708       * $cell1 = new html_table_cell();
2709       * $cell1->text = 'Harry Potter';
2710       * $cell1->colspan = 2;
2711       * $row1 = new html_table_row();
2712       * $row1->cells[] = $cell1;
2713       * $cell2 = new html_table_cell();
2714       * $cell2->text = 'Hermione Granger';
2715       * $cell3 = new html_table_cell();
2716       * $cell3->text = '100 %';
2717       * $row2 = new html_table_row();
2718       * $row2->cells = array($cell2, $cell3);
2719       * $t->data = array($row1, $row2);
2720       */
2721      public $data = [];
2722  
2723      /**
2724       * @deprecated since Moodle 2.0. Styling should be in the CSS.
2725       * @var string Width of the table, percentage of the page preferred.
2726       */
2727      public $width = null;
2728  
2729      /**
2730       * @deprecated since Moodle 2.0. Styling should be in the CSS.
2731       * @var string Alignment for the whole table. Can be 'right', 'left' or 'center' (default).
2732       */
2733      public $tablealign = null;
2734  
2735      /**
2736       * @deprecated since Moodle 2.0. Styling should be in the CSS.
2737       * @var int Padding on each cell, in pixels
2738       */
2739      public $cellpadding = null;
2740  
2741      /**
2742       * @var int Spacing between cells, in pixels
2743       * @deprecated since Moodle 2.0. Styling should be in the CSS.
2744       */
2745      public $cellspacing = null;
2746  
2747      /**
2748       * @var array Array of classes to add to particular rows, space-separated string.
2749       * Class 'lastrow' is added automatically for the last row in the table.
2750       *
2751       * Example of usage:
2752       * $t->rowclasses[9] = 'tenth'
2753       */
2754      public $rowclasses;
2755  
2756      /**
2757       * @var array An array of classes to add to every cell in a particular column,
2758       * space-separated string. Class 'cell' is added automatically by the renderer.
2759       * Classes 'c0' or 'c1' are added automatically for every odd or even column,
2760       * respectively. Class 'lastcol' is added automatically for all last cells
2761       * in a row.
2762       *
2763       * Example of usage:
2764       * $t->colclasses = array(null, 'grade');
2765       */
2766      public $colclasses;
2767  
2768      /**
2769       * @var string Description of the contents for screen readers.
2770       *
2771       * The "summary" attribute on the "table" element is not supported in HTML5.
2772       * Consider describing the structure of the table in a "caption" element or in a "figure" element containing the table;
2773       * or, simplify the structure of the table so that no description is needed.
2774       *
2775       * @deprecated since Moodle 3.9.
2776       */
2777      public $summary;
2778  
2779      /**
2780       * @var string Caption for the table, typically a title.
2781       *
2782       * Example of usage:
2783       * $t->caption = "TV Guide";
2784       */
2785      public $caption;
2786  
2787      /**
2788       * @var bool Whether to hide the table's caption from sighted users.
2789       *
2790       * Example of usage:
2791       * $t->caption = "TV Guide";
2792       * $t->captionhide = true;
2793       */
2794      public $captionhide = false;
2795  
2796      /**
2797       * Constructor
2798       */
2799      public function __construct() {
2800          $this->attributes['class'] = '';
2801      }
2802  }
2803  
2804  /**
2805   * Component representing a table row.
2806   *
2807   * @copyright 2009 Nicolas Connault
2808   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2809   * @since Moodle 2.0
2810   * @package core
2811   * @category output
2812   */
2813  class html_table_row {
2814  
2815      /**
2816       * @var string Value to use for the id attribute of the row.
2817       */
2818      public $id = null;
2819  
2820      /**
2821       * @var array Array of html_table_cell objects
2822       */
2823      public $cells = array();
2824  
2825      /**
2826       * @var string Value to use for the style attribute of the table row
2827       */
2828      public $style = null;
2829  
2830      /**
2831       * @var array Attributes of additional HTML attributes for the <tr> element
2832       */
2833      public $attributes = array();
2834  
2835      /**
2836       * Constructor
2837       * @param array $cells
2838       */
2839      public function __construct(array $cells=null) {
2840          $this->attributes['class'] = '';
2841          $cells = (array)$cells;
2842          foreach ($cells as $cell) {
2843              if ($cell instanceof html_table_cell) {
2844                  $this->cells[] = $cell;
2845              } else {
2846                  $this->cells[] = new html_table_cell($cell);
2847              }
2848          }
2849      }
2850  }
2851  
2852  /**
2853   * Component representing a table cell.
2854   *
2855   * @copyright 2009 Nicolas Connault
2856   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2857   * @since Moodle 2.0
2858   * @package core
2859   * @category output
2860   */
2861  class html_table_cell {
2862  
2863      /**
2864       * @var string Value to use for the id attribute of the cell.
2865       */
2866      public $id = null;
2867  
2868      /**
2869       * @var string The contents of the cell.
2870       */
2871      public $text;
2872  
2873      /**
2874       * @var string Abbreviated version of the contents of the cell.
2875       */
2876      public $abbr = null;
2877  
2878      /**
2879       * @var int Number of columns this cell should span.
2880       */
2881      public $colspan = null;
2882  
2883      /**
2884       * @var int Number of rows this cell should span.
2885       */
2886      public $rowspan = null;
2887  
2888      /**
2889       * @var string Defines a way to associate header cells and data cells in a table.
2890       */
2891      public $scope = null;
2892  
2893      /**
2894       * @var bool Whether or not this cell is a header cell.
2895       */
2896      public $header = null;
2897  
2898      /**
2899       * @var string Value to use for the style attribute of the table cell
2900       */
2901      public $style = null;
2902  
2903      /**
2904       * @var array Attributes of additional HTML attributes for the <td> element
2905       */
2906      public $attributes = array();
2907  
2908      /**
2909       * Constructs a table cell
2910       *
2911       * @param string $text
2912       */
2913      public function __construct($text = null) {
2914          $this->text = $text;
2915          $this->attributes['class'] = '';
2916      }
2917  }
2918  
2919  /**
2920   * Component representing a paging bar.
2921   *
2922   * @copyright 2009 Nicolas Connault
2923   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2924   * @since Moodle 2.0
2925   * @package core
2926   * @category output
2927   */
2928  class paging_bar implements renderable, templatable {
2929  
2930      /**
2931       * @var int The maximum number of pagelinks to display.
2932       */
2933      public $maxdisplay = 18;
2934  
2935      /**
2936       * @var int The total number of entries to be pages through..
2937       */
2938      public $totalcount;
2939  
2940      /**
2941       * @var int The page you are currently viewing.
2942       */
2943      public $page;
2944  
2945      /**
2946       * @var int The number of entries that should be shown per page.
2947       */
2948      public $perpage;
2949  
2950      /**
2951       * @var string|moodle_url If this  is a string then it is the url which will be appended with $pagevar,
2952       * an equals sign and the page number.
2953       * If this is a moodle_url object then the pagevar param will be replaced by
2954       * the page no, for each page.
2955       */
2956      public $baseurl;
2957  
2958      /**
2959       * @var string This is the variable name that you use for the pagenumber in your
2960       * code (ie. 'tablepage', 'blogpage', etc)
2961       */
2962      public $pagevar;
2963  
2964      /**
2965       * @var string A HTML link representing the "previous" page.
2966       */
2967      public $previouslink = null;
2968  
2969      /**
2970       * @var string A HTML link representing the "next" page.
2971       */
2972      public $nextlink = null;
2973  
2974      /**
2975       * @var string A HTML link representing the first page.
2976       */
2977      public $firstlink = null;
2978  
2979      /**
2980       * @var string A HTML link representing the last page.
2981       */
2982      public $lastlink = null;
2983  
2984      /**
2985       * @var array An array of strings. One of them is just a string: the current page
2986       */
2987      public $pagelinks = array();
2988  
2989      /**
2990       * Constructor paging_bar with only the required params.
2991       *
2992       * @param int $totalcount The total number of entries available to be paged through
2993       * @param int $page The page you are currently viewing
2994       * @param int $perpage The number of entries that should be shown per page
2995       * @param string|moodle_url $baseurl url of the current page, the $pagevar parameter is added
2996       * @param string $pagevar name of page parameter that holds the page number
2997       */
2998      public function __construct($totalcount, $page, $perpage, $baseurl, $pagevar = 'page') {
2999          $this->totalcount = $totalcount;
3000          $this->page       = $page;
3001          $this->perpage    = $perpage;
3002          $this->baseurl    = $baseurl;
3003          $this->pagevar    = $pagevar;
3004      }
3005  
3006      /**
3007       * Prepares the paging bar for output.
3008       *
3009       * This method validates the arguments set up for the paging bar and then
3010       * produces fragments of HTML to assist display later on.
3011       *
3012       * @param renderer_base $output
3013       * @param moodle_page $page
3014       * @param string $target
3015       * @throws coding_exception
3016       */
3017      public function prepare(renderer_base $output, moodle_page $page, $target) {
3018          if (!isset($this->totalcount) || is_null($this->totalcount)) {
3019              throw new coding_exception('paging_bar requires a totalcount value.');
3020          }
3021          if (!isset($this->page) || is_null($this->page)) {
3022              throw new coding_exception('paging_bar requires a page value.');
3023          }
3024          if (empty($this->perpage)) {
3025              throw new coding_exception('paging_bar requires a perpage value.');
3026          }
3027          if (empty($this->baseurl)) {
3028              throw new coding_exception('paging_bar requires a baseurl value.');
3029          }
3030  
3031          if ($this->totalcount > $this->perpage) {
3032              $pagenum = $this->page - 1;
3033  
3034              if ($this->page > 0) {
3035                  $this->previouslink = html_writer::link(new moodle_url($this->baseurl, array($this->pagevar=>$pagenum)), get_string('previous'), array('class'=>'previous'));
3036              }
3037  
3038              if ($this->perpage > 0) {
3039                  $lastpage = ceil($this->totalcount / $this->perpage);
3040              } else {
3041                  $lastpage = 1;
3042              }
3043  
3044              if ($this->page > round(($this->maxdisplay/3)*2)) {
3045                  $currpage = $this->page - round($this->maxdisplay/3);
3046  
3047                  $this->firstlink = html_writer::link(new moodle_url($this->baseurl, array($this->pagevar=>0)), '1', array('class'=>'first'));
3048              } else {
3049                  $currpage = 0;
3050              }
3051  
3052              $displaycount = $displaypage = 0;
3053  
3054              while ($displaycount < $this->maxdisplay and $currpage < $lastpage) {
3055                  $displaypage = $currpage + 1;
3056  
3057                  if ($this->page == $currpage) {
3058                      $this->pagelinks[] = html_writer::span($displaypage, 'current-page');
3059                  } else {
3060                      $pagelink = html_writer::link(new moodle_url($this->baseurl, array($this->pagevar=>$currpage)), $displaypage);
3061                      $this->pagelinks[] = $pagelink;
3062                  }
3063  
3064                  $displaycount++;
3065                  $currpage++;
3066              }
3067  
3068              if ($currpage < $lastpage) {
3069                  $lastpageactual = $lastpage - 1;
3070                  $this->lastlink = html_writer::link(new moodle_url($this->baseurl, array($this->pagevar=>$lastpageactual)), $lastpage, array('class'=>'last'));
3071              }
3072  
3073              $pagenum = $this->page + 1;
3074  
3075              if ($pagenum != $lastpage) {
3076                  $this->nextlink = html_writer::link(new moodle_url($this->baseurl, array($this->pagevar=>$pagenum)), get_string('next'), array('class'=>'next'));
3077              }
3078          }
3079      }
3080  
3081      /**
3082       * Export for template.
3083       *
3084       * @param renderer_base $output The renderer.
3085       * @return stdClass
3086       */
3087      public function export_for_template(renderer_base $output) {
3088          $data = new stdClass();
3089          $data->previous = null;
3090          $data->next = null;
3091          $data->first = null;
3092          $data->last = null;
3093          $data->label = get_string('page');
3094          $data->pages = [];
3095          $data->haspages = $this->totalcount > $this->perpage;
3096          $data->pagesize = $this->perpage;
3097  
3098          if (!$data->haspages) {
3099              return $data;
3100          }
3101  
3102          if ($this->page > 0) {
3103              $data->previous = [
3104                  'page' => $this->page,
3105                  'url' => (new moodle_url($this->baseurl, [$this->pagevar => $this->page - 1]))->out(false)
3106              ];
3107          }
3108  
3109          $currpage = 0;
3110          if ($this->page > round(($this->maxdisplay / 3) * 2)) {
3111              $currpage = $this->page - round($this->maxdisplay / 3);
3112              $data->first = [
3113                  'page' => 1,
3114                  'url' => (new moodle_url($this->baseurl, [$this->pagevar => 0]))->out(false)
3115              ];
3116          }
3117  
3118          $lastpage = 1;
3119          if ($this->perpage > 0) {
3120              $lastpage = ceil($this->totalcount / $this->perpage);
3121          }
3122  
3123          $displaycount = 0;
3124          $displaypage = 0;
3125          while ($displaycount < $this->maxdisplay and $currpage < $lastpage) {
3126              $displaypage = $currpage + 1;
3127  
3128              $iscurrent = $this->page == $currpage;
3129              $link = new moodle_url($this->baseurl, [$this->pagevar => $currpage]);
3130  
3131              $data->pages[] = [
3132                  'page' => $displaypage,
3133                  'active' => $iscurrent,
3134                  'url' => $iscurrent ? null : $link->out(false)
3135              ];
3136  
3137              $displaycount++;
3138              $currpage++;
3139          }
3140  
3141          if ($currpage < $lastpage) {
3142              $data->last = [
3143                  'page' => $lastpage,
3144                  'url' => (new moodle_url($this->baseurl, [$this->pagevar => $lastpage - 1]))->out(false)
3145              ];
3146          }
3147  
3148          if ($this->page + 1 != $lastpage) {
3149              $data->next = [
3150                  'page' => $this->page + 2,
3151                  'url' => (new moodle_url($this->baseurl, [$this->pagevar => $this->page + 1]))->out(false)
3152              ];
3153          }
3154  
3155          return $data;
3156      }
3157  }
3158  
3159  /**
3160   * Component representing initials bar.
3161   *
3162   * @copyright 2017 Ilya Tregubov
3163   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3164   * @since Moodle 3.3
3165   * @package core
3166   * @category output
3167   */
3168  class initials_bar implements renderable, templatable {
3169  
3170      /**
3171       * @var string Currently selected letter.
3172       */
3173      public $current;
3174  
3175      /**
3176       * @var string Class name to add to this initial bar.
3177       */
3178      public $class;
3179  
3180      /**
3181       * @var string The name to put in front of this initial bar.
3182       */
3183      public $title;
3184  
3185      /**
3186       * @var string URL parameter name for this initial.
3187       */
3188      public $urlvar;
3189  
3190      /**
3191       * @var string URL object.
3192       */
3193      public $url;
3194  
3195      /**
3196       * @var array An array of letters in the alphabet.
3197       */
3198      public $alpha;
3199  
3200      /**
3201       * Constructor initials_bar with only the required params.
3202       *
3203       * @param string $current the currently selected letter.
3204       * @param string $class class name to add to this initial bar.
3205       * @param string $title the name to put in front of this initial bar.
3206       * @param string $urlvar URL parameter name for this initial.
3207       * @param string $url URL object.
3208       * @param array $alpha of letters in the alphabet.
3209       */
3210      public function __construct($current, $class, $title, $urlvar, $url, $alpha = null) {
3211          $this->current       = $current;
3212          $this->class    = $class;
3213          $this->title    = $title;
3214          $this->urlvar    = $urlvar;
3215          $this->url    = $url;
3216          $this->alpha    = $alpha;
3217      }
3218  
3219      /**
3220       * Export for template.
3221       *
3222       * @param renderer_base $output The renderer.
3223       * @return stdClass
3224       */
3225      public function export_for_template(renderer_base $output) {
3226          $data = new stdClass();
3227  
3228          if ($this->alpha == null) {
3229              $this->alpha = explode(',', get_string('alphabet', 'langconfig'));
3230          }
3231  
3232          if ($this->current == 'all') {
3233              $this->current = '';
3234          }
3235  
3236          // We want to find a letter grouping size which suits the language so
3237          // find the largest group size which is less than 15 chars.
3238          // The choice of 15 chars is the largest number of chars that reasonably
3239          // fits on the smallest supported screen size. By always using a max number
3240          // of groups which is a factor of 2, we always get nice wrapping, and the
3241          // last row is always the shortest.
3242          $groupsize = count($this->alpha);
3243          $groups = 1;
3244          while ($groupsize > 15) {
3245              $groups *= 2;
3246              $groupsize = ceil(count($this->alpha) / $groups);
3247          }
3248  
3249          $groupsizelimit = 0;
3250          $groupnumber = 0;
3251          foreach ($this->alpha as $letter) {
3252              if ($groupsizelimit++ > 0 && $groupsizelimit % $groupsize == 1) {
3253                  $groupnumber++;
3254              }
3255              $groupletter = new stdClass();
3256              $groupletter->name = $letter;
3257              $groupletter->url = $this->url->out(false, array($this->urlvar => $letter));
3258              if ($letter == $this->current) {
3259                  $groupletter->selected = $this->current;
3260              }
3261              if (!isset($data->group[$groupnumber])) {
3262                  $data->group[$groupnumber] = new stdClass();
3263              }
3264              $data->group[$groupnumber]->letter[] = $groupletter;
3265          }
3266  
3267          $data->class = $this->class;
3268          $data->title = $this->title;
3269          $data->url = $this->url->out(false, array($this->urlvar => ''));
3270          $data->current = $this->current;
3271          $data->all = get_string('all');
3272  
3273          return $data;
3274      }
3275  }
3276  
3277  /**
3278   * This class represents how a block appears on a page.
3279   *
3280   * During output, each block instance is asked to return a block_contents object,
3281   * those are then passed to the $OUTPUT->block function for display.
3282   *
3283   * contents should probably be generated using a moodle_block_..._renderer.
3284   *
3285   * Other block-like things that need to appear on the page, for example the
3286   * add new block UI, are also represented as block_contents objects.
3287   *
3288   * @copyright 2009 Tim Hunt
3289   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3290   * @since Moodle 2.0
3291   * @package core
3292   * @category output
3293   */
3294  class block_contents {
3295  
3296      /** Used when the block cannot be collapsed **/
3297      const NOT_HIDEABLE = 0;
3298  
3299      /** Used when the block can be collapsed but currently is not **/
3300      const VISIBLE = 1;
3301  
3302      /** Used when the block has been collapsed **/
3303      const HIDDEN = 2;
3304  
3305      /**
3306       * @var int Used to set $skipid.
3307       */
3308      protected static $idcounter = 1;
3309  
3310      /**
3311       * @var int All the blocks (or things that look like blocks) printed on
3312       * a page are given a unique number that can be used to construct id="" attributes.
3313       * This is set automatically be the {@link prepare()} method.
3314       * Do not try to set it manually.
3315       */
3316      public $skipid;
3317  
3318      /**
3319       * @var int If this is the contents of a real block, this should be set
3320       * to the block_instance.id. Otherwise this should be set to 0.
3321       */
3322      public $blockinstanceid = 0;
3323  
3324      /**
3325       * @var int If this is a real block instance, and there is a corresponding
3326       * block_position.id for the block on this page, this should be set to that id.
3327       * Otherwise it should be 0.
3328       */
3329      public $blockpositionid = 0;
3330  
3331      /**
3332       * @var array An array of attribute => value pairs that are put on the outer div of this
3333       * block. {@link $id} and {@link $classes} attributes should be set separately.
3334       */
3335      public $attributes;
3336  
3337      /**
3338       * @var string The title of this block. If this came from user input, it should already
3339       * have had format_string() processing done on it. This will be output inside
3340       * <h2> tags. Please do not cause invalid XHTML.
3341       */
3342      public $title = '';
3343  
3344      /**
3345       * @var string The label to use when the block does not, or will not have a visible title.
3346       * You should never set this as well as title... it will just be ignored.
3347       */
3348      public $arialabel = '';
3349  
3350      /**
3351       * @var string HTML for the content
3352       */
3353      public $content = '';
3354  
3355      /**
3356       * @var array An alternative to $content, it you want a list of things with optional icons.
3357       */
3358      public $footer = '';
3359  
3360      /**
3361       * @var string Any small print that should appear under the block to explain
3362       * to the teacher about the block, for example 'This is a sticky block that was
3363       * added in the system context.'
3364       */
3365      public $annotation = '';
3366  
3367      /**
3368       * @var int One of the constants NOT_HIDEABLE, VISIBLE, HIDDEN. Whether
3369       * the user can toggle whether this block is visible.
3370       */
3371      public $collapsible = self::NOT_HIDEABLE;
3372  
3373      /**
3374       * Set this to true if the block is dockable.
3375       * @var bool
3376       */
3377      public $dockable = false;
3378  
3379      /**
3380       * @var array A (possibly empty) array of editing controls. Each element of
3381       * this array should be an array('url' => $url, 'icon' => $icon, 'caption' => $caption).
3382       * $icon is the icon name. Fed to $OUTPUT->image_url.
3383       */
3384      public $controls = array();
3385  
3386  
3387      /**
3388       * Create new instance of block content
3389       * @param array $attributes
3390       */
3391      public function __construct(array $attributes = null) {
3392          $this->skipid = self::$idcounter;
3393          self::$idcounter += 1;
3394  
3395          if ($attributes) {
3396              // standard block
3397              $this->attributes = $attributes;
3398          } else {
3399              // simple "fake" blocks used in some modules and "Add new block" block
3400              $this->attributes = array('class'=>'block');
3401          }
3402      }
3403  
3404      /**
3405       * Add html class to block
3406       *
3407       * @param string $class
3408       */
3409      public function add_class($class) {
3410          $this->attributes['class'] .= ' '.$class;
3411      }
3412  }
3413  
3414  
3415  /**
3416   * This class represents a target for where a block can go when it is being moved.
3417   *
3418   * This needs to be rendered as a form with the given hidden from fields, and
3419   * clicking anywhere in the form should submit it. The form action should be
3420   * $PAGE->url.
3421   *
3422   * @copyright 2009 Tim Hunt
3423   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3424   * @since Moodle 2.0
3425   * @package core
3426   * @category output
3427   */
3428  class block_move_target {
3429  
3430      /**
3431       * @var moodle_url Move url
3432       */
3433      public $url;
3434  
3435      /**
3436       * Constructor
3437       * @param moodle_url $url
3438       */
3439      public function __construct(moodle_url $url) {
3440          $this->url  = $url;
3441      }
3442  }
3443  
3444  /**
3445   * Custom menu item
3446   *
3447   * This class is used to represent one item within a custom menu that may or may
3448   * not have children.
3449   *
3450   * @copyright 2010 Sam Hemelryk
3451   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3452   * @since Moodle 2.0
3453   * @package core
3454   * @category output
3455   */
3456  class custom_menu_item implements renderable, templatable {
3457  
3458      /**
3459       * @var string The text to show for the item
3460       */
3461      protected $text;
3462  
3463      /**
3464       * @var moodle_url The link to give the icon if it has no children
3465       */
3466      protected $url;
3467  
3468      /**
3469       * @var string A title to apply to the item. By default the text
3470       */
3471      protected $title;
3472  
3473      /**
3474       * @var int A sort order for the item, not necessary if you order things in
3475       * the CFG var.
3476       */
3477      protected $sort;
3478  
3479      /**
3480       * @var custom_menu_item A reference to the parent for this item or NULL if
3481       * it is a top level item
3482       */
3483      protected $parent;
3484  
3485      /**
3486       * @var array A array in which to store children this item has.
3487       */
3488      protected $children = array();
3489  
3490      /**
3491       * @var int A reference to the sort var of the last child that was added
3492       */
3493      protected $lastsort = 0;
3494  
3495      /**
3496       * Constructs the new custom menu item
3497       *
3498       * @param string $text
3499       * @param moodle_url $url A moodle url to apply as the link for this item [Optional]
3500       * @param string $title A title to apply to this item [Optional]
3501       * @param int $sort A sort or to use if we need to sort differently [Optional]
3502       * @param custom_menu_item $parent A reference to the parent custom_menu_item this child
3503       *        belongs to, only if the child has a parent. [Optional]
3504       */
3505      public function __construct($text, moodle_url $url=null, $title=null, $sort = null, custom_menu_item $parent = null) {
3506          $this->text = $text;
3507          $this->url = $url;
3508          $this->title = $title;
3509          $this->sort = (int)$sort;
3510          $this->parent = $parent;
3511      }
3512  
3513      /**
3514       * Adds a custom menu item as a child of this node given its properties.
3515       *
3516       * @param string $text
3517       * @param moodle_url $url
3518       * @param string $title
3519       * @param int $sort
3520       * @return custom_menu_item
3521       */
3522      public function add($text, moodle_url $url = null, $title = null, $sort = null) {
3523          $key = count($this->children);
3524          if (empty($sort)) {
3525              $sort = $this->lastsort + 1;
3526          }
3527          $this->children[$key] = new custom_menu_item($text, $url, $title, $sort, $this);
3528          $this->lastsort = (int)$sort;
3529          return $this->children[$key];
3530      }
3531  
3532      /**
3533       * Removes a custom menu item that is a child or descendant to the current menu.
3534       *
3535       * Returns true if child was found and removed.
3536       *
3537       * @param custom_menu_item $menuitem
3538       * @return bool
3539       */
3540      public function remove_child(custom_menu_item $menuitem) {
3541          $removed = false;
3542          if (($key = array_search($menuitem, $this->children)) !== false) {
3543              unset($this->children[$key]);
3544              $this->children = array_values($this->children);
3545              $removed = true;
3546          } else {
3547              foreach ($this->children as $child) {
3548                  if ($removed = $child->remove_child($menuitem)) {
3549                      break;
3550                  }
3551              }
3552          }
3553          return $removed;
3554      }
3555  
3556      /**
3557       * Returns the text for this item
3558       * @return string
3559       */
3560      public function get_text() {
3561          return $this->text;
3562      }
3563  
3564      /**
3565       * Returns the url for this item
3566       * @return moodle_url
3567       */
3568      public function get_url() {
3569          return $this->url;
3570      }
3571  
3572      /**
3573       * Returns the title for this item
3574       * @return string
3575       */
3576      public function get_title() {
3577          return $this->title;
3578      }
3579  
3580      /**
3581       * Sorts and returns the children for this item
3582       * @return array
3583       */
3584      public function get_children() {
3585          $this->sort();
3586          return $this->children;
3587      }
3588  
3589      /**
3590       * Gets the sort order for this child
3591       * @return int
3592       */
3593      public function get_sort_order() {
3594          return $this->sort;
3595      }
3596  
3597      /**
3598       * Gets the parent this child belong to
3599       * @return custom_menu_item
3600       */
3601      public function get_parent() {
3602          return $this->parent;
3603      }
3604  
3605      /**
3606       * Sorts the children this item has
3607       */
3608      public function sort() {
3609          usort($this->children, array('custom_menu','sort_custom_menu_items'));
3610      }
3611  
3612      /**
3613       * Returns true if this item has any children
3614       * @return bool
3615       */
3616      public function has_children() {
3617          return (count($this->children) > 0);
3618      }
3619  
3620      /**
3621       * Sets the text for the node
3622       * @param string $text
3623       */
3624      public function set_text($text) {
3625          $this->text = (string)$text;
3626      }
3627  
3628      /**
3629       * Sets the title for the node
3630       * @param string $title
3631       */
3632      public function set_title($title) {
3633          $this->title = (string)$title;
3634      }
3635  
3636      /**
3637       * Sets the url for the node
3638       * @param moodle_url $url
3639       */
3640      public function set_url(moodle_url $url) {
3641          $this->url = $url;
3642      }
3643  
3644      /**
3645       * Export this data so it can be used as the context for a mustache template.
3646       *
3647       * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
3648       * @return array
3649       */
3650      public function export_for_template(renderer_base $output) {
3651          global $CFG;
3652  
3653          require_once($CFG->libdir . '/externallib.php');
3654  
3655          $syscontext = context_system::instance();
3656  
3657          $context = new stdClass();
3658          $context->text = external_format_string($this->text, $syscontext->id);
3659          $context->url = $this->url ? $this->url->out() : null;
3660          $context->title = external_format_string($this->title, $syscontext->id);
3661          $context->sort = $this->sort;
3662          $context->children = array();
3663          if (preg_match("/^#+$/", $this->text)) {
3664              $context->divider = true;
3665          }
3666          $context->haschildren = !empty($this->children) && (count($this->children) > 0);
3667          foreach ($this->children as $child) {
3668              $child = $child->export_for_template($output);
3669              array_push($context->children, $child);
3670          }
3671  
3672          return $context;
3673      }
3674  }
3675  
3676  /**
3677   * Custom menu class
3678   *
3679   * This class is used to operate a custom menu that can be rendered for the page.
3680   * The custom menu is built using $CFG->custommenuitems and is a structured collection
3681   * of custom_menu_item nodes that can be rendered by the core renderer.
3682   *
3683   * To configure the custom menu:
3684   *     Settings: Administration > Appearance > Themes > Theme settings
3685   *
3686   * @copyright 2010 Sam Hemelryk
3687   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3688   * @since Moodle 2.0
3689   * @package core
3690   * @category output
3691   */
3692  class custom_menu extends custom_menu_item {
3693  
3694      /**
3695       * @var string The language we should render for, null disables multilang support.
3696       */
3697      protected $currentlanguage = null;
3698  
3699      /**
3700       * Creates the custom menu
3701       *
3702       * @param string $definition the menu items definition in syntax required by {@link convert_text_to_menu_nodes()}
3703       * @param string $currentlanguage the current language code, null disables multilang support
3704       */
3705      public function __construct($definition = '', $currentlanguage = null) {
3706          $this->currentlanguage = $currentlanguage;
3707          parent::__construct('root'); // create virtual root element of the menu
3708          if (!empty($definition)) {
3709              $this->override_children(self::convert_text_to_menu_nodes($definition, $currentlanguage));
3710          }
3711      }
3712  
3713      /**
3714       * Overrides the children of this custom menu. Useful when getting children
3715       * from $CFG->custommenuitems
3716       *
3717       * @param array $children
3718       */
3719      public function override_children(array $children) {
3720          $this->children = array();
3721          foreach ($children as $child) {
3722              if ($child instanceof custom_menu_item) {
3723                  $this->children[] = $child;
3724              }
3725          }
3726      }
3727  
3728      /**
3729       * Converts a string into a structured array of custom_menu_items which can
3730       * then be added to a custom menu.
3731       *
3732       * Structure:
3733       *     text|url|title|langs
3734       * The number of hyphens at the start determines the depth of the item. The
3735       * languages are optional, comma separated list of languages the line is for.
3736       *
3737       * Example structure:
3738       *     First level first item|http://www.moodle.com/
3739       *     -Second level first item|http://www.moodle.com/partners/
3740       *     -Second level second item|http://www.moodle.com/hq/
3741       *     --Third level first item|http://www.moodle.com/jobs/
3742       *     -Second level third item|http://www.moodle.com/development/
3743       *     First level second item|http://www.moodle.com/feedback/
3744       *     First level third item
3745       *     English only|http://moodle.com|English only item|en
3746       *     German only|http://moodle.de|Deutsch|de,de_du,de_kids
3747       *
3748       *
3749       * @static
3750       * @param string $text the menu items definition
3751       * @param string $language the language code, null disables multilang support
3752       * @return array
3753       */
3754      public static function convert_text_to_menu_nodes($text, $language = null) {
3755          $root = new custom_menu();
3756          $lastitem = $root;
3757          $lastdepth = 0;
3758          $hiddenitems = array();
3759          $lines = explode("\n", $text);
3760          foreach ($lines as $linenumber => $line) {
3761              $line = trim($line);
3762              if (strlen($line) == 0) {
3763                  continue;
3764              }
3765              // Parse item settings.
3766              $itemtext = null;
3767              $itemurl = null;
3768              $itemtitle = null;
3769              $itemvisible = true;
3770              $settings = explode('|', $line);
3771              foreach ($settings as $i => $setting) {
3772                  $setting = trim($setting);
3773                  if (!empty($setting)) {
3774                      switch ($i) {
3775                          case 0: // Menu text.
3776                              $itemtext = ltrim($setting, '-');
3777                              break;
3778                          case 1: // URL.
3779                              try {
3780                                  $itemurl = new moodle_url($setting);
3781                              } catch (moodle_exception $exception) {
3782                                  // We're not actually worried about this, we don't want to mess up the display
3783                                  // just for a wrongly entered URL.
3784                                  $itemurl = null;
3785                              }
3786                              break;
3787                          case 2: // Title attribute.
3788                              $itemtitle = $setting;
3789                              break;
3790                          case 3: // Language.
3791                              if (!empty($language)) {
3792                                  $itemlanguages = array_map('trim', explode(',', $setting));
3793                                  $itemvisible &= in_array($language, $itemlanguages);
3794                              }
3795                              break;
3796                      }
3797                  }
3798              }
3799              // Get depth of new item.
3800              preg_match('/^(\-*)/', $line, $match);
3801              $itemdepth = strlen($match[1]) + 1;
3802              // Find parent item for new item.
3803              while (($lastdepth - $itemdepth) >= 0) {
3804                  $lastitem = $lastitem->get_parent();
3805                  $lastdepth--;
3806              }
3807              $lastitem = $lastitem->add($itemtext, $itemurl, $itemtitle, $linenumber + 1);
3808              $lastdepth++;
3809              if (!$itemvisible) {
3810                  $hiddenitems[] = $lastitem;
3811              }
3812          }
3813          foreach ($hiddenitems as $item) {
3814              $item->parent->remove_child($item);
3815          }
3816          return $root->get_children();
3817      }
3818  
3819      /**
3820       * Sorts two custom menu items
3821       *
3822       * This function is designed to be used with the usort method
3823       *     usort($this->children, array('custom_menu','sort_custom_menu_items'));
3824       *
3825       * @static
3826       * @param custom_menu_item $itema
3827       * @param custom_menu_item $itemb
3828       * @return int
3829       */
3830      public static function sort_custom_menu_items(custom_menu_item $itema, custom_menu_item $itemb) {
3831          $itema = $itema->get_sort_order();
3832          $itemb = $itemb->get_sort_order();
3833          if ($itema == $itemb) {
3834              return 0;
3835          }
3836          return ($itema > $itemb) ? +1 : -1;
3837      }
3838  }
3839  
3840  /**
3841   * Stores one tab
3842   *
3843   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3844   * @package core
3845   */
3846  class tabobject implements renderable, templatable {
3847      /** @var string unique id of the tab in this tree, it is used to find selected and/or inactive tabs */
3848      var $id;
3849      /** @var moodle_url|string link */
3850      var $link;
3851      /** @var string text on the tab */
3852      var $text;
3853      /** @var string title under the link, by defaul equals to text */
3854      var $title;
3855      /** @var bool whether to display a link under the tab name when it's selected */
3856      var $linkedwhenselected = false;
3857      /** @var bool whether the tab is inactive */
3858      var $inactive = false;
3859      /** @var bool indicates that this tab's child is selected */
3860      var $activated = false;
3861      /** @var bool indicates that this tab is selected */
3862      var $selected = false;
3863      /** @var array stores children tabobjects */
3864      var $subtree = array();
3865      /** @var int level of tab in the tree, 0 for root (instance of tabtree), 1 for the first row of tabs */
3866      var $level = 1;
3867  
3868      /**
3869       * Constructor
3870       *
3871       * @param string $id unique id of the tab in this tree, it is used to find selected and/or inactive tabs
3872       * @param string|moodle_url $link
3873       * @param string $text text on the tab
3874       * @param string $title title under the link, by defaul equals to text
3875       * @param bool $linkedwhenselected whether to display a link under the tab name when it's selected
3876       */
3877      public function __construct($id, $link = null, $text = '', $title = '', $linkedwhenselected = false) {
3878          $this->id = $id;
3879          $this->link = $link;
3880          $this->text = $text;
3881          $this->title = $title ? $title : $text;
3882          $this->linkedwhenselected = $linkedwhenselected;
3883      }
3884  
3885      /**
3886       * Travels through tree and finds the tab to mark as selected, all parents are automatically marked as activated
3887       *
3888       * @param string $selected the id of the selected tab (whatever row it's on),
3889       *    if null marks all tabs as unselected
3890       * @return bool whether this tab is selected or contains selected tab in its subtree
3891       */
3892      protected function set_selected($selected) {
3893          if ((string)$selected === (string)$this->id) {
3894              $this->selected = true;
3895              // This tab is selected. No need to travel through subtree.
3896              return true;
3897          }
3898          foreach ($this->subtree as $subitem) {
3899              if ($subitem->set_selected($selected)) {
3900                  // This tab has child that is selected. Mark it as activated. No need to check other children.
3901                  $this->activated = true;
3902                  return true;
3903              }
3904          }
3905          return false;
3906      }
3907  
3908      /**
3909       * Travels through tree and finds a tab with specified id
3910       *
3911       * @param string $id
3912       * @return tabtree|null
3913       */
3914      public function find($id) {
3915          if ((string)$this->id === (string)$id) {
3916              return $this;
3917          }
3918          foreach ($this->subtree as $tab) {
3919              if ($obj = $tab->find($id)) {
3920                  return $obj;
3921              }
3922          }
3923          return null;
3924      }
3925  
3926      /**
3927       * Allows to mark each tab's level in the tree before rendering.
3928       *
3929       * @param int $level
3930       */
3931      protected function set_level($level) {
3932          $this->level = $level;
3933          foreach ($this->subtree as $tab) {
3934              $tab->set_level($level + 1);
3935          }
3936      }
3937  
3938      /**
3939       * Export for template.
3940       *
3941       * @param renderer_base $output Renderer.
3942       * @return object
3943       */
3944      public function export_for_template(renderer_base $output) {
3945          if ($this->inactive || ($this->selected && !$this->linkedwhenselected) || $this->activated) {
3946              $link = null;
3947          } else {
3948              $link = $this->link;
3949          }
3950          $active = $this->activated || $this->selected;
3951  
3952          return (object) [
3953              'id' => $this->id,
3954              'link' => is_object($link) ? $link->out(false) : $link,
3955              'text' => $this->text,
3956              'title' => $this->title,
3957              'inactive' => !$active && $this->inactive,
3958              'active' => $active,
3959              'level' => $this->level,
3960          ];
3961      }
3962  
3963  }
3964  
3965  /**
3966   * Renderable for the main page header.
3967   *
3968   * @package core
3969   * @category output
3970   * @since 2.9
3971   * @copyright 2015 Adrian Greeve <adrian@moodle.com>
3972   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3973   */
3974  class context_header implements renderable {
3975  
3976      /**
3977       * @var string $heading Main heading.
3978       */
3979      public $heading;
3980      /**
3981       * @var int $headinglevel Main heading 'h' tag level.
3982       */
3983      public $headinglevel;
3984      /**
3985       * @var string|null $imagedata HTML code for the picture in the page header.
3986       */
3987      public $imagedata;
3988      /**
3989       * @var array $additionalbuttons Additional buttons for the header e.g. Messaging button for the user header.
3990       *      array elements - title => alternate text for the image, or if no image is available the button text.
3991       *                       url => Link for the button to head to. Should be a moodle_url.
3992       *                       image => location to the image, or name of the image in /pix/t/{image name}.
3993       *                       linkattributes => additional attributes for the <a href> element.
3994       *                       page => page object. Don't include if the image is an external image.
3995       */
3996      public $additionalbuttons;
3997  
3998      /**
3999       * Constructor.
4000       *
4001       * @param string $heading Main heading data.
4002       * @param int $headinglevel Main heading 'h' tag level.
4003       * @param string|null $imagedata HTML code for the picture in the page header.
4004       * @param string $additionalbuttons Buttons for the header e.g. Messaging button for the user header.
4005       */
4006      public function __construct($heading = null, $headinglevel = 1, $imagedata = null, $additionalbuttons = null) {
4007  
4008          $this->heading = $heading;
4009          $this->headinglevel = $headinglevel;
4010          $this->imagedata = $imagedata;
4011          $this->additionalbuttons = $additionalbuttons;
4012          // If we have buttons then format them.
4013          if (isset($this->additionalbuttons)) {
4014              $this->format_button_images();
4015          }
4016      }
4017  
4018      /**
4019       * Adds an array element for a formatted image.
4020       */
4021      protected function format_button_images() {
4022  
4023          foreach ($this->additionalbuttons as $buttontype => $button) {
4024              $page = $button['page'];
4025              // If no image is provided then just use the title.
4026              if (!isset($button['image'])) {
4027                  $this->additionalbuttons[$buttontype]['formattedimage'] = $button['title'];
4028              } else {
4029                  // Check to see if this is an internal Moodle icon.
4030                  $internalimage = $page->theme->resolve_image_location('t/' . $button['image'], 'moodle');
4031                  if ($internalimage) {
4032                      $this->additionalbuttons[$buttontype]['formattedimage'] = 't/' . $button['image'];
4033                  } else {
4034                      // Treat as an external image.
4035                      $this->additionalbuttons[$buttontype]['formattedimage'] = $button['image'];
4036                  }
4037              }
4038  
4039              if (isset($button['linkattributes']['class'])) {
4040                  $class = $button['linkattributes']['class'] . ' btn';
4041              } else {
4042                  $class = 'btn';
4043              }
4044              // Add the bootstrap 'btn' class for formatting.
4045              $this->additionalbuttons[$buttontype]['linkattributes'] = array_merge($button['linkattributes'],
4046                      array('class' => $class));
4047          }
4048      }
4049  }
4050  
4051  /**
4052   * Stores tabs list
4053   *
4054   * Example how to print a single line tabs:
4055   * $rows = array(
4056   *    new tabobject(...),
4057   *    new tabobject(...)
4058   * );
4059   * echo $OUTPUT->tabtree($rows, $selectedid);
4060   *
4061   * Multiple row tabs may not look good on some devices but if you want to use them
4062   * you can specify ->subtree for the active tabobject.
4063   *
4064   * @copyright 2013 Marina Glancy
4065   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4066   * @since Moodle 2.5
4067   * @package core
4068   * @category output
4069   */
4070  class tabtree extends tabobject {
4071      /**
4072       * Constuctor
4073       *
4074       * It is highly recommended to call constructor when list of tabs is already
4075       * populated, this way you ensure that selected and inactive tabs are located
4076       * and attribute level is set correctly.
4077       *
4078       * @param array $tabs array of tabs, each of them may have it's own ->subtree
4079       * @param string|null $selected which tab to mark as selected, all parent tabs will
4080       *     automatically be marked as activated
4081       * @param array|string|null $inactive list of ids of inactive tabs, regardless of
4082       *     their level. Note that you can as weel specify tabobject::$inactive for separate instances
4083       */
4084      public function __construct($tabs, $selected = null, $inactive = null) {
4085          $this->subtree = $tabs;
4086          if ($selected !== null) {
4087              $this->set_selected($selected);
4088          }
4089          if ($inactive !== null) {
4090              if (is_array($inactive)) {
4091                  foreach ($inactive as $id) {
4092                      if ($tab = $this->find($id)) {
4093                          $tab->inactive = true;
4094                      }
4095                  }
4096              } else if ($tab = $this->find($inactive)) {
4097                  $tab->inactive = true;
4098              }
4099          }
4100          $this->set_level(0);
4101      }
4102  
4103      /**
4104       * Export for template.
4105       *
4106       * @param renderer_base $output Renderer.
4107       * @return object
4108       */
4109      public function export_for_template(renderer_base $output) {
4110          $tabs = [];
4111          $secondrow = false;
4112  
4113          foreach ($this->subtree as $tab) {
4114              $tabs[] = $tab->export_for_template($output);
4115              if (!empty($tab->subtree) && ($tab->level == 0 || $tab->selected || $tab->activated)) {
4116                  $secondrow = new tabtree($tab->subtree);
4117              }
4118          }
4119  
4120          return (object) [
4121              'tabs' => $tabs,
4122              'secondrow' => $secondrow ? $secondrow->export_for_template($output) : false
4123          ];
4124      }
4125  }
4126  
4127  /**
4128   * An action menu.
4129   *
4130   * This action menu component takes a series of primary and secondary actions.
4131   * The primary actions are displayed permanently and the secondary attributes are displayed within a drop
4132   * down menu.
4133   *
4134   * @package core
4135   * @category output
4136   * @copyright 2013 Sam Hemelryk
4137   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4138   */
4139  class action_menu implements renderable, templatable {
4140  
4141      /**
4142       * Top right alignment.
4143       */
4144      const TL = 1;
4145  
4146      /**
4147       * Top right alignment.
4148       */
4149      const TR = 2;
4150  
4151      /**
4152       * Top right alignment.
4153       */
4154      const BL = 3;
4155  
4156      /**
4157       * Top right alignment.
4158       */
4159      const BR = 4;
4160  
4161      /**
4162       * The instance number. This is unique to this instance of the action menu.
4163       * @var int
4164       */
4165      protected $instance = 0;
4166  
4167      /**
4168       * An array of primary actions. Please use {@link action_menu::add_primary_action()} to add actions.
4169       * @var array
4170       */
4171      protected $primaryactions = array();
4172  
4173      /**
4174       * An array of secondary actions. Please use {@link action_menu::add_secondary_action()} to add actions.
4175       * @var array
4176       */
4177      protected $secondaryactions = array();
4178  
4179      /**
4180       * An array of attributes added to the container of the action menu.
4181       * Initialised with defaults during construction.
4182       * @var array
4183       */
4184      public $attributes = array();
4185      /**
4186       * An array of attributes added to the container of the primary actions.
4187       * Initialised with defaults during construction.
4188       * @var array
4189       */
4190      public $attributesprimary = array();
4191      /**
4192       * An array of attributes added to the container of the secondary actions.
4193       * Initialised with defaults during construction.
4194       * @var array
4195       */
4196      public $attributessecondary = array();
4197  
4198      /**
4199       * The string to use next to the icon for the action icon relating to the secondary (dropdown) menu.
4200       * @var array
4201       */
4202      public $actiontext = null;
4203  
4204      /**
4205       * The string to use for the accessible label for the menu.
4206       * @var array
4207       */
4208      public $actionlabel = null;
4209  
4210      /**
4211       * An icon to use for the toggling the secondary menu (dropdown).
4212       * @var pix_icon
4213       */
4214      public $actionicon;
4215  
4216      /**
4217       * Any text to use for the toggling the secondary menu (dropdown).
4218       * @var string
4219       */
4220      public $menutrigger = '';
4221  
4222      /**
4223       * Any extra classes for toggling to the secondary menu.
4224       * @var string
4225       */
4226      public $triggerextraclasses = '';
4227  
4228      /**
4229       * Place the action menu before all other actions.
4230       * @var bool
4231       */
4232      public $prioritise = false;
4233  
4234      /**
4235       * Constructs the action menu with the given items.
4236       *
4237       * @param array $actions An array of actions (action_menu_link|pix_icon|string).
4238       */
4239      public function __construct(array $actions = array()) {
4240          static $initialised = 0;
4241          $this->instance = $initialised;
4242          $initialised++;
4243  
4244          $this->attributes = array(
4245              'id' => 'action-menu-'.$this->instance,
4246              'class' => 'moodle-actionmenu',
4247              'data-enhance' => 'moodle-core-actionmenu'
4248          );
4249          $this->attributesprimary = array(
4250              'id' => 'action-menu-'.$this->instance.'-menubar',
4251              'class' => 'menubar',
4252              'role' => 'menubar'
4253          );
4254          $this->attributessecondary = array(
4255              'id' => 'action-menu-'.$this->instance.'-menu',
4256              'class' => 'menu',
4257              'data-rel' => 'menu-content',
4258              'aria-labelledby' => 'action-menu-toggle-'.$this->instance,
4259              'role' => 'menu'
4260          );
4261          $this->set_alignment(self::TR, self::BR);
4262          foreach ($actions as $action) {
4263              $this->add($action);
4264          }
4265      }
4266  
4267      /**
4268       * Sets the label for the menu trigger.
4269       *
4270       * @param string $label The text
4271       */
4272      public function set_action_label($label) {
4273          $this->actionlabel = $label;
4274      }
4275  
4276      /**
4277       * Sets the menu trigger text.
4278       *
4279       * @param string $trigger The text
4280       * @param string $extraclasses Extra classes to style the secondary menu toggle.
4281       */
4282      public function set_menu_trigger($trigger, $extraclasses = '') {
4283          $this->menutrigger = $trigger;
4284          $this->triggerextraclasses = $extraclasses;
4285      }
4286  
4287      /**
4288       * Return true if there is at least one visible link in the menu.
4289       *
4290       * @return bool
4291       */
4292      public function is_empty() {
4293          return !count($this->primaryactions) && !count($this->secondaryactions);
4294      }
4295  
4296      /**
4297       * Initialises JS required fore the action menu.
4298       * The JS is only required once as it manages all action menu's on the page.
4299       *
4300       * @param moodle_page $page
4301       */
4302      public function initialise_js(moodle_page $page) {
4303          static $initialised = false;
4304          if (!$initialised) {
4305              $page->requires->yui_module('moodle-core-actionmenu', 'M.core.actionmenu.init');
4306              $initialised = true;
4307          }
4308      }
4309  
4310      /**
4311       * Adds an action to this action menu.
4312       *
4313       * @param action_menu_link|pix_icon|string $action
4314       */
4315      public function add($action) {
4316          if ($action instanceof action_link) {
4317              if ($action->primary) {
4318                  $this->add_primary_action($action);
4319              } else {
4320                  $this->add_secondary_action($action);
4321              }
4322          } else if ($action instanceof pix_icon) {
4323              $this->add_primary_action($action);
4324          } else {
4325              $this->add_secondary_action($action);
4326          }
4327      }
4328  
4329      /**
4330       * Adds a primary action to the action menu.
4331       *
4332       * @param action_menu_link|action_link|pix_icon|string $action
4333       */
4334      public function add_primary_action($action) {
4335          if ($action instanceof action_link || $action instanceof pix_icon) {
4336              $action->attributes['role'] = 'menuitem';
4337              if ($action instanceof action_menu_link) {
4338                  $action->actionmenu = $this;
4339              }
4340          }
4341          $this->primaryactions[] = $action;
4342      }
4343  
4344      /**
4345       * Adds a secondary action to the action menu.
4346       *
4347       * @param action_link|pix_icon|string $action
4348       */
4349      public function add_secondary_action($action) {
4350          if ($action instanceof action_link || $action instanceof pix_icon) {
4351              $action->attributes['role'] = 'menuitem';
4352              if ($action instanceof action_menu_link) {
4353                  $action->actionmenu = $this;
4354              }
4355          }
4356          $this->secondaryactions[] = $action;
4357      }
4358  
4359      /**
4360       * Returns the primary actions ready to be rendered.
4361       *
4362       * @param core_renderer $output The renderer to use for getting icons.
4363       * @return array
4364       */
4365      public function get_primary_actions(core_renderer $output = null) {
4366          global $OUTPUT;
4367          if ($output === null) {
4368              $output = $OUTPUT;
4369          }
4370          $pixicon = $this->actionicon;
4371          $linkclasses = array('toggle-display');
4372  
4373          $title = '';
4374          if (!empty($this->menutrigger)) {
4375              $pixicon = '<b class="caret"></b>';
4376              $linkclasses[] = 'textmenu';
4377          } else {
4378              $title = new lang_string('actionsmenu', 'moodle');
4379              $this->actionicon = new pix_icon(
4380                  't/edit_menu',
4381                  '',
4382                  'moodle',
4383                  array('class' => 'iconsmall actionmenu', 'title' => '')
4384              );
4385              $pixicon = $this->actionicon;
4386          }
4387          if ($pixicon instanceof renderable) {
4388              $pixicon = $output->render($pixicon);
4389              if ($pixicon instanceof pix_icon && isset($pixicon->attributes['alt'])) {
4390                  $title = $pixicon->attributes['alt'];
4391              }
4392          }
4393          $string = '';
4394          if ($this->actiontext) {
4395              $string = $this->actiontext;
4396          }
4397          $label = '';
4398          if ($this->actionlabel) {
4399              $label = $this->actionlabel;
4400          } else {
4401              $label = $title;
4402          }
4403          $actions = $this->primaryactions;
4404          $attributes = array(
4405              'class' => implode(' ', $linkclasses),
4406              'title' => $title,
4407              'aria-label' => $label,
4408              'id' => 'action-menu-toggle-'.$this->instance,
4409              'role' => 'menuitem'
4410          );
4411          $link = html_writer::link('#', $string . $this->menutrigger . $pixicon, $attributes);
4412          if ($this->prioritise) {
4413              array_unshift($actions, $link);
4414          } else {
4415              $actions[] = $link;
4416          }
4417          return $actions;
4418      }
4419  
4420      /**
4421       * Returns the secondary actions ready to be rendered.
4422       * @return array
4423       */
4424      public function get_secondary_actions() {
4425          return $this->secondaryactions;
4426      }
4427  
4428      /**
4429       * Sets the selector that should be used to find the owning node of this menu.
4430       * @param string $selector A CSS/YUI selector to identify the owner of the menu.
4431       */
4432      public function set_owner_selector($selector) {
4433          $this->attributes['data-owner'] = $selector;
4434      }
4435  
4436      /**
4437       * Sets the alignment of the dialogue in relation to button used to toggle it.
4438       *
4439       * @param int $dialogue One of action_menu::TL, action_menu::TR, action_menu::BL, action_menu::BR.
4440       * @param int $button One of action_menu::TL, action_menu::TR, action_menu::BL, action_menu::BR.
4441       */
4442      public function set_alignment($dialogue, $button) {
4443          if (isset($this->attributessecondary['data-align'])) {
4444              // We've already got one set, lets remove the old class so as to avoid troubles.
4445              $class = $this->attributessecondary['class'];
4446              $search = 'align-'.$this->attributessecondary['data-align'];
4447              $this->attributessecondary['class'] = str_replace($search, '', $class);
4448          }
4449          $align = $this->get_align_string($dialogue) . '-' . $this->get_align_string($button);
4450          $this->attributessecondary['data-align'] = $align;
4451          $this->attributessecondary['class'] .= ' align-'.$align;
4452      }
4453  
4454      /**
4455       * Returns a string to describe the alignment.
4456       *
4457       * @param int $align One of action_menu::TL, action_menu::TR, action_menu::BL, action_menu::BR.
4458       * @return string
4459       */
4460      protected function get_align_string($align) {
4461          switch ($align) {
4462              case self::TL :
4463                  return 'tl';
4464              case self::TR :
4465                  return 'tr';
4466              case self::BL :
4467                  return 'bl';
4468              case self::BR :
4469                  return 'br';
4470              default :
4471                  return 'tl';
4472          }
4473      }
4474  
4475      /**
4476       * Sets a constraint for the dialogue.
4477       *
4478       * The constraint is applied when the dialogue is shown and limits the display of the dialogue to within the
4479       * element the constraint identifies.
4480       *
4481       * This is required whenever the action menu is displayed inside any CSS element with the .no-overflow class
4482       * (flexible_table and any of it's child classes are a likely candidate).
4483       *
4484       * @param string $ancestorselector A snippet of CSS used to identify the ancestor to contrain the dialogue to.
4485       */
4486      public function set_constraint($ancestorselector) {
4487          $this->attributessecondary['data-constraint'] = $ancestorselector;
4488      }
4489  
4490      /**
4491       * If you call this method the action menu will be displayed but will not be enhanced.
4492       *
4493       * By not displaying the menu enhanced all items will be displayed in a single row.
4494       *
4495       * @deprecated since Moodle 3.2
4496       */
4497      public function do_not_enhance() {
4498          debugging('The method action_menu::do_not_enhance() is deprecated, use a list of action_icon instead.', DEBUG_DEVELOPER);
4499      }
4500  
4501      /**
4502       * Returns true if this action menu will be enhanced.
4503       *
4504       * @return bool
4505       */
4506      public function will_be_enhanced() {
4507          return isset($this->attributes['data-enhance']);
4508      }
4509  
4510      /**
4511       * Sets nowrap on items. If true menu items should not wrap lines if they are longer than the available space.
4512       *
4513       * This property can be useful when the action menu is displayed within a parent element that is either floated
4514       * or relatively positioned.
4515       * In that situation the width of the menu is determined by the width of the parent element which may not be large
4516       * enough for the menu items without them wrapping.
4517       * This disables the wrapping so that the menu takes on the width of the longest item.
4518       *
4519       * @param bool $value If true nowrap gets set, if false it gets removed. Defaults to true.
4520       */
4521      public function set_nowrap_on_items($value = true) {
4522          $class = 'nowrap-items';
4523          if (!empty($this->attributes['class'])) {
4524              $pos = strpos($this->attributes['class'], $class);
4525              if ($value === true && $pos === false) {
4526                  // The value is true and the class has not been set yet. Add it.
4527                  $this->attributes['class'] .= ' '.$class;
4528              } else if ($value === false && $pos !== false) {
4529                  // The value is false and the class has been set. Remove it.
4530                  $this->attributes['class'] = substr($this->attributes['class'], $pos, strlen($class));
4531              }
4532          } else if ($value) {
4533              // The value is true and the class has not been set yet. Add it.
4534              $this->attributes['class'] = $class;
4535          }
4536      }
4537  
4538      /**
4539       * Export for template.
4540       *
4541       * @param renderer_base $output The renderer.
4542       * @return stdClass
4543       */
4544      public function export_for_template(renderer_base $output) {
4545          $data = new stdClass();
4546          $attributes = $this->attributes;
4547          $attributesprimary = $this->attributesprimary;
4548          $attributessecondary = $this->attributessecondary;
4549  
4550          $data->instance = $this->instance;
4551  
4552          $data->classes = isset($attributes['class']) ? $attributes['class'] : '';
4553          unset($attributes['class']);
4554  
4555          $data->attributes = array_map(function($key, $value) {
4556              return [ 'name' => $key, 'value' => $value ];
4557          }, array_keys($attributes), $attributes);
4558  
4559          $primary = new stdClass();
4560          $primary->title = '';
4561          $primary->prioritise = $this->prioritise;
4562  
4563          $primary->classes = isset($attributesprimary['class']) ? $attributesprimary['class'] : '';
4564          unset($attributesprimary['class']);
4565          $primary->attributes = array_map(function($key, $value) {
4566              return [ 'name' => $key, 'value' => $value ];
4567          }, array_keys($attributesprimary), $attributesprimary);
4568  
4569          $actionicon = $this->actionicon;
4570          if (!empty($this->menutrigger)) {
4571              $primary->menutrigger = $this->menutrigger;
4572              $primary->triggerextraclasses = $this->triggerextraclasses;
4573              if ($this->actionlabel) {
4574                  $primary->title = $this->actionlabel;
4575              } else if ($this->actiontext) {
4576                  $primary->title = $this->actiontext;
4577              } else {
4578                  $primary->title = strip_tags($this->menutrigger);
4579              }
4580          } else {
4581              $primary->title = get_string('actionsmenu');
4582              $iconattributes = ['class' => 'iconsmall actionmenu', 'title' => $primary->title];
4583              $actionicon = new pix_icon('t/edit_menu', '', 'moodle', $iconattributes);
4584          }
4585  
4586          if ($actionicon instanceof pix_icon) {
4587              $primary->icon = $actionicon->export_for_pix();
4588              if (!empty($actionicon->attributes['alt'])) {
4589                  $primary->title = $actionicon->attributes['alt'];
4590              }
4591          } else {
4592              $primary->iconraw = $actionicon ? $output->render($actionicon) : '';
4593          }
4594  
4595          $primary->actiontext = $this->actiontext ? (string) $this->actiontext : '';
4596          $primary->items = array_map(function($item) use ($output) {
4597              $data = (object) [];
4598              if ($item instanceof action_menu_link) {
4599                  $data->actionmenulink = $item->export_for_template($output);
4600              } else if ($item instanceof action_menu_filler) {
4601                  $data->actionmenufiller = $item->export_for_template($output);
4602              } else if ($item instanceof action_link) {
4603                  $data->actionlink = $item->export_for_template($output);
4604              } else if ($item instanceof pix_icon) {
4605                  $data->pixicon = $item->export_for_template($output);
4606              } else {
4607                  $data->rawhtml = ($item instanceof renderable) ? $output->render($item) : $item;
4608              }
4609              return $data;
4610          }, $this->primaryactions);
4611  
4612          $secondary = new stdClass();
4613          $secondary->classes = isset($attributessecondary['class']) ? $attributessecondary['class'] : '';
4614          unset($attributessecondary['class']);
4615          $secondary->attributes = array_map(function($key, $value) {
4616              return [ 'name' => $key, 'value' => $value ];
4617          }, array_keys($attributessecondary), $attributessecondary);
4618          $secondary->items = array_map(function($item) use ($output) {
4619              $data = (object) [];
4620              if ($item instanceof action_menu_link) {
4621                  $data->actionmenulink = $item->export_for_template($output);
4622              } else if ($item instanceof action_menu_filler) {
4623                  $data->actionmenufiller = $item->export_for_template($output);
4624              } else if ($item instanceof action_link) {
4625                  $data->actionlink = $item->export_for_template($output);
4626              } else if ($item instanceof pix_icon) {
4627                  $data->pixicon = $item->export_for_template($output);
4628              } else {
4629                  $data->rawhtml = ($item instanceof renderable) ? $output->render($item) : $item;
4630              }
4631              return $data;
4632          }, $this->secondaryactions);
4633  
4634          $data->primary = $primary;
4635          $data->secondary = $secondary;
4636  
4637          return $data;
4638      }
4639  
4640  }
4641  
4642  /**
4643   * An action menu filler
4644   *
4645   * @package core
4646   * @category output
4647   * @copyright 2013 Andrew Nicols
4648   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4649   */
4650  class action_menu_filler extends action_link implements renderable {
4651  
4652      /**
4653       * True if this is a primary action. False if not.
4654       * @var bool
4655       */
4656      public $primary = true;
4657  
4658      /**
4659       * Constructs the object.
4660       */
4661      public function __construct() {
4662      }
4663  }
4664  
4665  /**
4666   * An action menu action
4667   *
4668   * @package core
4669   * @category output
4670   * @copyright 2013 Sam Hemelryk
4671   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4672   */
4673  class action_menu_link extends action_link implements renderable {
4674  
4675      /**
4676       * True if this is a primary action. False if not.
4677       * @var bool
4678       */
4679      public $primary = true;
4680  
4681      /**
4682       * The action menu this link has been added to.
4683       * @var action_menu
4684       */
4685      public $actionmenu = null;
4686  
4687      /**
4688       * The number of instances of this action menu link (and its subclasses).
4689       * @var int
4690       */
4691      protected static $instance = 1;
4692  
4693      /**
4694       * Constructs the object.
4695       *
4696       * @param moodle_url $url The URL for the action.
4697       * @param pix_icon $icon The icon to represent the action.
4698       * @param string $text The text to represent the action.
4699       * @param bool $primary Whether this is a primary action or not.
4700       * @param array $attributes Any attribtues associated with the action.
4701       */
4702      public function __construct(moodle_url $url, pix_icon $icon = null, $text, $primary = true, array $attributes = array()) {
4703          parent::__construct($url, $text, null, $attributes, $icon);
4704          $this->primary = (bool)$primary;
4705          $this->add_class('menu-action');
4706          $this->attributes['role'] = 'menuitem';
4707      }
4708  
4709      /**
4710       * Export for template.
4711       *
4712       * @param renderer_base $output The renderer.
4713       * @return stdClass
4714       */
4715      public function export_for_template(renderer_base $output) {
4716          $data = parent::export_for_template($output);
4717          $data->instance = self::$instance++;
4718  
4719          // Ignore what the parent did with the attributes, except for ID and class.
4720          $data->attributes = [];
4721          $attributes = $this->attributes;
4722          unset($attributes['id']);
4723          unset($attributes['class']);
4724  
4725          // Handle text being a renderable.
4726          if ($this->text instanceof renderable) {
4727              $data->text = $this->render($this->text);
4728          }
4729  
4730          $data->showtext = (!$this->icon || $this->primary === false);
4731  
4732          $data->icon = null;
4733          if ($this->icon) {
4734              $icon = $this->icon;
4735              if ($this->primary || !$this->actionmenu->will_be_enhanced()) {
4736                  $attributes['title'] = $data->text;
4737              }
4738              $data->icon = $icon ? $icon->export_for_pix() : null;
4739          }
4740  
4741          $data->disabled = !empty($attributes['disabled']);
4742          unset($attributes['disabled']);
4743  
4744          $data->attributes = array_map(function($key, $value) {
4745              return [
4746                  'name' => $key,
4747                  'value' => $value
4748              ];
4749          }, array_keys($attributes), $attributes);
4750  
4751          return $data;
4752      }
4753  }
4754  
4755  /**
4756   * A primary action menu action
4757   *
4758   * @package core
4759   * @category output
4760   * @copyright 2013 Sam Hemelryk
4761   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4762   */
4763  class action_menu_link_primary extends action_menu_link {
4764      /**
4765       * Constructs the object.
4766       *
4767       * @param moodle_url $url
4768       * @param pix_icon $icon
4769       * @param string $text
4770       * @param array $attributes
4771       */
4772      public function __construct(moodle_url $url, pix_icon $icon = null, $text, array $attributes = array()) {
4773          parent::__construct($url, $icon, $text, true, $attributes);
4774      }
4775  }
4776  
4777  /**
4778   * A secondary action menu action
4779   *
4780   * @package core
4781   * @category output
4782   * @copyright 2013 Sam Hemelryk
4783   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4784   */
4785  class action_menu_link_secondary extends action_menu_link {
4786      /**
4787       * Constructs the object.
4788       *
4789       * @param moodle_url $url
4790       * @param pix_icon $icon
4791       * @param string $text
4792       * @param array $attributes
4793       */
4794      public function __construct(moodle_url $url, pix_icon $icon = null, $text, array $attributes = array()) {
4795          parent::__construct($url, $icon, $text, false, $attributes);
4796      }
4797  }
4798  
4799  /**
4800   * Represents a set of preferences groups.
4801   *
4802   * @package core
4803   * @category output
4804   * @copyright 2015 Frédéric Massart - FMCorz.net
4805   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4806   */
4807  class preferences_groups implements renderable {
4808  
4809      /**
4810       * Array of preferences_group.
4811       * @var array
4812       */
4813      public $groups;
4814  
4815      /**
4816       * Constructor.
4817       * @param array $groups of preferences_group
4818       */
4819      public function __construct($groups) {
4820          $this->groups = $groups;
4821      }
4822  
4823  }
4824  
4825  /**
4826   * Represents a group of preferences page link.
4827   *
4828   * @package core
4829   * @category output
4830   * @copyright 2015 Frédéric Massart - FMCorz.net
4831   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4832   */
4833  class preferences_group implements renderable {
4834  
4835      /**
4836       * Title of the group.
4837       * @var string
4838       */
4839      public $title;
4840  
4841      /**
4842       * Array of navigation_node.
4843       * @var array
4844       */
4845      public $nodes;
4846  
4847      /**
4848       * Constructor.
4849       * @param string $title The title.
4850       * @param array $nodes of navigation_node.
4851       */
4852      public function __construct($title, $nodes) {
4853          $this->title = $title;
4854          $this->nodes = $nodes;
4855      }
4856  }
4857  
4858  /**
4859   * Progress bar class.
4860   *
4861   * Manages the display of a progress bar.
4862   *
4863   * To use this class.
4864   * - construct
4865   * - call create (or use the 3rd param to the constructor)
4866   * - call update or update_full() or update() repeatedly
4867   *
4868   * @copyright 2008 jamiesensei
4869   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4870   * @package core
4871   * @category output
4872   */
4873  class progress_bar implements renderable, templatable {
4874      /** @var string html id */
4875      private $html_id;
4876      /** @var int total width */
4877      private $width;
4878      /** @var int last percentage printed */
4879      private $percent = 0;
4880      /** @var int time when last printed */
4881      private $lastupdate = 0;
4882      /** @var int when did we start printing this */
4883      private $time_start = 0;
4884  
4885      /**
4886       * Constructor
4887       *
4888       * Prints JS code if $autostart true.
4889       *
4890       * @param string $htmlid The container ID.
4891       * @param int $width The suggested width.
4892       * @param bool $autostart Whether to start the progress bar right away.
4893       */
4894      public function __construct($htmlid = '', $width = 500, $autostart = false) {
4895          if (!CLI_SCRIPT && !NO_OUTPUT_BUFFERING) {
4896              debugging('progress_bar used in a non-CLI script without setting NO_OUTPUT_BUFFERING.', DEBUG_DEVELOPER);
4897          }
4898  
4899          if (!empty($htmlid)) {
4900              $this->html_id  = $htmlid;
4901          } else {
4902              $this->html_id  = 'pbar_'.uniqid();
4903          }
4904  
4905          $this->width = $width;
4906  
4907          if ($autostart) {
4908              $this->create();
4909          }
4910      }
4911  
4912      /**
4913       * Create a new progress bar, this function will output html.
4914       *
4915       * @return void Echo's output
4916       */
4917      public function create() {
4918          global $OUTPUT;
4919  
4920          $this->time_start = microtime(true);
4921          if (CLI_SCRIPT) {
4922              return; // Temporary solution for cli scripts.
4923          }
4924  
4925          flush();
4926          echo $OUTPUT->render($this);
4927          flush();
4928      }
4929  
4930      /**
4931       * Update the progress bar.
4932       *
4933       * @param int $percent From 1-100.
4934       * @param string $msg The message.
4935       * @return void Echo's output
4936       * @throws coding_exception
4937       */
4938      private function _update($percent, $msg) {
4939          if (empty($this->time_start)) {
4940              throw new coding_exception('You must call create() (or use the $autostart ' .
4941                      'argument to the constructor) before you try updating the progress bar.');
4942          }
4943  
4944          if (CLI_SCRIPT) {
4945              return; // Temporary solution for cli scripts.
4946          }
4947  
4948          $estimate = $this->estimate($percent);
4949  
4950          if ($estimate === null) {
4951              // Always do the first and last updates.
4952          } else if ($estimate == 0) {
4953              // Always do the last updates.
4954          } else if ($this->lastupdate + 20 < time()) {
4955              // We must update otherwise browser would time out.
4956          } else if (round($this->percent, 2) === round($percent, 2)) {
4957              // No significant change, no need to update anything.
4958              return;
4959          }
4960  
4961          $estimatemsg = null;
4962          if (is_numeric($estimate)) {
4963              $estimatemsg = get_string('secondsleft', 'moodle', round($estimate, 2));
4964          }
4965  
4966          $this->percent = round($percent, 2);
4967          $this->lastupdate = microtime(true);
4968  
4969          echo html_writer::script(js_writer::function_call('updateProgressBar',
4970              array($this->html_id, $this->percent, $msg, $estimatemsg)));
4971          flush();
4972      }
4973  
4974      /**
4975       * Estimate how much time it is going to take.
4976       *
4977       * @param int $pt From 1-100.
4978       * @return mixed Null (unknown), or int.
4979       */
4980      private function estimate($pt) {
4981          if ($this->lastupdate == 0) {
4982              return null;
4983          }
4984          if ($pt < 0.00001) {
4985              return null; // We do not know yet how long it will take.
4986          }
4987          if ($pt > 99.99999) {
4988              return 0; // Nearly done, right?
4989          }
4990          $consumed = microtime(true) - $this->time_start;
4991          if ($consumed < 0.001) {
4992              return null;
4993          }
4994  
4995          return (100 - $pt) * ($consumed / $pt);
4996      }
4997  
4998      /**
4999       * Update progress bar according percent.
5000       *
5001       * @param int $percent From 1-100.
5002       * @param string $msg The message needed to be shown.
5003       */
5004      public function update_full($percent, $msg) {
5005          $percent = max(min($percent, 100), 0);
5006          $this->_update($percent, $msg);
5007      }
5008  
5009      /**
5010       * Update progress bar according the number of tasks.
5011       *
5012       * @param int $cur Current task number.
5013       * @param int $total Total task number.
5014       * @param string $msg The message needed to be shown.
5015       */
5016      public function update($cur, $total, $msg) {
5017          $percent = ($cur / $total) * 100;
5018          $this->update_full($percent, $msg);
5019      }
5020  
5021      /**
5022       * Restart the progress bar.
5023       */
5024      public function restart() {
5025          $this->percent    = 0;
5026          $this->lastupdate = 0;
5027          $this->time_start = 0;
5028      }
5029  
5030      /**
5031       * Export for template.
5032       *
5033       * @param  renderer_base $output The renderer.
5034       * @return array
5035       */
5036      public function export_for_template(renderer_base $output) {
5037          return [
5038              'id' => $this->html_id,
5039              'width' => $this->width,
5040          ];
5041      }
5042  }