6764caf9af25adb44629198ff6abc8572f19a68c
[moodle.git] / lib / adminlib.php
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/>.
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  */
105 defined('MOODLE_INTERNAL') || die();
107 /// Add libraries
108 require_once($CFG->libdir.'/ddllib.php');
109 require_once($CFG->libdir.'/xmlize.php');
110 require_once($CFG->libdir.'/messagelib.php');
112 define('INSECURE_DATAROOT_WARNING', 1);
113 define('INSECURE_DATAROOT_ERROR', 2);
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;
128     // This may take a long time.
129     core_php_time_limit::raise();
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);
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         }
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     }
157     $component = $type . '_' . $name;  // eg. 'qtype_multichoice' or 'workshopgrading_accumulative' or 'mod_forum'
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         }
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     }
176     echo $OUTPUT->heading($pluginname);
178     // Delete all tag areas, collections and instances associated with this plugin.
179     core_tag_area::uninstall($component);
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     }
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;
201     // perform clean-up task common for all the plugin/subplugin types
203     //delete the web service functions and pre-built services
204     require_once($CFG->dirroot.'/lib/externallib.php');
205     external_delete_descriptions($component);
207     // delete calendar events
208     $DB->delete_records('event', array('modulename' => $pluginname));
209     $DB->delete_records('event', ['component' => $component]);
211     // Delete scheduled tasks.
212     $DB->delete_records('task_scheduled', array('component' => $component));
214     // Delete Inbound Message datakeys.
215     $DB->delete_records_select('messageinbound_datakeys',
216             'handler IN (SELECT id FROM {messageinbound_handlers} WHERE component = ?)', array($component));
218     // Delete Inbound Message handlers.
219     $DB->delete_records('messageinbound_handlers', array('component' => $component));
221     // delete all the logs
222     $DB->delete_records('log', array('module' => $pluginname));
224     // delete log_display information
225     $DB->delete_records('log_display', array('component' => $component));
227     // delete the module configuration records
228     unset_all_config_for_plugin($component);
229     if ($type === 'mod') {
230         unset_all_config_for_plugin($pluginname);
231     }
233     // delete message provider
234     message_provider_uninstall($component);
236     // delete the plugin tables
237     $xmldbfilepath = $plugindirectory . '/db/install.xml';
238     drop_plugin_tables($component, $xmldbfilepath, false);
239     if ($type === 'mod' or $type === 'block') {
240         // non-frankenstyle table prefixes
241         drop_plugin_tables($name, $xmldbfilepath, false);
242     }
244     // delete the capabilities that were defined by this module
245     capabilities_cleanup($component);
247     // Delete all remaining files in the filepool owned by the component.
248     $fs = get_file_storage();
249     $fs->delete_component_files($component);
251     // Finally purge all caches.
252     purge_all_caches();
254     // Invalidate the hash used for upgrade detections.
255     set_config('allversionshash', '');
257     echo $OUTPUT->notification(get_string('success'), 'notifysuccess');
260 /**
261  * Returns the version of installed component
262  *
263  * @param string $component component name
264  * @param string $source either 'disk' or 'installed' - where to get the version information from
265  * @return string|bool version number or false if the component is not found
266  */
267 function get_component_version($component, $source='installed') {
268     global $CFG, $DB;
270     list($type, $name) = core_component::normalize_component($component);
272     // moodle core or a core subsystem
273     if ($type === 'core') {
274         if ($source === 'installed') {
275             if (empty($CFG->version)) {
276                 return false;
277             } else {
278                 return $CFG->version;
279             }
280         } else {
281             if (!is_readable($CFG->dirroot.'/version.php')) {
282                 return false;
283             } else {
284                 $version = null; //initialize variable for IDEs
285                 include($CFG->dirroot.'/version.php');
286                 return $version;
287             }
288         }
289     }
291     // activity module
292     if ($type === 'mod') {
293         if ($source === 'installed') {
294             if ($CFG->version < 2013092001.02) {
295                 return $DB->get_field('modules', 'version', array('name'=>$name));
296             } else {
297                 return get_config('mod_'.$name, 'version');
298             }
300         } else {
301             $mods = core_component::get_plugin_list('mod');
302             if (empty($mods[$name]) or !is_readable($mods[$name].'/version.php')) {
303                 return false;
304             } else {
305                 $plugin = new stdClass();
306                 $plugin->version = null;
307                 $module = $plugin;
308                 include($mods[$name].'/version.php');
309                 return $plugin->version;
310             }
311         }
312     }
314     // block
315     if ($type === 'block') {
316         if ($source === 'installed') {
317             if ($CFG->version < 2013092001.02) {
318                 return $DB->get_field('block', 'version', array('name'=>$name));
319             } else {
320                 return get_config('block_'.$name, 'version');
321             }
322         } else {
323             $blocks = core_component::get_plugin_list('block');
324             if (empty($blocks[$name]) or !is_readable($blocks[$name].'/version.php')) {
325                 return false;
326             } else {
327                 $plugin = new stdclass();
328                 include($blocks[$name].'/version.php');
329                 return $plugin->version;
330             }
331         }
332     }
334     // all other plugin types
335     if ($source === 'installed') {
336         return get_config($type.'_'.$name, 'version');
337     } else {
338         $plugins = core_component::get_plugin_list($type);
339         if (empty($plugins[$name])) {
340             return false;
341         } else {
342             $plugin = new stdclass();
343             include($plugins[$name].'/version.php');
344             return $plugin->version;
345         }
346     }
349 /**
350  * Delete all plugin tables
351  *
352  * @param string $name Name of plugin, used as table prefix
353  * @param string $file Path to install.xml file
354  * @param bool $feedback defaults to true
355  * @return bool Always returns true
356  */
357 function drop_plugin_tables($name, $file, $feedback=true) {
358     global $CFG, $DB;
360     // first try normal delete
361     if (file_exists($file) and $DB->get_manager()->delete_tables_from_xmldb_file($file)) {
362         return true;
363     }
365     // then try to find all tables that start with name and are not in any xml file
366     $used_tables = get_used_table_names();
368     $tables = $DB->get_tables();
370     /// Iterate over, fixing id fields as necessary
371     foreach ($tables as $table) {
372         if (in_array($table, $used_tables)) {
373             continue;
374         }
376         if (strpos($table, $name) !== 0) {
377             continue;
378         }
380         // found orphan table --> delete it
381         if ($DB->get_manager()->table_exists($table)) {
382             $xmldb_table = new xmldb_table($table);
383             $DB->get_manager()->drop_table($xmldb_table);
384         }
385     }
387     return true;
390 /**
391  * Returns names of all known tables == tables that moodle knows about.
392  *
393  * @return array Array of lowercase table names
394  */
395 function get_used_table_names() {
396     $table_names = array();
397     $dbdirs = get_db_directories();
399     foreach ($dbdirs as $dbdir) {
400         $file = $dbdir.'/install.xml';
402         $xmldb_file = new xmldb_file($file);
404         if (!$xmldb_file->fileExists()) {
405             continue;
406         }
408         $loaded    = $xmldb_file->loadXMLStructure();
409         $structure = $xmldb_file->getStructure();
411         if ($loaded and $tables = $structure->getTables()) {
412             foreach($tables as $table) {
413                 $table_names[] = strtolower($table->getName());
414             }
415         }
416     }
418     return $table_names;
421 /**
422  * Returns list of all directories where we expect install.xml files
423  * @return array Array of paths
424  */
425 function get_db_directories() {
426     global $CFG;
428     $dbdirs = array();
430     /// First, the main one (lib/db)
431     $dbdirs[] = $CFG->libdir.'/db';
433     /// Then, all the ones defined by core_component::get_plugin_types()
434     $plugintypes = core_component::get_plugin_types();
435     foreach ($plugintypes as $plugintype => $pluginbasedir) {
436         if ($plugins = core_component::get_plugin_list($plugintype)) {
437             foreach ($plugins as $plugin => $plugindir) {
438                 $dbdirs[] = $plugindir.'/db';
439             }
440         }
441     }
443     return $dbdirs;
446 /**
447  * Try to obtain or release the cron lock.
448  * @param string  $name  name of lock
449  * @param int  $until timestamp when this lock considered stale, null means remove lock unconditionally
450  * @param bool $ignorecurrent ignore current lock state, usually extend previous lock, defaults to false
451  * @return bool true if lock obtained
452  */
453 function set_cron_lock($name, $until, $ignorecurrent=false) {
454     global $DB;
455     if (empty($name)) {
456         debugging("Tried to get a cron lock for a null fieldname");
457         return false;
458     }
460     // remove lock by force == remove from config table
461     if (is_null($until)) {
462         set_config($name, null);
463         return true;
464     }
466     if (!$ignorecurrent) {
467         // read value from db - other processes might have changed it
468         $value = $DB->get_field('config', 'value', array('name'=>$name));
470         if ($value and $value > time()) {
471             //lock active
472             return false;
473         }
474     }
476     set_config($name, $until);
477     return true;
480 /**
481  * Test if and critical warnings are present
482  * @return bool
483  */
484 function admin_critical_warnings_present() {
485     global $SESSION;
487     if (!has_capability('moodle/site:config', context_system::instance())) {
488         return 0;
489     }
491     if (!isset($SESSION->admin_critical_warning)) {
492         $SESSION->admin_critical_warning = 0;
493         if (is_dataroot_insecure(true) === INSECURE_DATAROOT_ERROR) {
494             $SESSION->admin_critical_warning = 1;
495         }
496     }
498     return $SESSION->admin_critical_warning;
501 /**
502  * Detects if float supports at least 10 decimal digits
503  *
504  * Detects if float supports at least 10 decimal digits
505  * and also if float-->string conversion works as expected.
506  *
507  * @return bool true if problem found
508  */
509 function is_float_problem() {
510     $num1 = 2009010200.01;
511     $num2 = 2009010200.02;
513     return ((string)$num1 === (string)$num2 or $num1 === $num2 or $num2 <= (string)$num1);
516 /**
517  * Try to verify that dataroot is not accessible from web.
518  *
519  * Try to verify that dataroot is not accessible from web.
520  * It is not 100% correct but might help to reduce number of vulnerable sites.
521  * Protection from httpd.conf and .htaccess is not detected properly.
522  *
523  * @uses INSECURE_DATAROOT_WARNING
524  * @uses INSECURE_DATAROOT_ERROR
525  * @param bool $fetchtest try to test public access by fetching file, default false
526  * @return mixed empty means secure, INSECURE_DATAROOT_ERROR found a critical problem, INSECURE_DATAROOT_WARNING might be problematic
527  */
528 function is_dataroot_insecure($fetchtest=false) {
529     global $CFG;
531     $siteroot = str_replace('\\', '/', strrev($CFG->dirroot.'/')); // win32 backslash workaround
533     $rp = preg_replace('|https?://[^/]+|i', '', $CFG->wwwroot, 1);
534     $rp = strrev(trim($rp, '/'));
535     $rp = explode('/', $rp);
536     foreach($rp as $r) {
537         if (strpos($siteroot, '/'.$r.'/') === 0) {
538             $siteroot = substr($siteroot, strlen($r)+1); // moodle web in subdirectory
539         } else {
540             break; // probably alias root
541         }
542     }
544     $siteroot = strrev($siteroot);
545     $dataroot = str_replace('\\', '/', $CFG->dataroot.'/');
547     if (strpos($dataroot, $siteroot) !== 0) {
548         return false;
549     }
551     if (!$fetchtest) {
552         return INSECURE_DATAROOT_WARNING;
553     }
555     // now try all methods to fetch a test file using http protocol
557     $httpdocroot = str_replace('\\', '/', strrev($CFG->dirroot.'/'));
558     preg_match('|(https?://[^/]+)|i', $CFG->wwwroot, $matches);
559     $httpdocroot = $matches[1];
560     $datarooturl = $httpdocroot.'/'. substr($dataroot, strlen($siteroot));
561     make_upload_directory('diag');
562     $testfile = $CFG->dataroot.'/diag/public.txt';
563     if (!file_exists($testfile)) {
564         file_put_contents($testfile, 'test file, do not delete');
565         @chmod($testfile, $CFG->filepermissions);
566     }
567     $teststr = trim(file_get_contents($testfile));
568     if (empty($teststr)) {
569     // hmm, strange
570         return INSECURE_DATAROOT_WARNING;
571     }
573     $testurl = $datarooturl.'/diag/public.txt';
574     if (extension_loaded('curl') and
575         !(stripos(ini_get('disable_functions'), 'curl_init') !== FALSE) and
576         !(stripos(ini_get('disable_functions'), 'curl_setop') !== FALSE) and
577         ($ch = @curl_init($testurl)) !== false) {
578         curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
579         curl_setopt($ch, CURLOPT_HEADER, false);
580         $data = curl_exec($ch);
581         if (!curl_errno($ch)) {
582             $data = trim($data);
583             if ($data === $teststr) {
584                 curl_close($ch);
585                 return INSECURE_DATAROOT_ERROR;
586             }
587         }
588         curl_close($ch);
589     }
591     if ($data = @file_get_contents($testurl)) {
592         $data = trim($data);
593         if ($data === $teststr) {
594             return INSECURE_DATAROOT_ERROR;
595         }
596     }
598     preg_match('|https?://([^/]+)|i', $testurl, $matches);
599     $sitename = $matches[1];
600     $error = 0;
601     if ($fp = @fsockopen($sitename, 80, $error)) {
602         preg_match('|https?://[^/]+(.*)|i', $testurl, $matches);
603         $localurl = $matches[1];
604         $out = "GET $localurl HTTP/1.1\r\n";
605         $out .= "Host: $sitename\r\n";
606         $out .= "Connection: Close\r\n\r\n";
607         fwrite($fp, $out);
608         $data = '';
609         $incoming = false;
610         while (!feof($fp)) {
611             if ($incoming) {
612                 $data .= fgets($fp, 1024);
613             } else if (@fgets($fp, 1024) === "\r\n") {
614                     $incoming = true;
615                 }
616         }
617         fclose($fp);
618         $data = trim($data);
619         if ($data === $teststr) {
620             return INSECURE_DATAROOT_ERROR;
621         }
622     }
624     return INSECURE_DATAROOT_WARNING;
627 /**
628  * Enables CLI maintenance mode by creating new dataroot/climaintenance.html file.
629  */
630 function enable_cli_maintenance_mode() {
631     global $CFG, $SITE;
633     if (file_exists("$CFG->dataroot/climaintenance.html")) {
634         unlink("$CFG->dataroot/climaintenance.html");
635     }
637     if (isset($CFG->maintenance_message) and !html_is_blank($CFG->maintenance_message)) {
638         $data = $CFG->maintenance_message;
639         $data = bootstrap_renderer::early_error_content($data, null, null, null);
640         $data = bootstrap_renderer::plain_page(get_string('sitemaintenance', 'admin'), $data);
642     } else if (file_exists("$CFG->dataroot/climaintenance.template.html")) {
643         $data = file_get_contents("$CFG->dataroot/climaintenance.template.html");
645     } else {
646         $data = get_string('sitemaintenance', 'admin');
647         $data = bootstrap_renderer::early_error_content($data, null, null, null);
648         $data = bootstrap_renderer::plain_page(get_string('sitemaintenancetitle', 'admin', $SITE->fullname), $data);
649     }
651     file_put_contents("$CFG->dataroot/climaintenance.html", $data);
652     chmod("$CFG->dataroot/climaintenance.html", $CFG->filepermissions);
655 /// CLASS DEFINITIONS /////////////////////////////////////////////////////////
658 /**
659  * Interface for anything appearing in the admin tree
660  *
661  * The interface that is implemented by anything that appears in the admin tree
662  * block. It forces inheriting classes to define a method for checking user permissions
663  * and methods for finding something in the admin tree.
664  *
665  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
666  */
667 interface part_of_admin_tree {
669 /**
670  * Finds a named part_of_admin_tree.
671  *
672  * Used to find a part_of_admin_tree. If a class only inherits part_of_admin_tree
673  * and not parentable_part_of_admin_tree, then this function should only check if
674  * $this->name matches $name. If it does, it should return a reference to $this,
675  * otherwise, it should return a reference to NULL.
676  *
677  * If a class inherits parentable_part_of_admin_tree, this method should be called
678  * recursively on all child objects (assuming, of course, the parent object's name
679  * doesn't match the search criterion).
680  *
681  * @param string $name The internal name of the part_of_admin_tree we're searching for.
682  * @return mixed An object reference or a NULL reference.
683  */
684     public function locate($name);
686     /**
687      * Removes named part_of_admin_tree.
688      *
689      * @param string $name The internal name of the part_of_admin_tree we want to remove.
690      * @return bool success.
691      */
692     public function prune($name);
694     /**
695      * Search using query
696      * @param string $query
697      * @return mixed array-object structure of found settings and pages
698      */
699     public function search($query);
701     /**
702      * Verifies current user's access to this part_of_admin_tree.
703      *
704      * Used to check if the current user has access to this part of the admin tree or
705      * not. If a class only inherits part_of_admin_tree and not parentable_part_of_admin_tree,
706      * then this method is usually just a call to has_capability() in the site context.
707      *
708      * If a class inherits parentable_part_of_admin_tree, this method should return the
709      * logical OR of the return of check_access() on all child objects.
710      *
711      * @return bool True if the user has access, false if she doesn't.
712      */
713     public function check_access();
715     /**
716      * Mostly useful for removing of some parts of the tree in admin tree block.
717      *
718      * @return True is hidden from normal list view
719      */
720     public function is_hidden();
722     /**
723      * Show we display Save button at the page bottom?
724      * @return bool
725      */
726     public function show_save();
730 /**
731  * Interface implemented by any part_of_admin_tree that has children.
732  *
733  * The interface implemented by any part_of_admin_tree that can be a parent
734  * to other part_of_admin_tree's. (For now, this only includes admin_category.) Apart
735  * from ensuring part_of_admin_tree compliancy, it also ensures inheriting methods
736  * include an add method for adding other part_of_admin_tree objects as children.
737  *
738  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
739  */
740 interface parentable_part_of_admin_tree extends part_of_admin_tree {
742 /**
743  * Adds a part_of_admin_tree object to the admin tree.
744  *
745  * Used to add a part_of_admin_tree object to this object or a child of this
746  * object. $something should only be added if $destinationname matches
747  * $this->name. If it doesn't, add should be called on child objects that are
748  * also parentable_part_of_admin_tree's.
749  *
750  * $something should be appended as the last child in the $destinationname. If the
751  * $beforesibling is specified, $something should be prepended to it. If the given
752  * sibling is not found, $something should be appended to the end of $destinationname
753  * and a developer debugging message should be displayed.
754  *
755  * @param string $destinationname The internal name of the new parent for $something.
756  * @param part_of_admin_tree $something The object to be added.
757  * @return bool True on success, false on failure.
758  */
759     public function add($destinationname, $something, $beforesibling = null);
764 /**
765  * The object used to represent folders (a.k.a. categories) in the admin tree block.
766  *
767  * Each admin_category object contains a number of part_of_admin_tree objects.
768  *
769  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
770  */
771 class admin_category implements parentable_part_of_admin_tree {
773     /** @var part_of_admin_tree[] An array of part_of_admin_tree objects that are this object's children */
774     protected $children;
775     /** @var string An internal name for this category. Must be unique amongst ALL part_of_admin_tree objects */
776     public $name;
777     /** @var string The displayed name for this category. Usually obtained through get_string() */
778     public $visiblename;
779     /** @var bool Should this category be hidden in admin tree block? */
780     public $hidden;
781     /** @var mixed Either a string or an array or strings */
782     public $path;
783     /** @var mixed Either a string or an array or strings */
784     public $visiblepath;
786     /** @var array fast lookup category cache, all categories of one tree point to one cache */
787     protected $category_cache;
789     /** @var bool If set to true children will be sorted when calling {@link admin_category::get_children()} */
790     protected $sort = false;
791     /** @var bool If set to true children will be sorted in ascending order. */
792     protected $sortasc = true;
793     /** @var bool If set to true sub categories and pages will be split and then sorted.. */
794     protected $sortsplit = true;
795     /** @var bool $sorted True if the children have been sorted and don't need resorting */
796     protected $sorted = false;
798     /**
799      * Constructor for an empty admin category
800      *
801      * @param string $name The internal name for this category. Must be unique amongst ALL part_of_admin_tree objects
802      * @param string $visiblename The displayed named for this category. Usually obtained through get_string()
803      * @param bool $hidden hide category in admin tree block, defaults to false
804      */
805     public function __construct($name, $visiblename, $hidden=false) {
806         $this->children    = array();
807         $this->name        = $name;
808         $this->visiblename = $visiblename;
809         $this->hidden      = $hidden;
810     }
812     /**
813      * Returns a reference to the part_of_admin_tree object with internal name $name.
814      *
815      * @param string $name The internal name of the object we want.
816      * @param bool $findpath initialize path and visiblepath arrays
817      * @return mixed A reference to the object with internal name $name if found, otherwise a reference to NULL.
818      *                  defaults to false
819      */
820     public function locate($name, $findpath=false) {
821         if (!isset($this->category_cache[$this->name])) {
822             // somebody much have purged the cache
823             $this->category_cache[$this->name] = $this;
824         }
826         if ($this->name == $name) {
827             if ($findpath) {
828                 $this->visiblepath[] = $this->visiblename;
829                 $this->path[]        = $this->name;
830             }
831             return $this;
832         }
834         // quick category lookup
835         if (!$findpath and isset($this->category_cache[$name])) {
836             return $this->category_cache[$name];
837         }
839         $return = NULL;
840         foreach($this->children as $childid=>$unused) {
841             if ($return = $this->children[$childid]->locate($name, $findpath)) {
842                 break;
843             }
844         }
846         if (!is_null($return) and $findpath) {
847             $return->visiblepath[] = $this->visiblename;
848             $return->path[]        = $this->name;
849         }
851         return $return;
852     }
854     /**
855      * Search using query
856      *
857      * @param string query
858      * @return mixed array-object structure of found settings and pages
859      */
860     public function search($query) {
861         $result = array();
862         foreach ($this->get_children() as $child) {
863             $subsearch = $child->search($query);
864             if (!is_array($subsearch)) {
865                 debugging('Incorrect search result from '.$child->name);
866                 continue;
867             }
868             $result = array_merge($result, $subsearch);
869         }
870         return $result;
871     }
873     /**
874      * Removes part_of_admin_tree object with internal name $name.
875      *
876      * @param string $name The internal name of the object we want to remove.
877      * @return bool success
878      */
879     public function prune($name) {
881         if ($this->name == $name) {
882             return false;  //can not remove itself
883         }
885         foreach($this->children as $precedence => $child) {
886             if ($child->name == $name) {
887                 // clear cache and delete self
888                 while($this->category_cache) {
889                     // delete the cache, but keep the original array address
890                     array_pop($this->category_cache);
891                 }
892                 unset($this->children[$precedence]);
893                 return true;
894             } else if ($this->children[$precedence]->prune($name)) {
895                 return true;
896             }
897         }
898         return false;
899     }
901     /**
902      * Adds a part_of_admin_tree to a child or grandchild (or great-grandchild, and so forth) of this object.
903      *
904      * By default the new part of the tree is appended as the last child of the parent. You
905      * can specify a sibling node that the new part should be prepended to. If the given
906      * sibling is not found, the part is appended to the end (as it would be by default) and
907      * a developer debugging message is displayed.
908      *
909      * @throws coding_exception if the $beforesibling is empty string or is not string at all.
910      * @param string $destinationame The internal name of the immediate parent that we want for $something.
911      * @param mixed $something A part_of_admin_tree or setting instance to be added.
912      * @param string $beforesibling The name of the parent's child the $something should be prepended to.
913      * @return bool True if successfully added, false if $something can not be added.
914      */
915     public function add($parentname, $something, $beforesibling = null) {
916         global $CFG;
918         $parent = $this->locate($parentname);
919         if (is_null($parent)) {
920             debugging('parent does not exist!');
921             return false;
922         }
924         if ($something instanceof part_of_admin_tree) {
925             if (!($parent instanceof parentable_part_of_admin_tree)) {
926                 debugging('error - parts of tree can be inserted only into parentable parts');
927                 return false;
928             }
929             if ($CFG->debugdeveloper && !is_null($this->locate($something->name))) {
930                 // The name of the node is already used, simply warn the developer that this should not happen.
931                 // It is intentional to check for the debug level before performing the check.
932                 debugging('Duplicate admin page name: ' . $something->name, DEBUG_DEVELOPER);
933             }
934             if (is_null($beforesibling)) {
935                 // Append $something as the parent's last child.
936                 $parent->children[] = $something;
937             } else {
938                 if (!is_string($beforesibling) or trim($beforesibling) === '') {
939                     throw new coding_exception('Unexpected value of the beforesibling parameter');
940                 }
941                 // Try to find the position of the sibling.
942                 $siblingposition = null;
943                 foreach ($parent->children as $childposition => $child) {
944                     if ($child->name === $beforesibling) {
945                         $siblingposition = $childposition;
946                         break;
947                     }
948                 }
949                 if (is_null($siblingposition)) {
950                     debugging('Sibling '.$beforesibling.' not found', DEBUG_DEVELOPER);
951                     $parent->children[] = $something;
952                 } else {
953                     $parent->children = array_merge(
954                         array_slice($parent->children, 0, $siblingposition),
955                         array($something),
956                         array_slice($parent->children, $siblingposition)
957                     );
958                 }
959             }
960             if ($something instanceof admin_category) {
961                 if (isset($this->category_cache[$something->name])) {
962                     debugging('Duplicate admin category name: '.$something->name);
963                 } else {
964                     $this->category_cache[$something->name] = $something;
965                     $something->category_cache =& $this->category_cache;
966                     foreach ($something->children as $child) {
967                         // just in case somebody already added subcategories
968                         if ($child instanceof admin_category) {
969                             if (isset($this->category_cache[$child->name])) {
970                                 debugging('Duplicate admin category name: '.$child->name);
971                             } else {
972                                 $this->category_cache[$child->name] = $child;
973                                 $child->category_cache =& $this->category_cache;
974                             }
975                         }
976                     }
977                 }
978             }
979             return true;
981         } else {
982             debugging('error - can not add this element');
983             return false;
984         }
986     }
988     /**
989      * Checks if the user has access to anything in this category.
990      *
991      * @return bool True if the user has access to at least one child in this category, false otherwise.
992      */
993     public function check_access() {
994         foreach ($this->children as $child) {
995             if ($child->check_access()) {
996                 return true;
997             }
998         }
999         return false;
1000     }
1002     /**
1003      * Is this category hidden in admin tree block?
1004      *
1005      * @return bool True if hidden
1006      */
1007     public function is_hidden() {
1008         return $this->hidden;
1009     }
1011     /**
1012      * Show we display Save button at the page bottom?
1013      * @return bool
1014      */
1015     public function show_save() {
1016         foreach ($this->children as $child) {
1017             if ($child->show_save()) {
1018                 return true;
1019             }
1020         }
1021         return false;
1022     }
1024     /**
1025      * Sets sorting on this category.
1026      *
1027      * Please note this function doesn't actually do the sorting.
1028      * It can be called anytime.
1029      * Sorting occurs when the user calls get_children.
1030      * Code using the children array directly won't see the sorted results.
1031      *
1032      * @param bool $sort If set to true children will be sorted, if false they won't be.
1033      * @param bool $asc If true sorting will be ascending, otherwise descending.
1034      * @param bool $split If true we sort pages and sub categories separately.
1035      */
1036     public function set_sorting($sort, $asc = true, $split = true) {
1037         $this->sort = (bool)$sort;
1038         $this->sortasc = (bool)$asc;
1039         $this->sortsplit = (bool)$split;
1040     }
1042     /**
1043      * Returns the children associated with this category.
1044      *
1045      * @return part_of_admin_tree[]
1046      */
1047     public function get_children() {
1048         // If we should sort and it hasn't already been sorted.
1049         if ($this->sort && !$this->sorted) {
1050             if ($this->sortsplit) {
1051                 $categories = array();
1052                 $pages = array();
1053                 foreach ($this->children as $child) {
1054                     if ($child instanceof admin_category) {
1055                         $categories[] = $child;
1056                     } else {
1057                         $pages[] = $child;
1058                     }
1059                 }
1060                 core_collator::asort_objects_by_property($categories, 'visiblename');
1061                 core_collator::asort_objects_by_property($pages, 'visiblename');
1062                 if (!$this->sortasc) {
1063                     $categories = array_reverse($categories);
1064                     $pages = array_reverse($pages);
1065                 }
1066                 $this->children = array_merge($pages, $categories);
1067             } else {
1068                 core_collator::asort_objects_by_property($this->children, 'visiblename');
1069                 if (!$this->sortasc) {
1070                     $this->children = array_reverse($this->children);
1071                 }
1072             }
1073             $this->sorted = true;
1074         }
1075         return $this->children;
1076     }
1078     /**
1079      * Magically gets a property from this object.
1080      *
1081      * @param $property
1082      * @return part_of_admin_tree[]
1083      * @throws coding_exception
1084      */
1085     public function __get($property) {
1086         if ($property === 'children') {
1087             return $this->get_children();
1088         }
1089         throw new coding_exception('Invalid property requested.');
1090     }
1092     /**
1093      * Magically sets a property against this object.
1094      *
1095      * @param string $property
1096      * @param mixed $value
1097      * @throws coding_exception
1098      */
1099     public function __set($property, $value) {
1100         if ($property === 'children') {
1101             $this->sorted = false;
1102             $this->children = $value;
1103         } else {
1104             throw new coding_exception('Invalid property requested.');
1105         }
1106     }
1108     /**
1109      * Checks if an inaccessible property is set.
1110      *
1111      * @param string $property
1112      * @return bool
1113      * @throws coding_exception
1114      */
1115     public function __isset($property) {
1116         if ($property === 'children') {
1117             return isset($this->children);
1118         }
1119         throw new coding_exception('Invalid property requested.');
1120     }
1124 /**
1125  * Root of admin settings tree, does not have any parent.
1126  *
1127  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1128  */
1129 class admin_root extends admin_category {
1130 /** @var array List of errors */
1131     public $errors;
1132     /** @var string search query */
1133     public $search;
1134     /** @var bool full tree flag - true means all settings required, false only pages required */
1135     public $fulltree;
1136     /** @var bool flag indicating loaded tree */
1137     public $loaded;
1138     /** @var mixed site custom defaults overriding defaults in settings files*/
1139     public $custom_defaults;
1141     /**
1142      * @param bool $fulltree true means all settings required,
1143      *                            false only pages required
1144      */
1145     public function __construct($fulltree) {
1146         global $CFG;
1148         parent::__construct('root', get_string('administration'), false);
1149         $this->errors   = array();
1150         $this->search   = '';
1151         $this->fulltree = $fulltree;
1152         $this->loaded   = false;
1154         $this->category_cache = array();
1156         // load custom defaults if found
1157         $this->custom_defaults = null;
1158         $defaultsfile = "$CFG->dirroot/local/defaults.php";
1159         if (is_readable($defaultsfile)) {
1160             $defaults = array();
1161             include($defaultsfile);
1162             if (is_array($defaults) and count($defaults)) {
1163                 $this->custom_defaults = $defaults;
1164             }
1165         }
1166     }
1168     /**
1169      * Empties children array, and sets loaded to false
1170      *
1171      * @param bool $requirefulltree
1172      */
1173     public function purge_children($requirefulltree) {
1174         $this->children = array();
1175         $this->fulltree = ($requirefulltree || $this->fulltree);
1176         $this->loaded   = false;
1177         //break circular dependencies - this helps PHP 5.2
1178         while($this->category_cache) {
1179             array_pop($this->category_cache);
1180         }
1181         $this->category_cache = array();
1182     }
1186 /**
1187  * Links external PHP pages into the admin tree.
1188  *
1189  * See detailed usage example at the top of this document (adminlib.php)
1190  *
1191  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1192  */
1193 class admin_externalpage implements part_of_admin_tree {
1195     /** @var string An internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects */
1196     public $name;
1198     /** @var string The displayed name for this external page. Usually obtained through get_string(). */
1199     public $visiblename;
1201     /** @var string The external URL that we should link to when someone requests this external page. */
1202     public $url;
1204     /** @var array The role capability/permission a user must have to access this external page. */
1205     public $req_capability;
1207     /** @var object The context in which capability/permission should be checked, default is site context. */
1208     public $context;
1210     /** @var bool hidden in admin tree block. */
1211     public $hidden;
1213     /** @var mixed either string or array of string */
1214     public $path;
1216     /** @var array list of visible names of page parents */
1217     public $visiblepath;
1219     /**
1220      * Constructor for adding an external page into the admin tree.
1221      *
1222      * @param string $name The internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects.
1223      * @param string $visiblename The displayed name for this external page. Usually obtained through get_string().
1224      * @param string $url The external URL that we should link to when someone requests this external page.
1225      * @param mixed $req_capability The role capability/permission a user must have to access this external page. Defaults to 'moodle/site:config'.
1226      * @param boolean $hidden Is this external page hidden in admin tree block? Default false.
1227      * @param stdClass $context The context the page relates to. Not sure what happens
1228      *      if you specify something other than system or front page. Defaults to system.
1229      */
1230     public function __construct($name, $visiblename, $url, $req_capability='moodle/site:config', $hidden=false, $context=NULL) {
1231         $this->name        = $name;
1232         $this->visiblename = $visiblename;
1233         $this->url         = $url;
1234         if (is_array($req_capability)) {
1235             $this->req_capability = $req_capability;
1236         } else {
1237             $this->req_capability = array($req_capability);
1238         }
1239         $this->hidden = $hidden;
1240         $this->context = $context;
1241     }
1243     /**
1244      * Returns a reference to the part_of_admin_tree object with internal name $name.
1245      *
1246      * @param string $name The internal name of the object we want.
1247      * @param bool $findpath defaults to false
1248      * @return mixed A reference to the object with internal name $name if found, otherwise a reference to NULL.
1249      */
1250     public function locate($name, $findpath=false) {
1251         if ($this->name == $name) {
1252             if ($findpath) {
1253                 $this->visiblepath = array($this->visiblename);
1254                 $this->path        = array($this->name);
1255             }
1256             return $this;
1257         } else {
1258             $return = NULL;
1259             return $return;
1260         }
1261     }
1263     /**
1264      * This function always returns false, required function by interface
1265      *
1266      * @param string $name
1267      * @return false
1268      */
1269     public function prune($name) {
1270         return false;
1271     }
1273     /**
1274      * Search using query
1275      *
1276      * @param string $query
1277      * @return mixed array-object structure of found settings and pages
1278      */
1279     public function search($query) {
1280         $found = false;
1281         if (strpos(strtolower($this->name), $query) !== false) {
1282             $found = true;
1283         } else if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
1284                 $found = true;
1285             }
1286         if ($found) {
1287             $result = new stdClass();
1288             $result->page     = $this;
1289             $result->settings = array();
1290             return array($this->name => $result);
1291         } else {
1292             return array();
1293         }
1294     }
1296     /**
1297      * Determines if the current user has access to this external page based on $this->req_capability.
1298      *
1299      * @return bool True if user has access, false otherwise.
1300      */
1301     public function check_access() {
1302         global $CFG;
1303         $context = empty($this->context) ? context_system::instance() : $this->context;
1304         foreach($this->req_capability as $cap) {
1305             if (has_capability($cap, $context)) {
1306                 return true;
1307             }
1308         }
1309         return false;
1310     }
1312     /**
1313      * Is this external page hidden in admin tree block?
1314      *
1315      * @return bool True if hidden
1316      */
1317     public function is_hidden() {
1318         return $this->hidden;
1319     }
1321     /**
1322      * Show we display Save button at the page bottom?
1323      * @return bool
1324      */
1325     public function show_save() {
1326         return false;
1327     }
1330 /**
1331  * Used to store details of the dependency between two settings elements.
1332  *
1333  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1334  * @copyright 2017 Davo Smith, Synergy Learning
1335  */
1336 class admin_settingdependency {
1337     /** @var string the name of the setting to be shown/hidden */
1338     public $settingname;
1339     /** @var string the setting this is dependent on */
1340     public $dependenton;
1341     /** @var string the condition to show/hide the element */
1342     public $condition;
1343     /** @var string the value to compare against */
1344     public $value;
1346     /** @var string[] list of valid conditions */
1347     private static $validconditions = ['checked', 'notchecked', 'noitemselected', 'eq', 'neq', 'in'];
1349     /**
1350      * admin_settingdependency constructor.
1351      * @param string $settingname
1352      * @param string $dependenton
1353      * @param string $condition
1354      * @param string $value
1355      * @throws \coding_exception
1356      */
1357     public function __construct($settingname, $dependenton, $condition, $value) {
1358         $this->settingname = $this->parse_name($settingname);
1359         $this->dependenton = $this->parse_name($dependenton);
1360         $this->condition = $condition;
1361         $this->value = $value;
1363         if (!in_array($this->condition, self::$validconditions)) {
1364             throw new coding_exception("Invalid condition '$condition'");
1365         }
1366     }
1368     /**
1369      * Convert the setting name into the form field name.
1370      * @param string $name
1371      * @return string
1372      */
1373     private function parse_name($name) {
1374         $bits = explode('/', $name);
1375         $name = array_pop($bits);
1376         $plugin = '';
1377         if ($bits) {
1378             $plugin = array_pop($bits);
1379             if ($plugin === 'moodle') {
1380                 $plugin = '';
1381             }
1382         }
1383         return 's_'.$plugin.'_'.$name;
1384     }
1386     /**
1387      * Gather together all the dependencies in a format suitable for initialising javascript
1388      * @param admin_settingdependency[] $dependencies
1389      * @return array
1390      */
1391     public static function prepare_for_javascript($dependencies) {
1392         $result = [];
1393         foreach ($dependencies as $d) {
1394             if (!isset($result[$d->dependenton])) {
1395                 $result[$d->dependenton] = [];
1396             }
1397             if (!isset($result[$d->dependenton][$d->condition])) {
1398                 $result[$d->dependenton][$d->condition] = [];
1399             }
1400             if (!isset($result[$d->dependenton][$d->condition][$d->value])) {
1401                 $result[$d->dependenton][$d->condition][$d->value] = [];
1402             }
1403             $result[$d->dependenton][$d->condition][$d->value][] = $d->settingname;
1404         }
1405         return $result;
1406     }
1409 /**
1410  * Used to group a number of admin_setting objects into a page and add them to the admin tree.
1411  *
1412  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1413  */
1414 class admin_settingpage implements part_of_admin_tree {
1416     /** @var string An internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects */
1417     public $name;
1419     /** @var string The displayed name for this external page. Usually obtained through get_string(). */
1420     public $visiblename;
1422     /** @var mixed An array of admin_setting objects that are part of this setting page. */
1423     public $settings;
1425     /** @var admin_settingdependency[] list of settings to hide when certain conditions are met */
1426     protected $dependencies = [];
1428     /** @var array The role capability/permission a user must have to access this external page. */
1429     public $req_capability;
1431     /** @var object The context in which capability/permission should be checked, default is site context. */
1432     public $context;
1434     /** @var bool hidden in admin tree block. */
1435     public $hidden;
1437     /** @var mixed string of paths or array of strings of paths */
1438     public $path;
1440     /** @var array list of visible names of page parents */
1441     public $visiblepath;
1443     /**
1444      * see admin_settingpage for details of this function
1445      *
1446      * @param string $name The internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects.
1447      * @param string $visiblename The displayed name for this external page. Usually obtained through get_string().
1448      * @param mixed $req_capability The role capability/permission a user must have to access this external page. Defaults to 'moodle/site:config'.
1449      * @param boolean $hidden Is this external page hidden in admin tree block? Default false.
1450      * @param stdClass $context The context the page relates to. Not sure what happens
1451      *      if you specify something other than system or front page. Defaults to system.
1452      */
1453     public function __construct($name, $visiblename, $req_capability='moodle/site:config', $hidden=false, $context=NULL) {
1454         $this->settings    = new stdClass();
1455         $this->name        = $name;
1456         $this->visiblename = $visiblename;
1457         if (is_array($req_capability)) {
1458             $this->req_capability = $req_capability;
1459         } else {
1460             $this->req_capability = array($req_capability);
1461         }
1462         $this->hidden      = $hidden;
1463         $this->context     = $context;
1464     }
1466     /**
1467      * see admin_category
1468      *
1469      * @param string $name
1470      * @param bool $findpath
1471      * @return mixed Object (this) if name ==  this->name, else returns null
1472      */
1473     public function locate($name, $findpath=false) {
1474         if ($this->name == $name) {
1475             if ($findpath) {
1476                 $this->visiblepath = array($this->visiblename);
1477                 $this->path        = array($this->name);
1478             }
1479             return $this;
1480         } else {
1481             $return = NULL;
1482             return $return;
1483         }
1484     }
1486     /**
1487      * Search string in settings page.
1488      *
1489      * @param string $query
1490      * @return array
1491      */
1492     public function search($query) {
1493         $found = array();
1495         foreach ($this->settings as $setting) {
1496             if ($setting->is_related($query)) {
1497                 $found[] = $setting;
1498             }
1499         }
1501         if ($found) {
1502             $result = new stdClass();
1503             $result->page     = $this;
1504             $result->settings = $found;
1505             return array($this->name => $result);
1506         }
1508         $found = false;
1509         if (strpos(strtolower($this->name), $query) !== false) {
1510             $found = true;
1511         } else if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
1512                 $found = true;
1513             }
1514         if ($found) {
1515             $result = new stdClass();
1516             $result->page     = $this;
1517             $result->settings = array();
1518             return array($this->name => $result);
1519         } else {
1520             return array();
1521         }
1522     }
1524     /**
1525      * This function always returns false, required by interface
1526      *
1527      * @param string $name
1528      * @return bool Always false
1529      */
1530     public function prune($name) {
1531         return false;
1532     }
1534     /**
1535      * adds an admin_setting to this admin_settingpage
1536      *
1537      * 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
1538      * n.b. each admin_setting in an admin_settingpage must have a unique internal name
1539      *
1540      * @param object $setting is the admin_setting object you want to add
1541      * @return bool true if successful, false if not
1542      */
1543     public function add($setting) {
1544         if (!($setting instanceof admin_setting)) {
1545             debugging('error - not a setting instance');
1546             return false;
1547         }
1549         $name = $setting->name;
1550         if ($setting->plugin) {
1551             $name = $setting->plugin . $name;
1552         }
1553         $this->settings->{$name} = $setting;
1554         return true;
1555     }
1557     /**
1558      * Hide the named setting if the specified condition is matched.
1559      *
1560      * @param string $settingname
1561      * @param string $dependenton
1562      * @param string $condition
1563      * @param string $value
1564      */
1565     public function hide_if($settingname, $dependenton, $condition = 'notchecked', $value = '1') {
1566         $this->dependencies[] = new admin_settingdependency($settingname, $dependenton, $condition, $value);
1568         // Reformat the dependency name to the plugin | name format used in the display.
1569         $dependenton = str_replace('/', ' | ', $dependenton);
1571         // Let the setting know, so it can be displayed underneath.
1572         $findname = str_replace('/', '', $settingname);
1573         foreach ($this->settings as $name => $setting) {
1574             if ($name === $findname) {
1575                 $setting->add_dependent_on($dependenton);
1576             }
1577         }
1578     }
1580     /**
1581      * see admin_externalpage
1582      *
1583      * @return bool Returns true for yes false for no
1584      */
1585     public function check_access() {
1586         global $CFG;
1587         $context = empty($this->context) ? context_system::instance() : $this->context;
1588         foreach($this->req_capability as $cap) {
1589             if (has_capability($cap, $context)) {
1590                 return true;
1591             }
1592         }
1593         return false;
1594     }
1596     /**
1597      * outputs this page as html in a table (suitable for inclusion in an admin pagetype)
1598      * @return string Returns an XHTML string
1599      */
1600     public function output_html() {
1601         $adminroot = admin_get_root();
1602         $return = '<fieldset>'."\n".'<div class="clearer"><!-- --></div>'."\n";
1603         foreach($this->settings as $setting) {
1604             $fullname = $setting->get_full_name();
1605             if (array_key_exists($fullname, $adminroot->errors)) {
1606                 $data = $adminroot->errors[$fullname]->data;
1607             } else {
1608                 $data = $setting->get_setting();
1609                 // do not use defaults if settings not available - upgrade settings handles the defaults!
1610             }
1611             $return .= $setting->output_html($data);
1612         }
1613         $return .= '</fieldset>';
1614         return $return;
1615     }
1617     /**
1618      * Is this settings page hidden in admin tree block?
1619      *
1620      * @return bool True if hidden
1621      */
1622     public function is_hidden() {
1623         return $this->hidden;
1624     }
1626     /**
1627      * Show we display Save button at the page bottom?
1628      * @return bool
1629      */
1630     public function show_save() {
1631         foreach($this->settings as $setting) {
1632             if (empty($setting->nosave)) {
1633                 return true;
1634             }
1635         }
1636         return false;
1637     }
1639     /**
1640      * Should any of the settings on this page be shown / hidden based on conditions?
1641      * @return bool
1642      */
1643     public function has_dependencies() {
1644         return (bool)$this->dependencies;
1645     }
1647     /**
1648      * Format the setting show/hide conditions ready to initialise the page javascript
1649      * @return array
1650      */
1651     public function get_dependencies_for_javascript() {
1652         if (!$this->has_dependencies()) {
1653             return [];
1654         }
1655         return admin_settingdependency::prepare_for_javascript($this->dependencies);
1656     }
1660 /**
1661  * Admin settings class. Only exists on setting pages.
1662  * Read & write happens at this level; no authentication.
1663  *
1664  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1665  */
1666 abstract class admin_setting {
1667     /** @var string unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins. */
1668     public $name;
1669     /** @var string localised name */
1670     public $visiblename;
1671     /** @var string localised long description in Markdown format */
1672     public $description;
1673     /** @var mixed Can be string or array of string */
1674     public $defaultsetting;
1675     /** @var string */
1676     public $updatedcallback;
1677     /** @var mixed can be String or Null.  Null means main config table */
1678     public $plugin; // null means main config table
1679     /** @var bool true indicates this setting does not actually save anything, just information */
1680     public $nosave = false;
1681     /** @var bool if set, indicates that a change to this setting requires rebuild course cache */
1682     public $affectsmodinfo = false;
1683     /** @var array of admin_setting_flag - These are extra checkboxes attached to a setting. */
1684     private $flags = array();
1685     /** @var bool Whether this field must be forced LTR. */
1686     private $forceltr = null;
1687     /** @var array list of other settings that may cause this setting to be hidden */
1688     private $dependenton = [];
1689     /** @var bool Whether this setting uses a custom form control */
1690     protected $customcontrol = false;
1692     /**
1693      * Constructor
1694      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
1695      *                     or 'myplugin/mysetting' for ones in config_plugins.
1696      * @param string $visiblename localised name
1697      * @param string $description localised long description
1698      * @param mixed $defaultsetting string or array depending on implementation
1699      */
1700     public function __construct($name, $visiblename, $description, $defaultsetting) {
1701         $this->parse_setting_name($name);
1702         $this->visiblename    = $visiblename;
1703         $this->description    = $description;
1704         $this->defaultsetting = $defaultsetting;
1705     }
1707     /**
1708      * Generic function to add a flag to this admin setting.
1709      *
1710      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1711      * @param bool $default - The default for the flag
1712      * @param string $shortname - The shortname for this flag. Used as a suffix for the setting name.
1713      * @param string $displayname - The display name for this flag. Used as a label next to the checkbox.
1714      */
1715     protected function set_flag_options($enabled, $default, $shortname, $displayname) {
1716         if (empty($this->flags[$shortname])) {
1717             $this->flags[$shortname] = new admin_setting_flag($enabled, $default, $shortname, $displayname);
1718         } else {
1719             $this->flags[$shortname]->set_options($enabled, $default);
1720         }
1721     }
1723     /**
1724      * Set the enabled options flag on 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      */
1729     public function set_enabled_flag_options($enabled, $default) {
1730         $this->set_flag_options($enabled, $default, 'enabled', new lang_string('enabled', 'core_admin'));
1731     }
1733     /**
1734      * Set the advanced options flag on this admin setting.
1735      *
1736      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1737      * @param bool $default - The default for the flag
1738      */
1739     public function set_advanced_flag_options($enabled, $default) {
1740         $this->set_flag_options($enabled, $default, 'adv', new lang_string('advanced'));
1741     }
1744     /**
1745      * Set the locked options flag on this admin setting.
1746      *
1747      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1748      * @param bool $default - The default for the flag
1749      */
1750     public function set_locked_flag_options($enabled, $default) {
1751         $this->set_flag_options($enabled, $default, 'locked', new lang_string('locked', 'core_admin'));
1752     }
1754     /**
1755      * Set the required options flag on this admin setting.
1756      *
1757      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED.
1758      * @param bool $default - The default for the flag.
1759      */
1760     public function set_required_flag_options($enabled, $default) {
1761         $this->set_flag_options($enabled, $default, 'required', new lang_string('required', 'core_admin'));
1762     }
1764     /**
1765      * Is this option forced in config.php?
1766      *
1767      * @return bool
1768      */
1769     public function is_readonly(): bool {
1770         global $CFG;
1772         if (empty($this->plugin)) {
1773             if (array_key_exists($this->name, $CFG->config_php_settings)) {
1774                 return true;
1775             }
1776         } else {
1777             if (array_key_exists($this->plugin, $CFG->forced_plugin_settings)
1778                 and array_key_exists($this->name, $CFG->forced_plugin_settings[$this->plugin])) {
1779                 return true;
1780             }
1781         }
1782         return false;
1783     }
1785     /**
1786      * Get the currently saved value for a setting flag
1787      *
1788      * @param admin_setting_flag $flag - One of the admin_setting_flag for this admin_setting.
1789      * @return bool
1790      */
1791     public function get_setting_flag_value(admin_setting_flag $flag) {
1792         $value = $this->config_read($this->name . '_' . $flag->get_shortname());
1793         if (!isset($value)) {
1794             $value = $flag->get_default();
1795         }
1797         return !empty($value);
1798     }
1800     /**
1801      * Get the list of defaults for the flags on this setting.
1802      *
1803      * @param array of strings describing the defaults for this setting. This is appended to by this function.
1804      */
1805     public function get_setting_flag_defaults(& $defaults) {
1806         foreach ($this->flags as $flag) {
1807             if ($flag->is_enabled() && $flag->get_default()) {
1808                 $defaults[] = $flag->get_displayname();
1809             }
1810         }
1811     }
1813     /**
1814      * Output the input fields for the advanced and locked flags on this setting.
1815      *
1816      * @param bool $adv - The current value of the advanced flag.
1817      * @param bool $locked - The current value of the locked flag.
1818      * @return string $output - The html for the flags.
1819      */
1820     public function output_setting_flags() {
1821         $output = '';
1823         foreach ($this->flags as $flag) {
1824             if ($flag->is_enabled()) {
1825                 $output .= $flag->output_setting_flag($this);
1826             }
1827         }
1829         if (!empty($output)) {
1830             return html_writer::tag('span', $output, array('class' => 'adminsettingsflags'));
1831         }
1832         return $output;
1833     }
1835     /**
1836      * Write the values of the flags for this admin setting.
1837      *
1838      * @param array $data - The data submitted from the form or null to set the default value for new installs.
1839      * @return bool - true if successful.
1840      */
1841     public function write_setting_flags($data) {
1842         $result = true;
1843         foreach ($this->flags as $flag) {
1844             $result = $result && $flag->write_setting_flag($this, $data);
1845         }
1846         return $result;
1847     }
1849     /**
1850      * Set up $this->name and potentially $this->plugin
1851      *
1852      * Set up $this->name and possibly $this->plugin based on whether $name looks
1853      * like 'settingname' or 'plugin/settingname'. Also, do some sanity checking
1854      * on the names, that is, output a developer debug warning if the name
1855      * contains anything other than [a-zA-Z0-9_]+.
1856      *
1857      * @param string $name the setting name passed in to the constructor.
1858      */
1859     private function parse_setting_name($name) {
1860         $bits = explode('/', $name);
1861         if (count($bits) > 2) {
1862             throw new moodle_exception('invalidadminsettingname', '', '', $name);
1863         }
1864         $this->name = array_pop($bits);
1865         if (!preg_match('/^[a-zA-Z0-9_]+$/', $this->name)) {
1866             throw new moodle_exception('invalidadminsettingname', '', '', $name);
1867         }
1868         if (!empty($bits)) {
1869             $this->plugin = array_pop($bits);
1870             if ($this->plugin === 'moodle') {
1871                 $this->plugin = null;
1872             } else if (!preg_match('/^[a-zA-Z0-9_]+$/', $this->plugin)) {
1873                     throw new moodle_exception('invalidadminsettingname', '', '', $name);
1874                 }
1875         }
1876     }
1878     /**
1879      * Returns the fullname prefixed by the plugin
1880      * @return string
1881      */
1882     public function get_full_name() {
1883         return 's_'.$this->plugin.'_'.$this->name;
1884     }
1886     /**
1887      * Returns the ID string based on plugin and name
1888      * @return string
1889      */
1890     public function get_id() {
1891         return 'id_s_'.$this->plugin.'_'.$this->name;
1892     }
1894     /**
1895      * @param bool $affectsmodinfo If true, changes to this setting will
1896      *   cause the course cache to be rebuilt
1897      */
1898     public function set_affects_modinfo($affectsmodinfo) {
1899         $this->affectsmodinfo = $affectsmodinfo;
1900     }
1902     /**
1903      * Returns the config if possible
1904      *
1905      * @return mixed returns config if successful else null
1906      */
1907     public function config_read($name) {
1908         global $CFG;
1909         if (!empty($this->plugin)) {
1910             $value = get_config($this->plugin, $name);
1911             return $value === false ? NULL : $value;
1913         } else {
1914             if (isset($CFG->$name)) {
1915                 return $CFG->$name;
1916             } else {
1917                 return NULL;
1918             }
1919         }
1920     }
1922     /**
1923      * Used to set a config pair and log change
1924      *
1925      * @param string $name
1926      * @param mixed $value Gets converted to string if not null
1927      * @return bool Write setting to config table
1928      */
1929     public function config_write($name, $value) {
1930         global $DB, $USER, $CFG;
1932         if ($this->nosave) {
1933             return true;
1934         }
1936         // make sure it is a real change
1937         $oldvalue = get_config($this->plugin, $name);
1938         $oldvalue = ($oldvalue === false) ? null : $oldvalue; // normalise
1939         $value = is_null($value) ? null : (string)$value;
1941         if ($oldvalue === $value) {
1942             return true;
1943         }
1945         // store change
1946         set_config($name, $value, $this->plugin);
1948         // Some admin settings affect course modinfo
1949         if ($this->affectsmodinfo) {
1950             // Clear course cache for all courses
1951             rebuild_course_cache(0, true);
1952         }
1954         $this->add_to_config_log($name, $oldvalue, $value);
1956         return true; // BC only
1957     }
1959     /**
1960      * Log config changes if necessary.
1961      * @param string $name
1962      * @param string $oldvalue
1963      * @param string $value
1964      */
1965     protected function add_to_config_log($name, $oldvalue, $value) {
1966         add_to_config_log($name, $oldvalue, $value, $this->plugin);
1967     }
1969     /**
1970      * Returns current value of this setting
1971      * @return mixed array or string depending on instance, NULL means not set yet
1972      */
1973     public abstract function get_setting();
1975     /**
1976      * Returns default setting if exists
1977      * @return mixed array or string depending on instance; NULL means no default, user must supply
1978      */
1979     public function get_defaultsetting() {
1980         $adminroot =  admin_get_root(false, false);
1981         if (!empty($adminroot->custom_defaults)) {
1982             $plugin = is_null($this->plugin) ? 'moodle' : $this->plugin;
1983             if (isset($adminroot->custom_defaults[$plugin])) {
1984                 if (array_key_exists($this->name, $adminroot->custom_defaults[$plugin])) { // null is valid value here ;-)
1985                     return $adminroot->custom_defaults[$plugin][$this->name];
1986                 }
1987             }
1988         }
1989         return $this->defaultsetting;
1990     }
1992     /**
1993      * Store new setting
1994      *
1995      * @param mixed $data string or array, must not be NULL
1996      * @return string empty string if ok, string error message otherwise
1997      */
1998     public abstract function write_setting($data);
2000     /**
2001      * Return part of form with setting
2002      * This function should always be overwritten
2003      *
2004      * @param mixed $data array or string depending on setting
2005      * @param string $query
2006      * @return string
2007      */
2008     public function output_html($data, $query='') {
2009     // should be overridden
2010         return;
2011     }
2013     /**
2014      * Function called if setting updated - cleanup, cache reset, etc.
2015      * @param string $functionname Sets the function name
2016      * @return void
2017      */
2018     public function set_updatedcallback($functionname) {
2019         $this->updatedcallback = $functionname;
2020     }
2022     /**
2023      * Execute postupdatecallback if necessary.
2024      * @param mixed $original original value before write_setting()
2025      * @return bool true if changed, false if not.
2026      */
2027     public function post_write_settings($original) {
2028         // Comparison must work for arrays too.
2029         if (serialize($original) === serialize($this->get_setting())) {
2030             return false;
2031         }
2033         $callbackfunction = $this->updatedcallback;
2034         if (!empty($callbackfunction) and is_callable($callbackfunction)) {
2035             $callbackfunction($this->get_full_name());
2036         }
2037         return true;
2038     }
2040     /**
2041      * Is setting related to query text - used when searching
2042      * @param string $query
2043      * @return bool
2044      */
2045     public function is_related($query) {
2046         if (strpos(strtolower($this->name), $query) !== false) {
2047             return true;
2048         }
2049         if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
2050             return true;
2051         }
2052         if (strpos(core_text::strtolower($this->description), $query) !== false) {
2053             return true;
2054         }
2055         $current = $this->get_setting();
2056         if (!is_null($current)) {
2057             if (is_string($current)) {
2058                 if (strpos(core_text::strtolower($current), $query) !== false) {
2059                     return true;
2060                 }
2061             }
2062         }
2063         $default = $this->get_defaultsetting();
2064         if (!is_null($default)) {
2065             if (is_string($default)) {
2066                 if (strpos(core_text::strtolower($default), $query) !== false) {
2067                     return true;
2068                 }
2069             }
2070         }
2071         return false;
2072     }
2074     /**
2075      * Get whether this should be displayed in LTR mode.
2076      *
2077      * @return bool|null
2078      */
2079     public function get_force_ltr() {
2080         return $this->forceltr;
2081     }
2083     /**
2084      * Set whether to force LTR or not.
2085      *
2086      * @param bool $value True when forced, false when not force, null when unknown.
2087      */
2088     public function set_force_ltr($value) {
2089         $this->forceltr = $value;
2090     }
2092     /**
2093      * Add a setting to the list of those that could cause this one to be hidden
2094      * @param string $dependenton
2095      */
2096     public function add_dependent_on($dependenton) {
2097         $this->dependenton[] = $dependenton;
2098     }
2100     /**
2101      * Get a list of the settings that could cause this one to be hidden.
2102      * @return array
2103      */
2104     public function get_dependent_on() {
2105         return $this->dependenton;
2106     }
2108     /**
2109      * Whether this setting uses a custom form control.
2110      * This function is especially useful to decide if we should render a label element for this setting or not.
2111      *
2112      * @return bool
2113      */
2114     public function has_custom_form_control(): bool {
2115         return $this->customcontrol;
2116     }
2119 /**
2120  * An additional option that can be applied to an admin setting.
2121  * The currently supported options are 'ADVANCED', 'LOCKED' and 'REQUIRED'.
2122  *
2123  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2124  */
2125 class admin_setting_flag {
2126     /** @var bool Flag to indicate if this option can be toggled for this setting */
2127     private $enabled = false;
2128     /** @var bool Flag to indicate if this option defaults to true or false */
2129     private $default = false;
2130     /** @var string Short string used to create setting name - e.g. 'adv' */
2131     private $shortname = '';
2132     /** @var string String used as the label for this flag */
2133     private $displayname = '';
2134     /** @const Checkbox for this flag is displayed in admin page */
2135     const ENABLED = true;
2136     /** @const Checkbox for this flag is not displayed in admin page */
2137     const DISABLED = false;
2139     /**
2140      * Constructor
2141      *
2142      * @param bool $enabled Can this option can be toggled.
2143      *                      Should be one of admin_setting_flag::ENABLED or admin_setting_flag::DISABLED.
2144      * @param bool $default The default checked state for this setting option.
2145      * @param string $shortname The shortname of this flag. Currently supported flags are 'locked' and 'adv'
2146      * @param string $displayname The displayname of this flag. Used as a label for the flag.
2147      */
2148     public function __construct($enabled, $default, $shortname, $displayname) {
2149         $this->shortname = $shortname;
2150         $this->displayname = $displayname;
2151         $this->set_options($enabled, $default);
2152     }
2154     /**
2155      * Update the values of this setting options class
2156      *
2157      * @param bool $enabled Can this option can be toggled.
2158      *                      Should be one of admin_setting_flag::ENABLED or admin_setting_flag::DISABLED.
2159      * @param bool $default The default checked state for this setting option.
2160      */
2161     public function set_options($enabled, $default) {
2162         $this->enabled = $enabled;
2163         $this->default = $default;
2164     }
2166     /**
2167      * Should this option appear in the interface and be toggleable?
2168      *
2169      * @return bool Is it enabled?
2170      */
2171     public function is_enabled() {
2172         return $this->enabled;
2173     }
2175     /**
2176      * Should this option be checked by default?
2177      *
2178      * @return bool Is it on by default?
2179      */
2180     public function get_default() {
2181         return $this->default;
2182     }
2184     /**
2185      * Return the short name for this flag. e.g. 'adv' or 'locked'
2186      *
2187      * @return string
2188      */
2189     public function get_shortname() {
2190         return $this->shortname;
2191     }
2193     /**
2194      * Return the display name for this flag. e.g. 'Advanced' or 'Locked'
2195      *
2196      * @return string
2197      */
2198     public function get_displayname() {
2199         return $this->displayname;
2200     }
2202     /**
2203      * Save the submitted data for this flag - or set it to the default if $data is null.
2204      *
2205      * @param admin_setting $setting - The admin setting for this flag
2206      * @param array $data - The data submitted from the form or null to set the default value for new installs.
2207      * @return bool
2208      */
2209     public function write_setting_flag(admin_setting $setting, $data) {
2210         $result = true;
2211         if ($this->is_enabled()) {
2212             if (!isset($data)) {
2213                 $value = $this->get_default();
2214             } else {
2215                 $value = !empty($data[$setting->get_full_name() . '_' . $this->get_shortname()]);
2216             }
2217             $result = $setting->config_write($setting->name . '_' . $this->get_shortname(), $value);
2218         }
2220         return $result;
2222     }
2224     /**
2225      * Output the checkbox for this setting flag. Should only be called if the flag is enabled.
2226      *
2227      * @param admin_setting $setting - The admin setting for this flag
2228      * @return string - The html for the checkbox.
2229      */
2230     public function output_setting_flag(admin_setting $setting) {
2231         global $OUTPUT;
2233         $value = $setting->get_setting_flag_value($this);
2235         $context = new stdClass();
2236         $context->id = $setting->get_id() . '_' . $this->get_shortname();
2237         $context->name = $setting->get_full_name() .  '_' . $this->get_shortname();
2238         $context->value = 1;
2239         $context->checked = $value ? true : false;
2240         $context->label = $this->get_displayname();
2242         return $OUTPUT->render_from_template('core_admin/setting_flag', $context);
2243     }
2247 /**
2248  * No setting - just heading and text.
2249  *
2250  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2251  */
2252 class admin_setting_heading extends admin_setting {
2254     /**
2255      * not a setting, just text
2256      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2257      * @param string $heading heading
2258      * @param string $information text in box
2259      */
2260     public function __construct($name, $heading, $information) {
2261         $this->nosave = true;
2262         parent::__construct($name, $heading, $information, '');
2263     }
2265     /**
2266      * Always returns true
2267      * @return bool Always returns true
2268      */
2269     public function get_setting() {
2270         return true;
2271     }
2273     /**
2274      * Always returns true
2275      * @return bool Always returns true
2276      */
2277     public function get_defaultsetting() {
2278         return true;
2279     }
2281     /**
2282      * Never write settings
2283      * @return string Always returns an empty string
2284      */
2285     public function write_setting($data) {
2286     // do not write any setting
2287         return '';
2288     }
2290     /**
2291      * Returns an HTML string
2292      * @return string Returns an HTML string
2293      */
2294     public function output_html($data, $query='') {
2295         global $OUTPUT;
2296         $context = new stdClass();
2297         $context->title = $this->visiblename;
2298         $context->description = $this->description;
2299         $context->descriptionformatted = highlight($query, markdown_to_html($this->description));
2300         return $OUTPUT->render_from_template('core_admin/setting_heading', $context);
2301     }
2304 /**
2305  * No setting - just name and description in same row.
2306  *
2307  * @copyright 2018 onwards Amaia Anabitarte
2308  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2309  */
2310 class admin_setting_description extends admin_setting {
2312     /**
2313      * Not a setting, just text
2314      *
2315      * @param string $name
2316      * @param string $visiblename
2317      * @param string $description
2318      */
2319     public function __construct($name, $visiblename, $description) {
2320         $this->nosave = true;
2321         parent::__construct($name, $visiblename, $description, '');
2322     }
2324     /**
2325      * Always returns true
2326      *
2327      * @return bool Always returns true
2328      */
2329     public function get_setting() {
2330         return true;
2331     }
2333     /**
2334      * Always returns true
2335      *
2336      * @return bool Always returns true
2337      */
2338     public function get_defaultsetting() {
2339         return true;
2340     }
2342     /**
2343      * Never write settings
2344      *
2345      * @param mixed $data Gets converted to str for comparison against yes value
2346      * @return string Always returns an empty string
2347      */
2348     public function write_setting($data) {
2349         // Do not write any setting.
2350         return '';
2351     }
2353     /**
2354      * Returns an HTML string
2355      *
2356      * @param string $data
2357      * @param string $query
2358      * @return string Returns an HTML string
2359      */
2360     public function output_html($data, $query='') {
2361         global $OUTPUT;
2363         $context = new stdClass();
2364         $context->title = $this->visiblename;
2365         $context->description = $this->description;
2367         return $OUTPUT->render_from_template('core_admin/setting_description', $context);
2368     }
2373 /**
2374  * The most flexible setting, the user enters text.
2375  *
2376  * This type of field should be used for config settings which are using
2377  * English words and are not localised (passwords, database name, list of values, ...).
2378  *
2379  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2380  */
2381 class admin_setting_configtext extends admin_setting {
2383     /** @var mixed int means PARAM_XXX type, string is a allowed format in regex */
2384     public $paramtype;
2385     /** @var int default field size */
2386     public $size;
2388     /**
2389      * Config text constructor
2390      *
2391      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2392      * @param string $visiblename localised
2393      * @param string $description long localised info
2394      * @param string $defaultsetting
2395      * @param mixed $paramtype int means PARAM_XXX type, string is a allowed format in regex
2396      * @param int $size default field size
2397      */
2398     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $size=null) {
2399         $this->paramtype = $paramtype;
2400         if (!is_null($size)) {
2401             $this->size  = $size;
2402         } else {
2403             $this->size  = ($paramtype === PARAM_INT) ? 5 : 30;
2404         }
2405         parent::__construct($name, $visiblename, $description, $defaultsetting);
2406     }
2408     /**
2409      * Get whether this should be displayed in LTR mode.
2410      *
2411      * Try to guess from the PARAM type unless specifically set.
2412      */
2413     public function get_force_ltr() {
2414         $forceltr = parent::get_force_ltr();
2415         if ($forceltr === null) {
2416             return !is_rtl_compatible($this->paramtype);
2417         }
2418         return $forceltr;
2419     }
2421     /**
2422      * Return the setting
2423      *
2424      * @return mixed returns config if successful else null
2425      */
2426     public function get_setting() {
2427         return $this->config_read($this->name);
2428     }
2430     public function write_setting($data) {
2431         if ($this->paramtype === PARAM_INT and $data === '') {
2432         // do not complain if '' used instead of 0
2433             $data = 0;
2434         }
2435         // $data is a string
2436         $validated = $this->validate($data);
2437         if ($validated !== true) {
2438             return $validated;
2439         }
2440         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
2441     }
2443     /**
2444      * Validate data before storage
2445      * @param string data
2446      * @return mixed true if ok string if error found
2447      */
2448     public function validate($data) {
2449         // allow paramtype to be a custom regex if it is the form of /pattern/
2450         if (preg_match('#^/.*/$#', $this->paramtype)) {
2451             if (preg_match($this->paramtype, $data)) {
2452                 return true;
2453             } else {
2454                 return get_string('validateerror', 'admin');
2455             }
2457         } else if ($this->paramtype === PARAM_RAW) {
2458             return true;
2460         } else {
2461             $cleaned = clean_param($data, $this->paramtype);
2462             if ("$data" === "$cleaned") { // implicit conversion to string is needed to do exact comparison
2463                 return true;
2464             } else {
2465                 return get_string('validateerror', 'admin');
2466             }
2467         }
2468     }
2470     /**
2471      * Return an XHTML string for the setting
2472      * @return string Returns an XHTML string
2473      */
2474     public function output_html($data, $query='') {
2475         global $OUTPUT;
2477         $default = $this->get_defaultsetting();
2478         $context = (object) [
2479             'size' => $this->size,
2480             'id' => $this->get_id(),
2481             'name' => $this->get_full_name(),
2482             'value' => $data,
2483             'forceltr' => $this->get_force_ltr(),
2484             'readonly' => $this->is_readonly(),
2485         ];
2486         $element = $OUTPUT->render_from_template('core_admin/setting_configtext', $context);
2488         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2489     }
2492 /**
2493  * Text input with a maximum length constraint.
2494  *
2495  * @copyright 2015 onwards Ankit Agarwal
2496  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2497  */
2498 class admin_setting_configtext_with_maxlength extends admin_setting_configtext {
2500     /** @var int maximum number of chars allowed. */
2501     protected $maxlength;
2503     /**
2504      * Config text constructor
2505      *
2506      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
2507      *                     or 'myplugin/mysetting' for ones in config_plugins.
2508      * @param string $visiblename localised
2509      * @param string $description long localised info
2510      * @param string $defaultsetting
2511      * @param mixed $paramtype int means PARAM_XXX type, string is a allowed format in regex
2512      * @param int $size default field size
2513      * @param mixed $maxlength int maxlength allowed, 0 for infinite.
2514      */
2515     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW,
2516                                 $size=null, $maxlength = 0) {
2517         $this->maxlength = $maxlength;
2518         parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype, $size);
2519     }
2521     /**
2522      * Validate data before storage
2523      *
2524      * @param string $data data
2525      * @return mixed true if ok string if error found
2526      */
2527     public function validate($data) {
2528         $parentvalidation = parent::validate($data);
2529         if ($parentvalidation === true) {
2530             if ($this->maxlength > 0) {
2531                 // Max length check.
2532                 $length = core_text::strlen($data);
2533                 if ($length > $this->maxlength) {
2534                     return get_string('maximumchars', 'moodle',  $this->maxlength);
2535                 }
2536                 return true;
2537             } else {
2538                 return true; // No max length check needed.
2539             }
2540         } else {
2541             return $parentvalidation;
2542         }
2543     }
2546 /**
2547  * General text area without html editor.
2548  *
2549  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2550  */
2551 class admin_setting_configtextarea extends admin_setting_configtext {
2552     private $rows;
2553     private $cols;
2555     /**
2556      * @param string $name
2557      * @param string $visiblename
2558      * @param string $description
2559      * @param mixed $defaultsetting string or array
2560      * @param mixed $paramtype
2561      * @param string $cols The number of columns to make the editor
2562      * @param string $rows The number of rows to make the editor
2563      */
2564     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $cols='60', $rows='8') {
2565         $this->rows = $rows;
2566         $this->cols = $cols;
2567         parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype);
2568     }
2570     /**
2571      * Returns an XHTML string for the editor
2572      *
2573      * @param string $data
2574      * @param string $query
2575      * @return string XHTML string for the editor
2576      */
2577     public function output_html($data, $query='') {
2578         global $OUTPUT;
2580         $default = $this->get_defaultsetting();
2581         $defaultinfo = $default;
2582         if (!is_null($default) and $default !== '') {
2583             $defaultinfo = "\n".$default;
2584         }
2586         $context = (object) [
2587             'cols' => $this->cols,
2588             'rows' => $this->rows,
2589             'id' => $this->get_id(),
2590             'name' => $this->get_full_name(),
2591             'value' => $data,
2592             'forceltr' => $this->get_force_ltr(),
2593             'readonly' => $this->is_readonly(),
2594         ];
2595         $element = $OUTPUT->render_from_template('core_admin/setting_configtextarea', $context);
2597         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
2598     }
2601 /**
2602  * General text area with html editor.
2603  */
2604 class admin_setting_confightmleditor extends admin_setting_configtextarea {
2606     /**
2607      * @param string $name
2608      * @param string $visiblename
2609      * @param string $description
2610      * @param mixed $defaultsetting string or array
2611      * @param mixed $paramtype
2612      */
2613     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $cols='60', $rows='8') {
2614         parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype, $cols, $rows);
2615         $this->set_force_ltr(false);
2616         editors_head_setup();
2617     }
2619     /**
2620      * Returns an XHTML string for the editor
2621      *
2622      * @param string $data
2623      * @param string $query
2624      * @return string XHTML string for the editor
2625      */
2626     public function output_html($data, $query='') {
2627         $editor = editors_get_preferred_editor(FORMAT_HTML);
2628         $editor->set_text($data);
2629         $editor->use_editor($this->get_id(), array('noclean'=>true));
2630         return parent::output_html($data, $query);
2631     }
2633     /**
2634      * Checks if data has empty html.
2635      *
2636      * @param string $data
2637      * @return string Empty when no errors.
2638      */
2639     public function write_setting($data) {
2640         if (trim(html_to_text($data)) === '') {
2641             $data = '';
2642         }
2643         return parent::write_setting($data);
2644     }
2648 /**
2649  * Password field, allows unmasking of password
2650  *
2651  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2652  */
2653 class admin_setting_configpasswordunmask extends admin_setting_configtext {
2655     /**
2656      * Constructor
2657      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2658      * @param string $visiblename localised
2659      * @param string $description long localised info
2660      * @param string $defaultsetting default password
2661      */
2662     public function __construct($name, $visiblename, $description, $defaultsetting) {
2663         parent::__construct($name, $visiblename, $description, $defaultsetting, PARAM_RAW, 30);
2664     }
2666     /**
2667      * Log config changes if necessary.
2668      * @param string $name
2669      * @param string $oldvalue
2670      * @param string $value
2671      */
2672     protected function add_to_config_log($name, $oldvalue, $value) {
2673         if ($value !== '') {
2674             $value = '********';
2675         }
2676         if ($oldvalue !== '' and $oldvalue !== null) {
2677             $oldvalue = '********';
2678         }
2679         parent::add_to_config_log($name, $oldvalue, $value);
2680     }
2682     /**
2683      * Returns HTML for the field.
2684      *
2685      * @param   string  $data       Value for the field
2686      * @param   string  $query      Passed as final argument for format_admin_setting
2687      * @return  string              Rendered HTML
2688      */
2689     public function output_html($data, $query='') {
2690         global $OUTPUT;
2692         $context = (object) [
2693             'id' => $this->get_id(),
2694             'name' => $this->get_full_name(),
2695             'size' => $this->size,
2696             'value' => $this->is_readonly() ? null : $data,
2697             'forceltr' => $this->get_force_ltr(),
2698             'readonly' => $this->is_readonly(),
2699         ];
2700         $element = $OUTPUT->render_from_template('core_admin/setting_configpasswordunmask', $context);
2701         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', null, $query);
2702     }
2705 /**
2706  * Password field, allows unmasking of password, with an advanced checkbox that controls an additional $name.'_adv' setting.
2707  *
2708  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2709  * @copyright 2018 Paul Holden (pholden@greenhead.ac.uk)
2710  */
2711 class admin_setting_configpasswordunmask_with_advanced extends admin_setting_configpasswordunmask {
2713     /**
2714      * Constructor
2715      *
2716      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2717      * @param string $visiblename localised
2718      * @param string $description long localised info
2719      * @param array $defaultsetting ('value'=>string, 'adv'=>bool)
2720      */
2721     public function __construct($name, $visiblename, $description, $defaultsetting) {
2722         parent::__construct($name, $visiblename, $description, $defaultsetting['value']);
2723         $this->set_advanced_flag_options(admin_setting_flag::ENABLED, !empty($defaultsetting['adv']));
2724     }
2727 /**
2728  * Empty setting used to allow flags (advanced) on settings that can have no sensible default.
2729  * Note: Only advanced makes sense right now - locked does not.
2730  *
2731  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2732  */
2733 class admin_setting_configempty extends admin_setting_configtext {
2735     /**
2736      * @param string $name
2737      * @param string $visiblename
2738      * @param string $description
2739      */
2740     public function __construct($name, $visiblename, $description) {
2741         parent::__construct($name, $visiblename, $description, '', PARAM_RAW);
2742     }
2744     /**
2745      * Returns an XHTML string for the hidden field
2746      *
2747      * @param string $data
2748      * @param string $query
2749      * @return string XHTML string for the editor
2750      */
2751     public function output_html($data, $query='') {
2752         global $OUTPUT;
2754         $context = (object) [
2755             'id' => $this->get_id(),
2756             'name' => $this->get_full_name()
2757         ];
2758         $element = $OUTPUT->render_from_template('core_admin/setting_configempty', $context);
2760         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', get_string('none'), $query);
2761     }
2765 /**
2766  * Path to directory
2767  *
2768  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2769  */
2770 class admin_setting_configfile extends admin_setting_configtext {
2771     /**
2772      * Constructor
2773      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2774      * @param string $visiblename localised
2775      * @param string $description long localised info
2776      * @param string $defaultdirectory default directory location
2777      */
2778     public function __construct($name, $visiblename, $description, $defaultdirectory) {
2779         parent::__construct($name, $visiblename, $description, $defaultdirectory, PARAM_RAW, 50);
2780     }
2782     /**
2783      * Returns XHTML for the field
2784      *
2785      * Returns XHTML for the field and also checks whether the file
2786      * specified in $data exists using file_exists()
2787      *
2788      * @param string $data File name and path to use in value attr
2789      * @param string $query
2790      * @return string XHTML field
2791      */
2792     public function output_html($data, $query='') {
2793         global $CFG, $OUTPUT;
2795         $default = $this->get_defaultsetting();
2796         $context = (object) [
2797             'id' => $this->get_id(),
2798             'name' => $this->get_full_name(),
2799             'size' => $this->size,
2800             'value' => $data,
2801             'showvalidity' => !empty($data),
2802             'valid' => $data && file_exists($data),
2803             'readonly' => !empty($CFG->preventexecpath) || $this->is_readonly(),
2804             'forceltr' => $this->get_force_ltr(),
2805         ];
2807         if ($context->readonly) {
2808             $this->visiblename .= '<div class="alert alert-info">'.get_string('execpathnotallowed', 'admin').'</div>';
2809         }
2811         $element = $OUTPUT->render_from_template('core_admin/setting_configfile', $context);
2813         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2814     }
2816     /**
2817      * Checks if execpatch has been disabled in config.php
2818      */
2819     public function write_setting($data) {
2820         global $CFG;
2821         if (!empty($CFG->preventexecpath)) {
2822             if ($this->get_setting() === null) {
2823                 // Use default during installation.
2824                 $data = $this->get_defaultsetting();
2825                 if ($data === null) {
2826                     $data = '';
2827                 }
2828             } else {
2829                 return '';
2830             }
2831         }
2832         return parent::write_setting($data);
2833     }
2838 /**
2839  * Path to executable file
2840  *
2841  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2842  */
2843 class admin_setting_configexecutable extends admin_setting_configfile {
2845     /**
2846      * Returns an XHTML field
2847      *
2848      * @param string $data This is the value for the field
2849      * @param string $query
2850      * @return string XHTML field
2851      */
2852     public function output_html($data, $query='') {
2853         global $CFG, $OUTPUT;
2854         $default = $this->get_defaultsetting();
2855         require_once("$CFG->libdir/filelib.php");
2857         $context = (object) [
2858             'id' => $this->get_id(),
2859             'name' => $this->get_full_name(),
2860             'size' => $this->size,
2861             'value' => $data,
2862             'showvalidity' => !empty($data),
2863             'valid' => $data && file_exists($data) && !is_dir($data) && file_is_executable($data),
2864             'readonly' => !empty($CFG->preventexecpath),
2865             'forceltr' => $this->get_force_ltr()
2866         ];
2868         if (!empty($CFG->preventexecpath)) {
2869             $this->visiblename .= '<div class="alert alert-info">'.get_string('execpathnotallowed', 'admin').'</div>';
2870         }
2872         $element = $OUTPUT->render_from_template('core_admin/setting_configexecutable', $context);
2874         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2875     }
2879 /**
2880  * Path to directory
2881  *
2882  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2883  */
2884 class admin_setting_configdirectory extends admin_setting_configfile {
2886     /**
2887      * Returns an XHTML field
2888      *
2889      * @param string $data This is the value for the field
2890      * @param string $query
2891      * @return string XHTML
2892      */
2893     public function output_html($data, $query='') {
2894         global $CFG, $OUTPUT;
2895         $default = $this->get_defaultsetting();
2897         $context = (object) [
2898             'id' => $this->get_id(),
2899             'name' => $this->get_full_name(),
2900             'size' => $this->size,
2901             'value' => $data,
2902             'showvalidity' => !empty($data),
2903             'valid' => $data && file_exists($data) && is_dir($data),
2904             'readonly' => !empty($CFG->preventexecpath),
2905             'forceltr' => $this->get_force_ltr()
2906         ];
2908         if (!empty($CFG->preventexecpath)) {
2909             $this->visiblename .= '<div class="alert alert-info">'.get_string('execpathnotallowed', 'admin').'</div>';
2910         }
2912         $element = $OUTPUT->render_from_template('core_admin/setting_configdirectory', $context);
2914         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2915     }
2919 /**
2920  * Checkbox
2921  *
2922  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2923  */
2924 class admin_setting_configcheckbox extends admin_setting {
2925     /** @var string Value used when checked */
2926     public $yes;
2927     /** @var string Value used when not checked */
2928     public $no;
2930     /**
2931      * Constructor
2932      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2933      * @param string $visiblename localised
2934      * @param string $description long localised info
2935      * @param string $defaultsetting
2936      * @param string $yes value used when checked
2937      * @param string $no value used when not checked
2938      */
2939     public function __construct($name, $visiblename, $description, $defaultsetting, $yes='1', $no='0') {
2940         parent::__construct($name, $visiblename, $description, $defaultsetting);
2941         $this->yes = (string)$yes;
2942         $this->no  = (string)$no;
2943     }
2945     /**
2946      * Retrieves the current setting using the objects name
2947      *
2948      * @return string
2949      */
2950     public function get_setting() {
2951         return $this->config_read($this->name);
2952     }
2954     /**
2955      * Sets the value for the setting
2956      *
2957      * Sets the value for the setting to either the yes or no values
2958      * of the object by comparing $data to yes
2959      *
2960      * @param mixed $data Gets converted to str for comparison against yes value
2961      * @return string empty string or error
2962      */
2963     public function write_setting($data) {
2964         if ((string)$data === $this->yes) { // convert to strings before comparison
2965             $data = $this->yes;
2966         } else {
2967             $data = $this->no;
2968         }
2969         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
2970     }
2972     /**
2973      * Returns an XHTML checkbox field
2974      *
2975      * @param string $data If $data matches yes then checkbox is checked
2976      * @param string $query
2977      * @return string XHTML field
2978      */
2979     public function output_html($data, $query='') {
2980         global $OUTPUT;
2982         $context = (object) [
2983             'id' => $this->get_id(),
2984             'name' => $this->get_full_name(),
2985             'no' => $this->no,
2986             'value' => $this->yes,
2987             'checked' => (string) $data === $this->yes,
2988             'readonly' => $this->is_readonly(),
2989         ];
2991         $default = $this->get_defaultsetting();
2992         if (!is_null($default)) {
2993             if ((string)$default === $this->yes) {
2994                 $defaultinfo = get_string('checkboxyes', 'admin');
2995             } else {
2996                 $defaultinfo = get_string('checkboxno', 'admin');
2997             }
2998         } else {
2999             $defaultinfo = NULL;
3000         }
3002         $element = $OUTPUT->render_from_template('core_admin/setting_configcheckbox', $context);
3004         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
3005     }
3009 /**
3010  * Multiple checkboxes, each represents different value, stored in csv format
3011  *
3012  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3013  */
3014 class admin_setting_configmulticheckbox extends admin_setting {
3015     /** @var array Array of choices value=>label */
3016     public $choices;
3018     /**
3019      * Constructor: uses parent::__construct
3020      *
3021      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3022      * @param string $visiblename localised
3023      * @param string $description long localised info
3024      * @param array $defaultsetting array of selected
3025      * @param array $choices array of $value=>$label for each checkbox
3026      */
3027     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
3028         $this->choices = $choices;
3029         parent::__construct($name, $visiblename, $description, $defaultsetting);
3030     }
3032     /**
3033      * This public function may be used in ancestors for lazy loading of choices
3034      *
3035      * @todo Check if this function is still required content commented out only returns true
3036      * @return bool true if loaded, false if error
3037      */
3038     public function load_choices() {
3039         /*
3040         if (is_array($this->choices)) {
3041             return true;
3042         }
3043         .... load choices here
3044         */
3045         return true;
3046     }
3048     /**
3049      * Is setting related to query text - used when searching
3050      *
3051      * @param string $query
3052      * @return bool true on related, false on not or failure
3053      */
3054     public function is_related($query) {
3055         if (!$this->load_choices() or empty($this->choices)) {
3056             return false;
3057         }
3058         if (parent::is_related($query)) {
3059             return true;
3060         }
3062         foreach ($this->choices as $desc) {
3063             if (strpos(core_text::strtolower($desc), $query) !== false) {
3064                 return true;
3065             }
3066         }
3067         return false;
3068     }
3070     /**
3071      * Returns the current setting if it is set
3072      *
3073      * @return mixed null if null, else an array
3074      */
3075     public function get_setting() {
3076         $result = $this->config_read($this->name);
3078         if (is_null($result)) {
3079             return NULL;
3080         }
3081         if ($result === '') {
3082             return array();
3083         }
3084         $enabled = explode(',', $result);
3085         $setting = array();
3086         foreach ($enabled as $option) {
3087             $setting[$option] = 1;
3088         }
3089         return $setting;
3090     }
3092     /**
3093      * Saves the setting(s) provided in $data
3094      *
3095      * @param array $data An array of data, if not array returns empty str
3096      * @return mixed empty string on useless data or bool true=success, false=failed
3097      */
3098     public function write_setting($data) {
3099         if (!is_array($data)) {
3100             return ''; // ignore it
3101         }
3102         if (!$this->load_choices() or empty($this->choices)) {
3103             return '';
3104         }
3105         unset($data['xxxxx']);
3106         $result = array();
3107         foreach ($data as $key => $value) {
3108             if ($value and array_key_exists($key, $this->choices)) {
3109                 $result[] = $key;
3110             }
3111         }
3112         return $this->config_write($this->name, implode(',', $result)) ? '' : get_string('errorsetting', 'admin');
3113     }
3115     /**
3116      * Returns XHTML field(s) as required by choices
3117      *
3118      * Relies on data being an array should data ever be another valid vartype with
3119      * acceptable value this may cause a warning/error
3120      * if (!is_array($data)) would fix the problem
3121      *
3122      * @todo Add vartype handling to ensure $data is an array
3123      *
3124      * @param array $data An array of checked values
3125      * @param string $query
3126      * @return string XHTML field
3127      */
3128     public function output_html($data, $query='') {
3129         global $OUTPUT;
3131         if (!$this->load_choices() or empty($this->choices)) {
3132             return '';
3133         }
3135         $default = $this->get_defaultsetting();
3136         if (is_null($default)) {
3137             $default = array();
3138         }
3139         if (is_null($data)) {
3140             $data = array();
3141         }
3143         $context = (object) [
3144             'id' => $this->get_id(),
3145             'name' => $this->get_full_name(),
3146         ];
3148         $options = array();
3149         $defaults = array();
3150         foreach ($this->choices as $key => $description) {
3151             if (!empty($default[$key])) {
3152                 $defaults[] = $description;
3153             }
3155             $options[] = [
3156                 'key' => $key,
3157                 'checked' => !empty($data[$key]),
3158                 'label' => highlightfast($query, $description)
3159             ];
3160         }
3162         if (is_null($default)) {
3163             $defaultinfo = null;
3164         } else if (!empty($defaults)) {
3165             $defaultinfo = implode(', ', $defaults);
3166         } else {
3167             $defaultinfo = get_string('none');
3168         }
3170         $context->options = $options;
3171         $context->hasoptions = !empty($options);
3173         $element = $OUTPUT->render_from_template('core_admin/setting_configmulticheckbox', $context);
3175         return format_admin_setting($this, $this->visiblename, $element, $this->description, false, '', $defaultinfo, $query);
3177     }
3181 /**
3182  * Multiple checkboxes 2, value stored as string 00101011
3183  *
3184  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3185  */
3186 class admin_setting_configmulticheckbox2 extends admin_setting_configmulticheckbox {
3188     /**
3189      * Returns the setting if set
3190      *
3191      * @return mixed null if not set, else an array of set settings
3192      */
3193     public function get_setting() {
3194         $result = $this->config_read($this->name);
3195         if (is_null($result)) {
3196             return NULL;
3197         }
3198         if (!$this->load_choices()) {
3199             return NULL;
3200         }
3201         $result = str_pad($result, count($this->choices), '0');
3202         $result = preg_split('//', $result, -1, PREG_SPLIT_NO_EMPTY);
3203         $setting = array();
3204         foreach ($this->choices as $key=>$unused) {
3205             $value = array_shift($result);
3206             if ($value) {
3207                 $setting[$key] = 1;
3208             }
3209         }
3210         return $setting;
3211     }
3213     /**
3214      * Save setting(s) provided in $data param
3215      *
3216      * @param array $data An array of settings to save
3217      * @return mixed empty string for bad data or bool true=>success, false=>error
3218      */
3219     public function write_setting($data) {
3220         if (!is_array($data)) {
3221             return ''; // ignore it
3222         }
3223         if (!$this->load_choices() or empty($this->choices)) {
3224             return '';
3225         }
3226         $result = '';
3227         foreach ($this->choices as $key=>$unused) {
3228             if (!empty($data[$key])) {
3229                 $result .= '1';
3230             } else {
3231                 $result .= '0';
3232             }
3233         }
3234         return $this->config_write($this->name, $result) ? '' : get_string('errorsetting', 'admin');
3235     }
3239 /**
3240  * Select one value from list
3241  *
3242  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3243  */
3244 class admin_setting_configselect extends admin_setting {
3245     /** @var array Array of choices value=>label */
3246     public $choices;
3247     /** @var array Array of choices grouped using optgroups */
3248     public $optgroups;
3249     /** @var callable|null Loader function for choices */
3250     protected $choiceloader = null;
3251     /** @var callable|null Validation function */
3252     protected $validatefunction = null;
3254     /**
3255      * Constructor.
3256      *
3257      * If you want to lazy-load the choices, pass a callback function that returns a choice
3258      * array for the $choices parameter.
3259      *
3260      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3261      * @param string $visiblename localised
3262      * @param string $description long localised info
3263      * @param string|int $defaultsetting
3264      * @param array|callable|null $choices array of $value=>$label for each selection, or callback
3265      */
3266     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
3267         // Look for optgroup and single options.
3268         if (is_array($choices)) {
3269             $this->choices = [];
3270             foreach ($choices as $key => $val) {
3271                 if (is_array($val)) {
3272                     $this->optgroups[$key] = $val;
3273                     $this->choices = array_merge($this->choices, $val);
3274                 } else {
3275                     $this->choices[$key] = $val;
3276                 }
3277             }
3278         }
3279         if (is_callable($choices)) {
3280             $this->choiceloader = $choices;
3281         }
3283         parent::__construct($name, $visiblename, $description, $defaultsetting);
3284     }
3286     /**
3287      * Sets a validate function.
3288      *
3289      * The callback will be passed one parameter, the new setting value, and should return either
3290      * an empty string '' if the value is OK, or an error message if not.
3291      *
3292      * @param callable|null $validatefunction Validate function or null to clear
3293      * @since Moodle 3.10
3294      */
3295     public function set_validate_function(?callable $validatefunction = null) {
3296         $this->validatefunction = $validatefunction;
3297     }
3299     /**
3300      * This function may be used in ancestors for lazy loading of choices
3301      *
3302      * Override this method if loading of choices is expensive, such
3303      * as when it requires multiple db requests.
3304      *
3305      * @return bool true if loaded, false if error
3306      */
3307     public function load_choices() {
3308         if ($this->choiceloader) {
3309             if (!is_array($this->choices)) {
3310                 $this->choices = call_user_func($this->choiceloader);
3311             }
3312             return true;
3313         }
3314         return true;
3315     }
3317     /**
3318      * Check if this is $query is related to a choice
3319      *
3320      * @param string $query
3321      * @return bool true if related, false if not
3322      */
3323     public function is_related($query) {
3324         if (parent::is_related($query)) {
3325             return true;
3326         }
3327         if (!$this->load_choices()) {
3328             return false;
3329         }
3330         foreach ($this->choices as $key=>$value) {
3331             if (strpos(core_text::strtolower($key), $query) !== false) {
3332                 return true;
3333             }
3334             if (strpos(core_text::strtolower($value), $query) !== false) {
3335                 return true;
3336             }
3337         }
3338         return false;
3339     }
3341     /**
3342      * Return the setting
3343      *
3344      * @return mixed returns config if successful else null
3345      */
3346     public function get_setting() {
3347         return $this->config_read($this->name);
3348     }
3350     /**
3351      * Save a setting
3352      *
3353      * @param string $data
3354      * @return string empty of error string
3355      */
3356     public function write_setting($data) {
3357         if (!$this->load_choices() or empty($this->choices)) {
3358             return '';
3359         }
3360         if (!array_key_exists($data, $this->choices)) {
3361             return ''; // ignore it
3362         }
3364         // Validate the new setting.
3365         $error = $this->validate_setting($data);
3366         if ($error) {
3367             return $error;
3368         }
3370         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
3371     }
3373     /**
3374      * Validate the setting. This uses the callback function if provided; subclasses could override
3375      * to carry out validation directly in the class.
3376      *
3377      * @param string $data New value being set
3378      * @return string Empty string if valid, or error message text
3379      * @since Moodle 3.10
3380      */
3381     protected function validate_setting(string $data): string {
3382         // If validation function is specified, call it now.
3383         if ($this->validatefunction) {
3384             return call_user_func($this->validatefunction, $data);
3385         } else {
3386             return '';
3387         }
3388     }
3390     /**
3391      * Returns XHTML select field
3392      *
3393      * Ensure the options are loaded, and generate the XHTML for the select
3394      * element and any warning message. Separating this out from output_html
3395      * makes it easier to subclass this class.
3396      *
3397      * @param string $data the option to show as selected.
3398      * @param string $current the currently selected option in the database, null if none.
3399      * @param string $default the default selected option.
3400      * @return array the HTML for the select element, and a warning message.
3401      * @deprecated since Moodle 3.2
3402      */
3403     public function output_select_html($data, $current, $default, $extraname = '') {
3404         debugging('The method admin_setting_configselect::output_select_html is depreacted, do not use any more.', DEBUG_DEVELOPER);
3405     }
3407     /**
3408      * Returns XHTML select field and wrapping div(s)
3409      *
3410      * @see output_select_html()
3411      *
3412      * @param string $data the option to show as selected
3413      * @param string $query
3414      * @return string XHTML field and wrapping div
3415      */
3416     public function output_html($data, $query='') {
3417         global $OUTPUT;
3419         $default = $this->get_defaultsetting();
3420         $current = $this->get_setting();
3422         if (!$this->load_choices() || empty($this->choices)) {
3423             return '';
3424         }
3426         $context = (object) [
3427             'id' => $this->get_id(),
3428             'name' => $this->get_full_name(),
3429         ];
3431         if (!is_null($default) && array_key_exists($default, $this->choices)) {
3432             $defaultinfo = $this->choices[$default];
3433         } else {
3434             $defaultinfo = NULL;
3435         }
3437         // Warnings.
3438         $warning = '';
3439         if ($current === null) {
3440             // First run.
3441         } else if (empty($current) && (array_key_exists('', $this->choices) || array_key_exists(0, $this->choices))) {
3442             // No warning.
3443         } else if (!array_key_exists($current, $this->choices)) {
3444             $warning = get_string('warningcurrentsetting', 'admin', $current);
3445             if (!is_null($default) && $data == $current) {
3446                 $data = $default; // Use default instead of first value when showing the form.
3447             }
3448         }
3450         $options = [];
3451         $template = 'core_admin/setting_configselect';
3453         if (!empty($this->optgroups)) {
3454             $optgroups = [];
3455             foreach ($this->optgroups as $label => $choices) {
3456                 $optgroup = array('label' => $label, 'options' => []);
3457                 foreach ($choices as $value => $name) {
3458                     $optgroup['options'][] = [
3459                         'value' => $value,
3460                         'name' => $name,
3461                         'selected' => (string) $value == $data
3462                     ];
3463                     unset($this->choices[$value]);
3464                 }
3465                 $optgroups[] = $optgroup;
3466             }
3467             $context->options = $options;
3468             $context->optgroups = $optgroups;
3469             $template = 'core_admin/setting_configselect_optgroup';
3470         }
3472         foreach ($this->choices as $value => $name) {
3473             $options[] = [
3474                 'value' => $value,
3475                 'name' => $name,
3476                 'selected' => (string) $value == $data
3477             ];
3478         }
3479         $context->options = $options;
3480         $context->readonly = $this->is_readonly();
3482         $element = $OUTPUT->render_from_template($template, $context);
3484         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, $warning, $defaultinfo, $query);
3485     }
3488 /**
3489  * Select multiple items from list
3490  *
3491  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3492  */
3493 class admin_setting_configmultiselect extends admin_setting_configselect {
3494     /**
3495      * Constructor
3496      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3497      * @param string $visiblename localised
3498      * @param string $description long localised info
3499      * @param array $defaultsetting array of selected items
3500      * @param array $choices array of $value=>$label for each list item
3501      */
3502     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
3503         parent::__construct($name, $visiblename, $description, $defaultsetting, $choices);
3504     }
3506     /**
3507      * Returns the select setting(s)
3508      *
3509      * @return mixed null or array. Null if no settings else array of setting(s)
3510      */
3511     public function get_setting() {
3512         $result = $this->config_read($this->name);
3513         if (is_null($result)) {
3514             return NULL;
3515         }
3516         if ($result === '') {
3517             return array();
3518         }
3519         return explode(',', $result);
3520     }
3522     /**
3523      * Saves setting(s) provided through $data
3524      *
3525      * Potential bug in the works should anyone call with this function
3526      * using a vartype that is not an array
3527      *
3528      * @param array $data
3529      */
3530     public function write_setting($data) {
3531         if (!is_array($data)) {
3532             return ''; //ignore it
3533         }
3534         if (!$this->load_choices() or empty($this->choices)) {
3535             return '';
3536         }
3538         unset($data['xxxxx']);
3540         $save = array();
3541         foreach ($data as $value) {
3542             if (!array_key_exists($value, $this->choices)) {
3543                 continue; // ignore it
3544             }
3545             $save[] = $value;
3546         }
3548         return ($this->config_write($this->name, implode(',', $save)) ? '' : get_string('errorsetting', 'admin'));
3549     }
3551     /**
3552      * Is setting related to query text - used when searching
3553      *
3554      * @param string $query
3555      * @return bool true if related, false if not
3556      */
3557     public function is_related($query) {
3558         if (!$this->load_choices() or empty($this->choices)) {
3559             return false;
3560         }
3561         if (parent::is_related($query)) {
3562             return true;
3563         }
3565         foreach ($this->choices as $desc) {
3566             if (strpos(core_text::strtolower($desc), $query) !== false) {
3567                 return true;
3568             }
3569         }
3570         return false;
3571     }
3573     /**
3574      * Returns XHTML multi-select field
3575      *
3576      * @todo Add vartype handling to ensure $data is an array
3577      * @param array $data Array of values to select by default
3578      * @param string $query
3579      * @return string XHTML multi-select field
3580      */
3581     public function output_html($data, $query='') {
3582         global $OUTPUT;
3584         if (!$this->load_choices() or empty($this->choices)) {
3585             return '';
3586         }
3588         $default = $this->get_defaultsetting();
3589         if (is_null($default)) {
3590             $default = array();
3591         }
3592         if (is_null($data)) {
3593             $data = array();
3594         }
3596         $context = (object) [
3597             'id' => $this->get_id(),
3598             'name' => $this->get_full_name(),
3599             'size' => min(10, count($this->choices))
3600         ];
3602         $defaults = [];
3603         $options = [];
3604         $template = 'core_admin/setting_configmultiselect';
3606         if (!empty($this->optgroups)) {
3607             $optgroups = [];
3608             foreach ($this->optgroups as $label => $choices) {
3609                 $optgroup = array('label' => $label, 'options' => []);
3610                 foreach ($choices as $value => $name) {
3611                     if (in_array($value, $default)) {
3612                         $defaults[] = $name;
3613                     }
3614                     $optgroup['options'][] = [
3615                         'value' => $value,
3616                         'name' => $name,
3617                         'selected' => in_array($value, $data)
3618                     ];
3619                     unset($this->choices[$value]);
3620                 }
3621                 $optgroups[] = $optgroup;
3622             }
3623             $context->optgroups = $optgroups;
3624             $template = 'core_admin/setting_configmultiselect_optgroup';
3625         }
3627         foreach ($this->choices as $value => $name) {
3628             if (in_array($value, $default)) {
3629                 $defaults[] = $name;
3630             }
3631             $options[] = [
3632                 'value' => $value,
3633                 'name' => $name,
3634                 'selected' => in_array($value, $data)
3635             ];
3636         }
3637         $context->options = $options;
3638         $context->readonly = $this->is_readonly();
3640         if (is_null($default)) {
3641             $defaultinfo = NULL;
3642         } if (!empty($defaults)) {
3643             $defaultinfo = implode(', ', $defaults);
3644         } else {
3645             $defaultinfo = get_string('none');
3646         }
3648         $element = $OUTPUT->render_from_template($template, $context);
3650         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
3651     }
3654 /**
3655  * Time selector
3656  *
3657  * This is a liiitle bit messy. we're using two selects, but we're returning
3658  * them as an array named after $name (so we only use $name2 internally for the setting)
3659  *
3660  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3661  */
3662 class admin_setting_configtime extends admin_setting {
3663     /** @var string Used for setting second select (minutes) */
3664     public $name2;
3666     /**
3667      * Constructor
3668      * @param string $hoursname setting for hours
3669      * @param string $minutesname setting for hours
3670      * @param string $visiblename localised
3671      * @param string $description long localised info
3672      * @param array $defaultsetting array representing default time 'h'=>hours, 'm'=>minutes
3673      */
3674     public function __construct($hoursname, $minutesname, $visiblename, $description, $defaultsetting) {
3675         $this->name2 = $minutesname;
3676         parent::__construct($hoursname, $visiblename, $description, $defaultsetting);
3677     }
3679     /**
3680      * Get the selected time
3681      *
3682      * @return mixed An array containing 'h'=>xx, 'm'=>xx, or null if not set
3683      */
3684     public function get_setting() {