Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 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 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

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