Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
/lib/ -> adminlib.php (source)

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

   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   * Functions and classes used during installation, upgrades and for admin settings.
  19   *
  20   *  ADMIN SETTINGS TREE INTRODUCTION
  21   *
  22   *  This file performs the following tasks:
  23   *   -it defines the necessary objects and interfaces to build the Moodle
  24   *    admin hierarchy
  25   *   -it defines the admin_externalpage_setup()
  26   *
  27   *  ADMIN_SETTING OBJECTS
  28   *
  29   *  Moodle settings are represented by objects that inherit from the admin_setting
  30   *  class. These objects encapsulate how to read a setting, how to write a new value
  31   *  to a setting, and how to appropriately display the HTML to modify the setting.
  32   *
  33   *  ADMIN_SETTINGPAGE OBJECTS
  34   *
  35   *  The admin_setting objects are then grouped into admin_settingpages. The latter
  36   *  appear in the Moodle admin tree block. All interaction with admin_settingpage
  37   *  objects is handled by the admin/settings.php file.
  38   *
  39   *  ADMIN_EXTERNALPAGE OBJECTS
  40   *
  41   *  There are some settings in Moodle that are too complex to (efficiently) handle
  42   *  with admin_settingpages. (Consider, for example, user management and displaying
  43   *  lists of users.) In this case, we use the admin_externalpage object. This object
  44   *  places a link to an external PHP file in the admin tree block.
  45   *
  46   *  If you're using an admin_externalpage object for some settings, you can take
  47   *  advantage of the admin_externalpage_* functions. For example, suppose you wanted
  48   *  to add a foo.php file into admin. First off, you add the following line to
  49   *  admin/settings/first.php (at the end of the file) or to some other file in
  50   *  admin/settings:
  51   * <code>
  52   *     $ADMIN->add('userinterface', new admin_externalpage('foo', get_string('foo'),
  53   *         $CFG->wwwdir . '/' . '$CFG->admin . '/foo.php', 'some_role_permission'));
  54   * </code>
  55   *
  56   *  Next, in foo.php, your file structure would resemble the following:
  57   * <code>
  58   *         require(__DIR__.'/../../config.php');
  59   *         require_once($CFG->libdir.'/adminlib.php');
  60   *         admin_externalpage_setup('foo');
  61   *         // functionality like processing form submissions goes here
  62   *         echo $OUTPUT->header();
  63   *         // your HTML goes here
  64   *         echo $OUTPUT->footer();
  65   * </code>
  66   *
  67   *  The admin_externalpage_setup() function call ensures the user is logged in,
  68   *  and makes sure that they have the proper role permission to access the page.
  69   *  It also configures all $PAGE properties needed for navigation.
  70   *
  71   *  ADMIN_CATEGORY OBJECTS
  72   *
  73   *  Above and beyond all this, we have admin_category objects. These objects
  74   *  appear as folders in the admin tree block. They contain admin_settingpage's,
  75   *  admin_externalpage's, and other admin_category's.
  76   *
  77   *  OTHER NOTES
  78   *
  79   *  admin_settingpage's, admin_externalpage's, and admin_category's all inherit
  80   *  from part_of_admin_tree (a pseudointerface). This interface insists that
  81   *  a class has a check_access method for access permissions, a locate method
  82   *  used to find a specific node in the admin tree and find parent path.
  83   *
  84   *  admin_category's inherit from parentable_part_of_admin_tree. This pseudo-
  85   *  interface ensures that the class implements a recursive add function which
  86   *  accepts a part_of_admin_tree object and searches for the proper place to
  87   *  put it. parentable_part_of_admin_tree implies part_of_admin_tree.
  88   *
  89   *  Please note that the $this->name field of any part_of_admin_tree must be
  90   *  UNIQUE throughout the ENTIRE admin tree.
  91   *
  92   *  The $this->name field of an admin_setting object (which is *not* part_of_
  93   *  admin_tree) must be unique on the respective admin_settingpage where it is
  94   *  used.
  95   *
  96   * Original author: Vincenzo K. Marcovecchio
  97   * Maintainer:      Petr Skoda
  98   *
  99   * @package    core
 100   * @subpackage admin
 101   * @copyright  1999 onwards Martin Dougiamas  http://dougiamas.com
 102   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 103   */
 104  
 105  defined('MOODLE_INTERNAL') || die();
 106  
 107  /// Add libraries
 108  require_once($CFG->libdir.'/ddllib.php');
 109  require_once($CFG->libdir.'/xmlize.php');
 110  require_once($CFG->libdir.'/messagelib.php');
 111  
 112  define('INSECURE_DATAROOT_WARNING', 1);
 113  define('INSECURE_DATAROOT_ERROR', 2);
 114  
 115  /**
 116   * Automatically clean-up all plugin data and remove the plugin DB tables
 117   *
 118   * NOTE: do not call directly, use new /admin/plugins.php?uninstall=component instead!
 119   *
 120   * @param string $type The plugin type, eg. 'mod', 'qtype', 'workshopgrading' etc.
 121   * @param string $name The plugin name, eg. 'forum', 'multichoice', 'accumulative' etc.
 122   * @uses global $OUTPUT to produce notices and other messages
 123   * @return void
 124   */
 125  function uninstall_plugin($type, $name) {
 126      global $CFG, $DB, $OUTPUT;
 127  
 128      // This may take a long time.
 129      core_php_time_limit::raise();
 130  
 131      // Recursively uninstall all subplugins first.
 132      $subplugintypes = core_component::get_plugin_types_with_subplugins();
 133      if (isset($subplugintypes[$type])) {
 134          $base = core_component::get_plugin_directory($type, $name);
 135  
 136          $subpluginsfile = "{$base}/db/subplugins.json";
 137          if (file_exists($subpluginsfile)) {
 138              $subplugins = (array) json_decode(file_get_contents($subpluginsfile))->plugintypes;
 139          } else if (file_exists("{$base}/db/subplugins.php")) {
 140              debugging('Use of subplugins.php has been deprecated. ' .
 141                      'Please update your plugin to provide a subplugins.json file instead.',
 142                      DEBUG_DEVELOPER);
 143              $subplugins = [];
 144              include("{$base}/db/subplugins.php");
 145          }
 146  
 147          if (!empty($subplugins)) {
 148              foreach (array_keys($subplugins) as $subplugintype) {
 149                  $instances = core_component::get_plugin_list($subplugintype);
 150                  foreach ($instances as $subpluginname => $notusedpluginpath) {
 151                      uninstall_plugin($subplugintype, $subpluginname);
 152                  }
 153              }
 154          }
 155      }
 156  
 157      $component = $type . '_' . $name;  // eg. 'qtype_multichoice' or 'workshopgrading_accumulative' or 'mod_forum'
 158  
 159      if ($type === 'mod') {
 160          $pluginname = $name;  // eg. 'forum'
 161          if (get_string_manager()->string_exists('modulename', $component)) {
 162              $strpluginname = get_string('modulename', $component);
 163          } else {
 164              $strpluginname = $component;
 165          }
 166  
 167      } else {
 168          $pluginname = $component;
 169          if (get_string_manager()->string_exists('pluginname', $component)) {
 170              $strpluginname = get_string('pluginname', $component);
 171          } else {
 172              $strpluginname = $component;
 173          }
 174      }
 175  
 176      echo $OUTPUT->heading($pluginname);
 177  
 178      // Delete all tag areas, collections and instances associated with this plugin.
 179      core_tag_area::uninstall($component);
 180  
 181      // Custom plugin uninstall.
 182      $plugindirectory = core_component::get_plugin_directory($type, $name);
 183      $uninstalllib = $plugindirectory . '/db/uninstall.php';
 184      if (file_exists($uninstalllib)) {
 185          require_once($uninstalllib);
 186          $uninstallfunction = 'xmldb_' . $pluginname . '_uninstall';    // eg. 'xmldb_workshop_uninstall()'
 187          if (function_exists($uninstallfunction)) {
 188              // Do not verify result, let plugin complain if necessary.
 189              $uninstallfunction();
 190          }
 191      }
 192  
 193      // Specific plugin type cleanup.
 194      $plugininfo = core_plugin_manager::instance()->get_plugin_info($component);
 195      if ($plugininfo) {
 196          $plugininfo->uninstall_cleanup();
 197          core_plugin_manager::reset_caches();
 198      }
 199      $plugininfo = null;
 200  
 201      // perform clean-up task common for all the plugin/subplugin types
 202  
 203      //delete the web service functions and pre-built services
 204      require_once($CFG->dirroot.'/lib/externallib.php');
 205      external_delete_descriptions($component);
 206  
 207      // delete calendar events
 208      $DB->delete_records('event', array('modulename' => $pluginname));
 209      $DB->delete_records('event', ['component' => $component]);
 210  
 211      // Delete scheduled tasks.
 212      $DB->delete_records('task_adhoc', ['component' => $component]);
 213      $DB->delete_records('task_scheduled', array('component' => $component));
 214  
 215      // Delete Inbound Message datakeys.
 216      $DB->delete_records_select('messageinbound_datakeys',
 217              'handler IN (SELECT id FROM {messageinbound_handlers} WHERE component = ?)', array($component));
 218  
 219      // Delete Inbound Message handlers.
 220      $DB->delete_records('messageinbound_handlers', array('component' => $component));
 221  
 222      // delete all the logs
 223      $DB->delete_records('log', array('module' => $pluginname));
 224  
 225      // delete log_display information
 226      $DB->delete_records('log_display', array('component' => $component));
 227  
 228      // delete the module configuration records
 229      unset_all_config_for_plugin($component);
 230      if ($type === 'mod') {
 231          unset_all_config_for_plugin($pluginname);
 232      }
 233  
 234      // delete message provider
 235      message_provider_uninstall($component);
 236  
 237      // delete the plugin tables
 238      $xmldbfilepath = $plugindirectory . '/db/install.xml';
 239      drop_plugin_tables($component, $xmldbfilepath, false);
 240      if ($type === 'mod' or $type === 'block') {
 241          // non-frankenstyle table prefixes
 242          drop_plugin_tables($name, $xmldbfilepath, false);
 243      }
 244  
 245      // delete the capabilities that were defined by this module
 246      capabilities_cleanup($component);
 247  
 248      // Delete all remaining files in the filepool owned by the component.
 249      $fs = get_file_storage();
 250      $fs->delete_component_files($component);
 251  
 252      // Finally purge all caches.
 253      purge_all_caches();
 254  
 255      // Invalidate the hash used for upgrade detections.
 256      set_config('allversionshash', '');
 257  
 258      echo $OUTPUT->notification(get_string('success'), 'notifysuccess');
 259  }
 260  
 261  /**
 262   * Returns the version of installed component
 263   *
 264   * @param string $component component name
 265   * @param string $source either 'disk' or 'installed' - where to get the version information from
 266   * @return string|bool version number or false if the component is not found
 267   */
 268  function get_component_version($component, $source='installed') {
 269      global $CFG, $DB;
 270  
 271      list($type, $name) = core_component::normalize_component($component);
 272  
 273      // moodle core or a core subsystem
 274      if ($type === 'core') {
 275          if ($source === 'installed') {
 276              if (empty($CFG->version)) {
 277                  return false;
 278              } else {
 279                  return $CFG->version;
 280              }
 281          } else {
 282              if (!is_readable($CFG->dirroot.'/version.php')) {
 283                  return false;
 284              } else {
 285                  $version = null; //initialize variable for IDEs
 286                  include($CFG->dirroot.'/version.php');
 287                  return $version;
 288              }
 289          }
 290      }
 291  
 292      // activity module
 293      if ($type === 'mod') {
 294          if ($source === 'installed') {
 295              if ($CFG->version < 2013092001.02) {
 296                  return $DB->get_field('modules', 'version', array('name'=>$name));
 297              } else {
 298                  return get_config('mod_'.$name, 'version');
 299              }
 300  
 301          } else {
 302              $mods = core_component::get_plugin_list('mod');
 303              if (empty($mods[$name]) or !is_readable($mods[$name].'/version.php')) {
 304                  return false;
 305              } else {
 306                  $plugin = new stdClass();
 307                  $plugin->version = null;
 308                  $module = $plugin;
 309                  include($mods[$name].'/version.php');
 310                  return $plugin->version;
 311              }
 312          }
 313      }
 314  
 315      // block
 316      if ($type === 'block') {
 317          if ($source === 'installed') {
 318              if ($CFG->version < 2013092001.02) {
 319                  return $DB->get_field('block', 'version', array('name'=>$name));
 320              } else {
 321                  return get_config('block_'.$name, 'version');
 322              }
 323          } else {
 324              $blocks = core_component::get_plugin_list('block');
 325              if (empty($blocks[$name]) or !is_readable($blocks[$name].'/version.php')) {
 326                  return false;
 327              } else {
 328                  $plugin = new stdclass();
 329                  include($blocks[$name].'/version.php');
 330                  return $plugin->version;
 331              }
 332          }
 333      }
 334  
 335      // all other plugin types
 336      if ($source === 'installed') {
 337          return get_config($type.'_'.$name, 'version');
 338      } else {
 339          $plugins = core_component::get_plugin_list($type);
 340          if (empty($plugins[$name])) {
 341              return false;
 342          } else {
 343              $plugin = new stdclass();
 344              include($plugins[$name].'/version.php');
 345              return $plugin->version;
 346          }
 347      }
 348  }
 349  
 350  /**
 351   * Delete all plugin tables
 352   *
 353   * @param string $name Name of plugin, used as table prefix
 354   * @param string $file Path to install.xml file
 355   * @param bool $feedback defaults to true
 356   * @return bool Always returns true
 357   */
 358  function drop_plugin_tables($name, $file, $feedback=true) {
 359      global $CFG, $DB;
 360  
 361      // first try normal delete
 362      if (file_exists($file) and $DB->get_manager()->delete_tables_from_xmldb_file($file)) {
 363          return true;
 364      }
 365  
 366      // then try to find all tables that start with name and are not in any xml file
 367      $used_tables = get_used_table_names();
 368  
 369      $tables = $DB->get_tables();
 370  
 371      /// Iterate over, fixing id fields as necessary
 372      foreach ($tables as $table) {
 373          if (in_array($table, $used_tables)) {
 374              continue;
 375          }
 376  
 377          if (strpos($table, $name) !== 0) {
 378              continue;
 379          }
 380  
 381          // found orphan table --> delete it
 382          if ($DB->get_manager()->table_exists($table)) {
 383              $xmldb_table = new xmldb_table($table);
 384              $DB->get_manager()->drop_table($xmldb_table);
 385          }
 386      }
 387  
 388      return true;
 389  }
 390  
 391  /**
 392   * Returns names of all known tables == tables that moodle knows about.
 393   *
 394   * @return array Array of lowercase table names
 395   */
 396  function get_used_table_names() {
 397      $table_names = array();
 398      $dbdirs = get_db_directories();
 399  
 400      foreach ($dbdirs as $dbdir) {
 401          $file = $dbdir.'/install.xml';
 402  
 403          $xmldb_file = new xmldb_file($file);
 404  
 405          if (!$xmldb_file->fileExists()) {
 406              continue;
 407          }
 408  
 409          $loaded    = $xmldb_file->loadXMLStructure();
 410          $structure = $xmldb_file->getStructure();
 411  
 412          if ($loaded and $tables = $structure->getTables()) {
 413              foreach($tables as $table) {
 414                  $table_names[] = strtolower($table->getName());
 415              }
 416          }
 417      }
 418  
 419      return $table_names;
 420  }
 421  
 422  /**
 423   * Returns list of all directories where we expect install.xml files
 424   * @return array Array of paths
 425   */
 426  function get_db_directories() {
 427      global $CFG;
 428  
 429      $dbdirs = array();
 430  
 431      /// First, the main one (lib/db)
 432      $dbdirs[] = $CFG->libdir.'/db';
 433  
 434      /// Then, all the ones defined by core_component::get_plugin_types()
 435      $plugintypes = core_component::get_plugin_types();
 436      foreach ($plugintypes as $plugintype => $pluginbasedir) {
 437          if ($plugins = core_component::get_plugin_list($plugintype)) {
 438              foreach ($plugins as $plugin => $plugindir) {
 439                  $dbdirs[] = $plugindir.'/db';
 440              }
 441          }
 442      }
 443  
 444      return $dbdirs;
 445  }
 446  
 447  /**
 448   * Try to obtain or release the cron lock.
 449   * @param string  $name  name of lock
 450   * @param int  $until timestamp when this lock considered stale, null means remove lock unconditionally
 451   * @param bool $ignorecurrent ignore current lock state, usually extend previous lock, defaults to false
 452   * @return bool true if lock obtained
 453   */
 454  function set_cron_lock($name, $until, $ignorecurrent=false) {
 455      global $DB;
 456      if (empty($name)) {
 457          debugging("Tried to get a cron lock for a null fieldname");
 458          return false;
 459      }
 460  
 461      // remove lock by force == remove from config table
 462      if (is_null($until)) {
 463          set_config($name, null);
 464          return true;
 465      }
 466  
 467      if (!$ignorecurrent) {
 468          // read value from db - other processes might have changed it
 469          $value = $DB->get_field('config', 'value', array('name'=>$name));
 470  
 471          if ($value and $value > time()) {
 472              //lock active
 473              return false;
 474          }
 475      }
 476  
 477      set_config($name, $until);
 478      return true;
 479  }
 480  
 481  /**
 482   * Test if and critical warnings are present
 483   * @return bool
 484   */
 485  function admin_critical_warnings_present() {
 486      global $SESSION;
 487  
 488      if (!has_capability('moodle/site:config', context_system::instance())) {
 489          return 0;
 490      }
 491  
 492      if (!isset($SESSION->admin_critical_warning)) {
 493          $SESSION->admin_critical_warning = 0;
 494          if (is_dataroot_insecure(true) === INSECURE_DATAROOT_ERROR) {
 495              $SESSION->admin_critical_warning = 1;
 496          }
 497      }
 498  
 499      return $SESSION->admin_critical_warning;
 500  }
 501  
 502  /**
 503   * Detects if float supports at least 10 decimal digits
 504   *
 505   * Detects if float supports at least 10 decimal digits
 506   * and also if float-->string conversion works as expected.
 507   *
 508   * @return bool true if problem found
 509   */
 510  function is_float_problem() {
 511      $num1 = 2009010200.01;
 512      $num2 = 2009010200.02;
 513  
 514      return ((string)$num1 === (string)$num2 or $num1 === $num2 or $num2 <= (string)$num1);
 515  }
 516  
 517  /**
 518   * Try to verify that dataroot is not accessible from web.
 519   *
 520   * Try to verify that dataroot is not accessible from web.
 521   * It is not 100% correct but might help to reduce number of vulnerable sites.
 522   * Protection from httpd.conf and .htaccess is not detected properly.
 523   *
 524   * @uses INSECURE_DATAROOT_WARNING
 525   * @uses INSECURE_DATAROOT_ERROR
 526   * @param bool $fetchtest try to test public access by fetching file, default false
 527   * @return mixed empty means secure, INSECURE_DATAROOT_ERROR found a critical problem, INSECURE_DATAROOT_WARNING might be problematic
 528   */
 529  function is_dataroot_insecure($fetchtest=false) {
 530      global $CFG;
 531  
 532      $siteroot = str_replace('\\', '/', strrev($CFG->dirroot.'/')); // win32 backslash workaround
 533  
 534      $rp = preg_replace('|https?://[^/]+|i', '', $CFG->wwwroot, 1);
 535      $rp = strrev(trim($rp, '/'));
 536      $rp = explode('/', $rp);
 537      foreach($rp as $r) {
 538          if (strpos($siteroot, '/'.$r.'/') === 0) {
 539              $siteroot = substr($siteroot, strlen($r)+1); // moodle web in subdirectory
 540          } else {
 541              break; // probably alias root
 542          }
 543      }
 544  
 545      $siteroot = strrev($siteroot);
 546      $dataroot = str_replace('\\', '/', $CFG->dataroot.'/');
 547  
 548      if (strpos($dataroot, $siteroot) !== 0) {
 549          return false;
 550      }
 551  
 552      if (!$fetchtest) {
 553          return INSECURE_DATAROOT_WARNING;
 554      }
 555  
 556      // now try all methods to fetch a test file using http protocol
 557  
 558      $httpdocroot = str_replace('\\', '/', strrev($CFG->dirroot.'/'));
 559      preg_match('|(https?://[^/]+)|i', $CFG->wwwroot, $matches);
 560      $httpdocroot = $matches[1];
 561      $datarooturl = $httpdocroot.'/'. substr($dataroot, strlen($siteroot));
 562      make_upload_directory('diag');
 563      $testfile = $CFG->dataroot.'/diag/public.txt';
 564      if (!file_exists($testfile)) {
 565          file_put_contents($testfile, 'test file, do not delete');
 566          @chmod($testfile, $CFG->filepermissions);
 567      }
 568      $teststr = trim(file_get_contents($testfile));
 569      if (empty($teststr)) {
 570      // hmm, strange
 571          return INSECURE_DATAROOT_WARNING;
 572      }
 573  
 574      $testurl = $datarooturl.'/diag/public.txt';
 575      if (extension_loaded('curl') and
 576          !(stripos(ini_get('disable_functions'), 'curl_init') !== FALSE) and
 577          !(stripos(ini_get('disable_functions'), 'curl_setop') !== FALSE) and
 578          ($ch = @curl_init($testurl)) !== false) {
 579          curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 580          curl_setopt($ch, CURLOPT_HEADER, false);
 581          $data = curl_exec($ch);
 582          if (!curl_errno($ch)) {
 583              $data = trim($data);
 584              if ($data === $teststr) {
 585                  curl_close($ch);
 586                  return INSECURE_DATAROOT_ERROR;
 587              }
 588          }
 589          curl_close($ch);
 590      }
 591  
 592      if ($data = @file_get_contents($testurl)) {
 593          $data = trim($data);
 594          if ($data === $teststr) {
 595              return INSECURE_DATAROOT_ERROR;
 596          }
 597      }
 598  
 599      preg_match('|https?://([^/]+)|i', $testurl, $matches);
 600      $sitename = $matches[1];
 601      $error = 0;
 602      if ($fp = @fsockopen($sitename, 80, $error)) {
 603          preg_match('|https?://[^/]+(.*)|i', $testurl, $matches);
 604          $localurl = $matches[1];
 605          $out = "GET $localurl HTTP/1.1\r\n";
 606          $out .= "Host: $sitename\r\n";
 607          $out .= "Connection: Close\r\n\r\n";
 608          fwrite($fp, $out);
 609          $data = '';
 610          $incoming = false;
 611          while (!feof($fp)) {
 612              if ($incoming) {
 613                  $data .= fgets($fp, 1024);
 614              } else if (@fgets($fp, 1024) === "\r\n") {
 615                      $incoming = true;
 616                  }
 617          }
 618          fclose($fp);
 619          $data = trim($data);
 620          if ($data === $teststr) {
 621              return INSECURE_DATAROOT_ERROR;
 622          }
 623      }
 624  
 625      return INSECURE_DATAROOT_WARNING;
 626  }
 627  
 628  /**
 629   * Enables CLI maintenance mode by creating new dataroot/climaintenance.html file.
 630   */
 631  function enable_cli_maintenance_mode() {
 632      global $CFG, $SITE;
 633  
 634      if (file_exists("$CFG->dataroot/climaintenance.html")) {
 635          unlink("$CFG->dataroot/climaintenance.html");
 636      }
 637  
 638      if (isset($CFG->maintenance_message) and !html_is_blank($CFG->maintenance_message)) {
 639          $data = $CFG->maintenance_message;
 640          $data = bootstrap_renderer::early_error_content($data, null, null, null);
 641          $data = bootstrap_renderer::plain_page(get_string('sitemaintenance', 'admin'), $data);
 642  
 643      } else if (file_exists("$CFG->dataroot/climaintenance.template.html")) {
 644          $data = file_get_contents("$CFG->dataroot/climaintenance.template.html");
 645  
 646      } else {
 647          $data = get_string('sitemaintenance', 'admin');
 648          $data = bootstrap_renderer::early_error_content($data, null, null, null);
 649          $data = bootstrap_renderer::plain_page(get_string('sitemaintenancetitle', 'admin',
 650              format_string($SITE->fullname, true, ['context' => context_system::instance()])), $data);
 651      }
 652  
 653      file_put_contents("$CFG->dataroot/climaintenance.html", $data);
 654      chmod("$CFG->dataroot/climaintenance.html", $CFG->filepermissions);
 655  }
 656  
 657  /// CLASS DEFINITIONS /////////////////////////////////////////////////////////
 658  
 659  
 660  /**
 661   * Interface for anything appearing in the admin tree
 662   *
 663   * The interface that is implemented by anything that appears in the admin tree
 664   * block. It forces inheriting classes to define a method for checking user permissions
 665   * and methods for finding something in the admin tree.
 666   *
 667   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 668   */
 669  interface part_of_admin_tree {
 670  
 671  /**
 672   * Finds a named part_of_admin_tree.
 673   *
 674   * Used to find a part_of_admin_tree. If a class only inherits part_of_admin_tree
 675   * and not parentable_part_of_admin_tree, then this function should only check if
 676   * $this->name matches $name. If it does, it should return a reference to $this,
 677   * otherwise, it should return a reference to NULL.
 678   *
 679   * If a class inherits parentable_part_of_admin_tree, this method should be called
 680   * recursively on all child objects (assuming, of course, the parent object's name
 681   * doesn't match the search criterion).
 682   *
 683   * @param string $name The internal name of the part_of_admin_tree we're searching for.
 684   * @return mixed An object reference or a NULL reference.
 685   */
 686      public function locate($name);
 687  
 688      /**
 689       * Removes named part_of_admin_tree.
 690       *
 691       * @param string $name The internal name of the part_of_admin_tree we want to remove.
 692       * @return bool success.
 693       */
 694      public function prune($name);
 695  
 696      /**
 697       * Search using query
 698       * @param string $query
 699       * @return mixed array-object structure of found settings and pages
 700       */
 701      public function search($query);
 702  
 703      /**
 704       * Verifies current user's access to this part_of_admin_tree.
 705       *
 706       * Used to check if the current user has access to this part of the admin tree or
 707       * not. If a class only inherits part_of_admin_tree and not parentable_part_of_admin_tree,
 708       * then this method is usually just a call to has_capability() in the site context.
 709       *
 710       * If a class inherits parentable_part_of_admin_tree, this method should return the
 711       * logical OR of the return of check_access() on all child objects.
 712       *
 713       * @return bool True if the user has access, false if she doesn't.
 714       */
 715      public function check_access();
 716  
 717      /**
 718       * Mostly useful for removing of some parts of the tree in admin tree block.
 719       *
 720       * @return True is hidden from normal list view
 721       */
 722      public function is_hidden();
 723  
 724      /**
 725       * Show we display Save button at the page bottom?
 726       * @return bool
 727       */
 728      public function show_save();
 729  }
 730  
 731  
 732  /**
 733   * Interface implemented by any part_of_admin_tree that has children.
 734   *
 735   * The interface implemented by any part_of_admin_tree that can be a parent
 736   * to other part_of_admin_tree's. (For now, this only includes admin_category.) Apart
 737   * from ensuring part_of_admin_tree compliancy, it also ensures inheriting methods
 738   * include an add method for adding other part_of_admin_tree objects as children.
 739   *
 740   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 741   */
 742  interface parentable_part_of_admin_tree extends part_of_admin_tree {
 743  
 744  /**
 745   * Adds a part_of_admin_tree object to the admin tree.
 746   *
 747   * Used to add a part_of_admin_tree object to this object or a child of this
 748   * object. $something should only be added if $destinationname matches
 749   * $this->name. If it doesn't, add should be called on child objects that are
 750   * also parentable_part_of_admin_tree's.
 751   *
 752   * $something should be appended as the last child in the $destinationname. If the
 753   * $beforesibling is specified, $something should be prepended to it. If the given
 754   * sibling is not found, $something should be appended to the end of $destinationname
 755   * and a developer debugging message should be displayed.
 756   *
 757   * @param string $destinationname The internal name of the new parent for $something.
 758   * @param part_of_admin_tree $something The object to be added.
 759   * @return bool True on success, false on failure.
 760   */
 761      public function add($destinationname, $something, $beforesibling = null);
 762  
 763  }
 764  
 765  
 766  /**
 767   * The object used to represent folders (a.k.a. categories) in the admin tree block.
 768   *
 769   * Each admin_category object contains a number of part_of_admin_tree objects.
 770   *
 771   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 772   */
 773  class admin_category implements parentable_part_of_admin_tree {
 774  
 775      /** @var part_of_admin_tree[] An array of part_of_admin_tree objects that are this object's children */
 776      protected $children;
 777      /** @var string An internal name for this category. Must be unique amongst ALL part_of_admin_tree objects */
 778      public $name;
 779      /** @var string The displayed name for this category. Usually obtained through get_string() */
 780      public $visiblename;
 781      /** @var bool Should this category be hidden in admin tree block? */
 782      public $hidden;
 783      /** @var mixed Either a string or an array or strings */
 784      public $path;
 785      /** @var mixed Either a string or an array or strings */
 786      public $visiblepath;
 787  
 788      /** @var array fast lookup category cache, all categories of one tree point to one cache */
 789      protected $category_cache;
 790  
 791      /** @var bool If set to true children will be sorted when calling {@link admin_category::get_children()} */
 792      protected $sort = false;
 793      /** @var bool If set to true children will be sorted in ascending order. */
 794      protected $sortasc = true;
 795      /** @var bool If set to true sub categories and pages will be split and then sorted.. */
 796      protected $sortsplit = true;
 797      /** @var bool $sorted True if the children have been sorted and don't need resorting */
 798      protected $sorted = false;
 799  
 800      /**
 801       * Constructor for an empty admin category
 802       *
 803       * @param string $name The internal name for this category. Must be unique amongst ALL part_of_admin_tree objects
 804       * @param string $visiblename The displayed named for this category. Usually obtained through get_string()
 805       * @param bool $hidden hide category in admin tree block, defaults to false
 806       */
 807      public function __construct($name, $visiblename, $hidden=false) {
 808          $this->children    = array();
 809          $this->name        = $name;
 810          $this->visiblename = $visiblename;
 811          $this->hidden      = $hidden;
 812      }
 813  
 814      /**
 815       * Get the URL to view this page.
 816       *
 817       * @return moodle_url
 818       */
 819      public function get_settings_page_url(): moodle_url {
 820          return new moodle_url(
 821              '/admin/category.php',
 822              [
 823                  'category' => $this->name,
 824              ]
 825          );
 826      }
 827  
 828      /**
 829       * Returns a reference to the part_of_admin_tree object with internal name $name.
 830       *
 831       * @param string $name The internal name of the object we want.
 832       * @param bool $findpath initialize path and visiblepath arrays
 833       * @return mixed A reference to the object with internal name $name if found, otherwise a reference to NULL.
 834       *                  defaults to false
 835       */
 836      public function locate($name, $findpath=false) {
 837          if (!isset($this->category_cache[$this->name])) {
 838              // somebody much have purged the cache
 839              $this->category_cache[$this->name] = $this;
 840          }
 841  
 842          if ($this->name == $name) {
 843              if ($findpath) {
 844                  $this->visiblepath[] = $this->visiblename;
 845                  $this->path[]        = $this->name;
 846              }
 847              return $this;
 848          }
 849  
 850          // quick category lookup
 851          if (!$findpath and isset($this->category_cache[$name])) {
 852              return $this->category_cache[$name];
 853          }
 854  
 855          $return = NULL;
 856          foreach($this->children as $childid=>$unused) {
 857              if ($return = $this->children[$childid]->locate($name, $findpath)) {
 858                  break;
 859              }
 860          }
 861  
 862          if (!is_null($return) and $findpath) {
 863              $return->visiblepath[] = $this->visiblename;
 864              $return->path[]        = $this->name;
 865          }
 866  
 867          return $return;
 868      }
 869  
 870      /**
 871       * Search using query
 872       *
 873       * @param string query
 874       * @return mixed array-object structure of found settings and pages
 875       */
 876      public function search($query) {
 877          $result = array();
 878          foreach ($this->get_children() as $child) {
 879              $subsearch = $child->search($query);
 880              if (!is_array($subsearch)) {
 881                  debugging('Incorrect search result from '.$child->name);
 882                  continue;
 883              }
 884              $result = array_merge($result, $subsearch);
 885          }
 886          return $result;
 887      }
 888  
 889      /**
 890       * Removes part_of_admin_tree object with internal name $name.
 891       *
 892       * @param string $name The internal name of the object we want to remove.
 893       * @return bool success
 894       */
 895      public function prune($name) {
 896  
 897          if ($this->name == $name) {
 898              return false;  //can not remove itself
 899          }
 900  
 901          foreach($this->children as $precedence => $child) {
 902              if ($child->name == $name) {
 903                  // clear cache and delete self
 904                  while($this->category_cache) {
 905                      // delete the cache, but keep the original array address
 906                      array_pop($this->category_cache);
 907                  }
 908                  unset($this->children[$precedence]);
 909                  return true;
 910              } else if ($this->children[$precedence]->prune($name)) {
 911                  return true;
 912              }
 913          }
 914          return false;
 915      }
 916  
 917      /**
 918       * Adds a part_of_admin_tree to a child or grandchild (or great-grandchild, and so forth) of this object.
 919       *
 920       * By default the new part of the tree is appended as the last child of the parent. You
 921       * can specify a sibling node that the new part should be prepended to. If the given
 922       * sibling is not found, the part is appended to the end (as it would be by default) and
 923       * a developer debugging message is displayed.
 924       *
 925       * @throws coding_exception if the $beforesibling is empty string or is not string at all.
 926       * @param string $destinationame The internal name of the immediate parent that we want for $something.
 927       * @param mixed $something A part_of_admin_tree or setting instance to be added.
 928       * @param string $beforesibling The name of the parent's child the $something should be prepended to.
 929       * @return bool True if successfully added, false if $something can not be added.
 930       */
 931      public function add($parentname, $something, $beforesibling = null) {
 932          global $CFG;
 933  
 934          $parent = $this->locate($parentname);
 935          if (is_null($parent)) {
 936              debugging('parent does not exist!');
 937              return false;
 938          }
 939  
 940          if ($something instanceof part_of_admin_tree) {
 941              if (!($parent instanceof parentable_part_of_admin_tree)) {
 942                  debugging('error - parts of tree can be inserted only into parentable parts');
 943                  return false;
 944              }
 945              if ($CFG->debugdeveloper && !is_null($this->locate($something->name))) {
 946                  // The name of the node is already used, simply warn the developer that this should not happen.
 947                  // It is intentional to check for the debug level before performing the check.
 948                  debugging('Duplicate admin page name: ' . $something->name, DEBUG_DEVELOPER);
 949              }
 950              if (is_null($beforesibling)) {
 951                  // Append $something as the parent's last child.
 952                  $parent->children[] = $something;
 953              } else {
 954                  if (!is_string($beforesibling) or trim($beforesibling) === '') {
 955                      throw new coding_exception('Unexpected value of the beforesibling parameter');
 956                  }
 957                  // Try to find the position of the sibling.
 958                  $siblingposition = null;
 959                  foreach ($parent->children as $childposition => $child) {
 960                      if ($child->name === $beforesibling) {
 961                          $siblingposition = $childposition;
 962                          break;
 963                      }
 964                  }
 965                  if (is_null($siblingposition)) {
 966                      debugging('Sibling '.$beforesibling.' not found', DEBUG_DEVELOPER);
 967                      $parent->children[] = $something;
 968                  } else {
 969                      $parent->children = array_merge(
 970                          array_slice($parent->children, 0, $siblingposition),
 971                          array($something),
 972                          array_slice($parent->children, $siblingposition)
 973                      );
 974                  }
 975              }
 976              if ($something instanceof admin_category) {
 977                  if (isset($this->category_cache[$something->name])) {
 978                      debugging('Duplicate admin category name: '.$something->name);
 979                  } else {
 980                      $this->category_cache[$something->name] = $something;
 981                      $something->category_cache =& $this->category_cache;
 982                      foreach ($something->children as $child) {
 983                          // just in case somebody already added subcategories
 984                          if ($child instanceof admin_category) {
 985                              if (isset($this->category_cache[$child->name])) {
 986                                  debugging('Duplicate admin category name: '.$child->name);
 987                              } else {
 988                                  $this->category_cache[$child->name] = $child;
 989                                  $child->category_cache =& $this->category_cache;
 990                              }
 991                          }
 992                      }
 993                  }
 994              }
 995              return true;
 996  
 997          } else {
 998              debugging('error - can not add this element');
 999              return false;
1000          }
1001  
1002      }
1003  
1004      /**
1005       * Checks if the user has access to anything in this category.
1006       *
1007       * @return bool True if the user has access to at least one child in this category, false otherwise.
1008       */
1009      public function check_access() {
1010          foreach ($this->children as $child) {
1011              if ($child->check_access()) {
1012                  return true;
1013              }
1014          }
1015          return false;
1016      }
1017  
1018      /**
1019       * Is this category hidden in admin tree block?
1020       *
1021       * @return bool True if hidden
1022       */
1023      public function is_hidden() {
1024          return $this->hidden;
1025      }
1026  
1027      /**
1028       * Show we display Save button at the page bottom?
1029       * @return bool
1030       */
1031      public function show_save() {
1032          foreach ($this->children as $child) {
1033              if ($child->show_save()) {
1034                  return true;
1035              }
1036          }
1037          return false;
1038      }
1039  
1040      /**
1041       * Sets sorting on this category.
1042       *
1043       * Please note this function doesn't actually do the sorting.
1044       * It can be called anytime.
1045       * Sorting occurs when the user calls get_children.
1046       * Code using the children array directly won't see the sorted results.
1047       *
1048       * @param bool $sort If set to true children will be sorted, if false they won't be.
1049       * @param bool $asc If true sorting will be ascending, otherwise descending.
1050       * @param bool $split If true we sort pages and sub categories separately.
1051       */
1052      public function set_sorting($sort, $asc = true, $split = true) {
1053          $this->sort = (bool)$sort;
1054          $this->sortasc = (bool)$asc;
1055          $this->sortsplit = (bool)$split;
1056      }
1057  
1058      /**
1059       * Returns the children associated with this category.
1060       *
1061       * @return part_of_admin_tree[]
1062       */
1063      public function get_children() {
1064          // If we should sort and it hasn't already been sorted.
1065          if ($this->sort && !$this->sorted) {
1066              if ($this->sortsplit) {
1067                  $categories = array();
1068                  $pages = array();
1069                  foreach ($this->children as $child) {
1070                      if ($child instanceof admin_category) {
1071                          $categories[] = $child;
1072                      } else {
1073                          $pages[] = $child;
1074                      }
1075                  }
1076                  core_collator::asort_objects_by_property($categories, 'visiblename');
1077                  core_collator::asort_objects_by_property($pages, 'visiblename');
1078                  if (!$this->sortasc) {
1079                      $categories = array_reverse($categories);
1080                      $pages = array_reverse($pages);
1081                  }
1082                  $this->children = array_merge($pages, $categories);
1083              } else {
1084                  core_collator::asort_objects_by_property($this->children, 'visiblename');
1085                  if (!$this->sortasc) {
1086                      $this->children = array_reverse($this->children);
1087                  }
1088              }
1089              $this->sorted = true;
1090          }
1091          return $this->children;
1092      }
1093  
1094      /**
1095       * Magically gets a property from this object.
1096       *
1097       * @param $property
1098       * @return part_of_admin_tree[]
1099       * @throws coding_exception
1100       */
1101      public function __get($property) {
1102          if ($property === 'children') {
1103              return $this->get_children();
1104          }
1105          throw new coding_exception('Invalid property requested.');
1106      }
1107  
1108      /**
1109       * Magically sets a property against this object.
1110       *
1111       * @param string $property
1112       * @param mixed $value
1113       * @throws coding_exception
1114       */
1115      public function __set($property, $value) {
1116          if ($property === 'children') {
1117              $this->sorted = false;
1118              $this->children = $value;
1119          } else {
1120              throw new coding_exception('Invalid property requested.');
1121          }
1122      }
1123  
1124      /**
1125       * Checks if an inaccessible property is set.
1126       *
1127       * @param string $property
1128       * @return bool
1129       * @throws coding_exception
1130       */
1131      public function __isset($property) {
1132          if ($property === 'children') {
1133              return isset($this->children);
1134          }
1135          throw new coding_exception('Invalid property requested.');
1136      }
1137  }
1138  
1139  
1140  /**
1141   * Root of admin settings tree, does not have any parent.
1142   *
1143   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1144   */
1145  class admin_root extends admin_category {
1146  /** @var array List of errors */
1147      public $errors;
1148      /** @var string search query */
1149      public $search;
1150      /** @var bool full tree flag - true means all settings required, false only pages required */
1151      public $fulltree;
1152      /** @var bool flag indicating loaded tree */
1153      public $loaded;
1154      /** @var mixed site custom defaults overriding defaults in settings files*/
1155      public $custom_defaults;
1156  
1157      /**
1158       * @param bool $fulltree true means all settings required,
1159       *                            false only pages required
1160       */
1161      public function __construct($fulltree) {
1162          global $CFG;
1163  
1164          parent::__construct('root', get_string('administration'), false);
1165          $this->errors   = array();
1166          $this->search   = '';
1167          $this->fulltree = $fulltree;
1168          $this->loaded   = false;
1169  
1170          $this->category_cache = array();
1171  
1172          // load custom defaults if found
1173          $this->custom_defaults = null;
1174          $defaultsfile = "$CFG->dirroot/local/defaults.php";
1175          if (is_readable($defaultsfile)) {
1176              $defaults = array();
1177              include($defaultsfile);
1178              if (is_array($defaults) and count($defaults)) {
1179                  $this->custom_defaults = $defaults;
1180              }
1181          }
1182      }
1183  
1184      /**
1185       * Empties children array, and sets loaded to false
1186       *
1187       * @param bool $requirefulltree
1188       */
1189      public function purge_children($requirefulltree) {
1190          $this->children = array();
1191          $this->fulltree = ($requirefulltree || $this->fulltree);
1192          $this->loaded   = false;
1193          //break circular dependencies - this helps PHP 5.2
1194          while($this->category_cache) {
1195              array_pop($this->category_cache);
1196          }
1197          $this->category_cache = array();
1198      }
1199  }
1200  
1201  
1202  /**
1203   * Links external PHP pages into the admin tree.
1204   *
1205   * See detailed usage example at the top of this document (adminlib.php)
1206   *
1207   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1208   */
1209  class admin_externalpage implements part_of_admin_tree {
1210  
1211      /** @var string An internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects */
1212      public $name;
1213  
1214      /** @var string The displayed name for this external page. Usually obtained through get_string(). */
1215      public $visiblename;
1216  
1217      /** @var string The external URL that we should link to when someone requests this external page. */
1218      public $url;
1219  
1220      /** @var array The role capability/permission a user must have to access this external page. */
1221      public $req_capability;
1222  
1223      /** @var object The context in which capability/permission should be checked, default is site context. */
1224      public $context;
1225  
1226      /** @var bool hidden in admin tree block. */
1227      public $hidden;
1228  
1229      /** @var mixed either string or array of string */
1230      public $path;
1231  
1232      /** @var array list of visible names of page parents */
1233      public $visiblepath;
1234  
1235      /**
1236       * Constructor for adding an external page into the admin tree.
1237       *
1238       * @param string $name The internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects.
1239       * @param string $visiblename The displayed name for this external page. Usually obtained through get_string().
1240       * @param string $url The external URL that we should link to when someone requests this external page.
1241       * @param mixed $req_capability The role capability/permission a user must have to access this external page. Defaults to 'moodle/site:config'.
1242       * @param boolean $hidden Is this external page hidden in admin tree block? Default false.
1243       * @param stdClass $context The context the page relates to. Not sure what happens
1244       *      if you specify something other than system or front page. Defaults to system.
1245       */
1246      public function __construct($name, $visiblename, $url, $req_capability='moodle/site:config', $hidden=false, $context=NULL) {
1247          $this->name        = $name;
1248          $this->visiblename = $visiblename;
1249          $this->url         = $url;
1250          if (is_array($req_capability)) {
1251              $this->req_capability = $req_capability;
1252          } else {
1253              $this->req_capability = array($req_capability);
1254          }
1255          $this->hidden = $hidden;
1256          $this->context = $context;
1257      }
1258  
1259      /**
1260       * Returns a reference to the part_of_admin_tree object with internal name $name.
1261       *
1262       * @param string $name The internal name of the object we want.
1263       * @param bool $findpath defaults to false
1264       * @return mixed A reference to the object with internal name $name if found, otherwise a reference to NULL.
1265       */
1266      public function locate($name, $findpath=false) {
1267          if ($this->name == $name) {
1268              if ($findpath) {
1269                  $this->visiblepath = array($this->visiblename);
1270                  $this->path        = array($this->name);
1271              }
1272              return $this;
1273          } else {
1274              $return = NULL;
1275              return $return;
1276          }
1277      }
1278  
1279      /**
1280       * This function always returns false, required function by interface
1281       *
1282       * @param string $name
1283       * @return false
1284       */
1285      public function prune($name) {
1286          return false;
1287      }
1288  
1289      /**
1290       * Search using query
1291       *
1292       * @param string $query
1293       * @return mixed array-object structure of found settings and pages
1294       */
1295      public function search($query) {
1296          $found = false;
1297          if (strpos(strtolower($this->name), $query) !== false) {
1298              $found = true;
1299          } else if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
1300                  $found = true;
1301              }
1302          if ($found) {
1303              $result = new stdClass();
1304              $result->page     = $this;
1305              $result->settings = array();
1306              return array($this->name => $result);
1307          } else {
1308              return array();
1309          }
1310      }
1311  
1312      /**
1313       * Determines if the current user has access to this external page based on $this->req_capability.
1314       *
1315       * @return bool True if user has access, false otherwise.
1316       */
1317      public function check_access() {
1318          global $CFG;
1319          $context = empty($this->context) ? context_system::instance() : $this->context;
1320          foreach($this->req_capability as $cap) {
1321              if (has_capability($cap, $context)) {
1322                  return true;
1323              }
1324          }
1325          return false;
1326      }
1327  
1328      /**
1329       * Is this external page hidden in admin tree block?
1330       *
1331       * @return bool True if hidden
1332       */
1333      public function is_hidden() {
1334          return $this->hidden;
1335      }
1336  
1337      /**
1338       * Show we display Save button at the page bottom?
1339       * @return bool
1340       */
1341      public function show_save() {
1342          return false;
1343      }
1344  }
1345  
1346  /**
1347   * Used to store details of the dependency between two settings elements.
1348   *
1349   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1350   * @copyright 2017 Davo Smith, Synergy Learning
1351   */
1352  class admin_settingdependency {
1353      /** @var string the name of the setting to be shown/hidden */
1354      public $settingname;
1355      /** @var string the setting this is dependent on */
1356      public $dependenton;
1357      /** @var string the condition to show/hide the element */
1358      public $condition;
1359      /** @var string the value to compare against */
1360      public $value;
1361  
1362      /** @var string[] list of valid conditions */
1363      private static $validconditions = ['checked', 'notchecked', 'noitemselected', 'eq', 'neq', 'in'];
1364  
1365      /**
1366       * admin_settingdependency constructor.
1367       * @param string $settingname
1368       * @param string $dependenton
1369       * @param string $condition
1370       * @param string $value
1371       * @throws \coding_exception
1372       */
1373      public function __construct($settingname, $dependenton, $condition, $value) {
1374          $this->settingname = $this->parse_name($settingname);
1375          $this->dependenton = $this->parse_name($dependenton);
1376          $this->condition = $condition;
1377          $this->value = $value;
1378  
1379          if (!in_array($this->condition, self::$validconditions)) {
1380              throw new coding_exception("Invalid condition '$condition'");
1381          }
1382      }
1383  
1384      /**
1385       * Convert the setting name into the form field name.
1386       * @param string $name
1387       * @return string
1388       */
1389      private function parse_name($name) {
1390          $bits = explode('/', $name);
1391          $name = array_pop($bits);
1392          $plugin = '';
1393          if ($bits) {
1394              $plugin = array_pop($bits);
1395              if ($plugin === 'moodle') {
1396                  $plugin = '';
1397              }
1398          }
1399          return 's_'.$plugin.'_'.$name;
1400      }
1401  
1402      /**
1403       * Gather together all the dependencies in a format suitable for initialising javascript
1404       * @param admin_settingdependency[] $dependencies
1405       * @return array
1406       */
1407      public static function prepare_for_javascript($dependencies) {
1408          $result = [];
1409          foreach ($dependencies as $d) {
1410              if (!isset($result[$d->dependenton])) {
1411                  $result[$d->dependenton] = [];
1412              }
1413              if (!isset($result[$d->dependenton][$d->condition])) {
1414                  $result[$d->dependenton][$d->condition] = [];
1415              }
1416              if (!isset($result[$d->dependenton][$d->condition][$d->value])) {
1417                  $result[$d->dependenton][$d->condition][$d->value] = [];
1418              }
1419              $result[$d->dependenton][$d->condition][$d->value][] = $d->settingname;
1420          }
1421          return $result;
1422      }
1423  }
1424  
1425  /**
1426   * Used to group a number of admin_setting objects into a page and add them to the admin tree.
1427   *
1428   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1429   */
1430  class admin_settingpage implements part_of_admin_tree {
1431  
1432      /** @var string An internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects */
1433      public $name;
1434  
1435      /** @var string The displayed name for this external page. Usually obtained through get_string(). */
1436      public $visiblename;
1437  
1438      /** @var mixed An array of admin_setting objects that are part of this setting page. */
1439      public $settings;
1440  
1441      /** @var admin_settingdependency[] list of settings to hide when certain conditions are met */
1442      protected $dependencies = [];
1443  
1444      /** @var array The role capability/permission a user must have to access this external page. */
1445      public $req_capability;
1446  
1447      /** @var object The context in which capability/permission should be checked, default is site context. */
1448      public $context;
1449  
1450      /** @var bool hidden in admin tree block. */
1451      public $hidden;
1452  
1453      /** @var mixed string of paths or array of strings of paths */
1454      public $path;
1455  
1456      /** @var array list of visible names of page parents */
1457      public $visiblepath;
1458  
1459      /**
1460       * see admin_settingpage for details of this function
1461       *
1462       * @param string $name The internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects.
1463       * @param string $visiblename The displayed name for this external page. Usually obtained through get_string().
1464       * @param mixed $req_capability The role capability/permission a user must have to access this external page. Defaults to 'moodle/site:config'.
1465       * @param boolean $hidden Is this external page hidden in admin tree block? Default false.
1466       * @param stdClass $context The context the page relates to. Not sure what happens
1467       *      if you specify something other than system or front page. Defaults to system.
1468       */
1469      public function __construct($name, $visiblename, $req_capability='moodle/site:config', $hidden=false, $context=NULL) {
1470          $this->settings    = new stdClass();
1471          $this->name        = $name;
1472          $this->visiblename = $visiblename;
1473          if (is_array($req_capability)) {
1474              $this->req_capability = $req_capability;
1475          } else {
1476              $this->req_capability = array($req_capability);
1477          }
1478          $this->hidden      = $hidden;
1479          $this->context     = $context;
1480      }
1481  
1482      /**
1483       * see admin_category
1484       *
1485       * @param string $name
1486       * @param bool $findpath
1487       * @return mixed Object (this) if name ==  this->name, else returns null
1488       */
1489      public function locate($name, $findpath=false) {
1490          if ($this->name == $name) {
1491              if ($findpath) {
1492                  $this->visiblepath = array($this->visiblename);
1493                  $this->path        = array($this->name);
1494              }
1495              return $this;
1496          } else {
1497              $return = NULL;
1498              return $return;
1499          }
1500      }
1501  
1502      /**
1503       * Search string in settings page.
1504       *
1505       * @param string $query
1506       * @return array
1507       */
1508      public function search($query) {
1509          $found = array();
1510  
1511          foreach ($this->settings as $setting) {
1512              if ($setting->is_related($query)) {
1513                  $found[] = $setting;
1514              }
1515          }
1516  
1517          if ($found) {
1518              $result = new stdClass();
1519              $result->page     = $this;
1520              $result->settings = $found;
1521              return array($this->name => $result);
1522          }
1523  
1524          $found = false;
1525          if (strpos(strtolower($this->name), $query) !== false) {
1526              $found = true;
1527          } else if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
1528                  $found = true;
1529              }
1530          if ($found) {
1531              $result = new stdClass();
1532              $result->page     = $this;
1533              $result->settings = array();
1534              return array($this->name => $result);
1535          } else {
1536              return array();
1537          }
1538      }
1539  
1540      /**
1541       * This function always returns false, required by interface
1542       *
1543       * @param string $name
1544       * @return bool Always false
1545       */
1546      public function prune($name) {
1547          return false;
1548      }
1549  
1550      /**
1551       * adds an admin_setting to this admin_settingpage
1552       *
1553       * not the same as add for admin_category. adds an admin_setting to this admin_settingpage. settings appear (on the settingpage) in the order in which they're added
1554       * n.b. each admin_setting in an admin_settingpage must have a unique internal name
1555       *
1556       * @param object $setting is the admin_setting object you want to add
1557       * @return bool true if successful, false if not
1558       */
1559      public function add($setting) {
1560          if (!($setting instanceof admin_setting)) {
1561              debugging('error - not a setting instance');
1562              return false;
1563          }
1564  
1565          $name = $setting->name;
1566          if ($setting->plugin) {
1567              $name = $setting->plugin . $name;
1568          }
1569          $this->settings->{$name} = $setting;
1570          return true;
1571      }
1572  
1573      /**
1574       * Hide the named setting if the specified condition is matched.
1575       *
1576       * @param string $settingname
1577       * @param string $dependenton
1578       * @param string $condition
1579       * @param string $value
1580       */
1581      public function hide_if($settingname, $dependenton, $condition = 'notchecked', $value = '1') {
1582          $this->dependencies[] = new admin_settingdependency($settingname, $dependenton, $condition, $value);
1583  
1584          // Reformat the dependency name to the plugin | name format used in the display.
1585          $dependenton = str_replace('/', ' | ', $dependenton);
1586  
1587          // Let the setting know, so it can be displayed underneath.
1588          $findname = str_replace('/', '', $settingname);
1589          foreach ($this->settings as $name => $setting) {
1590              if ($name === $findname) {
1591                  $setting->add_dependent_on($dependenton);
1592              }
1593          }
1594      }
1595  
1596      /**
1597       * see admin_externalpage
1598       *
1599       * @return bool Returns true for yes false for no
1600       */
1601      public function check_access() {
1602          global $CFG;
1603          $context = empty($this->context) ? context_system::instance() : $this->context;
1604          foreach($this->req_capability as $cap) {
1605              if (has_capability($cap, $context)) {
1606                  return true;
1607              }
1608          }
1609          return false;
1610      }
1611  
1612      /**
1613       * outputs this page as html in a table (suitable for inclusion in an admin pagetype)
1614       * @return string Returns an XHTML string
1615       */
1616      public function output_html() {
1617          $adminroot = admin_get_root();
1618          $return = '<fieldset>'."\n".'<div class="clearer"><!-- --></div>'."\n";
1619          foreach($this->settings as $setting) {
1620              $fullname = $setting->get_full_name();
1621              if (array_key_exists($fullname, $adminroot->errors)) {
1622                  $data = $adminroot->errors[$fullname]->data;
1623              } else {
1624                  $data = $setting->get_setting();
1625                  // do not use defaults if settings not available - upgrade settings handles the defaults!
1626              }
1627              $return .= $setting->output_html($data);
1628          }
1629          $return .= '</fieldset>';
1630          return $return;
1631      }
1632  
1633      /**
1634       * Is this settings page hidden in admin tree block?
1635       *
1636       * @return bool True if hidden
1637       */
1638      public function is_hidden() {
1639          return $this->hidden;
1640      }
1641  
1642      /**
1643       * Show we display Save button at the page bottom?
1644       * @return bool
1645       */
1646      public function show_save() {
1647          foreach($this->settings as $setting) {
1648              if (empty($setting->nosave)) {
1649                  return true;
1650              }
1651          }
1652          return false;
1653      }
1654  
1655      /**
1656       * Should any of the settings on this page be shown / hidden based on conditions?
1657       * @return bool
1658       */
1659      public function has_dependencies() {
1660          return (bool)$this->dependencies;
1661      }
1662  
1663      /**
1664       * Format the setting show/hide conditions ready to initialise the page javascript
1665       * @return array
1666       */
1667      public function get_dependencies_for_javascript() {
1668          if (!$this->has_dependencies()) {
1669              return [];
1670          }
1671          return admin_settingdependency::prepare_for_javascript($this->dependencies);
1672      }
1673  }
1674  
1675  
1676  /**
1677   * Admin settings class. Only exists on setting pages.
1678   * Read & write happens at this level; no authentication.
1679   *
1680   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1681   */
1682  abstract class admin_setting {
1683      /** @var string unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins. */
1684      public $name;
1685      /** @var string localised name */
1686      public $visiblename;
1687      /** @var string localised long description in Markdown format */
1688      public $description;
1689      /** @var mixed Can be string or array of string */
1690      public $defaultsetting;
1691      /** @var string */
1692      public $updatedcallback;
1693      /** @var mixed can be String or Null.  Null means main config table */
1694      public $plugin; // null means main config table
1695      /** @var bool true indicates this setting does not actually save anything, just information */
1696      public $nosave = false;
1697      /** @var bool if set, indicates that a change to this setting requires rebuild course cache */
1698      public $affectsmodinfo = false;
1699      /** @var array of admin_setting_flag - These are extra checkboxes attached to a setting. */
1700      private $flags = array();
1701      /** @var bool Whether this field must be forced LTR. */
1702      private $forceltr = null;
1703      /** @var array list of other settings that may cause this setting to be hidden */
1704      private $dependenton = [];
1705      /** @var bool Whether this setting uses a custom form control */
1706      protected $customcontrol = false;
1707  
1708      /**
1709       * Constructor
1710       * @param string $name unique ascii name, either 'mysetting' for settings that in config,
1711       *                     or 'myplugin/mysetting' for ones in config_plugins.
1712       * @param string $visiblename localised name
1713       * @param string $description localised long description
1714       * @param mixed $defaultsetting string or array depending on implementation
1715       */
1716      public function __construct($name, $visiblename, $description, $defaultsetting) {
1717          $this->parse_setting_name($name);
1718          $this->visiblename    = $visiblename;
1719          $this->description    = $description;
1720          $this->defaultsetting = $defaultsetting;
1721      }
1722  
1723      /**
1724       * Generic function to add a flag to this admin setting.
1725       *
1726       * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1727       * @param bool $default - The default for the flag
1728       * @param string $shortname - The shortname for this flag. Used as a suffix for the setting name.
1729       * @param string $displayname - The display name for this flag. Used as a label next to the checkbox.
1730       */
1731      protected function set_flag_options($enabled, $default, $shortname, $displayname) {
1732          if (empty($this->flags[$shortname])) {
1733              $this->flags[$shortname] = new admin_setting_flag($enabled, $default, $shortname, $displayname);
1734          } else {
1735              $this->flags[$shortname]->set_options($enabled, $default);
1736          }
1737      }
1738  
1739      /**
1740       * Set the enabled options flag on this admin setting.
1741       *
1742       * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1743       * @param bool $default - The default for the flag
1744       */
1745      public function set_enabled_flag_options($enabled, $default) {
1746          $this->set_flag_options($enabled, $default, 'enabled', new lang_string('enabled', 'core_admin'));
1747      }
1748  
1749      /**
1750       * Set the advanced options flag on this admin setting.
1751       *
1752       * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1753       * @param bool $default - The default for the flag
1754       */
1755      public function set_advanced_flag_options($enabled, $default) {
1756          $this->set_flag_options($enabled, $default, 'adv', new lang_string('advanced'));
1757      }
1758  
1759  
1760      /**
1761       * Set the locked options flag on this admin setting.
1762       *
1763       * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1764       * @param bool $default - The default for the flag
1765       */
1766      public function set_locked_flag_options($enabled, $default) {
1767          $this->set_flag_options($enabled, $default, 'locked', new lang_string('locked', 'core_admin'));
1768      }
1769  
1770      /**
1771       * Set the required options flag on this admin setting.
1772       *
1773       * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED.
1774       * @param bool $default - The default for the flag.
1775       */
1776      public function set_required_flag_options($enabled, $default) {
1777          $this->set_flag_options($enabled, $default, 'required', new lang_string('required', 'core_admin'));
1778      }
1779  
1780      /**
1781       * Is this option forced in config.php?
1782       *
1783       * @return bool
1784       */
1785      public function is_readonly(): bool {
1786          global $CFG;
1787  
1788          if (empty($this->plugin)) {
1789              if (array_key_exists($this->name, $CFG->config_php_settings)) {
1790                  return true;
1791              }
1792          } else {
1793              if (array_key_exists($this->plugin, $CFG->forced_plugin_settings)
1794                  and array_key_exists($this->name, $CFG->forced_plugin_settings[$this->plugin])) {
1795                  return true;
1796              }
1797          }
1798          return false;
1799      }
1800  
1801      /**
1802       * Get the currently saved value for a setting flag
1803       *
1804       * @param admin_setting_flag $flag - One of the admin_setting_flag for this admin_setting.
1805       * @return bool
1806       */
1807      public function get_setting_flag_value(admin_setting_flag $flag) {
1808          $value = $this->config_read($this->name . '_' . $flag->get_shortname());
1809          if (!isset($value)) {
1810              $value = $flag->get_default();
1811          }
1812  
1813          return !empty($value);
1814      }
1815  
1816      /**
1817       * Get the list of defaults for the flags on this setting.
1818       *
1819       * @param array of strings describing the defaults for this setting. This is appended to by this function.
1820       */
1821      public function get_setting_flag_defaults(& $defaults) {
1822          foreach ($this->flags as $flag) {
1823              if ($flag->is_enabled() && $flag->get_default()) {
1824                  $defaults[] = $flag->get_displayname();
1825              }
1826          }
1827      }
1828  
1829      /**
1830       * Output the input fields for the advanced and locked flags on this setting.
1831       *
1832       * @param bool $adv - The current value of the advanced flag.
1833       * @param bool $locked - The current value of the locked flag.
1834       * @return string $output - The html for the flags.
1835       */
1836      public function output_setting_flags() {
1837          $output = '';
1838  
1839          foreach ($this->flags as $flag) {
1840              if ($flag->is_enabled()) {
1841                  $output .= $flag->output_setting_flag($this);
1842              }
1843          }
1844  
1845          if (!empty($output)) {
1846              return html_writer::tag('span', $output, array('class' => 'adminsettingsflags'));
1847          }
1848          return $output;
1849      }
1850  
1851      /**
1852       * Write the values of the flags for this admin setting.
1853       *
1854       * @param array $data - The data submitted from the form or null to set the default value for new installs.
1855       * @return bool - true if successful.
1856       */
1857      public function write_setting_flags($data) {
1858          $result = true;
1859          foreach ($this->flags as $flag) {
1860              $result = $result && $flag->write_setting_flag($this, $data);
1861          }
1862          return $result;
1863      }
1864  
1865      /**
1866       * Set up $this->name and potentially $this->plugin
1867       *
1868       * Set up $this->name and possibly $this->plugin based on whether $name looks
1869       * like 'settingname' or 'plugin/settingname'. Also, do some sanity checking
1870       * on the names, that is, output a developer debug warning if the name
1871       * contains anything other than [a-zA-Z0-9_]+.
1872       *
1873       * @param string $name the setting name passed in to the constructor.
1874       */
1875      private function parse_setting_name($name) {
1876          $bits = explode('/', $name);
1877          if (count($bits) > 2) {
1878              throw new moodle_exception('invalidadminsettingname', '', '', $name);
1879          }
1880          $this->name = array_pop($bits);
1881          if (!preg_match('/^[a-zA-Z0-9_]+$/', $this->name)) {
1882              throw new moodle_exception('invalidadminsettingname', '', '', $name);
1883          }
1884          if (!empty($bits)) {
1885              $this->plugin = array_pop($bits);
1886              if ($this->plugin === 'moodle') {
1887                  $this->plugin = null;
1888              } else if (!preg_match('/^[a-zA-Z0-9_]+$/', $this->plugin)) {
1889                      throw new moodle_exception('invalidadminsettingname', '', '', $name);
1890                  }
1891          }
1892      }
1893  
1894      /**
1895       * Returns the fullname prefixed by the plugin
1896       * @return string
1897       */
1898      public function get_full_name() {
1899          return 's_'.$this->plugin.'_'.$this->name;
1900      }
1901  
1902      /**
1903       * Returns the ID string based on plugin and name
1904       * @return string
1905       */
1906      public function get_id() {
1907          return 'id_s_'.$this->plugin.'_'.$this->name;
1908      }
1909  
1910      /**
1911       * @param bool $affectsmodinfo If true, changes to this setting will
1912       *   cause the course cache to be rebuilt
1913       */
1914      public function set_affects_modinfo($affectsmodinfo) {
1915          $this->affectsmodinfo = $affectsmodinfo;
1916      }
1917  
1918      /**
1919       * Returns the config if possible
1920       *
1921       * @return mixed returns config if successful else null
1922       */
1923      public function config_read($name) {
1924          global $CFG;
1925          if (!empty($this->plugin)) {
1926              $value = get_config($this->plugin, $name);
1927              return $value === false ? NULL : $value;
1928  
1929          } else {
1930              if (isset($CFG->$name)) {
1931                  return $CFG->$name;
1932              } else {
1933                  return NULL;
1934              }
1935          }
1936      }
1937  
1938      /**
1939       * Used to set a config pair and log change
1940       *
1941       * @param string $name
1942       * @param mixed $value Gets converted to string if not null
1943       * @return bool Write setting to config table
1944       */
1945      public function config_write($name, $value) {
1946          global $DB, $USER, $CFG;
1947  
1948          if ($this->nosave) {
1949              return true;
1950          }
1951  
1952          // make sure it is a real change
1953          $oldvalue = get_config($this->plugin, $name);
1954          $oldvalue = ($oldvalue === false) ? null : $oldvalue; // normalise
1955          $value = is_null($value) ? null : (string)$value;
1956  
1957          if ($oldvalue === $value) {
1958              return true;
1959          }
1960  
1961          // store change
1962          set_config($name, $value, $this->plugin);
1963  
1964          // Some admin settings affect course modinfo
1965          if ($this->affectsmodinfo) {
1966              // Clear course cache for all courses
1967              rebuild_course_cache(0, true);
1968          }
1969  
1970          $this->add_to_config_log($name, $oldvalue, $value);
1971  
1972          return true; // BC only
1973      }
1974  
1975      /**
1976       * Log config changes if necessary.
1977       * @param string $name
1978       * @param string $oldvalue
1979       * @param string $value
1980       */
1981      protected function add_to_config_log($name, $oldvalue, $value) {
1982          add_to_config_log($name, $oldvalue, $value, $this->plugin);
1983      }
1984  
1985      /**
1986       * Returns current value of this setting
1987       * @return mixed array or string depending on instance, NULL means not set yet
1988       */
1989      public abstract function get_setting();
1990  
1991      /**
1992       * Returns default setting if exists
1993       * @return mixed array or string depending on instance; NULL means no default, user must supply
1994       */
1995      public function get_defaultsetting() {
1996          $adminroot =  admin_get_root(false, false);
1997          if (!empty($adminroot->custom_defaults)) {
1998              $plugin = is_null($this->plugin) ? 'moodle' : $this->plugin;
1999              if (isset($adminroot->custom_defaults[$plugin])) {
2000                  if (array_key_exists($this->name, $adminroot->custom_defaults[$plugin])) { // null is valid value here ;-)
2001                      return $adminroot->custom_defaults[$plugin][$this->name];
2002                  }
2003              }
2004          }
2005          return $this->defaultsetting;
2006      }
2007  
2008      /**
2009       * Store new setting
2010       *
2011       * @param mixed $data string or array, must not be NULL
2012       * @return string empty string if ok, string error message otherwise
2013       */
2014      public abstract function write_setting($data);
2015  
2016      /**
2017       * Return part of form with setting
2018       * This function should always be overwritten
2019       *
2020       * @param mixed $data array or string depending on setting
2021       * @param string $query
2022       * @return string
2023       */
2024      public function output_html($data, $query='') {
2025      // should be overridden
2026          return;
2027      }
2028  
2029      /**
2030       * Function called if setting updated - cleanup, cache reset, etc.
2031       * @param string $functionname Sets the function name
2032       * @return void
2033       */
2034      public function set_updatedcallback($functionname) {
2035          $this->updatedcallback = $functionname;
2036      }
2037  
2038      /**
2039       * Execute postupdatecallback if necessary.
2040       * @param mixed $original original value before write_setting()
2041       * @return bool true if changed, false if not.
2042       */
2043      public function post_write_settings($original) {
2044          // Comparison must work for arrays too.
2045          if (serialize($original) === serialize($this->get_setting())) {
2046              return false;
2047          }
2048  
2049          $callbackfunction = $this->updatedcallback;
2050          if (!empty($callbackfunction) and is_callable($callbackfunction)) {
2051              $callbackfunction($this->get_full_name());
2052          }
2053          return true;
2054      }
2055  
2056      /**
2057       * Is setting related to query text - used when searching
2058       * @param string $query
2059       * @return bool
2060       */
2061      public function is_related($query) {
2062          if (strpos(strtolower($this->name), $query) !== false) {
2063              return true;
2064          }
2065          if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
2066              return true;
2067          }
2068          if (strpos(core_text::strtolower($this->description), $query) !== false) {
2069              return true;
2070          }
2071          $current = $this->get_setting();
2072          if (!is_null($current)) {
2073              if (is_string($current)) {
2074                  if (strpos(core_text::strtolower($current), $query) !== false) {
2075                      return true;
2076                  }
2077              }
2078          }
2079          $default = $this->get_defaultsetting();
2080          if (!is_null($default)) {
2081              if (is_string($default)) {
2082                  if (strpos(core_text::strtolower($default), $query) !== false) {
2083                      return true;
2084                  }
2085              }
2086          }
2087          return false;
2088      }
2089  
2090      /**
2091       * Get whether this should be displayed in LTR mode.
2092       *
2093       * @return bool|null
2094       */
2095      public function get_force_ltr() {
2096          return $this->forceltr;
2097      }
2098  
2099      /**
2100       * Set whether to force LTR or not.
2101       *
2102       * @param bool $value True when forced, false when not force, null when unknown.
2103       */
2104      public function set_force_ltr($value) {
2105          $this->forceltr = $value;
2106      }
2107  
2108      /**
2109       * Add a setting to the list of those that could cause this one to be hidden
2110       * @param string $dependenton
2111       */
2112      public function add_dependent_on($dependenton) {
2113          $this->dependenton[] = $dependenton;
2114      }
2115  
2116      /**
2117       * Get a list of the settings that could cause this one to be hidden.
2118       * @return array
2119       */
2120      public function get_dependent_on() {
2121          return $this->dependenton;
2122      }
2123  
2124      /**
2125       * Whether this setting uses a custom form control.
2126       * This function is especially useful to decide if we should render a label element for this setting or not.
2127       *
2128       * @return bool
2129       */
2130      public function has_custom_form_control(): bool {
2131          return $this->customcontrol;
2132      }
2133  }
2134  
2135  /**
2136   * An additional option that can be applied to an admin setting.
2137   * The currently supported options are 'ADVANCED', 'LOCKED' and 'REQUIRED'.
2138   *
2139   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2140   */
2141  class admin_setting_flag {
2142      /** @var bool Flag to indicate if this option can be toggled for this setting */
2143      private $enabled = false;
2144      /** @var bool Flag to indicate if this option defaults to true or false */
2145      private $default = false;
2146      /** @var string Short string used to create setting name - e.g. 'adv' */
2147      private $shortname = '';
2148      /** @var string String used as the label for this flag */
2149      private $displayname = '';
2150      /** @const Checkbox for this flag is displayed in admin page */
2151      const ENABLED = true;
2152      /** @const Checkbox for this flag is not displayed in admin page */
2153      const DISABLED = false;
2154  
2155      /**
2156       * Constructor
2157       *
2158       * @param bool $enabled Can this option can be toggled.
2159       *                      Should be one of admin_setting_flag::ENABLED or admin_setting_flag::DISABLED.
2160       * @param bool $default The default checked state for this setting option.
2161       * @param string $shortname The shortname of this flag. Currently supported flags are 'locked' and 'adv'
2162       * @param string $displayname The displayname of this flag. Used as a label for the flag.
2163       */
2164      public function __construct($enabled, $default, $shortname, $displayname) {
2165          $this->shortname = $shortname;
2166          $this->displayname = $displayname;
2167          $this->set_options($enabled, $default);
2168      }
2169  
2170      /**
2171       * Update the values of this setting options class
2172       *
2173       * @param bool $enabled Can this option can be toggled.
2174       *                      Should be one of admin_setting_flag::ENABLED or admin_setting_flag::DISABLED.
2175       * @param bool $default The default checked state for this setting option.
2176       */
2177      public function set_options($enabled, $default) {
2178          $this->enabled = $enabled;
2179          $this->default = $default;
2180      }
2181  
2182      /**
2183       * Should this option appear in the interface and be toggleable?
2184       *
2185       * @return bool Is it enabled?
2186       */
2187      public function is_enabled() {
2188          return $this->enabled;
2189      }
2190  
2191      /**
2192       * Should this option be checked by default?
2193       *
2194       * @return bool Is it on by default?
2195       */
2196      public function get_default() {
2197          return $this->default;
2198      }
2199  
2200      /**
2201       * Return the short name for this flag. e.g. 'adv' or 'locked'
2202       *
2203       * @return string
2204       */
2205      public function get_shortname() {
2206          return $this->shortname;
2207      }
2208  
2209      /**
2210       * Return the display name for this flag. e.g. 'Advanced' or 'Locked'
2211       *
2212       * @return string
2213       */
2214      public function get_displayname() {
2215          return $this->displayname;
2216      }
2217  
2218      /**
2219       * Save the submitted data for this flag - or set it to the default if $data is null.
2220       *
2221       * @param admin_setting $setting - The admin setting for this flag
2222       * @param array $data - The data submitted from the form or null to set the default value for new installs.
2223       * @return bool
2224       */
2225      public function write_setting_flag(admin_setting $setting, $data) {
2226          $result = true;
2227          if ($this->is_enabled()) {
2228              if (!isset($data)) {
2229                  $value = $this->get_default();
2230              } else {
2231                  $value = !empty($data[$setting->get_full_name() . '_' . $this->get_shortname()]);
2232              }
2233              $result = $setting->config_write($setting->name . '_' . $this->get_shortname(), $value);
2234          }
2235  
2236          return $result;
2237  
2238      }
2239  
2240      /**
2241       * Output the checkbox for this setting flag. Should only be called if the flag is enabled.
2242       *
2243       * @param admin_setting $setting - The admin setting for this flag
2244       * @return string - The html for the checkbox.
2245       */
2246      public function output_setting_flag(admin_setting $setting) {
2247          global $OUTPUT;
2248  
2249          $value = $setting->get_setting_flag_value($this);
2250  
2251          $context = new stdClass();
2252          $context->id = $setting->get_id() . '_' . $this->get_shortname();
2253          $context->name = $setting->get_full_name() .  '_' . $this->get_shortname();
2254          $context->value = 1;
2255          $context->checked = $value ? true : false;
2256          $context->label = $this->get_displayname();
2257  
2258          return $OUTPUT->render_from_template('core_admin/setting_flag', $context);
2259      }
2260  }
2261  
2262  
2263  /**
2264   * No setting - just heading and text.
2265   *
2266   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2267   */
2268  class admin_setting_heading extends admin_setting {
2269  
2270      /**
2271       * not a setting, just text
2272       * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2273       * @param string $heading heading
2274       * @param string $information text in box
2275       */
2276      public function __construct($name, $heading, $information) {
2277          $this->nosave = true;
2278          parent::__construct($name, $heading, $information, '');
2279      }
2280  
2281      /**
2282       * Always returns true
2283       * @return bool Always returns true
2284       */
2285      public function get_setting() {
2286          return true;
2287      }
2288  
2289      /**
2290       * Always returns true
2291       * @return bool Always returns true
2292       */
2293      public function get_defaultsetting() {
2294          return true;
2295      }
2296  
2297      /**
2298       * Never write settings
2299       * @return string Always returns an empty string
2300       */
2301      public function write_setting($data) {
2302      // do not write any setting
2303          return '';
2304      }
2305  
2306      /**
2307       * Returns an HTML string
2308       * @return string Returns an HTML string
2309       */
2310      public function output_html($data, $query='') {
2311          global $OUTPUT;
2312          $context = new stdClass();
2313          $context->title = $this->visiblename;
2314          $context->description = $this->description;
2315          $context->descriptionformatted = highlight($query, markdown_to_html($this->description));
2316          return $OUTPUT->render_from_template('core_admin/setting_heading', $context);
2317      }
2318  }
2319  
2320  /**
2321   * No setting - just name and description in same row.
2322   *
2323   * @copyright 2018 onwards Amaia Anabitarte
2324   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2325   */
2326  class admin_setting_description extends admin_setting {
2327  
2328      /**
2329       * Not a setting, just text
2330       *
2331       * @param string $name
2332       * @param string $visiblename
2333       * @param string $description
2334       */
2335      public function __construct($name, $visiblename, $description) {
2336          $this->nosave = true;
2337          parent::__construct($name, $visiblename, $description, '');
2338      }
2339  
2340      /**
2341       * Always returns true
2342       *
2343       * @return bool Always returns true
2344       */
2345      public function get_setting() {
2346          return true;
2347      }
2348  
2349      /**
2350       * Always returns true
2351       *
2352       * @return bool Always returns true
2353       */
2354      public function get_defaultsetting() {
2355          return true;
2356      }
2357  
2358      /**
2359       * Never write settings
2360       *
2361       * @param mixed $data Gets converted to str for comparison against yes value
2362       * @return string Always returns an empty string
2363       */
2364      public function write_setting($data) {
2365          // Do not write any setting.
2366          return '';
2367      }
2368  
2369      /**
2370       * Returns an HTML string
2371       *
2372       * @param string $data
2373       * @param string $query
2374       * @return string Returns an HTML string
2375       */
2376      public function output_html($data, $query='') {
2377          global $OUTPUT;
2378  
2379          $context = new stdClass();
2380          $context->title = $this->visiblename;
2381          $context->description = $this->description;
2382  
2383          return $OUTPUT->render_from_template('core_admin/setting_description', $context);
2384      }
2385  }
2386  
2387  
2388  
2389  /**
2390   * The most flexible setting, the user enters text.
2391   *
2392   * This type of field should be used for config settings which are using
2393   * English words and are not localised (passwords, database name, list of values, ...).
2394   *
2395   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2396   */
2397  class admin_setting_configtext extends admin_setting {
2398  
2399      /** @var mixed int means PARAM_XXX type, string is a allowed format in regex */
2400      public $paramtype;
2401      /** @var int default field size */
2402      public $size;
2403  
2404      /**
2405       * Config text constructor
2406       *
2407       * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2408       * @param string $visiblename localised
2409       * @param string $description long localised info
2410       * @param string $defaultsetting
2411       * @param mixed $paramtype int means PARAM_XXX type, string is a allowed format in regex
2412       * @param int $size default field size
2413       */
2414      public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $size=null) {
2415          $this->paramtype = $paramtype;
2416          if (!is_null($size)) {
2417              $this->size  = $size;
2418          } else {
2419              $this->size  = ($paramtype === PARAM_INT) ? 5 : 30;
2420          }
2421          parent::__construct($name, $visiblename, $description, $defaultsetting);
2422      }
2423  
2424      /**
2425       * Get whether this should be displayed in LTR mode.
2426       *
2427       * Try to guess from the PARAM type unless specifically set.
2428       */
2429      public function get_force_ltr() {
2430          $forceltr = parent::get_force_ltr();
2431          if ($forceltr === null) {
2432              return !is_rtl_compatible($this->paramtype);
2433          }
2434          return $forceltr;
2435      }
2436  
2437      /**
2438       * Return the setting
2439       *
2440       * @return mixed returns config if successful else null
2441       */
2442      public function get_setting() {
2443          return $this->config_read($this->name);
2444      }
2445  
2446      public function write_setting($data) {
2447          if ($this->paramtype === PARAM_INT and $data === '') {
2448          // do not complain if '' used instead of 0
2449              $data = 0;
2450          }
2451          // $data is a string
2452          $validated = $this->validate($data);
2453          if ($validated !== true) {
2454              return $validated;
2455          }
2456          return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
2457      }
2458  
2459      /**
2460       * Validate data before storage
2461       * @param string data
2462       * @return mixed true if ok string if error found
2463       */
2464      public function validate($data) {
2465          // allow paramtype to be a custom regex if it is the form of /pattern/
2466          if (preg_match('#^/.*/$#', $this->paramtype)) {
2467              if (preg_match($this->paramtype, $data)) {
2468                  return true;
2469              } else {
2470                  return get_string('validateerror', 'admin');
2471              }
2472  
2473          } else if ($this->paramtype === PARAM_RAW) {
2474              return true;
2475  
2476          } else {
2477              $cleaned = clean_param($data, $this->paramtype);
2478              if ("$data" === "$cleaned") { // implicit conversion to string is needed to do exact comparison
2479                  return true;
2480              } else {
2481                  return get_string('validateerror', 'admin');
2482              }
2483          }
2484      }
2485  
2486      /**
2487       * Return an XHTML string for the setting
2488       * @return string Returns an XHTML string
2489       */
2490      public function output_html($data, $query='') {
2491          global $OUTPUT;
2492  
2493          $default = $this->get_defaultsetting();
2494          $context = (object) [
2495              'size' => $this->size,
2496              'id' => $this->get_id(),
2497              'name' => $this->get_full_name(),
2498              'value' => $data,
2499              'forceltr' => $this->get_force_ltr(),
2500              'readonly' => $this->is_readonly(),
2501          ];
2502          $element = $OUTPUT->render_from_template('core_admin/setting_configtext', $context);
2503  
2504          return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2505      }
2506  }
2507  
2508  /**
2509   * Text input with a maximum length constraint.
2510   *
2511   * @copyright 2015 onwards Ankit Agarwal
2512   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2513   */
2514  class admin_setting_configtext_with_maxlength extends admin_setting_configtext {
2515  
2516      /** @var int maximum number of chars allowed. */
2517      protected $maxlength;
2518  
2519      /**
2520       * Config text constructor
2521       *
2522       * @param string $name unique ascii name, either 'mysetting' for settings that in config,
2523       *                     or 'myplugin/mysetting' for ones in config_plugins.
2524       * @param string $visiblename localised
2525       * @param string $description long localised info
2526       * @param string $defaultsetting
2527       * @param mixed $paramtype int means PARAM_XXX type, string is a allowed format in regex
2528       * @param int $size default field size
2529       * @param mixed $maxlength int maxlength allowed, 0 for infinite.
2530       */
2531      public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW,
2532                                  $size=null, $maxlength = 0) {
2533          $this->maxlength = $maxlength;
2534          parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype, $size);
2535      }
2536  
2537      /**
2538       * Validate data before storage
2539       *
2540       * @param string $data data
2541       * @return mixed true if ok string if error found
2542       */
2543      public function validate($data) {
2544          $parentvalidation = parent::validate($data);
2545          if ($parentvalidation === true) {
2546              if ($this->maxlength > 0) {
2547                  // Max length check.
2548                  $length = core_text::strlen($data);
2549                  if ($length > $this->maxlength) {
2550                      return get_string('maximumchars', 'moodle',  $this->maxlength);
2551                  }
2552                  return true;
2553              } else {
2554                  return true; // No max length check needed.
2555              }
2556          } else {
2557              return $parentvalidation;
2558          }
2559      }
2560  }
2561  
2562  /**
2563   * General text area without html editor.
2564   *
2565   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2566   */
2567  class admin_setting_configtextarea extends admin_setting_configtext {
2568      private $rows;
2569      private $cols;
2570  
2571      /**
2572       * @param string $name
2573       * @param string $visiblename
2574       * @param string $description
2575       * @param mixed $defaultsetting string or array
2576       * @param mixed $paramtype
2577       * @param string $cols The number of columns to make the editor
2578       * @param string $rows The number of rows to make the editor
2579       */
2580      public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $cols='60', $rows='8') {
2581          $this->rows = $rows;
2582          $this->cols = $cols;
2583          parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype);
2584      }
2585  
2586      /**
2587       * Returns an XHTML string for the editor
2588       *
2589       * @param string $data
2590       * @param string $query
2591       * @return string XHTML string for the editor
2592       */
2593      public function output_html($data, $query='') {
2594          global $OUTPUT;
2595  
2596          $default = $this->get_defaultsetting();
2597          $defaultinfo = $default;
2598          if (!is_null($default) and $default !== '') {
2599              $defaultinfo = "\n".$default;
2600          }
2601  
2602          $context = (object) [
2603              'cols' => $this->cols,
2604              'rows' => $this->rows,
2605              'id' => $this->get_id(),
2606              'name' => $this->get_full_name(),
2607              'value' => $data,
2608              'forceltr' => $this->get_force_ltr(),
2609              'readonly' => $this->is_readonly(),
2610          ];
2611          $element = $OUTPUT->render_from_template('core_admin/setting_configtextarea', $context);
2612  
2613          return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
2614      }
2615  }
2616  
2617  /**
2618   * General text area with html editor.
2619   */
2620  class admin_setting_confightmleditor extends admin_setting_configtextarea {
2621  
2622      /**
2623       * @param string $name
2624       * @param string $visiblename
2625       * @param string $description
2626       * @param mixed $defaultsetting string or array
2627       * @param mixed $paramtype
2628       */
2629      public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $cols='60', $rows='8') {
2630          parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype, $cols, $rows);
2631          $this->set_force_ltr(false);
2632          editors_head_setup();
2633      }
2634  
2635      /**
2636       * Returns an XHTML string for the editor
2637       *
2638       * @param string $data
2639       * @param string $query
2640       * @return string XHTML string for the editor
2641       */
2642      public function output_html($data, $query='') {
2643          $editor = editors_get_preferred_editor(FORMAT_HTML);
2644          $editor->set_text($data);
2645          $editor->use_editor($this->get_id(), array('noclean'=>true));
2646          return parent::output_html($data, $query);
2647      }
2648  
2649      /**
2650       * Checks if data has empty html.
2651       *
2652       * @param string $data
2653       * @return string Empty when no errors.
2654       */
2655      public function write_setting($data) {
2656          if (trim(html_to_text($data)) === '') {
2657              $data = '';
2658          }
2659          return parent::write_setting($data);
2660      }
2661  }
2662  
2663  
2664  /**
2665   * Password field, allows unmasking of password
2666   *
2667   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2668   */
2669  class admin_setting_configpasswordunmask extends admin_setting_configtext {
2670  
2671      /**
2672       * Constructor
2673       * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2674       * @param string $visiblename localised
2675       * @param string $description long localised info
2676       * @param string $defaultsetting default password
2677       */
2678      public function __construct($name, $visiblename, $description, $defaultsetting) {
2679          parent::__construct($name, $visiblename, $description, $defaultsetting, PARAM_RAW, 30);
2680      }
2681  
2682      /**
2683       * Log config changes if necessary.
2684       * @param string $name
2685       * @param string $oldvalue
2686       * @param string $value
2687       */
2688      protected function add_to_config_log($name, $oldvalue, $value) {
2689          if ($value !== '') {
2690              $value = '********';
2691          }
2692          if ($oldvalue !== '' and $oldvalue !== null) {
2693              $oldvalue = '********';
2694          }
2695          parent::add_to_config_log($name, $oldvalue, $value);
2696      }
2697  
2698      /**
2699       * Returns HTML for the field.
2700       *
2701       * @param   string  $data       Value for the field
2702       * @param   string  $query      Passed as final argument for format_admin_setting
2703       * @return  string              Rendered HTML
2704       */
2705      public function output_html($data, $query='') {
2706          global $OUTPUT;
2707  
2708          $context = (object) [
2709              'id' => $this->get_id(),
2710              'name' => $this->get_full_name(),
2711              'size' => $this->size,
2712              'value' => $this->is_readonly() ? null : $data,
2713              'forceltr' => $this->get_force_ltr(),
2714              'readonly' => $this->is_readonly(),
2715          ];
2716          $element = $OUTPUT->render_from_template('core_admin/setting_configpasswordunmask', $context);
2717          return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', null, $query);
2718      }
2719  }
2720  
2721  /**
2722   * Password field, allows unmasking of password, with an advanced checkbox that controls an additional $name.'_adv' setting.
2723   *
2724   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2725   * @copyright 2018 Paul Holden (pholden@greenhead.ac.uk)
2726   */
2727  class admin_setting_configpasswordunmask_with_advanced extends admin_setting_configpasswordunmask {
2728  
2729      /**
2730       * Constructor
2731       *
2732       * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2733       * @param string $visiblename localised
2734       * @param string $description long localised info
2735       * @param array $defaultsetting ('value'=>string, 'adv'=>bool)
2736       */
2737      public function __construct($name, $visiblename, $description, $defaultsetting) {
2738          parent::__construct($name, $visiblename, $description, $defaultsetting['value']);
2739          $this->set_advanced_flag_options(admin_setting_flag::ENABLED, !empty($defaultsetting['adv']));
2740      }
2741  }
2742  
2743  /**
2744   * Admin setting class for encrypted values using secure encryption.
2745   *
2746   * @copyright 2019 The Open University
2747   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2748   */
2749  class admin_setting_encryptedpassword extends admin_setting {
2750  
2751      /**
2752       * Constructor. Same as parent except that the default value is always an empty string.
2753       *
2754       * @param string $name Internal name used in config table
2755       * @param string $visiblename Name shown on form
2756       * @param string $description Description that appears below field
2757       */
2758      public function __construct(string $name, string $visiblename, string $description) {
2759          parent::__construct($name, $visiblename, $description, '');
2760      }
2761  
2762      public function get_setting() {
2763          return $this->config_read($this->name);
2764      }
2765  
2766      public function write_setting($data) {
2767          $data = trim($data);
2768          if ($data === '') {
2769              // Value can really be set to nothing.
2770              $savedata = '';
2771          } else {
2772              // Encrypt value before saving it.
2773              $savedata = \core\encryption::encrypt($data);
2774          }
2775          return ($this->config_write($this->name, $savedata) ? '' : get_string('errorsetting', 'admin'));
2776      }
2777  
2778      public function output_html($data, $query='') {
2779          global $OUTPUT;
2780  
2781          $default = $this->get_defaultsetting();
2782          $context = (object) [
2783              'id' => $this->get_id(),
2784              'name' => $this->get_full_name(),
2785              'set' => $data !== '',
2786              'novalue' => $this->get_setting() === null
2787          ];
2788          $element = $OUTPUT->render_from_template('core_admin/setting_encryptedpassword', $context);
2789  
2790          return format_admin_setting($this, $this->visiblename, $element, $this->description,
2791                  true, '', $default, $query);
2792      }
2793  }
2794  
2795  /**
2796   * Empty setting used to allow flags (advanced) on settings that can have no sensible default.
2797   * Note: Only advanced makes sense right now - locked does not.
2798   *
2799   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2800   */
2801  class admin_setting_configempty extends admin_setting_configtext {
2802  
2803      /**
2804       * @param string $name
2805       * @param string $visiblename
2806       * @param string $description
2807       */
2808      public function __construct($name, $visiblename, $description) {
2809          parent::__construct($name, $visiblename, $description, '', PARAM_RAW);
2810      }
2811  
2812      /**
2813       * Returns an XHTML string for the hidden field
2814       *
2815       * @param string $data
2816       * @param string $query
2817       * @return string XHTML string for the editor
2818       */
2819      public function output_html($data, $query='') {
2820          global $OUTPUT;
2821  
2822          $context = (object) [
2823              'id' => $this->get_id(),
2824              'name' => $this->get_full_name()
2825          ];
2826          $element = $OUTPUT->render_from_template('core_admin/setting_configempty', $context);
2827  
2828          return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', get_string('none'), $query);
2829      }
2830  }
2831  
2832  
2833  /**
2834   * Path to directory
2835   *
2836   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2837   */
2838  class admin_setting_configfile extends admin_setting_configtext {
2839      /**
2840       * Constructor
2841       * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2842       * @param string $visiblename localised
2843       * @param string $description long localised info
2844       * @param string $defaultdirectory default directory location
2845       */
2846      public function __construct($name, $visiblename, $description, $defaultdirectory) {
2847          parent::__construct($name, $visiblename, $description, $defaultdirectory, PARAM_RAW, 50);
2848      }
2849  
2850      /**
2851       * Returns XHTML for the field
2852       *
2853       * Returns XHTML for the field and also checks whether the file
2854       * specified in $data exists using file_exists()
2855       *
2856       * @param string $data File name and path to use in value attr
2857       * @param string $query
2858       * @return string XHTML field
2859       */
2860      public function output_html($data, $query='') {
2861          global $CFG, $OUTPUT;
2862  
2863          $default = $this->get_defaultsetting();
2864          $context = (object) [
2865              'id' => $this->get_id(),
2866              'name' => $this->get_full_name(),
2867              'size' => $this->size,
2868              'value' => $data,
2869              'showvalidity' => !empty($data),
2870              'valid' => $data && file_exists($data),
2871              'readonly' => !empty($CFG->preventexecpath) || $this->is_readonly(),
2872              'forceltr' => $this->get_force_ltr(),
2873          ];
2874  
2875          if ($context->readonly) {
2876              $this->visiblename .= '<div class="alert alert-info">'.get_string('execpathnotallowed', 'admin').'</div>';
2877          }
2878  
2879          $element = $OUTPUT->render_from_template('core_admin/setting_configfile', $context);
2880  
2881          return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2882      }
2883  
2884      /**
2885       * Checks if execpatch has been disabled in config.php
2886       */
2887      public function write_setting($data) {
2888          global $CFG;
2889          if (!empty($CFG->preventexecpath)) {
2890              if ($this->get_setting() === null) {
2891                  // Use default during installation.
2892                  $data = $this->get_defaultsetting();
2893                  if ($data === null) {
2894                      $data = '';
2895                  }
2896              } else {
2897                  return '';
2898              }
2899          }
2900          return parent::write_setting($data);
2901      }
2902  
2903  }
2904  
2905  
2906  /**
2907   * Path to executable file
2908   *
2909   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2910   */
2911  class admin_setting_configexecutable extends admin_setting_configfile {
2912  
2913      /**
2914       * Returns an XHTML field
2915       *
2916       * @param string $data This is the value for the field
2917       * @param string $query
2918       * @return string XHTML field
2919       */
2920      public function output_html($data, $query='') {
2921          global $CFG, $OUTPUT;
2922          $default = $this->get_defaultsetting();
2923          require_once("$CFG->libdir/filelib.php");
2924  
2925          $context = (object) [
2926              'id' => $this->get_id(),
2927              'name' => $this->get_full_name(),
2928              'size' => $this->size,
2929              'value' => $data,
2930              'showvalidity' => !empty($data),
2931              'valid' => $data && file_exists($data) && !is_dir($data) && file_is_executable($data),
2932              'readonly' => !empty($CFG->preventexecpath),
2933              'forceltr' => $this->get_force_ltr()
2934          ];
2935  
2936          if (!empty($CFG->preventexecpath)) {
2937              $this->visiblename .= '<div class="alert alert-info">'.get_string('execpathnotallowed', 'admin').'</div>';
2938          }
2939  
2940          $element = $OUTPUT->render_from_template('core_admin/setting_configexecutable', $context);
2941  
2942          return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2943      }
2944  }
2945  
2946  
2947  /**
2948   * Path to directory
2949   *
2950   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2951   */
2952  class admin_setting_configdirectory extends admin_setting_configfile {
2953  
2954      /**
2955       * Returns an XHTML field
2956       *
2957       * @param string $data This is the value for the field
2958       * @param string $query
2959       * @return string XHTML
2960       */
2961      public function output_html($data, $query='') {
2962          global $CFG, $OUTPUT;
2963          $default = $this->get_defaultsetting();
2964  
2965          $context = (object) [
2966              'id' => $this->get_id(),
2967              'name' => $this->get_full_name(),
2968              'size' => $this->size,
2969              'value' => $data,
2970              'showvalidity' => !empty($data),
2971              'valid' => $data && file_exists($data) && is_dir($data),
2972              'readonly' => !empty($CFG->preventexecpath),
2973              'forceltr' => $this->get_force_ltr()
2974          ];
2975  
2976          if (!empty($CFG->preventexecpath)) {
2977              $this->visiblename .= '<div class="alert alert-info">'.get_string('execpathnotallowed', 'admin').'</div>';
2978          }
2979  
2980          $element = $OUTPUT->render_from_template('core_admin/setting_configdirectory', $context);
2981  
2982          return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2983      }
2984  }
2985  
2986  
2987  /**
2988   * Checkbox
2989   *
2990   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2991   */
2992  class admin_setting_configcheckbox extends admin_setting {
2993      /** @var string Value used when checked */
2994      public $yes;
2995      /** @var string Value used when not checked */
2996      public $no;
2997  
2998      /**
2999       * Constructor
3000       * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3001       * @param string $visiblename localised
3002       * @param string $description long localised info
3003       * @param string $defaultsetting
3004       * @param string $yes value used when checked
3005       * @param string $no value used when not checked
3006       */
3007      public function __construct($name, $visiblename, $description, $defaultsetting, $yes='1', $no='0') {
3008          parent::__construct($name, $visiblename, $description, $defaultsetting);
3009          $this->yes = (string)$yes;
3010          $this->no  = (string)$no;
3011      }
3012  
3013      /**
3014       * Retrieves the current setting using the objects name
3015       *
3016       * @return string
3017       */
3018      public function get_setting() {
3019          return $this->config_read($this->name);
3020      }
3021  
3022      /**
3023       * Sets the value for the setting
3024       *
3025       * Sets the value for the setting to either the yes or no values
3026       * of the object by comparing $data to yes
3027       *
3028       * @param mixed $data Gets converted to str for comparison against yes value
3029       * @return string empty string or error
3030       */
3031      public function write_setting($data) {
3032          if ((string)$data === $this->yes) { // convert to strings before comparison
3033              $data = $this->yes;
3034          } else {
3035              $data = $this->no;
3036          }
3037          return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
3038      }
3039  
3040      /**
3041       * Returns an XHTML checkbox field
3042       *
3043       * @param string $data If $data matches yes then checkbox is checked
3044       * @param string $query
3045       * @return string XHTML field
3046       */
3047      public function output_html($data, $query='') {
3048          global $OUTPUT;
3049  
3050          $context = (object) [
3051              'id' => $this->get_id(),
3052              'name' => $this->get_full_name(),
3053              'no' => $this->no,
3054              'value' => $this->yes,
3055              'checked' => (string) $data === $this->yes,
3056              'readonly' => $this->is_readonly(),
3057          ];
3058  
3059          $default = $this->get_defaultsetting();
3060          if (!is_null($default)) {
3061              if ((string)$default === $this->yes) {
3062                  $defaultinfo = get_string('checkboxyes', 'admin');
3063              } else {
3064                  $defaultinfo = get_string('checkboxno', 'admin');
3065              }
3066          } else {
3067              $defaultinfo = NULL;
3068          }
3069  
3070          $element = $OUTPUT->render_from_template('core_admin/setting_configcheckbox', $context);
3071  
3072          return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
3073      }
3074  }
3075  
3076  
3077  /**
3078   * Multiple checkboxes, each represents different value, stored in csv format
3079   *
3080   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3081   */
3082  class admin_setting_configmulticheckbox extends admin_setting {
3083      /** @var array Array of choices value=>label */
3084      public $choices;
3085      /** @var callable|null Loader function for choices */
3086      protected $choiceloader = null;
3087  
3088      /**
3089       * Constructor: uses parent::__construct
3090       *
3091       * The $choices parameter may be either an array of $value => $label format,
3092       * e.g. [1 => get_string('yes')], or a callback function which takes no parameters and
3093       * returns an array in that format.
3094       *
3095       * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3096       * @param string $visiblename localised
3097       * @param string $description long localised info
3098       * @param array $defaultsetting array of selected
3099       * @param array|callable $choices array of $value => $label for each checkbox, or a callback
3100       */
3101      public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
3102          if (is_array($choices)) {
3103              $this->choices = $choices;
3104          }
3105          if (is_callable($choices)) {
3106              $this->choiceloader = $choices;
3107          }
3108          parent::__construct($name, $visiblename, $description, $defaultsetting);
3109      }
3110  
3111      /**
3112       * This function may be used in ancestors for lazy loading of choices
3113       *
3114       * Override this method if loading of choices is expensive, such
3115       * as when it requires multiple db requests.
3116       *
3117       * @return bool true if loaded, false if error
3118       */
3119      public function load_choices() {
3120          if ($this->choiceloader) {
3121              if (!is_array($this->choices)) {
3122                  $this->choices = call_user_func($this->choiceloader);
3123              }
3124          }
3125          return true;
3126      }
3127  
3128      /**
3129       * Is setting related to query text - used when searching
3130       *
3131       * @param string $query
3132       * @return bool true on related, false on not or failure
3133       */
3134      public function is_related($query) {
3135          if (!$this->load_choices() or empty($this->choices)) {
3136              return false;
3137          }
3138          if (parent::is_related($query)) {
3139              return true;
3140          }
3141  
3142          foreach ($this->choices as $desc) {
3143              if (strpos(core_text::strtolower($desc), $query) !== false) {
3144                  return true;
3145              }
3146          }
3147          return false;
3148      }
3149  
3150      /**
3151       * Returns the current setting if it is set
3152       *
3153       * @return mixed null if null, else an array
3154       */
3155      public function get_setting() {
3156          $result = $this->config_read($this->name);
3157  
3158          if (is_null($result)) {
3159              return NULL;
3160          }
3161          if ($result === '') {
3162              return array();
3163          }
3164          $enabled = explode(',', $result);
3165          $setting = array();
3166          foreach ($enabled as $option) {
3167              $setting[$option] = 1;
3168          }
3169          return $setting;
3170      }
3171  
3172      /**
3173       * Saves the setting(s) provided in $data
3174       *
3175       * @param array $data An array of data, if not array returns empty str
3176       * @return mixed empty string on useless data or bool true=success, false=failed
3177       */
3178      public function write_setting($data) {
3179          if (!is_array($data)) {
3180              return ''; // ignore it
3181          }
3182          if (!$this->load_choices() or empty($this->choices)) {
3183              return '';
3184          }
3185          unset($data['xxxxx']);
3186          $result = array();
3187          foreach ($data as $key => $value) {
3188              if ($value and array_key_exists($key, $this->choices)) {
3189                  $result[] = $key;
3190              }
3191          }
3192          return $this->config_write($this->name, implode(',', $result)) ? '' : get_string('errorsetting', 'admin');
3193      }
3194  
3195      /**
3196       * Returns XHTML field(s) as required by choices
3197       *
3198       * Relies on data being an array should data ever be another valid vartype with
3199       * acceptable value this may cause a warning/error
3200       * if (!is_array($data)) would fix the problem
3201       *
3202       * @todo Add vartype handling to ensure $data is an array
3203       *
3204       * @param array $data An array of checked values
3205       * @param string $query
3206       * @return string XHTML field
3207       */
3208      public function output_html($data, $query='') {
3209          global $OUTPUT;
3210  
3211          if (!$this->load_choices() or empty($this->choices)) {
3212              return '';
3213          }
3214  
3215          $default = $this->get_defaultsetting();
3216          if (is_null($default)) {
3217              $default = array();
3218          }
3219          if (is_null($data)) {
3220              $data = array();
3221          }
3222  
3223          $context = (object) [
3224              'id' => $this->get_id(),
3225              'name' => $this->get_full_name(),
3226          ];
3227  
3228          $options = array();
3229          $defaults = array();
3230          foreach ($this->choices as $key => $description) {
3231              if (!empty($default[$key])) {
3232                  $defaults[] = $description;
3233              }
3234  
3235              $options[] = [
3236                  'key' => $key,
3237                  'checked' => !empty($data[$key]),
3238                  'label' => highlightfast($query, $description)
3239              ];
3240          }
3241  
3242          if (is_null($default)) {
3243              $defaultinfo = null;
3244          } else if (!empty($defaults)) {
3245              $defaultinfo = implode(', ', $defaults);
3246          } else {
3247              $defaultinfo = get_string('none');
3248          }
3249  
3250          $context->options = $options;
3251          $context->hasoptions = !empty($options);
3252  
3253          $element = $OUTPUT->render_from_template('core_admin/setting_configmulticheckbox', $context);
3254  
3255          return format_admin_setting($this, $this->visiblename, $element, $this->description, false, '', $defaultinfo, $query);
3256  
3257      }
3258  }
3259  
3260  
3261  /**
3262   * Multiple checkboxes 2, value stored as string 00101011
3263   *
3264   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3265   */
3266  class admin_setting_configmulticheckbox2 extends admin_setting_configmulticheckbox {
3267  
3268      /**
3269       * Returns the setting if set
3270       *
3271       * @return mixed null if not set, else an array of set settings
3272       */
3273      public function get_setting() {
3274          $result = $this->config_read($this->name);
3275          if (is_null($result)) {
3276              return NULL;
3277          }
3278          if (!$this->load_choices()) {
3279              return NULL;
3280          }
3281          $result = str_pad($result, count($this->choices), '0');
3282          $result = preg_split('//', $result, -1, PREG_SPLIT_NO_EMPTY);
3283          $setting = array();
3284          foreach ($this->choices as $key=>$unused) {
3285              $value = array_shift($result);
3286              if ($value) {
3287                  $setting[$key] = 1;
3288              }
3289          }
3290          return $setting;
3291      }
3292  
3293      /**
3294       * Save setting(s) provided in $data param
3295       *
3296       * @param array $data An array of settings to save
3297       * @return mixed empty string for bad data or bool true=>success, false=>error
3298       */
3299      public function write_setting($data) {
3300          if (!is_array($data)) {
3301              return ''; // ignore it
3302          }
3303          if (!$this->load_choices() or empty($this->choices)) {
3304              return '';
3305          }
3306          $result = '';
3307          foreach ($this->choices as $key=>$unused) {
3308              if (!empty($data[$key])) {
3309                  $result .= '1';
3310              } else {
3311                  $result .= '0';
3312              }
3313          }
3314          return $this->config_write($this->name, $result) ? '' : get_string('errorsetting', 'admin');
3315      }
3316  }
3317  
3318  
3319  /**
3320   * Select one value from list
3321   *
3322   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3323   */
3324  class admin_setting_configselect extends admin_setting {
3325      /** @var array Array of choices value=>label */
3326      public $choices;
3327      /** @var array Array of choices grouped using optgroups */
3328      public $optgroups;
3329      /** @var callable|null Loader function for choices */
3330      protected $choiceloader = null;
3331      /** @var callable|null Validation function */
3332      protected $validatefunction = null;
3333  
3334      /**
3335       * Constructor.
3336       *
3337       * If you want to lazy-load the choices, pass a callback function that returns a choice
3338       * array for the $choices parameter.
3339       *
3340       * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3341       * @param string $visiblename localised
3342       * @param string $description long localised info
3343       * @param string|int $defaultsetting
3344       * @param array|callable|null $choices array of $value=>$label for each selection, or callback
3345       */
3346      public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
3347          // Look for optgroup and single options.
3348          if (is_array($choices)) {
3349              $this->choices = [];
3350              foreach ($choices as $key => $val) {
3351                  if (is_array($val)) {
3352                      $this->optgroups[$key] = $val;
3353                      $this->choices = array_merge($this->choices, $val);
3354                  } else {
3355                      $this->choices[$key] = $val;
3356                  }
3357              }
3358          }
3359          if (is_callable($choices)) {
3360              $this->choiceloader = $choices;
3361          }
3362  
3363          parent::__construct($name, $visiblename, $description, $defaultsetting);
3364      }
3365  
3366      /**
3367       * Sets a validate function.
3368       *
3369       * The callback will be passed one parameter, the new setting value, and should return either
3370       * an empty string '' if the value is OK, or an error message if not.
3371       *
3372       * @param callable|null $validatefunction Validate function or null to clear
3373       * @since Moodle 3.10
3374       */
3375      public function set_validate_function(?callable $validatefunction = null) {
3376          $this->validatefunction = $validatefunction;
3377      }
3378  
3379      /**
3380       * This function may be used in ancestors for lazy loading of choices
3381       *
3382       * Override this method if loading of choices is expensive, such
3383       * as when it requires multiple db requests.
3384       *
3385       * @return bool true if loaded, false if error
3386       */
3387      public function load_choices() {
3388          if ($this->choiceloader) {
3389              if (!is_array($this->choices)) {
3390                  $this->choices = call_user_func($this->choiceloader);
3391              }
3392              return true;
3393          }
3394          return true;
3395      }
3396  
3397      /**
3398       * Check if this is $query is related to a choice
3399       *
3400       * @param string $query
3401       * @return bool true if related, false if not
3402       */
3403      public function is_related($query) {
3404          if (parent::is_related($query)) {
3405              return true;
3406          }
3407          if (!$this->load_choices()) {
3408              return false;
3409          }
3410          foreach ($this->choices as $key=>$value) {
3411              if (strpos(core_text::strtolower($key), $query) !== false) {
3412                  return true;
3413              }
3414              if (strpos(core_text::strtolower($value), $query) !== false) {
3415                  return true;
3416              }
3417          }
3418          return false;
3419      }
3420  
3421      /**
3422       * Return the setting
3423       *
3424       * @return mixed returns config if successful else null
3425       */
3426      public function get_setting() {
3427          return $this->config_read($this->name);
3428      }
3429  
3430      /**
3431       * Save a setting
3432       *
3433       * @param string $data
3434       * @return string empty of error string
3435       */
3436      public function write_setting($data) {
3437          if (!$this->load_choices() or empty($this->choices)) {
3438              return '';
3439          }
3440          if (!array_key_exists($data, $this->choices)) {
3441              return ''; // ignore it
3442          }
3443  
3444          // Validate the new setting.
3445          $error = $this->validate_setting($data);
3446          if ($error) {
3447              return $error;
3448          }
3449  
3450          return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
3451      }
3452  
3453      /**
3454       * Validate the setting. This uses the callback function if provided; subclasses could override
3455       * to carry out validation directly in the class.
3456       *
3457       * @param string $data New value being set
3458       * @return string Empty string if valid, or error message text
3459       * @since Moodle 3.10
3460       */
3461      protected function validate_setting(string $data): string {
3462          // If validation function is specified, call it now.
3463          if ($this->validatefunction) {
3464              return call_user_func($this->validatefunction, $data);
3465          } else {
3466              return '';
3467          }
3468      }
3469  
3470      /**
3471       * Returns XHTML select field
3472       *
3473       * Ensure the options are loaded, and generate the XHTML for the select
3474       * element and any warning message. Separating this out from output_html
3475       * makes it easier to subclass this class.
3476       *
3477       * @param string $data the option to show as selected.
3478       * @param string $current the currently selected option in the database, null if none.
3479       * @param string $default the default selected option.
3480       * @return array the HTML for the select element, and a warning message.
3481       * @deprecated since Moodle 3.2
3482       */
3483      public function output_select_html($data, $current, $default, $extraname = '') {
3484          debugging('The method admin_setting_configselect::output_select_html is depreacted, do not use any more.', DEBUG_DEVELOPER);
3485      }
3486  
3487      /**
3488       * Returns XHTML select field and wrapping div(s)
3489       *
3490       * @see output_select_html()
3491       *
3492       * @param string $data the option to show as selected
3493       * @param string $query
3494       * @return string XHTML field and wrapping div
3495       */
3496      public function output_html($data, $query='') {
3497          global $OUTPUT;
3498  
3499          $default = $this->get_defaultsetting();
3500          $current = $this->get_setting();
3501  
3502          if (!$this->load_choices() || empty($this->choices)) {
3503              return '';
3504          }
3505  
3506          $context = (object) [
3507              'id' => $this->get_id(),
3508              'name' => $this->get_full_name(),
3509          ];
3510  
3511          if (!is_null($default) && array_key_exists($default, $this->choices)) {
3512              $defaultinfo = $this->choices[$default];
3513          } else {
3514              $defaultinfo = NULL;
3515          }
3516  
3517          // Warnings.
3518          $warning = '';
3519          if ($current === null) {
3520              // First run.
3521          } else if (empty($current) && (array_key_exists('', $this->choices) || array_key_exists(0, $this->choices))) {
3522              // No warning.
3523          } else if (!array_key_exists($current, $this->choices)) {
3524              $warning = get_string('warningcurrentsetting', 'admin', $current);
3525              if (!is_null($default) && $data == $current) {
3526                  $data = $default; // Use default instead of first value when showing the form.
3527              }
3528          }
3529  
3530          $options = [];
3531          $template = 'core_admin/setting_configselect';
3532  
3533          if (!empty($this->optgroups)) {
3534              $optgroups = [];
3535              foreach ($this->optgroups as $label => $choices) {
3536                  $optgroup = array('label' => $label, 'options' => []);
3537                  foreach ($choices as $value => $name) {
3538                      $optgroup['options'][] = [
3539                          'value' => $value,
3540                          'name' => $name,
3541                          'selected' => (string) $value == $data
3542                      ];
3543                      unset($this->choices[$value]);
3544                  }
3545                  $optgroups[] = $optgroup;
3546              }
3547              $context->options = $options;
3548              $context->optgroups = $optgroups;
3549              $template = 'core_admin/setting_configselect_optgroup';
3550          }
3551  
3552          foreach ($this->choices as $value => $name) {
3553              $options[] = [
3554                  'value' => $value,
3555                  'name' => $name,
3556                  'selected' => (string) $value == $data
3557              ];
3558          }
3559          $context->options = $options;
3560          $context->readonly = $this->is_readonly();
3561  
3562          $element = $OUTPUT->render_from_template($template, $context);
3563  
3564          return format_admin_setting($this, $this->visiblename, $element, $this->description, true, $warning, $defaultinfo, $query);
3565      }
3566  }
3567  
3568  /**
3569   * Select multiple items from list
3570   *
3571   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3572   */
3573  class admin_setting_configmultiselect extends admin_setting_configselect {
3574      /**
3575       * Constructor
3576       * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3577       * @param string $visiblename localised
3578       * @param string $description long localised info
3579       * @param array $defaultsetting array of selected items
3580       * @param array $choices array of $value=>$label for each list item
3581       */
3582      public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
3583          parent::__construct($name, $visiblename, $description, $defaultsetting, $choices);
3584      }
3585  
3586      /**
3587       * Returns the select setting(s)
3588       *
3589       * @return mixed null or array. Null if no settings else array of setting(s)
3590       */
3591      public function get_setting() {
3592          $result = $this->config_read($this->name);
3593          if (is_null($result)) {
3594              return NULL;
3595          }
3596          if ($result === '') {
3597              return array();
3598          }
3599          return explode(',', $result);
3600      }
3601  
3602      /**
3603       * Saves setting(s) provided through $data
3604       *
3605       * Potential bug in the works should anyone call with this function
3606       * using a vartype that is not an array
3607       *
3608       * @param array $data
3609       */
3610      public function write_setting($data) {
3611          if (!is_array($data)) {
3612              return ''; //ignore it
3613          }
3614          if (!$this->load_choices() or empty($this->choices)) {
3615              return '';
3616          }
3617  
3618          unset($data['xxxxx']);
3619  
3620          $save = array();
3621          foreach ($data as $value) {
3622              if (!array_key_exists($value, $this->choices)) {
3623                  continue; // ignore it
3624              }
3625              $save[] = $value;
3626          }
3627  
3628          return ($this->config_write($this->name, implode(',', $save)) ? '' : get_string('errorsetting', 'admin'));
3629      }
3630  
3631      /**
3632       * Is setting related to query text - used when searching
3633       *
3634       * @param string $query
3635       * @return bool true if related, false if not
3636       */
3637      public function is_related($query) {
3638          if (!$this->load_choices() or empty($this->choices)) {
3639              return false;
3640          }
3641          if (parent::is_related($query)) {
3642              return true;
3643          }
3644  
3645          foreach ($this->choices as $desc) {
3646              if (strpos(core_text::strtolower($desc), $query) !== false) {
3647                  return true;
3648              }
3649          }
3650          return false;
3651      }
3652  
3653      /**
3654       * Returns XHTML multi-select field
3655       *
3656       * @todo Add vartype handling to ensure $data is an array
3657       * @param array $data Array of values to select by default
3658       * @param string $query
3659       * @return string XHTML multi-select field
3660       */
3661      public function output_html($data, $query='') {
3662          global $OUTPUT;
3663  
3664          if (!$this->load_choices() or empty($this->choices)) {
3665              return '';
3666          }
3667  
3668          $default = $this->get_defaultsetting();
3669          if (is_null($default)) {
3670              $default = array();
3671          }
3672          if (is_null($data)) {
3673              $data = array();
3674          }
3675  
3676          $context = (object) [
3677              'id' => $this->get_id(),
3678              'name' => $this->get_full_name(),
3679              'size' => min(10, count($this->choices))
3680          ];
3681  
3682          $defaults = [];
3683          $options = [];
3684          $template = 'core_admin/setting_configmultiselect';
3685  
3686          if (!empty($this->optgroups)) {
3687              $optgroups = [];
3688              foreach ($this->optgroups as $label => $choices) {
3689                  $optgroup = array('label' => $label, 'options' => []);
3690                  foreach ($choices as $value => $name) {
3691                      if (in_array($value, $default)) {
3692                          $defaults[] = $name;
3693                      }
3694                      $optgroup['options'][] = [
3695                          'value' => $value,
3696                          'name' => $name,
3697                          'selected' => in_array($value, $data)
3698                      ];
3699                      unset($this->choices[$value]);
3700                  }
3701                  $optgroups[] = $optgroup;
3702              }
3703              $context->optgroups = $optgroups;
3704              $template = 'core_admin/setting_configmultiselect_optgroup';
3705          }
3706  
3707          foreach ($this->choices as $value => $name) {
3708              if (in_array($value, $default)) {
3709                  $defaults[] = $name;
3710              }
3711              $options[] = [
3712                  'value' => $value,
3713                  'name' => $name,
3714                  'selected' => in_array($value, $data)
3715              ];
3716          }
3717          $context->options = $options;
3718          $context->readonly = $this->is_readonly();
3719  
3720          if (is_null($default)) {
3721              $defaultinfo = NULL;
3722          } if (!empty($defaults)) {
3723              $defaultinfo = implode(', ', $defaults);
3724          } else {
3725              $defaultinfo = get_string('none');
3726          }
3727  
3728          $element = $OUTPUT->render_from_template($template, $context);
3729  
3730          return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
3731      }
3732  }
3733  
3734  /**
3735   * Time selector
3736   *
3737   * This is a liiitle bit messy. we're using two selects, but we're returning
3738   * them as an array named after $name (so we only use $name2 internally for the setting)
3739   *
3740   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3741   */
3742  class admin_setting_configtime extends admin_setting {
3743      /** @var string Used for setting second select (minutes) */
3744      public $name2;
3745  
3746      /**
3747       * Constructor
3748       * @param string $hoursname setting for hours
3749       * @param string $minutesname setting for hours
3750       * @param string $visiblename localised
3751       * @param string $description long localised info
3752       * @param array $defaultsetting array representing default time 'h'=>hours, 'm'=>minutes
3753       */
3754      public function __construct($hoursname, $minutesname, $visiblename, $description, $defaultsetting) {
3755          $this->name2 = $minutesname;
3756          parent::__construct($hoursname, $visiblename, $description, $defaultsetting);
3757      }
3758  
3759      /**
3760       * Get the selected time
3761       *
3762       * @return mixed An array containing 'h'=>xx, 'm'=>xx, or null if not set
3763       */
3764      public function get_setting() {
3765          $result1 = $this->config_read($this->name);
3766          $result2 = $this->config_read($this->name2);
3767          if (is_null($result1) or is_null($result2)) {
3768              return NULL;
3769          }
3770  
3771          return array('h' => $result1, 'm' => $result2);
3772      }
3773  
3774      /**
3775       * Store the time (hours and minutes)
3776       *
3777       * @param array $data Must be form 'h'=>xx, 'm'=>xx
3778       * @return bool true if success, false if not
3779       */
3780      public function write_setting($data) {
3781          if (!is_array($data)) {
3782              return '';
3783          }
3784  
3785          $result = $this->config_write($this->name, (int)$data['h']) && $this->config_write($this->name2, (int)$data['m']);
3786          return ($result ? '' : get_string('errorsetting', 'admin'));
3787      }
3788  
3789      /**
3790       * Returns XHTML time select fields
3791       *
3792       * @param array $data Must be form 'h'=>xx, 'm'=>xx
3793       * @param string $query
3794       * @return string XHTML time select fields and wrapping div(s)
3795       */
3796      public function output_html($data, $query='') {
3797          global $OUTPUT;
3798  
3799          $default = $this->get_defaultsetting();
3800          if (is_array($default)) {
3801              $defaultinfo = $default['h'].':'.$default['m'];
3802          } else {
3803              $defaultinfo = NULL;
3804          }
3805  
3806          $context = (object) [
3807              'id' => $this->get_id(),
3808              'name' => $this->get_full_name(),
3809              'readonly' => $this->is_readonly(),
3810              'hours' => array_map(function($i) use ($data) {
3811                  return [
3812                      'value' => $i,
3813                      'name' => $i,
3814                      'selected' => $i == $data['h']
3815                  ];
3816              }, range(0, 23)),
3817              'minutes' => array_map(function($i) use ($data) {
3818                  return [
3819                      'value' => $i,
3820                      'name' => $i,
3821                      'selected' => $i == $data['m']
3822                  ];
3823              }, range(0, 59, 5))
3824          ];
3825  
3826          $element = $OUTPUT->render_from_template('core_admin/setting_configtime', $context);
3827  
3828          return format_admin_setting($this, $this->visiblename, $element, $this->description,
3829              $this->get_id() . 'h', '', $defaultinfo, $query);
3830      }
3831  
3832  }
3833  
3834  
3835  /**
3836   * Seconds duration setting.
3837   *
3838   * @copyright 2012 Petr Skoda (http://skodak.org)
3839   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3840   */
3841  class admin_setting_configduration extends admin_setting {
3842  
3843      /** @var int default duration unit */
3844      protected $defaultunit;
3845      /** @var callable|null Validation function */
3846      protected $validatefunction = null;
3847  
3848      /**
3849       * Constructor
3850       * @param string $name unique ascii name, either 'mysetting' for settings that in config,
3851       *                     or 'myplugin/mysetting' for ones in config_plugins.
3852       * @param string $visiblename localised name
3853       * @param string $description localised long description
3854       * @param mixed $defaultsetting string or array depending on implementation
3855       * @param int $defaultunit - day, week, etc. (in seconds)
3856       */
3857      public function __construct($name, $visiblename, $description, $defaultsetting, $defaultunit = 86400) {
3858          if (is_number($defaultsetting)) {
3859              $defaultsetting = self::parse_seconds($defaultsetting);
3860          }
3861          $units = self::get_units();
3862          if (isset($units[$defaultunit])) {
3863              $this->defaultunit = $defaultunit;
3864          } else {
3865              $this->defaultunit = 86400;
3866          }
3867          parent::__construct($name, $visiblename, $description, $defaultsetting);
3868      }
3869  
3870      /**
3871       * Sets a validate function.
3872       *
3873       * The callback will be passed one parameter, the new setting value, and should return either
3874       * an empty string '' if the value is OK, or an error message if not.
3875       *
3876       * @param callable|null $validatefunction Validate function or null to clear
3877       * @since Moodle 3.10
3878       */
3879      public function set_validate_function(?callable $validatefunction = null) {
3880          $this->validatefunction = $validatefunction;
3881      }
3882  
3883      /**
3884       * Validate the setting. This uses the callback function if provided; subclasses could override
3885       * to carry out validation directly in the class.
3886       *
3887       * @param int $data New value being set
3888       * @return string Empty string if valid, or error message text
3889       * @since Moodle 3.10
3890       */
3891      protected function validate_setting(int $data): string {
3892          // If validation function is specified, call it now.
3893          if ($this->validatefunction) {
3894              return call_user_func($this->validatefunction, $data);
3895          } else {
3896              if ($data < 0) {
3897                  return get_string('errorsetting', 'admin');
3898              }
3899              return '';
3900          }
3901      }
3902  
3903      /**
3904       * Returns selectable units.
3905       * @static
3906       * @return array
3907       */
3908      protected static function get_units() {
3909          return array(
3910              604800 => get_string('weeks'),
3911              86400 => get_string('days'),
3912              3600 => get_string('hours'),
3913              60 => get_string('minutes'),
3914              1 => get_string('seconds'),
3915          );
3916      }
3917  
3918      /**
3919       * Converts seconds to some more user friendly string.
3920       * @static
3921       * @param int $seconds
3922       * @return string
3923       */
3924      protected static function get_duration_text($seconds) {
3925          if (empty($seconds)) {
3926              return get_string('none');
3927          }
3928          $data = self::parse_seconds($seconds);
3929          switch ($data['u']) {
3930              case (60*60*24*7):
3931                  return get_string('numweeks', '', $data['v']);
3932              case (60*60*24):
3933                  return get_string('numdays', '', $data['v']);
3934              case (60*60):
3935                  return get_string('numhours', '', $data['v']);
3936              case (60):
3937                  return get_string('numminutes', '', $data['v']);
3938              default:
3939                  return get_string('numseconds', '', $data['v']*$data['u']);
3940          }
3941      }
3942  
3943      /**
3944       * Finds suitable units for given duration.
3945       * @static
3946       * @param int $seconds
3947       * @return array
3948       */
3949      protected static function parse_seconds($seconds) {
3950          foreach (self::get_units() as $unit => $unused) {
3951              if ($seconds % $unit === 0) {
3952                  return array('v'=>(int)($seconds/$unit), 'u'=>$unit);
3953              }
3954          }
3955          return array('v'=>(int)$seconds, 'u'=>1);
3956      }
3957  
3958      /**
3959       * Get the selected duration as array.
3960       *
3961       * @return mixed An array containing 'v'=>xx, 'u'=>xx, or null if not set
3962       */
3963      public function get_setting() {
3964          $seconds = $this->config_read($this->name);
3965          if (is_null($seconds)) {
3966              return null;
3967          }
3968  
3969          return self::parse_seconds($seconds);
3970      }
3971  
3972      /**
3973       * Store the duration as seconds.
3974       *
3975       * @param array $data Must be form 'h'=>xx, 'm'=>xx
3976       * @return bool true if success, false if not
3977       */
3978      public function write_setting($data) {
3979          if (!is_array($data)) {
3980              return '';
3981          }
3982  
3983          $unit = (int)$data['u'];
3984          $value = (int)$data['v'];
3985          $seconds = $value * $unit;
3986  
3987          // Validate the new setting.
3988          $error = $this->validate_setting($seconds);
3989          if ($error) {
3990              return $error;
3991          }
3992  
3993          $result = $this->config_write($this->name, $seconds);
3994          return ($result ? '' : get_string('errorsetting', 'admin'));
3995      }
3996  
3997      /**
3998       * Returns duration text+select fields.
3999       *
4000       * @param array $data Must be form 'v'=>xx, 'u'=>xx
4001       * @param string $query
4002       * @return string duration text+select fields and wrapping div(s)
4003       */
4004      public function output_html($data, $query='') {
4005          global $OUTPUT;
4006  
4007          $default = $this->get_defaultsetting();
4008          if (is_number($default)) {
4009              $defaultinfo = self::get_duration_text($default);
4010          } else if (is_array($default)) {
4011              $defaultinfo = self::get_duration_text($default['v']*$default['u']);
4012          } else {
4013              $defaultinfo = null;
4014          }
4015  
4016          $inputid = $this->get_id() . 'v';
4017          $units = self::get_units();
4018          $defaultunit = $this->defaultunit;
4019  
4020          $context = (object) [
4021              'id' => $this->get_id(),
4022              'name' => $this->get_full_name(),
4023              'value' => $data['v'],
4024              'readonly' => $this->is_readonly(),
4025              'options' => array_map(function($unit) use ($units, $data, $defaultunit) {
4026                  return [
4027                      'value' => $unit,
4028                      'name' => $units[$unit],
4029                      'selected' => ($data['v'] == 0 && $unit == $defaultunit) || $unit == $data['u']
4030                  ];
4031              }, array_keys($units))
4032          ];
4033  
4034          $element = $OUTPUT->render_from_template('core_admin/setting_configduration', $context);
4035  
4036          return format_admin_setting($this, $this->visiblename, $element, $this->description, $inputid, '', $defaultinfo, $query);
4037      }
4038  }
4039  
4040  
4041  /**
4042   * Seconds duration setting with an advanced checkbox, that controls a additional
4043   * $name.'_adv' setting.
4044   *
4045   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4046   * @copyright 2014 The Open University
4047   */
4048  class admin_setting_configduration_with_advanced extends admin_setting_configduration {
4049      /**
4050       * Constructor
4051       * @param string $name unique ascii name, either 'mysetting' for settings that in config,
4052       *                     or 'myplugin/mysetting' for ones in config_plugins.
4053       * @param string $visiblename localised name
4054       * @param string $description localised long description
4055       * @param array  $defaultsetting array of int value, and bool whether it is
4056       *                     is advanced by default.
4057       * @param int $defaultunit - day, week, etc. (in seconds)
4058       */
4059      public function __construct($name, $visiblename, $description, $defaultsetting, $defaultunit = 86400) {
4060          parent::__construct($name, $visiblename, $description, $defaultsetting['value'], $defaultunit);
4061          $this->set_advanced_flag_options(admin_setting_flag::ENABLED, !empty($defaultsetting['adv']));
4062      }
4063  }
4064  
4065  
4066  /**
4067   * Used to validate a textarea used for ip addresses
4068   *
4069   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4070   * @copyright 2011 Petr Skoda (http://skodak.org)
4071   */
4072  class admin_setting_configiplist extends admin_setting_configtextarea {
4073  
4074      /**
4075       * Validate the contents of the textarea as IP addresses
4076       *
4077       * Used to validate a new line separated list of IP addresses collected from
4078       * a textarea control
4079       *
4080       * @param string $data A list of IP Addresses separated by new lines
4081       * @return mixed bool true for success or string:error on failure
4082       */
4083      public function validate($data) {
4084          if(!empty($data)) {
4085              $lines = explode("\n", $data);
4086          } else {
4087              return true;
4088          }
4089          $result = true;
4090          $badips = array();
4091          foreach ($lines as $line) {
4092              $tokens = explode('#', $line);
4093              $ip = trim($tokens[0]);
4094              if (empty($ip)) {
4095                  continue;
4096              }
4097              if (preg_match('#^(\d{1,3})(\.\d{1,3}){0,3}$#', $ip, $match) ||
4098                  preg_match('#^(\d{1,3})(\.\d{1,3}){0,3}(\/\d{1,2})$#', $ip, $match) ||
4099                  preg_match('#^(\d{1,3})(\.\d{1,3}){3}(-\d{1,3})$#', $ip, $match)) {
4100              } else {
4101                  $result = false;
4102                  $badips[] = $ip;
4103              }
4104          }
4105          if($result) {
4106              return true;
4107          } else {
4108              return get_string('validateiperror', 'admin', join(', ', $badips));
4109          }
4110      }
4111  }
4112  
4113  /**
4114   * Used to validate a textarea used for domain names, wildcard domain names and IP addresses/ranges (both IPv4 and IPv6 format).
4115   *
4116   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4117   * @copyright 2016 Jake Dallimore (jrhdallimore@gmail.com)
4118   */
4119  class admin_setting_configmixedhostiplist extends admin_setting_configtextarea {
4120  
4121      /**
4122       * Validate the contents of the textarea as either IP addresses, domain name or wildcard domain name (RFC 4592).
4123       * Used to validate a new line separated list of entries collected from a textarea control.
4124       *
4125       * This setting provides support for internationalised domain names (IDNs), however, such UTF-8 names will be converted to
4126       * their ascii-compatible encoding (punycode) on save, and converted back to their UTF-8 representation when fetched
4127       * via the get_setting() method, which has been overriden.
4128       *
4129       * @param string $data A list of FQDNs, DNS wildcard format domains, and IP addresses, separated by new lines.
4130       * @return mixed bool true for success or string:error on failure
4131       */
4132      public function validate($data) {
4133          if (empty($data)) {
4134              return true;
4135          }
4136          $entries = explode("\n", $data);
4137          $badentries = [];
4138  
4139          foreach ($entries as $key => $entry) {
4140              $entry = trim($entry);
4141              if (empty($entry)) {
4142                  return get_string('validateemptylineerror', 'admin');
4143              }
4144  
4145              // Validate each string entry against the supported formats.
4146              if (\core\ip_utils::is_ip_address($entry) || \core\ip_utils::is_ipv6_range($entry)
4147                      || \core\ip_utils::is_ipv4_range($entry) || \core\ip_utils::is_domain_name($entry)
4148                      || \core\ip_utils::is_domain_matching_pattern($entry)) {
4149                  continue;
4150              }
4151  
4152              // Otherwise, the entry is invalid.
4153              $badentries[] = $entry;
4154          }
4155  
4156          if ($badentries) {
4157              return get_string('validateerrorlist', 'admin', join(', ', $badentries));
4158          }
4159          return true;
4160      }
4161  
4162      /**
4163       * Convert any lines containing international domain names (IDNs) to their ascii-compatible encoding (ACE).
4164       *
4165       * @param string $data the setting data, as sent from the web form.
4166       * @return string $data the setting data, with all IDNs converted (using punycode) to their ascii encoded version.
4167       */
4168      protected function ace_encode($data) {
4169          if (empty($data)) {
4170              return $data;
4171          }
4172          $entries = explode("\n", $data);
4173          foreach ($entries as $key => $entry) {
4174              $entry = trim($entry);
4175              // This regex matches any string that has non-ascii character.
4176              if (preg_match('/[^\x00-\x7f]/', $entry)) {
4177                  // If we can convert the unicode string to an idn, do so.
4178                  // Otherwise, leave the original unicode string alone and let the validation function handle it (it will fail).
4179                  $val = idn_to_ascii($entry, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
4180                  $entries[$key] = $val ? $val : $entry;
4181              }
4182          }
4183          return implode("\n", $entries);
4184      }
4185  
4186      /**
4187       * Decode any ascii-encoded domain names back to their utf-8 representation for display.
4188       *
4189       * @param string $data the setting data, as found in the database.
4190       * @return string $data the setting data, with all ascii-encoded IDNs decoded back to their utf-8 representation.
4191       */
4192      protected function ace_decode($data) {
4193          $entries = explode("\n", $data);
4194          foreach ($entries as $key => $entry) {
4195              $entry = trim($entry);
4196              if (strpos($entry, 'xn--') !== false) {
4197                  $entries[$key] = idn_to_utf8($entry, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
4198              }
4199          }
4200          return implode("\n", $entries);
4201      }
4202  
4203      /**
4204       * Override, providing utf8-decoding for ascii-encoded IDN strings.
4205       *
4206       * @return mixed returns punycode-converted setting string if successful, else null.
4207       */
4208      public function get_setting() {
4209          // Here, we need to decode any ascii-encoded IDNs back to their native, utf-8 representation.
4210          $data = $this->config_read($this->name);
4211          if (function_exists('idn_to_utf8') && !is_null($data)) {
4212              $data = $this->ace_decode($data);
4213          }
4214          return $data;
4215      }
4216  
4217      /**
4218       * Override, providing ascii-encoding for utf8 (native) IDN strings.
4219       *
4220       * @param string $data
4221       * @return string
4222       */
4223      public function write_setting($data) {
4224          if ($this->paramtype === PARAM_INT and $data === '') {
4225              // Do not complain if '' used instead of 0.
4226              $data = 0;
4227          }
4228  
4229          // Try to convert any non-ascii domains to ACE prior to validation - we can't modify anything in validate!
4230          if (function_exists('idn_to_ascii')) {
4231              $data = $this->ace_encode($data);
4232          }
4233  
4234          $validated = $this->validate($data);
4235          if ($validated !== true) {
4236              return $validated;
4237          }
4238          return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
4239      }
4240  }
4241  
4242  /**
4243   * Used to validate a textarea used for port numbers.
4244   *
4245   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4246   * @copyright 2016 Jake Dallimore (jrhdallimore@gmail.com)
4247   */
4248  class admin_setting_configportlist extends admin_setting_configtextarea {
4249  
4250      /**
4251       * Validate the contents of the textarea as port numbers.
4252       * Used to validate a new line separated list of ports collected from a textarea control.
4253       *
4254       * @param string $data A list of ports separated by new lines
4255       * @return mixed bool true for success or string:error on failure
4256       */
4257      public function validate($data) {
4258          if (empty($data)) {
4259              return true;
4260          }
4261          $ports = explode("\n", $data);
4262          $badentries = [];
4263          foreach ($ports as $port) {
4264              $port = trim($port);
4265              if (empty($port)) {
4266                  return get_string('validateemptylineerror', 'admin');
4267              }
4268  
4269              // Is the string a valid integer number?
4270              if (strval(intval($port)) !== $port || intval($port) <= 0) {
4271                  $badentries[] = $port;
4272              }
4273          }
4274          if ($badentries) {
4275              return get_string('validateerrorlist', 'admin', $badentries);
4276          }
4277          return true;
4278      }
4279  }
4280  
4281  
4282  /**
4283   * An admin setting for selecting one or more users who have a capability
4284   * in the system context
4285   *
4286   * An admin setting for selecting one or more users, who have a particular capability
4287   * in the system context. Warning, make sure the list will never be too long. There is
4288   * no paging or searching of this list.
4289   *
4290   * To correctly get a list of users from this config setting, you need to call the
4291   * get_users_from_config($CFG->mysetting, $capability); function in moodlelib.php.
4292   *
4293   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4294   */
4295  class admin_setting_users_with_capability extends admin_setting_configmultiselect {
4296      /** @var string The capabilities name */
4297      protected $capability;
4298      /** @var int include admin users too */
4299      protected $includeadmins;
4300  
4301      /**
4302       * Constructor.
4303       *
4304       * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
4305       * @param string $visiblename localised name
4306       * @param string $description localised long description
4307       * @param array $defaultsetting array of usernames
4308       * @param string $capability string capability name.
4309       * @param bool $includeadmins include administrators
4310       */
4311      function __construct($name, $visiblename, $description, $defaultsetting, $capability, $includeadmins = true) {
4312          $this->capability    = $capability;
4313          $this->includeadmins = $includeadmins;
4314          parent::__construct($name, $visiblename, $description, $defaultsetting, NULL);
4315      }
4316  
4317      /**
4318       * Load all of the uses who have the capability into choice array
4319       *
4320       * @return bool Always returns true
4321       */
4322      function load_choices() {
4323          if (is_array($this->choices)) {
4324              return true;
4325          }
4326          list($sort, $sortparams) = users_order_by_sql('u');
4327          if (!empty($sortparams)) {
4328              throw new coding_exception('users_order_by_sql returned some query parameters. ' .
4329                      'This is unexpected, and a problem because there is no way to pass these ' .
4330                      'parameters to get_users_by_capability. See MDL-34657.');
4331          }
4332          $userfieldsapi = \core_user\fields::for_name();
4333          $userfields = 'u.id, u.username, ' . $userfieldsapi->get_sql('u', false, '', '', false)->selects;
4334          $users = get_users_by_capability(context_system::instance(), $this->capability, $userfields, $sort);
4335          $this->choices = array(
4336              '$@NONE@$' => get_string('nobody'),
4337              '$@ALL@$' => get_string('everyonewhocan', 'admin', get_capability_string($this->capability)),
4338          );
4339          if ($this->includeadmins) {
4340              $admins = get_admins();
4341              foreach ($admins as $user) {
4342                  $this->choices[$user->id] = fullname($user);
4343              }
4344          }
4345          if (is_array($users)) {
4346              foreach ($users as $user) {
4347                  $this->choices[$user->id] = fullname($user);
4348              }
4349          }
4350          return true;
4351      }
4352  
4353      /**
4354       * Returns the default setting for class
4355       *
4356       * @return mixed Array, or string. Empty string if no default
4357       */
4358      public function get_defaultsetting() {
4359          $this->load_choices();
4360          $defaultsetting = parent::get_defaultsetting();
4361          if (empty($defaultsetting)) {
4362              return array('$@NONE@$');
4363          } else if (array_key_exists($defaultsetting, $this->choices)) {
4364                  return $defaultsetting;
4365              } else {
4366                  return '';
4367              }
4368      }
4369  
4370      /**
4371       * Returns the current setting
4372       *
4373       * @return mixed array or string
4374       */
4375      public function get_setting() {
4376          $result = parent::get_setting();
4377          if ($result === null) {
4378              // this is necessary for settings upgrade
4379              return null;
4380          }
4381          if (empty($result)) {
4382              $result = array('$@NONE@$');
4383          }
4384          return $result;
4385      }
4386  
4387      /**
4388       * Save the chosen setting provided as $data
4389       *
4390       * @param array $data
4391       * @return mixed string or array
4392       */
4393      public function write_setting($data) {
4394      // If all is selected, remove any explicit options.
4395          if (in_array('$@ALL@$', $data)) {
4396              $data = array('$@ALL@$');
4397          }
4398          // None never needs to be written to the DB.
4399          if (in_array('$@NONE@$', $data)) {
4400              unset($data[array_search('$@NONE@$', $data)]);
4401          }
4402          return parent::write_setting($data);
4403      }
4404  }
4405  
4406  
4407  /**
4408   * Special checkbox for calendar - resets SESSION vars.
4409   *
4410   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4411   */
4412  class admin_setting_special_adminseesall extends admin_setting_configcheckbox {
4413      /**
4414       * Calls the parent::__construct with default values
4415       *
4416       * name =>  calendar_adminseesall
4417       * visiblename => get_string('adminseesall', 'admin')
4418       * description => get_string('helpadminseesall', 'admin')
4419       * defaultsetting => 0
4420       */
4421      public function __construct() {
4422          parent::__construct('calendar_adminseesall', get_string('adminseesall', 'admin'),
4423              get_string('helpadminseesall', 'admin'), '0');
4424      }
4425  
4426      /**
4427       * Stores the setting passed in $data
4428       *
4429       * @param mixed gets converted to string for comparison
4430       * @return string empty string or error message
4431       */
4432      public function write_setting($data) {
4433          global $SESSION;
4434          return parent::write_setting($data);
4435      }
4436  }
4437  
4438  /**
4439   * Special select for settings that are altered in setup.php and can not be altered on the fly
4440   *
4441   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4442   */
4443  class admin_setting_special_selectsetup extends admin_setting_configselect {
4444      /**
4445       * Reads the setting directly from the database
4446       *
4447       * @return mixed
4448       */
4449      public function get_setting() {
4450      // read directly from db!
4451          return get_config(NULL, $this->name);
4452      }
4453  
4454      /**
4455       * Save the setting passed in $data
4456       *
4457       * @param string $data The setting to save
4458       * @return string empty or error message
4459       */
4460      public function write_setting($data) {
4461          global $CFG;
4462          // do not change active CFG setting!
4463          $current = $CFG->{$this->name};
4464          $result = parent::write_setting($data);
4465          $CFG->{$this->name} = $current;
4466          return $result;
4467      }
4468  }
4469  
4470  
4471  /**
4472   * Special select for frontpage - stores data in course table
4473   *
4474   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4475   */
4476  class admin_setting_sitesetselect extends admin_setting_configselect {
4477      /**
4478       * Returns the site name for the selected site
4479       *
4480       * @see get_site()
4481       * @return string The site name of the selected site
4482       */
4483      public function get_setting() {
4484          $site = course_get_format(get_site())->get_course();
4485          return $site->{$this->name};
4486      }
4487  
4488      /**
4489       * Updates the database and save the setting
4490       *
4491       * @param string data
4492       * @return string empty or error message
4493       */
4494      public function write_setting($data) {
4495          global $DB, $SITE, $COURSE;
4496          if (!in_array($data, array_keys($this->choices))) {
4497              return get_string('errorsetting', 'admin');
4498          }
4499          $record = new stdClass();
4500          $record->id           = SITEID;
4501          $temp                 = $this->name;
4502          $record->$temp        = $data;
4503          $record->timemodified = time();
4504  
4505          course_get_format($SITE)->update_course_format_options($record);
4506          $DB->update_record('course', $record);
4507  
4508          // Reset caches.
4509          $SITE = $DB->get_record('course', array('id'=>$SITE->id), '*', MUST_EXIST);
4510          if ($SITE->id == $COURSE->id) {
4511              $COURSE = $SITE;
4512          }
4513          format_base::reset_course_cache($SITE->id);
4514  
4515          return '';
4516  
4517      }
4518  }
4519  
4520  
4521  /**
4522   * Select for blog's bloglevel setting: if set to 0, will set blog_menu
4523   * block to hidden.
4524   *
4525   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4526   */
4527  class admin_setting_bloglevel extends admin_setting_configselect {
4528      /**
4529       * Updates the database and save the setting
4530       *
4531       * @param string data
4532       * @return string empty or error message
4533       */
4534      public function write_setting($data) {
4535          global $DB, $CFG;
4536          if ($data == 0) {
4537              $blogblocks = $DB->get_records_select('block', "name LIKE 'blog_%' AND visible = 1");
4538              foreach ($blogblocks as $block) {
4539                  $DB->set_field('block', 'visible', 0, array('id' => $block->id));
4540              }
4541          } else {
4542              // reenable all blocks only when switching from disabled blogs
4543              if (isset($CFG->bloglevel) and $CFG->bloglevel == 0) {
4544                  $blogblocks = $DB->get_records_select('block', "name LIKE 'blog_%' AND visible = 0");
4545                  foreach ($blogblocks as $block) {
4546                      $DB->set_field('block', 'visible', 1, array('id' => $block->id));
4547                  }
4548              }
4549          }
4550          return parent::write_setting($data);
4551      }
4552  }
4553  
4554  
4555  /**
4556   * Special select - lists on the frontpage - hacky
4557   *
4558   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4559   */
4560  class admin_setting_courselist_frontpage extends admin_setting {
4561      /** @var array Array of choices value=>label */
4562      public $choices;
4563  
4564      /**
4565       * Construct override, requires one param
4566       *
4567       * @param bool $loggedin Is the user logged in
4568       */
4569      public function __construct($loggedin) {
4570          global $CFG;
4571          require_once($CFG->dirroot.'/course/lib.php');
4572          $name        = 'frontpage'.($loggedin ? 'loggedin' : '');
4573          $visiblename = get_string('frontpage'.($loggedin ? 'loggedin' : ''),'admin');
4574          $description =