Search moodle.org's
Developer Documentation

  • 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 37 and 311] [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'.(