b93d393d2106ea687d848f56fde415c15ea33981
[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      * Get the currently saved value for a setting flag
1766      *
1767      * @param admin_setting_flag $flag - One of the admin_setting_flag for this admin_setting.
1768      * @return bool
1769      */
1770     public function get_setting_flag_value(admin_setting_flag $flag) {
1771         $value = $this->config_read($this->name . '_' . $flag->get_shortname());
1772         if (!isset($value)) {
1773             $value = $flag->get_default();
1774         }
1776         return !empty($value);
1777     }
1779     /**
1780      * Get the list of defaults for the flags on this setting.
1781      *
1782      * @param array of strings describing the defaults for this setting. This is appended to by this function.
1783      */
1784     public function get_setting_flag_defaults(& $defaults) {
1785         foreach ($this->flags as $flag) {
1786             if ($flag->is_enabled() && $flag->get_default()) {
1787                 $defaults[] = $flag->get_displayname();
1788             }
1789         }
1790     }
1792     /**
1793      * Output the input fields for the advanced and locked flags on this setting.
1794      *
1795      * @param bool $adv - The current value of the advanced flag.
1796      * @param bool $locked - The current value of the locked flag.
1797      * @return string $output - The html for the flags.
1798      */
1799     public function output_setting_flags() {
1800         $output = '';
1802         foreach ($this->flags as $flag) {
1803             if ($flag->is_enabled()) {
1804                 $output .= $flag->output_setting_flag($this);
1805             }
1806         }
1808         if (!empty($output)) {
1809             return html_writer::tag('span', $output, array('class' => 'adminsettingsflags'));
1810         }
1811         return $output;
1812     }
1814     /**
1815      * Write the values of the flags for this admin setting.
1816      *
1817      * @param array $data - The data submitted from the form or null to set the default value for new installs.
1818      * @return bool - true if successful.
1819      */
1820     public function write_setting_flags($data) {
1821         $result = true;
1822         foreach ($this->flags as $flag) {
1823             $result = $result && $flag->write_setting_flag($this, $data);
1824         }
1825         return $result;
1826     }
1828     /**
1829      * Set up $this->name and potentially $this->plugin
1830      *
1831      * Set up $this->name and possibly $this->plugin based on whether $name looks
1832      * like 'settingname' or 'plugin/settingname'. Also, do some sanity checking
1833      * on the names, that is, output a developer debug warning if the name
1834      * contains anything other than [a-zA-Z0-9_]+.
1835      *
1836      * @param string $name the setting name passed in to the constructor.
1837      */
1838     private function parse_setting_name($name) {
1839         $bits = explode('/', $name);
1840         if (count($bits) > 2) {
1841             throw new moodle_exception('invalidadminsettingname', '', '', $name);
1842         }
1843         $this->name = array_pop($bits);
1844         if (!preg_match('/^[a-zA-Z0-9_]+$/', $this->name)) {
1845             throw new moodle_exception('invalidadminsettingname', '', '', $name);
1846         }
1847         if (!empty($bits)) {
1848             $this->plugin = array_pop($bits);
1849             if ($this->plugin === 'moodle') {
1850                 $this->plugin = null;
1851             } else if (!preg_match('/^[a-zA-Z0-9_]+$/', $this->plugin)) {
1852                     throw new moodle_exception('invalidadminsettingname', '', '', $name);
1853                 }
1854         }
1855     }
1857     /**
1858      * Returns the fullname prefixed by the plugin
1859      * @return string
1860      */
1861     public function get_full_name() {
1862         return 's_'.$this->plugin.'_'.$this->name;
1863     }
1865     /**
1866      * Returns the ID string based on plugin and name
1867      * @return string
1868      */
1869     public function get_id() {
1870         return 'id_s_'.$this->plugin.'_'.$this->name;
1871     }
1873     /**
1874      * @param bool $affectsmodinfo If true, changes to this setting will
1875      *   cause the course cache to be rebuilt
1876      */
1877     public function set_affects_modinfo($affectsmodinfo) {
1878         $this->affectsmodinfo = $affectsmodinfo;
1879     }
1881     /**
1882      * Returns the config if possible
1883      *
1884      * @return mixed returns config if successful else null
1885      */
1886     public function config_read($name) {
1887         global $CFG;
1888         if (!empty($this->plugin)) {
1889             $value = get_config($this->plugin, $name);
1890             return $value === false ? NULL : $value;
1892         } else {
1893             if (isset($CFG->$name)) {
1894                 return $CFG->$name;
1895             } else {
1896                 return NULL;
1897             }
1898         }
1899     }
1901     /**
1902      * Used to set a config pair and log change
1903      *
1904      * @param string $name
1905      * @param mixed $value Gets converted to string if not null
1906      * @return bool Write setting to config table
1907      */
1908     public function config_write($name, $value) {
1909         global $DB, $USER, $CFG;
1911         if ($this->nosave) {
1912             return true;
1913         }
1915         // make sure it is a real change
1916         $oldvalue = get_config($this->plugin, $name);
1917         $oldvalue = ($oldvalue === false) ? null : $oldvalue; // normalise
1918         $value = is_null($value) ? null : (string)$value;
1920         if ($oldvalue === $value) {
1921             return true;
1922         }
1924         // store change
1925         set_config($name, $value, $this->plugin);
1927         // Some admin settings affect course modinfo
1928         if ($this->affectsmodinfo) {
1929             // Clear course cache for all courses
1930             rebuild_course_cache(0, true);
1931         }
1933         $this->add_to_config_log($name, $oldvalue, $value);
1935         return true; // BC only
1936     }
1938     /**
1939      * Log config changes if necessary.
1940      * @param string $name
1941      * @param string $oldvalue
1942      * @param string $value
1943      */
1944     protected function add_to_config_log($name, $oldvalue, $value) {
1945         add_to_config_log($name, $oldvalue, $value, $this->plugin);
1946     }
1948     /**
1949      * Returns current value of this setting
1950      * @return mixed array or string depending on instance, NULL means not set yet
1951      */
1952     public abstract function get_setting();
1954     /**
1955      * Returns default setting if exists
1956      * @return mixed array or string depending on instance; NULL means no default, user must supply
1957      */
1958     public function get_defaultsetting() {
1959         $adminroot =  admin_get_root(false, false);
1960         if (!empty($adminroot->custom_defaults)) {
1961             $plugin = is_null($this->plugin) ? 'moodle' : $this->plugin;
1962             if (isset($adminroot->custom_defaults[$plugin])) {
1963                 if (array_key_exists($this->name, $adminroot->custom_defaults[$plugin])) { // null is valid value here ;-)
1964                     return $adminroot->custom_defaults[$plugin][$this->name];
1965                 }
1966             }
1967         }
1968         return $this->defaultsetting;
1969     }
1971     /**
1972      * Store new setting
1973      *
1974      * @param mixed $data string or array, must not be NULL
1975      * @return string empty string if ok, string error message otherwise
1976      */
1977     public abstract function write_setting($data);
1979     /**
1980      * Return part of form with setting
1981      * This function should always be overwritten
1982      *
1983      * @param mixed $data array or string depending on setting
1984      * @param string $query
1985      * @return string
1986      */
1987     public function output_html($data, $query='') {
1988     // should be overridden
1989         return;
1990     }
1992     /**
1993      * Function called if setting updated - cleanup, cache reset, etc.
1994      * @param string $functionname Sets the function name
1995      * @return void
1996      */
1997     public function set_updatedcallback($functionname) {
1998         $this->updatedcallback = $functionname;
1999     }
2001     /**
2002      * Execute postupdatecallback if necessary.
2003      * @param mixed $original original value before write_setting()
2004      * @return bool true if changed, false if not.
2005      */
2006     public function post_write_settings($original) {
2007         // Comparison must work for arrays too.
2008         if (serialize($original) === serialize($this->get_setting())) {
2009             return false;
2010         }
2012         $callbackfunction = $this->updatedcallback;
2013         if (!empty($callbackfunction) and is_callable($callbackfunction)) {
2014             $callbackfunction($this->get_full_name());
2015         }
2016         return true;
2017     }
2019     /**
2020      * Is setting related to query text - used when searching
2021      * @param string $query
2022      * @return bool
2023      */
2024     public function is_related($query) {
2025         if (strpos(strtolower($this->name), $query) !== false) {
2026             return true;
2027         }
2028         if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
2029             return true;
2030         }
2031         if (strpos(core_text::strtolower($this->description), $query) !== false) {
2032             return true;
2033         }
2034         $current = $this->get_setting();
2035         if (!is_null($current)) {
2036             if (is_string($current)) {
2037                 if (strpos(core_text::strtolower($current), $query) !== false) {
2038                     return true;
2039                 }
2040             }
2041         }
2042         $default = $this->get_defaultsetting();
2043         if (!is_null($default)) {
2044             if (is_string($default)) {
2045                 if (strpos(core_text::strtolower($default), $query) !== false) {
2046                     return true;
2047                 }
2048             }
2049         }
2050         return false;
2051     }
2053     /**
2054      * Get whether this should be displayed in LTR mode.
2055      *
2056      * @return bool|null
2057      */
2058     public function get_force_ltr() {
2059         return $this->forceltr;
2060     }
2062     /**
2063      * Set whether to force LTR or not.
2064      *
2065      * @param bool $value True when forced, false when not force, null when unknown.
2066      */
2067     public function set_force_ltr($value) {
2068         $this->forceltr = $value;
2069     }
2071     /**
2072      * Add a setting to the list of those that could cause this one to be hidden
2073      * @param string $dependenton
2074      */
2075     public function add_dependent_on($dependenton) {
2076         $this->dependenton[] = $dependenton;
2077     }
2079     /**
2080      * Get a list of the settings that could cause this one to be hidden.
2081      * @return array
2082      */
2083     public function get_dependent_on() {
2084         return $this->dependenton;
2085     }
2087     /**
2088      * Whether this setting uses a custom form control.
2089      * This function is especially useful to decide if we should render a label element for this setting or not.
2090      *
2091      * @return bool
2092      */
2093     public function has_custom_form_control(): bool {
2094         return $this->customcontrol;
2095     }
2098 /**
2099  * An additional option that can be applied to an admin setting.
2100  * The currently supported options are 'ADVANCED', 'LOCKED' and 'REQUIRED'.
2101  *
2102  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2103  */
2104 class admin_setting_flag {
2105     /** @var bool Flag to indicate if this option can be toggled for this setting */
2106     private $enabled = false;
2107     /** @var bool Flag to indicate if this option defaults to true or false */
2108     private $default = false;
2109     /** @var string Short string used to create setting name - e.g. 'adv' */
2110     private $shortname = '';
2111     /** @var string String used as the label for this flag */
2112     private $displayname = '';
2113     /** @const Checkbox for this flag is displayed in admin page */
2114     const ENABLED = true;
2115     /** @const Checkbox for this flag is not displayed in admin page */
2116     const DISABLED = false;
2118     /**
2119      * Constructor
2120      *
2121      * @param bool $enabled Can this option can be toggled.
2122      *                      Should be one of admin_setting_flag::ENABLED or admin_setting_flag::DISABLED.
2123      * @param bool $default The default checked state for this setting option.
2124      * @param string $shortname The shortname of this flag. Currently supported flags are 'locked' and 'adv'
2125      * @param string $displayname The displayname of this flag. Used as a label for the flag.
2126      */
2127     public function __construct($enabled, $default, $shortname, $displayname) {
2128         $this->shortname = $shortname;
2129         $this->displayname = $displayname;
2130         $this->set_options($enabled, $default);
2131     }
2133     /**
2134      * Update the values of this setting options class
2135      *
2136      * @param bool $enabled Can this option can be toggled.
2137      *                      Should be one of admin_setting_flag::ENABLED or admin_setting_flag::DISABLED.
2138      * @param bool $default The default checked state for this setting option.
2139      */
2140     public function set_options($enabled, $default) {
2141         $this->enabled = $enabled;
2142         $this->default = $default;
2143     }
2145     /**
2146      * Should this option appear in the interface and be toggleable?
2147      *
2148      * @return bool Is it enabled?
2149      */
2150     public function is_enabled() {
2151         return $this->enabled;
2152     }
2154     /**
2155      * Should this option be checked by default?
2156      *
2157      * @return bool Is it on by default?
2158      */
2159     public function get_default() {
2160         return $this->default;
2161     }
2163     /**
2164      * Return the short name for this flag. e.g. 'adv' or 'locked'
2165      *
2166      * @return string
2167      */
2168     public function get_shortname() {
2169         return $this->shortname;
2170     }
2172     /**
2173      * Return the display name for this flag. e.g. 'Advanced' or 'Locked'
2174      *
2175      * @return string
2176      */
2177     public function get_displayname() {
2178         return $this->displayname;
2179     }
2181     /**
2182      * Save the submitted data for this flag - or set it to the default if $data is null.
2183      *
2184      * @param admin_setting $setting - The admin setting for this flag
2185      * @param array $data - The data submitted from the form or null to set the default value for new installs.
2186      * @return bool
2187      */
2188     public function write_setting_flag(admin_setting $setting, $data) {
2189         $result = true;
2190         if ($this->is_enabled()) {
2191             if (!isset($data)) {
2192                 $value = $this->get_default();
2193             } else {
2194                 $value = !empty($data[$setting->get_full_name() . '_' . $this->get_shortname()]);
2195             }
2196             $result = $setting->config_write($setting->name . '_' . $this->get_shortname(), $value);
2197         }
2199         return $result;
2201     }
2203     /**
2204      * Output the checkbox for this setting flag. Should only be called if the flag is enabled.
2205      *
2206      * @param admin_setting $setting - The admin setting for this flag
2207      * @return string - The html for the checkbox.
2208      */
2209     public function output_setting_flag(admin_setting $setting) {
2210         global $OUTPUT;
2212         $value = $setting->get_setting_flag_value($this);
2214         $context = new stdClass();
2215         $context->id = $setting->get_id() . '_' . $this->get_shortname();
2216         $context->name = $setting->get_full_name() .  '_' . $this->get_shortname();
2217         $context->value = 1;
2218         $context->checked = $value ? true : false;
2219         $context->label = $this->get_displayname();
2221         return $OUTPUT->render_from_template('core_admin/setting_flag', $context);
2222     }
2226 /**
2227  * No setting - just heading and text.
2228  *
2229  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2230  */
2231 class admin_setting_heading extends admin_setting {
2233     /**
2234      * not a setting, just text
2235      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2236      * @param string $heading heading
2237      * @param string $information text in box
2238      */
2239     public function __construct($name, $heading, $information) {
2240         $this->nosave = true;
2241         parent::__construct($name, $heading, $information, '');
2242     }
2244     /**
2245      * Always returns true
2246      * @return bool Always returns true
2247      */
2248     public function get_setting() {
2249         return true;
2250     }
2252     /**
2253      * Always returns true
2254      * @return bool Always returns true
2255      */
2256     public function get_defaultsetting() {
2257         return true;
2258     }
2260     /**
2261      * Never write settings
2262      * @return string Always returns an empty string
2263      */
2264     public function write_setting($data) {
2265     // do not write any setting
2266         return '';
2267     }
2269     /**
2270      * Returns an HTML string
2271      * @return string Returns an HTML string
2272      */
2273     public function output_html($data, $query='') {
2274         global $OUTPUT;
2275         $context = new stdClass();
2276         $context->title = $this->visiblename;
2277         $context->description = $this->description;
2278         $context->descriptionformatted = highlight($query, markdown_to_html($this->description));
2279         return $OUTPUT->render_from_template('core_admin/setting_heading', $context);
2280     }
2283 /**
2284  * No setting - just name and description in same row.
2285  *
2286  * @copyright 2018 onwards Amaia Anabitarte
2287  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2288  */
2289 class admin_setting_description extends admin_setting {
2291     /**
2292      * Not a setting, just text
2293      *
2294      * @param string $name
2295      * @param string $visiblename
2296      * @param string $description
2297      */
2298     public function __construct($name, $visiblename, $description) {
2299         $this->nosave = true;
2300         parent::__construct($name, $visiblename, $description, '');
2301     }
2303     /**
2304      * Always returns true
2305      *
2306      * @return bool Always returns true
2307      */
2308     public function get_setting() {
2309         return true;
2310     }
2312     /**
2313      * Always returns true
2314      *
2315      * @return bool Always returns true
2316      */
2317     public function get_defaultsetting() {
2318         return true;
2319     }
2321     /**
2322      * Never write settings
2323      *
2324      * @param mixed $data Gets converted to str for comparison against yes value
2325      * @return string Always returns an empty string
2326      */
2327     public function write_setting($data) {
2328         // Do not write any setting.
2329         return '';
2330     }
2332     /**
2333      * Returns an HTML string
2334      *
2335      * @param string $data
2336      * @param string $query
2337      * @return string Returns an HTML string
2338      */
2339     public function output_html($data, $query='') {
2340         global $OUTPUT;
2342         $context = new stdClass();
2343         $context->title = $this->visiblename;
2344         $context->description = $this->description;
2346         return $OUTPUT->render_from_template('core_admin/setting_description', $context);
2347     }
2352 /**
2353  * The most flexible setting, the user enters text.
2354  *
2355  * This type of field should be used for config settings which are using
2356  * English words and are not localised (passwords, database name, list of values, ...).
2357  *
2358  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2359  */
2360 class admin_setting_configtext extends admin_setting {
2362     /** @var mixed int means PARAM_XXX type, string is a allowed format in regex */
2363     public $paramtype;
2364     /** @var int default field size */
2365     public $size;
2367     /**
2368      * Config text constructor
2369      *
2370      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2371      * @param string $visiblename localised
2372      * @param string $description long localised info
2373      * @param string $defaultsetting
2374      * @param mixed $paramtype int means PARAM_XXX type, string is a allowed format in regex
2375      * @param int $size default field size
2376      */
2377     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $size=null) {
2378         $this->paramtype = $paramtype;
2379         if (!is_null($size)) {
2380             $this->size  = $size;
2381         } else {
2382             $this->size  = ($paramtype === PARAM_INT) ? 5 : 30;
2383         }
2384         parent::__construct($name, $visiblename, $description, $defaultsetting);
2385     }
2387     /**
2388      * Get whether this should be displayed in LTR mode.
2389      *
2390      * Try to guess from the PARAM type unless specifically set.
2391      */
2392     public function get_force_ltr() {
2393         $forceltr = parent::get_force_ltr();
2394         if ($forceltr === null) {
2395             return !is_rtl_compatible($this->paramtype);
2396         }
2397         return $forceltr;
2398     }
2400     /**
2401      * Return the setting
2402      *
2403      * @return mixed returns config if successful else null
2404      */
2405     public function get_setting() {
2406         return $this->config_read($this->name);
2407     }
2409     public function write_setting($data) {
2410         if ($this->paramtype === PARAM_INT and $data === '') {
2411         // do not complain if '' used instead of 0
2412             $data = 0;
2413         }
2414         // $data is a string
2415         $validated = $this->validate($data);
2416         if ($validated !== true) {
2417             return $validated;
2418         }
2419         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
2420     }
2422     /**
2423      * Validate data before storage
2424      * @param string data
2425      * @return mixed true if ok string if error found
2426      */
2427     public function validate($data) {
2428         // allow paramtype to be a custom regex if it is the form of /pattern/
2429         if (preg_match('#^/.*/$#', $this->paramtype)) {
2430             if (preg_match($this->paramtype, $data)) {
2431                 return true;
2432             } else {
2433                 return get_string('validateerror', 'admin');
2434             }
2436         } else if ($this->paramtype === PARAM_RAW) {
2437             return true;
2439         } else {
2440             $cleaned = clean_param($data, $this->paramtype);
2441             if ("$data" === "$cleaned") { // implicit conversion to string is needed to do exact comparison
2442                 return true;
2443             } else {
2444                 return get_string('validateerror', 'admin');
2445             }
2446         }
2447     }
2449     /**
2450      * Return an XHTML string for the setting
2451      * @return string Returns an XHTML string
2452      */
2453     public function output_html($data, $query='') {
2454         global $OUTPUT;
2456         $default = $this->get_defaultsetting();
2457         $context = (object) [
2458             'size' => $this->size,
2459             'id' => $this->get_id(),
2460             'name' => $this->get_full_name(),
2461             'value' => $data,
2462             'forceltr' => $this->get_force_ltr(),
2463         ];
2464         $element = $OUTPUT->render_from_template('core_admin/setting_configtext', $context);
2466         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2467     }
2470 /**
2471  * Text input with a maximum length constraint.
2472  *
2473  * @copyright 2015 onwards Ankit Agarwal
2474  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2475  */
2476 class admin_setting_configtext_with_maxlength extends admin_setting_configtext {
2478     /** @var int maximum number of chars allowed. */
2479     protected $maxlength;
2481     /**
2482      * Config text constructor
2483      *
2484      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
2485      *                     or 'myplugin/mysetting' for ones in config_plugins.
2486      * @param string $visiblename localised
2487      * @param string $description long localised info
2488      * @param string $defaultsetting
2489      * @param mixed $paramtype int means PARAM_XXX type, string is a allowed format in regex
2490      * @param int $size default field size
2491      * @param mixed $maxlength int maxlength allowed, 0 for infinite.
2492      */
2493     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW,
2494                                 $size=null, $maxlength = 0) {
2495         $this->maxlength = $maxlength;
2496         parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype, $size);
2497     }
2499     /**
2500      * Validate data before storage
2501      *
2502      * @param string $data data
2503      * @return mixed true if ok string if error found
2504      */
2505     public function validate($data) {
2506         $parentvalidation = parent::validate($data);
2507         if ($parentvalidation === true) {
2508             if ($this->maxlength > 0) {
2509                 // Max length check.
2510                 $length = core_text::strlen($data);
2511                 if ($length > $this->maxlength) {
2512                     return get_string('maximumchars', 'moodle',  $this->maxlength);
2513                 }
2514                 return true;
2515             } else {
2516                 return true; // No max length check needed.
2517             }
2518         } else {
2519             return $parentvalidation;
2520         }
2521     }
2524 /**
2525  * General text area without html editor.
2526  *
2527  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2528  */
2529 class admin_setting_configtextarea extends admin_setting_configtext {
2530     private $rows;
2531     private $cols;
2533     /**
2534      * @param string $name
2535      * @param string $visiblename
2536      * @param string $description
2537      * @param mixed $defaultsetting string or array
2538      * @param mixed $paramtype
2539      * @param string $cols The number of columns to make the editor
2540      * @param string $rows The number of rows to make the editor
2541      */
2542     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $cols='60', $rows='8') {
2543         $this->rows = $rows;
2544         $this->cols = $cols;
2545         parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype);
2546     }
2548     /**
2549      * Returns an XHTML string for the editor
2550      *
2551      * @param string $data
2552      * @param string $query
2553      * @return string XHTML string for the editor
2554      */
2555     public function output_html($data, $query='') {
2556         global $OUTPUT;
2558         $default = $this->get_defaultsetting();
2559         $defaultinfo = $default;
2560         if (!is_null($default) and $default !== '') {
2561             $defaultinfo = "\n".$default;
2562         }
2564         $context = (object) [
2565             'cols' => $this->cols,
2566             'rows' => $this->rows,
2567             'id' => $this->get_id(),
2568             'name' => $this->get_full_name(),
2569             'value' => $data,
2570             'forceltr' => $this->get_force_ltr(),
2571         ];
2572         $element = $OUTPUT->render_from_template('core_admin/setting_configtextarea', $context);
2574         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
2575     }
2578 /**
2579  * General text area with html editor.
2580  */
2581 class admin_setting_confightmleditor extends admin_setting_configtextarea {
2583     /**
2584      * @param string $name
2585      * @param string $visiblename
2586      * @param string $description
2587      * @param mixed $defaultsetting string or array
2588      * @param mixed $paramtype
2589      */
2590     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $cols='60', $rows='8') {
2591         parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype, $cols, $rows);
2592         $this->set_force_ltr(false);
2593         editors_head_setup();
2594     }
2596     /**
2597      * Returns an XHTML string for the editor
2598      *
2599      * @param string $data
2600      * @param string $query
2601      * @return string XHTML string for the editor
2602      */
2603     public function output_html($data, $query='') {
2604         $editor = editors_get_preferred_editor(FORMAT_HTML);
2605         $editor->set_text($data);
2606         $editor->use_editor($this->get_id(), array('noclean'=>true));
2607         return parent::output_html($data, $query);
2608     }
2610     /**
2611      * Checks if data has empty html.
2612      *
2613      * @param string $data
2614      * @return string Empty when no errors.
2615      */
2616     public function write_setting($data) {
2617         if (trim(html_to_text($data)) === '') {
2618             $data = '';
2619         }
2620         return parent::write_setting($data);
2621     }
2625 /**
2626  * Password field, allows unmasking of password
2627  *
2628  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2629  */
2630 class admin_setting_configpasswordunmask extends admin_setting_configtext {
2632     /**
2633      * Constructor
2634      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2635      * @param string $visiblename localised
2636      * @param string $description long localised info
2637      * @param string $defaultsetting default password
2638      */
2639     public function __construct($name, $visiblename, $description, $defaultsetting) {
2640         parent::__construct($name, $visiblename, $description, $defaultsetting, PARAM_RAW, 30);
2641     }
2643     /**
2644      * Log config changes if necessary.
2645      * @param string $name
2646      * @param string $oldvalue
2647      * @param string $value
2648      */
2649     protected function add_to_config_log($name, $oldvalue, $value) {
2650         if ($value !== '') {
2651             $value = '********';
2652         }
2653         if ($oldvalue !== '' and $oldvalue !== null) {
2654             $oldvalue = '********';
2655         }
2656         parent::add_to_config_log($name, $oldvalue, $value);
2657     }
2659     /**
2660      * Returns HTML for the field.
2661      *
2662      * @param   string  $data       Value for the field
2663      * @param   string  $query      Passed as final argument for format_admin_setting
2664      * @return  string              Rendered HTML
2665      */
2666     public function output_html($data, $query='') {
2667         global $OUTPUT, $CFG;
2668         $forced = false;
2669         if (empty($this->plugin)) {
2670             if (array_key_exists($this->name, $CFG->config_php_settings)) {
2671                 $forced = true;
2672             }
2673         } else {
2674             if (array_key_exists($this->plugin, $CFG->forced_plugin_settings)
2675                 and array_key_exists($this->name, $CFG->forced_plugin_settings[$this->plugin])) {
2676                 $forced = true;
2677             }
2678         }
2679         $context = (object) [
2680             'id' => $this->get_id(),
2681             'name' => $this->get_full_name(),
2682             'size' => $this->size,
2683             'value' => $forced ? null : $data,
2684             'forceltr' => $this->get_force_ltr(),
2685             'forced' => $forced
2686         ];
2687         $element = $OUTPUT->render_from_template('core_admin/setting_configpasswordunmask', $context);
2688         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', null, $query);
2689     }
2692 /**
2693  * Password field, allows unmasking of password, with an advanced checkbox that controls an additional $name.'_adv' setting.
2694  *
2695  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2696  * @copyright 2018 Paul Holden (pholden@greenhead.ac.uk)
2697  */
2698 class admin_setting_configpasswordunmask_with_advanced extends admin_setting_configpasswordunmask {
2700     /**
2701      * Constructor
2702      *
2703      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2704      * @param string $visiblename localised
2705      * @param string $description long localised info
2706      * @param array $defaultsetting ('value'=>string, 'adv'=>bool)
2707      */
2708     public function __construct($name, $visiblename, $description, $defaultsetting) {
2709         parent::__construct($name, $visiblename, $description, $defaultsetting['value']);
2710         $this->set_advanced_flag_options(admin_setting_flag::ENABLED, !empty($defaultsetting['adv']));
2711     }
2714 /**
2715  * Empty setting used to allow flags (advanced) on settings that can have no sensible default.
2716  * Note: Only advanced makes sense right now - locked does not.
2717  *
2718  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2719  */
2720 class admin_setting_configempty extends admin_setting_configtext {
2722     /**
2723      * @param string $name
2724      * @param string $visiblename
2725      * @param string $description
2726      */
2727     public function __construct($name, $visiblename, $description) {
2728         parent::__construct($name, $visiblename, $description, '', PARAM_RAW);
2729     }
2731     /**
2732      * Returns an XHTML string for the hidden field
2733      *
2734      * @param string $data
2735      * @param string $query
2736      * @return string XHTML string for the editor
2737      */
2738     public function output_html($data, $query='') {
2739         global $OUTPUT;
2741         $context = (object) [
2742             'id' => $this->get_id(),
2743             'name' => $this->get_full_name()
2744         ];
2745         $element = $OUTPUT->render_from_template('core_admin/setting_configempty', $context);
2747         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', get_string('none'), $query);
2748     }
2752 /**
2753  * Path to directory
2754  *
2755  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2756  */
2757 class admin_setting_configfile extends admin_setting_configtext {
2758     /**
2759      * Constructor
2760      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2761      * @param string $visiblename localised
2762      * @param string $description long localised info
2763      * @param string $defaultdirectory default directory location
2764      */
2765     public function __construct($name, $visiblename, $description, $defaultdirectory) {
2766         parent::__construct($name, $visiblename, $description, $defaultdirectory, PARAM_RAW, 50);
2767     }
2769     /**
2770      * Returns XHTML for the field
2771      *
2772      * Returns XHTML for the field and also checks whether the file
2773      * specified in $data exists using file_exists()
2774      *
2775      * @param string $data File name and path to use in value attr
2776      * @param string $query
2777      * @return string XHTML field
2778      */
2779     public function output_html($data, $query='') {
2780         global $CFG, $OUTPUT;
2782         $default = $this->get_defaultsetting();
2783         $context = (object) [
2784             'id' => $this->get_id(),
2785             'name' => $this->get_full_name(),
2786             'size' => $this->size,
2787             'value' => $data,
2788             'showvalidity' => !empty($data),
2789             'valid' => $data && file_exists($data),
2790             'readonly' => !empty($CFG->preventexecpath),
2791             'forceltr' => $this->get_force_ltr(),
2792         ];
2794         if ($context->readonly) {
2795             $this->visiblename .= '<div class="alert alert-info">'.get_string('execpathnotallowed', 'admin').'</div>';
2796         }
2798         $element = $OUTPUT->render_from_template('core_admin/setting_configfile', $context);
2800         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2801     }
2803     /**
2804      * Checks if execpatch has been disabled in config.php
2805      */
2806     public function write_setting($data) {
2807         global $CFG;
2808         if (!empty($CFG->preventexecpath)) {
2809             if ($this->get_setting() === null) {
2810                 // Use default during installation.
2811                 $data = $this->get_defaultsetting();
2812                 if ($data === null) {
2813                     $data = '';
2814                 }
2815             } else {
2816                 return '';
2817             }
2818         }
2819         return parent::write_setting($data);
2820     }
2825 /**
2826  * Path to executable file
2827  *
2828  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2829  */
2830 class admin_setting_configexecutable extends admin_setting_configfile {
2832     /**
2833      * Returns an XHTML field
2834      *
2835      * @param string $data This is the value for the field
2836      * @param string $query
2837      * @return string XHTML field
2838      */
2839     public function output_html($data, $query='') {
2840         global $CFG, $OUTPUT;
2841         $default = $this->get_defaultsetting();
2842         require_once("$CFG->libdir/filelib.php");
2844         $context = (object) [
2845             'id' => $this->get_id(),
2846             'name' => $this->get_full_name(),
2847             'size' => $this->size,
2848             'value' => $data,
2849             'showvalidity' => !empty($data),
2850             'valid' => $data && file_exists($data) && !is_dir($data) && file_is_executable($data),
2851             'readonly' => !empty($CFG->preventexecpath),
2852             'forceltr' => $this->get_force_ltr()
2853         ];
2855         if (!empty($CFG->preventexecpath)) {
2856             $this->visiblename .= '<div class="alert alert-info">'.get_string('execpathnotallowed', 'admin').'</div>';
2857         }
2859         $element = $OUTPUT->render_from_template('core_admin/setting_configexecutable', $context);
2861         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2862     }
2866 /**
2867  * Path to directory
2868  *
2869  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2870  */
2871 class admin_setting_configdirectory extends admin_setting_configfile {
2873     /**
2874      * Returns an XHTML field
2875      *
2876      * @param string $data This is the value for the field
2877      * @param string $query
2878      * @return string XHTML
2879      */
2880     public function output_html($data, $query='') {
2881         global $CFG, $OUTPUT;
2882         $default = $this->get_defaultsetting();
2884         $context = (object) [
2885             'id' => $this->get_id(),
2886             'name' => $this->get_full_name(),
2887             'size' => $this->size,
2888             'value' => $data,
2889             'showvalidity' => !empty($data),
2890             'valid' => $data && file_exists($data) && is_dir($data),
2891             'readonly' => !empty($CFG->preventexecpath),
2892             'forceltr' => $this->get_force_ltr()
2893         ];
2895         if (!empty($CFG->preventexecpath)) {
2896             $this->visiblename .= '<div class="alert alert-info">'.get_string('execpathnotallowed', 'admin').'</div>';
2897         }
2899         $element = $OUTPUT->render_from_template('core_admin/setting_configdirectory', $context);
2901         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2902     }
2906 /**
2907  * Checkbox
2908  *
2909  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2910  */
2911 class admin_setting_configcheckbox extends admin_setting {
2912     /** @var string Value used when checked */
2913     public $yes;
2914     /** @var string Value used when not checked */
2915     public $no;
2917     /**
2918      * Constructor
2919      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2920      * @param string $visiblename localised
2921      * @param string $description long localised info
2922      * @param string $defaultsetting
2923      * @param string $yes value used when checked
2924      * @param string $no value used when not checked
2925      */
2926     public function __construct($name, $visiblename, $description, $defaultsetting, $yes='1', $no='0') {
2927         parent::__construct($name, $visiblename, $description, $defaultsetting);
2928         $this->yes = (string)$yes;
2929         $this->no  = (string)$no;
2930     }
2932     /**
2933      * Retrieves the current setting using the objects name
2934      *
2935      * @return string
2936      */
2937     public function get_setting() {
2938         return $this->config_read($this->name);
2939     }
2941     /**
2942      * Sets the value for the setting
2943      *
2944      * Sets the value for the setting to either the yes or no values
2945      * of the object by comparing $data to yes
2946      *
2947      * @param mixed $data Gets converted to str for comparison against yes value
2948      * @return string empty string or error
2949      */
2950     public function write_setting($data) {
2951         if ((string)$data === $this->yes) { // convert to strings before comparison
2952             $data = $this->yes;
2953         } else {
2954             $data = $this->no;
2955         }
2956         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
2957     }
2959     /**
2960      * Returns an XHTML checkbox field
2961      *
2962      * @param string $data If $data matches yes then checkbox is checked
2963      * @param string $query
2964      * @return string XHTML field
2965      */
2966     public function output_html($data, $query='') {
2967         global $OUTPUT;
2969         $context = (object) [
2970             'id' => $this->get_id(),
2971             'name' => $this->get_full_name(),
2972             'no' => $this->no,
2973             'value' => $this->yes,
2974             'checked' => (string) $data === $this->yes,
2975         ];
2977         $default = $this->get_defaultsetting();
2978         if (!is_null($default)) {
2979             if ((string)$default === $this->yes) {
2980                 $defaultinfo = get_string('checkboxyes', 'admin');
2981             } else {
2982                 $defaultinfo = get_string('checkboxno', 'admin');
2983             }
2984         } else {
2985             $defaultinfo = NULL;
2986         }
2988         $element = $OUTPUT->render_from_template('core_admin/setting_configcheckbox', $context);
2990         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
2991     }
2995 /**
2996  * Multiple checkboxes, each represents different value, stored in csv format
2997  *
2998  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2999  */
3000 class admin_setting_configmulticheckbox extends admin_setting {
3001     /** @var array Array of choices value=>label */
3002     public $choices;
3004     /**
3005      * Constructor: uses parent::__construct
3006      *
3007      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3008      * @param string $visiblename localised
3009      * @param string $description long localised info
3010      * @param array $defaultsetting array of selected
3011      * @param array $choices array of $value=>$label for each checkbox
3012      */
3013     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
3014         $this->choices = $choices;
3015         parent::__construct($name, $visiblename, $description, $defaultsetting);
3016     }
3018     /**
3019      * This public function may be used in ancestors for lazy loading of choices
3020      *
3021      * @todo Check if this function is still required content commented out only returns true
3022      * @return bool true if loaded, false if error
3023      */
3024     public function load_choices() {
3025         /*
3026         if (is_array($this->choices)) {
3027             return true;
3028         }
3029         .... load choices here
3030         */
3031         return true;
3032     }
3034     /**
3035      * Is setting related to query text - used when searching
3036      *
3037      * @param string $query
3038      * @return bool true on related, false on not or failure
3039      */
3040     public function is_related($query) {
3041         if (!$this->load_choices() or empty($this->choices)) {
3042             return false;
3043         }
3044         if (parent::is_related($query)) {
3045             return true;
3046         }
3048         foreach ($this->choices as $desc) {
3049             if (strpos(core_text::strtolower($desc), $query) !== false) {
3050                 return true;
3051             }
3052         }
3053         return false;
3054     }
3056     /**
3057      * Returns the current setting if it is set
3058      *
3059      * @return mixed null if null, else an array
3060      */
3061     public function get_setting() {
3062         $result = $this->config_read($this->name);
3064         if (is_null($result)) {
3065             return NULL;
3066         }
3067         if ($result === '') {
3068             return array();
3069         }
3070         $enabled = explode(',', $result);
3071         $setting = array();
3072         foreach ($enabled as $option) {
3073             $setting[$option] = 1;
3074         }
3075         return $setting;
3076     }
3078     /**
3079      * Saves the setting(s) provided in $data
3080      *
3081      * @param array $data An array of data, if not array returns empty str
3082      * @return mixed empty string on useless data or bool true=success, false=failed
3083      */
3084     public function write_setting($data) {
3085         if (!is_array($data)) {
3086             return ''; // ignore it
3087         }
3088         if (!$this->load_choices() or empty($this->choices)) {
3089             return '';
3090         }
3091         unset($data['xxxxx']);
3092         $result = array();
3093         foreach ($data as $key => $value) {
3094             if ($value and array_key_exists($key, $this->choices)) {
3095                 $result[] = $key;
3096             }
3097         }
3098         return $this->config_write($this->name, implode(',', $result)) ? '' : get_string('errorsetting', 'admin');
3099     }
3101     /**
3102      * Returns XHTML field(s) as required by choices
3103      *
3104      * Relies on data being an array should data ever be another valid vartype with
3105      * acceptable value this may cause a warning/error
3106      * if (!is_array($data)) would fix the problem
3107      *
3108      * @todo Add vartype handling to ensure $data is an array
3109      *
3110      * @param array $data An array of checked values
3111      * @param string $query
3112      * @return string XHTML field
3113      */
3114     public function output_html($data, $query='') {
3115         global $OUTPUT;
3117         if (!$this->load_choices() or empty($this->choices)) {
3118             return '';
3119         }
3121         $default = $this->get_defaultsetting();
3122         if (is_null($default)) {
3123             $default = array();
3124         }
3125         if (is_null($data)) {
3126             $data = array();
3127         }
3129         $context = (object) [
3130             'id' => $this->get_id(),
3131             'name' => $this->get_full_name(),
3132         ];
3134         $options = array();
3135         $defaults = array();
3136         foreach ($this->choices as $key => $description) {
3137             if (!empty($default[$key])) {
3138                 $defaults[] = $description;
3139             }
3141             $options[] = [
3142                 'key' => $key,
3143                 'checked' => !empty($data[$key]),
3144                 'label' => highlightfast($query, $description)
3145             ];
3146         }
3148         if (is_null($default)) {
3149             $defaultinfo = null;
3150         } else if (!empty($defaults)) {
3151             $defaultinfo = implode(', ', $defaults);
3152         } else {
3153             $defaultinfo = get_string('none');
3154         }
3156         $context->options = $options;
3157         $context->hasoptions = !empty($options);
3159         $element = $OUTPUT->render_from_template('core_admin/setting_configmulticheckbox', $context);
3161         return format_admin_setting($this, $this->visiblename, $element, $this->description, false, '', $defaultinfo, $query);
3163     }
3167 /**
3168  * Multiple checkboxes 2, value stored as string 00101011
3169  *
3170  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3171  */
3172 class admin_setting_configmulticheckbox2 extends admin_setting_configmulticheckbox {
3174     /**
3175      * Returns the setting if set
3176      *
3177      * @return mixed null if not set, else an array of set settings
3178      */
3179     public function get_setting() {
3180         $result = $this->config_read($this->name);
3181         if (is_null($result)) {
3182             return NULL;
3183         }
3184         if (!$this->load_choices()) {
3185             return NULL;
3186         }
3187         $result = str_pad($result, count($this->choices), '0');
3188         $result = preg_split('//', $result, -1, PREG_SPLIT_NO_EMPTY);
3189         $setting = array();
3190         foreach ($this->choices as $key=>$unused) {
3191             $value = array_shift($result);
3192             if ($value) {
3193                 $setting[$key] = 1;
3194             }
3195         }
3196         return $setting;
3197     }
3199     /**
3200      * Save setting(s) provided in $data param
3201      *
3202      * @param array $data An array of settings to save
3203      * @return mixed empty string for bad data or bool true=>success, false=>error
3204      */
3205     public function write_setting($data) {
3206         if (!is_array($data)) {
3207             return ''; // ignore it
3208         }
3209         if (!$this->load_choices() or empty($this->choices)) {
3210             return '';
3211         }
3212         $result = '';
3213         foreach ($this->choices as $key=>$unused) {
3214             if (!empty($data[$key])) {
3215                 $result .= '1';
3216             } else {
3217                 $result .= '0';
3218             }
3219         }
3220         return $this->config_write($this->name, $result) ? '' : get_string('errorsetting', 'admin');
3221     }
3225 /**
3226  * Select one value from list
3227  *
3228  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3229  */
3230 class admin_setting_configselect extends admin_setting {
3231     /** @var array Array of choices value=>label */
3232     public $choices;
3233     /** @var array Array of choices grouped using optgroups */
3234     public $optgroups;
3235     /** @var callable|null Loader function for choices */
3236     protected $choiceloader = null;
3237     /** @var callable|null Validation function */
3238     protected $validatefunction = null;
3240     /**
3241      * Constructor.
3242      *
3243      * If you want to lazy-load the choices, pass a callback function that returns a choice
3244      * array for the $choices parameter.
3245      *
3246      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3247      * @param string $visiblename localised
3248      * @param string $description long localised info
3249      * @param string|int $defaultsetting
3250      * @param array|callable|null $choices array of $value=>$label for each selection, or callback
3251      */
3252     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
3253         // Look for optgroup and single options.
3254         if (is_array($choices)) {
3255             $this->choices = [];
3256             foreach ($choices as $key => $val) {
3257                 if (is_array($val)) {
3258                     $this->optgroups[$key] = $val;
3259                     $this->choices = array_merge($this->choices, $val);
3260                 } else {
3261                     $this->choices[$key] = $val;
3262                 }
3263             }
3264         }
3265         if (is_callable($choices)) {
3266             $this->choiceloader = $choices;
3267         }
3269         parent::__construct($name, $visiblename, $description, $defaultsetting);
3270     }
3272     /**
3273      * Sets a validate function.
3274      *
3275      * The callback will be passed one parameter, the new setting value, and should return either
3276      * an empty string '' if the value is OK, or an error message if not.
3277      *
3278      * @param callable|null $validatefunction Validate function or null to clear
3279      * @since Moodle 4.0
3280      */
3281     public function set_validate_function(?callable $validatefunction = null) {
3282         $this->validatefunction = $validatefunction;
3283     }
3285     /**
3286      * This function may be used in ancestors for lazy loading of choices
3287      *
3288      * Override this method if loading of choices is expensive, such
3289      * as when it requires multiple db requests.
3290      *
3291      * @return bool true if loaded, false if error
3292      */
3293     public function load_choices() {
3294         if ($this->choiceloader) {
3295             if (!is_array($this->choices)) {
3296                 $this->choices = call_user_func($this->choiceloader);
3297             }
3298             return true;
3299         }
3300         return true;
3301     }
3303     /**
3304      * Check if this is $query is related to a choice
3305      *
3306      * @param string $query
3307      * @return bool true if related, false if not
3308      */
3309     public function is_related($query) {
3310         if (parent::is_related($query)) {
3311             return true;
3312         }
3313         if (!$this->load_choices()) {
3314             return false;
3315         }
3316         foreach ($this->choices as $key=>$value) {
3317             if (strpos(core_text::strtolower($key), $query) !== false) {
3318                 return true;
3319             }
3320             if (strpos(core_text::strtolower($value), $query) !== false) {
3321                 return true;
3322             }
3323         }
3324         return false;
3325     }
3327     /**
3328      * Return the setting
3329      *
3330      * @return mixed returns config if successful else null
3331      */
3332     public function get_setting() {
3333         return $this->config_read($this->name);
3334     }
3336     /**
3337      * Save a setting
3338      *
3339      * @param string $data
3340      * @return string empty of error string
3341      */
3342     public function write_setting($data) {
3343         if (!$this->load_choices() or empty($this->choices)) {
3344             return '';
3345         }
3346         if (!array_key_exists($data, $this->choices)) {
3347             return ''; // ignore it
3348         }
3350         // Validate the new setting.
3351         $error = $this->validate_setting($data);
3352         if ($error) {
3353             return $error;
3354         }
3356         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
3357     }
3359     /**
3360      * Validate the setting. This uses the callback function if provided; subclasses could override
3361      * to carry out validation directly in the class.
3362      *
3363      * @param string $data New value being set
3364      * @return string Empty string if valid, or error message text
3365      * @since Moodle 4.0
3366      */
3367     protected function validate_setting(string $data): string {
3368         // If validation function is specified, call it now.
3369         if ($this->validatefunction) {
3370             return call_user_func($this->validatefunction, $data);
3371         } else {
3372             return '';
3373         }
3374     }
3376     /**
3377      * Returns XHTML select field
3378      *
3379      * Ensure the options are loaded, and generate the XHTML for the select
3380      * element and any warning message. Separating this out from output_html
3381      * makes it easier to subclass this class.
3382      *
3383      * @param string $data the option to show as selected.
3384      * @param string $current the currently selected option in the database, null if none.
3385      * @param string $default the default selected option.
3386      * @return array the HTML for the select element, and a warning message.
3387      * @deprecated since Moodle 3.2
3388      */
3389     public function output_select_html($data, $current, $default, $extraname = '') {
3390         debugging('The method admin_setting_configselect::output_select_html is depreacted, do not use any more.', DEBUG_DEVELOPER);
3391     }
3393     /**
3394      * Returns XHTML select field and wrapping div(s)
3395      *
3396      * @see output_select_html()
3397      *
3398      * @param string $data the option to show as selected
3399      * @param string $query
3400      * @return string XHTML field and wrapping div
3401      */
3402     public function output_html($data, $query='') {
3403         global $OUTPUT;
3405         $default = $this->get_defaultsetting();
3406         $current = $this->get_setting();
3408         if (!$this->load_choices() || empty($this->choices)) {
3409             return '';
3410         }
3412         $context = (object) [
3413             'id' => $this->get_id(),
3414             'name' => $this->get_full_name(),
3415         ];
3417         if (!is_null($default) && array_key_exists($default, $this->choices)) {
3418             $defaultinfo = $this->choices[$default];
3419         } else {
3420             $defaultinfo = NULL;
3421         }
3423         // Warnings.
3424         $warning = '';
3425         if ($current === null) {
3426             // First run.
3427         } else if (empty($current) && (array_key_exists('', $this->choices) || array_key_exists(0, $this->choices))) {
3428             // No warning.
3429         } else if (!array_key_exists($current, $this->choices)) {
3430             $warning = get_string('warningcurrentsetting', 'admin', $current);
3431             if (!is_null($default) && $data == $current) {
3432                 $data = $default; // Use default instead of first value when showing the form.
3433             }
3434         }
3436         $options = [];
3437         $template = 'core_admin/setting_configselect';
3439         if (!empty($this->optgroups)) {
3440             $optgroups = [];
3441             foreach ($this->optgroups as $label => $choices) {
3442                 $optgroup = array('label' => $label, 'options' => []);
3443                 foreach ($choices as $value => $name) {
3444                     $optgroup['options'][] = [
3445                         'value' => $value,
3446                         'name' => $name,
3447                         'selected' => (string) $value == $data
3448                     ];
3449                     unset($this->choices[$value]);
3450                 }
3451                 $optgroups[] = $optgroup;
3452             }
3453             $context->options = $options;
3454             $context->optgroups = $optgroups;
3455             $template = 'core_admin/setting_configselect_optgroup';
3456         }
3458         foreach ($this->choices as $value => $name) {
3459             $options[] = [
3460                 'value' => $value,
3461                 'name' => $name,
3462                 'selected' => (string) $value == $data
3463             ];
3464         }
3465         $context->options = $options;
3467         $element = $OUTPUT->render_from_template($template, $context);
3469         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, $warning, $defaultinfo, $query);
3470     }
3473 /**
3474  * Select multiple items from list
3475  *
3476  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3477  */
3478 class admin_setting_configmultiselect extends admin_setting_configselect {
3479     /**
3480      * Constructor
3481      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3482      * @param string $visiblename localised
3483      * @param string $description long localised info
3484      * @param array $defaultsetting array of selected items
3485      * @param array $choices array of $value=>$label for each list item
3486      */
3487     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
3488         parent::__construct($name, $visiblename, $description, $defaultsetting, $choices);
3489     }
3491     /**
3492      * Returns the select setting(s)
3493      *
3494      * @return mixed null or array. Null if no settings else array of setting(s)
3495      */
3496     public function get_setting() {
3497         $result = $this->config_read($this->name);
3498         if (is_null($result)) {
3499             return NULL;
3500         }
3501         if ($result === '') {
3502             return array();
3503         }
3504         return explode(',', $result);
3505     }
3507     /**
3508      * Saves setting(s) provided through $data
3509      *
3510      * Potential bug in the works should anyone call with this function
3511      * using a vartype that is not an array
3512      *
3513      * @param array $data
3514      */
3515     public function write_setting($data) {
3516         if (!is_array($data)) {
3517             return ''; //ignore it
3518         }
3519         if (!$this->load_choices() or empty($this->choices)) {
3520             return '';
3521         }
3523         unset($data['xxxxx']);
3525         $save = array();
3526         foreach ($data as $value) {
3527             if (!array_key_exists($value, $this->choices)) {
3528                 continue; // ignore it
3529             }
3530             $save[] = $value;
3531         }
3533         return ($this->config_write($this->name, implode(',', $save)) ? '' : get_string('errorsetting', 'admin'));
3534     }
3536     /**
3537      * Is setting related to query text - used when searching
3538      *
3539      * @param string $query
3540      * @return bool true if related, false if not
3541      */
3542     public function is_related($query) {
3543         if (!$this->load_choices() or empty($this->choices)) {
3544             return false;
3545         }
3546         if (parent::is_related($query)) {
3547             return true;
3548         }
3550         foreach ($this->choices as $desc) {
3551             if (strpos(core_text::strtolower($desc), $query) !== false) {
3552                 return true;
3553             }
3554         }
3555         return false;
3556     }
3558     /**
3559      * Returns XHTML multi-select field
3560      *
3561      * @todo Add vartype handling to ensure $data is an array
3562      * @param array $data Array of values to select by default
3563      * @param string $query
3564      * @return string XHTML multi-select field
3565      */
3566     public function output_html($data, $query='') {
3567         global $OUTPUT;
3569         if (!$this->load_choices() or empty($this->choices)) {
3570             return '';
3571         }
3573         $default = $this->get_defaultsetting();
3574         if (is_null($default)) {
3575             $default = array();
3576         }
3577         if (is_null($data)) {
3578             $data = array();
3579         }
3581         $context = (object) [
3582             'id' => $this->get_id(),
3583             'name' => $this->get_full_name(),
3584             'size' => min(10, count($this->choices))
3585         ];
3587         $defaults = [];
3588         $options = [];
3589         $template = 'core_admin/setting_configmultiselect';
3591         if (!empty($this->optgroups)) {
3592             $optgroups = [];
3593             foreach ($this->optgroups as $label => $choices) {
3594                 $optgroup = array('label' => $label, 'options' => []);
3595                 foreach ($choices as $value => $name) {
3596                     if (in_array($value, $default)) {
3597                         $defaults[] = $name;
3598                     }
3599                     $optgroup['options'][] = [
3600                         'value' => $value,
3601                         'name' => $name,
3602                         'selected' => in_array($value, $data)
3603                     ];
3604                     unset($this->choices[$value]);
3605                 }
3606                 $optgroups[] = $optgroup;
3607             }
3608             $context->optgroups = $optgroups;
3609             $template = 'core_admin/setting_configmultiselect_optgroup';
3610         }
3612         foreach ($this->choices as $value => $name) {
3613             if (in_array($value, $default)) {
3614                 $defaults[] = $name;
3615             }
3616             $options[] = [
3617                 'value' => $value,
3618                 'name' => $name,
3619                 'selected' => in_array($value, $data)
3620             ];
3621         }
3622         $context->options = $options;
3624         if (is_null($default)) {
3625             $defaultinfo = NULL;
3626         } if (!empty($defaults)) {
3627             $defaultinfo = implode(', ', $defaults);
3628         } else {
3629             $defaultinfo = get_string('none');
3630         }
3632         $element = $OUTPUT->render_from_template($template, $context);
3634         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
3635     }
3638 /**
3639  * Time selector
3640  *
3641  * This is a liiitle bit messy. we're using two selects, but we're returning
3642  * them as an array named after $name (so we only use $name2 internally for the setting)
3643  *
3644  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3645  */
3646 class admin_setting_configtime extends admin_setting {
3647     /** @var string Used for setting second select (minutes) */
3648     public $name2;
3650     /**
3651      * Constructor
3652      * @param string $hoursname setting for hours
3653      * @param string $minutesname setting for hours
3654      * @param string $visiblename localised
3655      * @param string $description long localised info
3656      * @param array $defaultsetting array representing default time 'h'=>hours, 'm'=>minutes
3657      */
3658     public function __construct($hoursname, $minutesname, $visiblename, $description, $defaultsetting) {
3659         $this->name2 = $minutesname;
3660         parent::__construct($hoursname, $visiblename, $description, $defaultsetting);
3661     }
3663     /**
3664      * Get the selected time
3665      *
3666      * @return mixed An array containing 'h'=>xx, 'm'=>xx, or null if not set
3667      */
3668     public function get_setting() {
3669         $result1 = $this->config_read($this->name);
3670         $result2 = $this->config_read($this->name2);
3671         if (is_null($result1) or is_null($result2)) {
3672             return NULL;
3673         }
3675         return array('h' => $result1, 'm' => $result2);
3676     }
3678     /**
3679      * Store the time (hours and minutes)
3680      *
3681      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3682      * @return bool true if success, false if not
3683      */
3684     public function write_setting($data) {
3685         if (!is_array($data)) {
3686             return '';
3687         }
3689         $result = $this->config_write($this->name, (int)$data['h']) && $this->config_write($this->name2, (int)$data['m']);
3690         return ($result ? '' : get_string('errorsetting', 'admin'));
3691     }
3693     /**
3694      * Returns XHTML time select fields
3695      *
3696      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3697      * @param string $query
3698      * @return string XHTML time select fields and wrapping div(s)
3699      */
3700     public function output_html($data, $query='') {
3701         global $OUTPUT;
3703         $default = $this->get_defaultsetting();
3704         if (is_array($default)) {
3705             $defaultinfo = $default['h'].':'.$default['m'];
3706         } else {
3707             $defaultinfo = NULL;
3708         }
3710         $context = (object) [
3711             'id' => $this->get_id(),
3712             'name' => $this->get_full_name(),
3713             'hours' => array_map(function($i) use ($data) {
3714                 return [
3715                     'value' => $i,
3716                     'name' => $i,
3717                     'selected' => $i == $data['h']
3718                 ];
3719             }, range(0, 23)),
3720             'minutes' => array_map(function($i) use ($data) {
3721                 return [
3722                     'value' => $i,
3723                     'name' => $i,
3724                     'selected' => $i == $data['m']
3725                 ];
3726             }, range(0, 59, 5))
3727         ];
3729         $element = $OUTPUT->render_from_template('core_admin/setting_configtime', $context);
3731         return format_admin_setting($this, $this->visiblename, $element, $this->description,
3732             $this->get_id() . 'h', '', $defaultinfo, $query);
3733     }
3738 /**
3739  * Seconds duration setting.
3740  *
3741  * @copyright 2012 Petr Skoda (http://skodak.org)
3742  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3743  */
3744 class admin_setting_configduration extends admin_setting {
3746     /** @var int default duration unit */
3747     protected $defaultunit;
3749     /**
3750      * Constructor
3751      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
3752      *                     or 'myplugin/mysetting' for ones in config_plugins.
3753      * @param string $visiblename localised name
3754      * @param string $description localised long description
3755      * @param mixed $defaultsetting string or array depending on implementation
3756      * @param int $defaultunit - day, week, etc. (in seconds)
3757      */
3758     public function __construct($name, $visiblename, $description, $defaultsetting, $defaultunit = 86400) {
3759         if (is_number($defaultsetting)) {
3760             $defaultsetting = self::parse_seconds($defaultsetting);
3761         }
3762         $units = self::get_units();
3763         if (isset($units[$defaultunit])) {
3764             $this->defaultunit = $defaultunit;
3765         } else {
3766             $this->defaultunit = 86400;
3767         }
3768         parent::__construct($name, $visiblename, $description, $defaultsetting);
3769     }
3771     /**
3772      * Returns selectable units.
3773      * @static
3774      * @return array
3775      */
3776     protected static function get_units() {
3777         return array(
3778             604800 => get_string('weeks'),
3779             86400 => get_string('days'),
3780             3600 => get_string('hours'),
3781             60 => get_string('minutes'),
3782             1 => get_string('seconds'),
3783         );
3784     }
3786     /**
3787      * Converts seconds to some more user friendly string.
3788      * @static
3789      * @param int $seconds
3790      * @return string
3791      */
3792     protected static function get_duration_text($seconds) {
3793         if (empty($seconds)) {
3794             return get_string('none');
3795         }
3796         $data = self::parse_seconds($seconds);
3797         switch ($data['u']) {
3798             case (60*60*24*7):
3799                 return get_string('numweeks', '', $data['v']);
3800             case (60*60*24):
3801                 return get_string('numdays', '', $data['v']);
3802             case (60*60):
3803                 return get_string('numhours', '', $data['v']);
3804             case (60):
3805                 return get_string('numminutes', '', $data['v']);
3806             default:
3807                 return get_string('numseconds', '', $data['v']*$data['u']);
3808         }
3809     }
3811     /**
3812      * Finds suitable units for given duration.
3813      * @static
3814      * @param int $seconds
3815      * @return array
3816      */
3817     protected static function parse_seconds($seconds) {
3818         foreach (self::get_units() as $unit => $unused) {
3819             if ($seconds % $unit === 0) {
3820                 return array('v'=>(int)($seconds/$unit), 'u'=>$unit);
3821             }
3822         }
3823         return array('v'=>(int)$seconds, 'u'=>1);
3824     }
3826     /**
3827      * Get the selected duration as array.
3828      *
3829      * @return mixed An array containing 'v'=>xx, 'u'=>xx, or null if not set
3830      */
3831     public function get_setting() {
3832         $seconds = $this->config_read($this->name);
3833         if (is_null($seconds)) {
3834             return null;
3835         }
3837         return self::parse_seconds($seconds);
3838     }
3840     /**
3841      * Store the duration as seconds.
3842      *
3843      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3844      * @return bool true if success, false if not
3845      */
3846     public function write_setting($data) {
3847         if (!is_array($data)) {
3848             return '';
3849         }
3851         $seconds = (int)($data['v']*$data['u']);
3852         if ($seconds < 0) {
3853             return get_string('errorsetting', 'admin');
3854         }
3856         $result = $this->config_write($this->name, $seconds);
3857         return ($result ? '' : get_string('errorsetting', 'admin'));
3858     }
3860     /**
3861      * Returns duration text+select fields.
3862      *
3863      * @param array $data Must be form 'v'=>xx, 'u'=>xx
3864      * @param string $query
3865      * @return string duration text+select fields and wrapping div(s)
3866      */
3867     public function output_html($data, $query='') {
3868         global $OUTPUT;
3870         $default = $this->get_defaultsetting();
3871         if (is_number($default)) {
3872             $defaultinfo = self::get_duration_text($default);
3873         } else if (is_array($default)) {
3874             $defaultinfo = self::get_duration_text($default['v']*$default['u']);
3875         } else {
3876             $defaultinfo = null;
3877         }
3879         $inputid = $this->get_id() . 'v';
3880         $units = self::get_units();
3881         $defaultunit = $this->defaultunit;
3883         $context = (object) [
3884             'id' => $this->get_id(),
3885             'name' => $this->get_full_name(),
3886             'value' => $data['v'],
3887             'options' => array_map(function($unit) use ($units, $data, $defaultunit) {
3888                 return [
3889                     'value' => $unit,
3890                     'name' => $units[$unit],
3891                     'selected' => ($data['v'] == 0 && $unit == $defaultunit) || $unit == $data['u']
3892                 ];
3893             }, array_keys($units))
3894         ];
3896         $element = $OUTPUT->render_from_template('core_admin/setting_configduration', $context);
3898         return format_admin_setting($this, $this->visiblename, $element, $this->description, $inputid, '', $defaultinfo, $query);
3899     }
3903 /**
3904  * Seconds duration setting with an advanced checkbox, that controls a additional
3905  * $name.'_adv' setting.
3906  *
3907  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3908  * @copyright 2014 The Open University
3909  */
3910 class admin_setting_configduration_with_advanced extends admin_setting_configduration {
3911     /**
3912      * Constructor
3913      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
3914      *                     or 'myplugin/mysetting' for ones in config_plugins.
3915      * @param string $visiblename localised name
3916      * @param string $description localised long description
3917      * @param array  $defaultsetting array of int value, and bool whether it is
3918      *                     is advanced by default.
3919      * @param int $defaultunit - day, week, etc. (in seconds)
3920      */
3921     public function __construct($name, $visiblename, $description, $defaultsetting, $defaultunit = 86400) {
3922         parent::__construct($name, $visiblename, $description, $defaultsetting['value'], $defaultunit);
3923         $this->set_advanced_flag_options(admin_setting_flag::ENABLED, !empty($defaultsetting['adv']));
3924     }
3928 /**
3929  * Used to validate a textarea used for ip addresses
3930  *
3931  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3932  * @copyright 2011 Petr Skoda (http://skodak.org)
3933  */
3934 class admin_setting_configiplist extends admin_setting_configtextarea {
3936     /**
3937      * Validate the contents of the textarea as IP addresses
3938      *
3939      * Used to validate a new line separated list of IP addresses collected from
3940      * a textarea control
3941      *
3942      * @param string $data A list of IP Addresses separated by new lines
3943      * @return mixed bool true for success or string:error on failure
3944      */
3945     public function validate($data) {
3946         if(!empty($data)) {
3947             $lines = explode("\n", $data);
3948         } else {
3949             return true;
3950         }
3951         $result = true;
3952         $badips = array();
3953         foreach ($lines as $line) {
3954             $tokens = explode('#', $line);
3955             $ip = trim($tokens[0]);
3956             if (empty($ip)) {
3957                 continue;
3958             }
3959             if (preg_match('#^(\d{1,3})(\.\d{1,3}){0,3}$#', $ip, $match) ||
3960                 preg_match('#^(\d{1,3})(\.\d{1,3}){0,3}(\/\d{1,2})$#', $ip, $match) ||
3961                 preg_match('#^(\d{1,3})(\.\d{1,3}){3}(-\d{1,3})$#', $ip, $match)) {
3962             } else {
3963                 $result = false;
3964                 $badips[] = $ip;
3965             }
3966         }
3967         if($result) {
3968             return true;
3969         } else {
3970             return get_string('validateiperror', 'admin', join(', ', $badips));
3971         }
3972     }
3975 /**
3976  * Used to validate a textarea used for domain names, wildcard domain names and IP addresses/ranges (both IPv4 and IPv6 format).
3977  *
3978  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3979  * @copyright 2016 Jake Dallimore (jrhdallimore@gmail.com)
3980  */
3981 class admin_setting_configmixedhostiplist extends admin_setting_configtextarea {
3983     /**
3984      * Validate the contents of the textarea as either IP addresses, domain name or wildcard domain name (RFC 4592).
3985      * Used to validate a new line separated list of entries collected from a textarea control.
3986      *
3987      * This setting provides support for internationalised domain names (IDNs), however, such UTF-8 names will be converted to
3988      * their ascii-compatible encoding (punycode) on save, and converted back to their UTF-8 representation when fetched
3989      * via the get_setting() method, which has been overriden.
3990      *
3991      * @param string $data A list of FQDNs, DNS wildcard format domains, and IP addresses, separated by new lines.
3992      * @return mixed bool true for success or string:error on failure
3993      */
3994     public function validate($data) {
3995         if (empty($data)) {
3996             return true;
3997         }
3998         $entries = explode("\n", $data);
3999         $badentries = [];
4001         foreach ($entries as $key => $entry) {
4002             $entry = trim($entry);
4003             if (empty($entry)) {
4004                 return get_string('validateemptylineerror', 'admin');
4005             }
4007             // Validate each string entry against the supported formats.
4008             if (\core\ip_utils::is_ip_address($entry) || \core\ip_utils::is_ipv6_range($entry)
4009                     || \core\ip_utils::is_ipv4_range($entry) || \core\ip_utils::is_domain_name($entry)
4010                     || \core\ip_utils::is_domain_matching_pattern($entry)) {
4011                 continue;
4012             }
4014             // Otherwise, the entry is invalid.
4015             $badentries[] = $entry;
4016         }
4018         if ($badentries) {
4019             return get_string('validateerrorlist', 'admin', join(', ', $badentries));
4020         }
4021         return true;
4022     }
4024     /**
4025      * Convert any lines containing international domain names (IDNs) to their ascii-compatible encoding (ACE).
4026      *
4027      * @param string $data the setting data, as sent from the web form.
4028      * @return string $data the setting data, with all IDNs converted (using punycode) to their ascii encoded version.
4029      */
4030     protected function ace_encode($data) {
4031         if (empty($data)) {
4032             return $data;
4033         }
4034         $entries = explode("\n", $data);
4035         foreach ($entries as $key => $entry) {
4036             $entry = trim($entry);
4037             // This regex matches any string that has non-ascii character.
4038             if (preg_match('/[^\x00-\x7f]/', $entry)) {
4039                 // If we can convert the unicode string to an idn, do so.
4040                 // Otherwise, leave the original unicode string alone and let the validation function handle it (it will fail).
4041                 $val = idn_to_ascii($entry, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
4042                 $entries[$key] = $val ? $val : $entry;
4043             }
4044         }
4045         return implode("\n", $entries);
4046     }
4048     /**
4049      * Decode any ascii-encoded domain names back to their utf-8 representation for display.
4050      *
4051      * @param string $data the setting data, as found in the database.
4052      * @return string $data the setting data, with all ascii-encoded IDNs decoded back to their utf-8 representation.
4053      */
4054     protected function ace_decode($data) {
4055         $entries = explode("\n", $data);
4056         foreach ($entries as $key => $entry) {
4057             $entry = trim($entry);
4058             if (strpos($entry, 'xn--') !== false) {
4059                 $entries[$key] = idn_to_utf8($entry, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
4060             }
4061         }
4062         return implode("\n", $entries);
4063     }
4065     /**
4066      * Override, providing utf8-decoding for ascii-encoded IDN strings.
4067      *
4068      * @return mixed returns punycode-converted setting string if successful, else null.
4069      */
4070     public function get_setting() {
4071         // Here, we need to decode any ascii-encoded IDNs back to their native, utf-8 representation.
4072         $data = $this->config_read($this->name);
4073         if (function_exists('idn_to_utf8') && !is_null($data)) {
4074             $data = $this->ace_decode($data);
4075         }
4076         return $data;
4077     }
4079     /**
4080      * Override, providing ascii-encoding for utf8 (native) IDN strings.
4081      *
4082      * @param string $data
4083      * @return string
4084      */
4085     public function write_setting($data) {
4086         if ($this->paramtype === PARAM_INT and $data === '') {
4087             // Do not complain if '' used instead of 0.
4088             $data = 0;
4089         }
4091         // Try to convert any non-ascii domains to ACE prior to validation - we can't modify anything in validate!
4092         if (function_exists('idn_to_ascii')) {
4093             $data = $this->ace_encode($data);
4094         }
4096         $validated = $this->validate($data);
4097         if ($validated !== true) {
4098             return $validated;
4099         }
4100         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
4101     }
4104 /**
4105  * Used to validate a textarea used for port numbers.
4106  *
4107  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4108  * @copyright 2016 Jake Dallimore (jrhdallimore@gmail.com)
4109  */
4110 class admin_setting_configportlist extends admin_setting_configtextarea {
4112     /**
4113      * Validate the contents of the textarea as port numbers.
4114      * Used to validate a new line separated list of ports collected from a textarea control.
4115      *
4116      * @param string $data A list of ports separated by new lines
4117      * @return mixed bool true for success or string:error on failure
4118      */
4119     public function validate($data) {
4120         if (empty($data)) {
4121             return true;
4122         }
4123         $ports = explode("\n", $data);
4124         $badentries = [];
4125         foreach ($ports as $port) {
4126             $port = trim($port);
4127             if (empty($port)) {
4128                 return get_string('validateemptylineerror', 'admin');
4129             }
4131             // Is the string a valid integer number?
4132             if (strval(intval($port)) !== $port || intval($port) <= 0) {
4133                 $badentries[] = $port;
4134             }
4135         }
4136         if ($badentries) {
4137             return get_string('validateerrorlist', 'admin', $badentries);
4138         }
4139         return true;
4140     }
4144 /**
4145  * An admin setting for selecting one or more users who have a capability
4146  * in the system context
4147  *
4148  * An admin setting for selecting one or more users, who have a particular capability
4149  * in the system context. Warning, make sure the list will never be too long. There is
4150  * no paging or searching of this list.
4151  *
4152  * To correctly get a list of users from this config setting, you need to call the
4153  * get_users_from_config($CFG->mysetting, $capability); function in moodlelib.php.
4154  *
4155  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4156  */
4157 class admin_setting_users_with_capability extends admin_setting_configmultiselect {
4158     /** @var string The capabilities name */
4159     protected $capability;
4160     /** @var int include admin users too */
4161     protected $includeadmins;
4163     /**
4164      * Constructor.
4165      *
4166      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
4167      * @param string $visiblename localised name
4168      * @param string $description localised long description
4169      * @param array $defaultsetting array of usernames
4170      * @param string $capability string capability name.
4171      * @param bool $includeadmins include administrators
4172      */
4173     function __construct($name, $visiblename, $description, $defaultsetting, $capability, $includeadmins = true) {
4174         $this->capability    = $capability;
4175         $this->includeadmins = $includeadmins;
4176         parent::__construct($name, $visiblename, $description, $defaultsetting, NULL);
4177     }
4179     /**
4180      * Load all of the uses who have the capability into choice array
4181      *
4182      * @return bool Always returns true
4183      */
4184     function load_choices() {
4185         if (is_array($this->choices)) {
4186             return true;
4187         }
4188         list($sort, $sortparams) = users_order_by_sql('u');
4189         if (!empty($sortparams)) {
4190             throw new coding_exception('users_order_by_sql returned some query parameters. ' .
4191                     'This is unexpected, and a problem because there is no way to pass these ' .
4192                     'parameters to get_users_by_capability. See MDL-34657.');
4193         }
4194         $userfields = 'u.id, u.username, ' . get_all_user_name_fields(true, 'u');
4195         $users = get_users_by_capability(context_system::instance(), $this->capability, $userfields, $sort);
4196         $this->choices = array(
4197             '$@NONE@$' => get_string('nobody'),
4198             '$@ALL@$' => get_string('everyonewhocan', 'admin', get_capability_string($this->capability)),
4199         );
4200         if ($this->includeadmins) {
4201             $admins = get_admins();
4202             foreach ($admins as $user) {
4203                 $this->choices[$user->id] = fullname($user);
4204             }
4205         }
4206         if (is_array($users)) {
4207             foreach ($users as $user) {
4208                 $this->choices[$user->id] = fullname($user);
4209             }
4210         }
4211         return true;
4212     }
4214     /**
4215      * Returns the default setting for class
4216      *
4217      * @return mixed Array, or string. Empty string if no default
4218      */
4219     public function get_defaultsetting() {
4220         $this->load_choices();
4221         $defaultsetting = parent::get_defaultsetting();
4222         if (empty($defaultsetting)) {
4223             return array('$@NONE@$');
4224         } else if (array_key_exists($defaultsetting, $this->choices)) {
4225                 return $defaultsetting;
4226             } else {
4227                 return '';
4228             }
4229     }
4231     /**
4232      * Returns the current setting
4233      *
4234      * @return mixed array or string
4235      */
4236     public function get_setting() {
4237         $result = parent::get_setting();
4238         if ($result === null) {
4239             // this is necessary for settings upgrade
4240             return null;
4241         }
4242         if (empty($result)) {
4243             $result = array('$@NONE@$');
4244         }
4245         return $result;
4246     }
4248     /**
4249      * Save the chosen setting provided as $data
4250      *
4251      * @param array $data
4252      * @return mixed string or array
4253      */
4254     public function write_setting($data) {
4255     // If all is selected, remove any explicit options.
4256         if (in_array('$@ALL@$', $data)) {
4257             $data = array('$@ALL@$');
4258         }
4259         // None never needs to be written to the DB.
4260         if (in_array('$@NONE@$', $data)) {
4261             unset($data[array_search('$@NONE@$', $data)]);
4262         }
4263         return parent::write_setting($data);
4264     }
4268 /**
4269  * Special checkbox for calendar - resets SESSION vars.
4270  *
4271  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4272  */
4273 class admin_setting_special_adminseesall extends admin_setting_configcheckbox {
4274     /**
4275      * Calls the parent::__construct with default values
4276      *
4277      * name =>  calendar_adminseesall
4278      * visiblename => get_string('adminseesall', 'admin')
4279      * description => get_string('helpadminseesall', 'admin')
4280      * defaultsetting => 0
4281      */
4282     public function __construct() {
4283         parent::__construct('calendar_adminseesall', get_string('adminseesall', 'admin'),
4284             get_string('helpadminseesall', 'admin'), '0');
4285     }
4287     /**
4288      * Stores the setting passed in $data
4289      *
4290      * @param mixed gets converted to string for comparison
4291      * @return string empty string or error message
4292      */
4293     public function write_setting($data) {
4294         global $SESSION;
4295         return parent::write_setting($data);
4296     }
4299 /**
4300  * Special select for settings that are altered in setup.php and can not be altered on the fly
4301  *
4302  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4303  */
4304 class admin_setting_special_selectsetup extends admin_setting_configselect {
4305     /**
4306      * Reads the setting directly from the database
4307      *
4308      * @return mixed
4309      */
4310     public function get_setting() {
4311     // read directly from db!
4312         return get_config(NULL, $this->name);
4313     }
4315     /**
4316      * Save the setting passed in $data
4317      *
4318      * @param string $data The setting to save
4319      * @return string empty or error message
4320      */
4321     public function write_setting($data) {
4322         global $CFG;
4323         // do not change active CFG setting!
4324         $current = $CFG->{$this->name};
4325         $result = parent::write_setting($data);
4326         $CFG->{$this->name} = $current;
4327         return $result;
4328     }
4332 /**
4333  * Special select for frontpage - stores data in course table
4334  *
4335  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4336  */
4337 class admin_setting_sitesetselect extends admin_setting_configselect {
4338     /**
4339      * Returns the site name for the selected site
4340      *
4341      * @see get_site()
4342      * @return string The site name of the selected site
4343      */
4344     public function get_setting() {
4345         $site = course_get_format(get_site())->get_course();
4346         return $site->{$this->name};
4347     }
4349     /**
4350      * Updates the database and save the setting