Merge branch 'MDL-67518' of https://github.com/stronk7/moodle
[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;
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('sitemaintenance', 'admin'), $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 string 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 string 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;
3236     /**
3237      * Constructor
3238      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3239      * @param string $visiblename localised
3240      * @param string $description long localised info
3241      * @param string|int $defaultsetting
3242      * @param array $choices array of $value=>$label for each selection
3243      */
3244     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
3245         // Look for optgroup and single options.
3246         if (is_array($choices)) {
3247             $this->choices = [];
3248             foreach ($choices as $key => $val) {
3249                 if (is_array($val)) {
3250                     $this->optgroups[$key] = $val;
3251                     $this->choices = array_merge($this->choices, $val);
3252                 } else {
3253                     $this->choices[$key] = $val;
3254                 }
3255             }
3256         }
3258         parent::__construct($name, $visiblename, $description, $defaultsetting);
3259     }
3261     /**
3262      * This function may be used in ancestors for lazy loading of choices
3263      *
3264      * Override this method if loading of choices is expensive, such
3265      * as when it requires multiple db requests.
3266      *
3267      * @return bool true if loaded, false if error
3268      */
3269     public function load_choices() {
3270         /*
3271         if (is_array($this->choices)) {
3272             return true;
3273         }
3274         .... load choices here
3275         */
3276         return true;
3277     }
3279     /**
3280      * Check if this is $query is related to a choice
3281      *
3282      * @param string $query
3283      * @return bool true if related, false if not
3284      */
3285     public function is_related($query) {
3286         if (parent::is_related($query)) {
3287             return true;
3288         }
3289         if (!$this->load_choices()) {
3290             return false;
3291         }
3292         foreach ($this->choices as $key=>$value) {
3293             if (strpos(core_text::strtolower($key), $query) !== false) {
3294                 return true;
3295             }
3296             if (strpos(core_text::strtolower($value), $query) !== false) {
3297                 return true;
3298             }
3299         }
3300         return false;
3301     }
3303     /**
3304      * Return the setting
3305      *
3306      * @return mixed returns config if successful else null
3307      */
3308     public function get_setting() {
3309         return $this->config_read($this->name);
3310     }
3312     /**
3313      * Save a setting
3314      *
3315      * @param string $data
3316      * @return string empty of error string
3317      */
3318     public function write_setting($data) {
3319         if (!$this->load_choices() or empty($this->choices)) {
3320             return '';
3321         }
3322         if (!array_key_exists($data, $this->choices)) {
3323             return ''; // ignore it
3324         }
3326         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
3327     }
3329     /**
3330      * Returns XHTML select field
3331      *
3332      * Ensure the options are loaded, and generate the XHTML for the select
3333      * element and any warning message. Separating this out from output_html
3334      * makes it easier to subclass this class.
3335      *
3336      * @param string $data the option to show as selected.
3337      * @param string $current the currently selected option in the database, null if none.
3338      * @param string $default the default selected option.
3339      * @return array the HTML for the select element, and a warning message.
3340      * @deprecated since Moodle 3.2
3341      */
3342     public function output_select_html($data, $current, $default, $extraname = '') {
3343         debugging('The method admin_setting_configselect::output_select_html is depreacted, do not use any more.', DEBUG_DEVELOPER);
3344     }
3346     /**
3347      * Returns XHTML select field and wrapping div(s)
3348      *
3349      * @see output_select_html()
3350      *
3351      * @param string $data the option to show as selected
3352      * @param string $query
3353      * @return string XHTML field and wrapping div
3354      */
3355     public function output_html($data, $query='') {
3356         global $OUTPUT;
3358         $default = $this->get_defaultsetting();
3359         $current = $this->get_setting();
3361         if (!$this->load_choices() || empty($this->choices)) {
3362             return '';
3363         }
3365         $context = (object) [
3366             'id' => $this->get_id(),
3367             'name' => $this->get_full_name(),
3368         ];
3370         if (!is_null($default) && array_key_exists($default, $this->choices)) {
3371             $defaultinfo = $this->choices[$default];
3372         } else {
3373             $defaultinfo = NULL;
3374         }
3376         // Warnings.
3377         $warning = '';
3378         if ($current === null) {
3379             // First run.
3380         } else if (empty($current) && (array_key_exists('', $this->choices) || array_key_exists(0, $this->choices))) {
3381             // No warning.
3382         } else if (!array_key_exists($current, $this->choices)) {
3383             $warning = get_string('warningcurrentsetting', 'admin', $current);
3384             if (!is_null($default) && $data == $current) {
3385                 $data = $default; // Use default instead of first value when showing the form.
3386             }
3387         }
3389         $options = [];
3390         $template = 'core_admin/setting_configselect';
3392         if (!empty($this->optgroups)) {
3393             $optgroups = [];
3394             foreach ($this->optgroups as $label => $choices) {
3395                 $optgroup = array('label' => $label, 'options' => []);
3396                 foreach ($choices as $value => $name) {
3397                     $optgroup['options'][] = [
3398                         'value' => $value,
3399                         'name' => $name,
3400                         'selected' => (string) $value == $data
3401                     ];
3402                     unset($this->choices[$value]);
3403                 }
3404                 $optgroups[] = $optgroup;
3405             }
3406             $context->options = $options;
3407             $context->optgroups = $optgroups;
3408             $template = 'core_admin/setting_configselect_optgroup';
3409         }
3411         foreach ($this->choices as $value => $name) {
3412             $options[] = [
3413                 'value' => $value,
3414                 'name' => $name,
3415                 'selected' => (string) $value == $data
3416             ];
3417         }
3418         $context->options = $options;
3420         $element = $OUTPUT->render_from_template($template, $context);
3422         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, $warning, $defaultinfo, $query);
3423     }
3427 /**
3428  * Select multiple items from list
3429  *
3430  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3431  */
3432 class admin_setting_configmultiselect extends admin_setting_configselect {
3433     /**
3434      * Constructor
3435      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3436      * @param string $visiblename localised
3437      * @param string $description long localised info
3438      * @param array $defaultsetting array of selected items
3439      * @param array $choices array of $value=>$label for each list item
3440      */
3441     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
3442         parent::__construct($name, $visiblename, $description, $defaultsetting, $choices);
3443     }
3445     /**
3446      * Returns the select setting(s)
3447      *
3448      * @return mixed null or array. Null if no settings else array of setting(s)
3449      */
3450     public function get_setting() {
3451         $result = $this->config_read($this->name);
3452         if (is_null($result)) {
3453             return NULL;
3454         }
3455         if ($result === '') {
3456             return array();
3457         }
3458         return explode(',', $result);
3459     }
3461     /**
3462      * Saves setting(s) provided through $data
3463      *
3464      * Potential bug in the works should anyone call with this function
3465      * using a vartype that is not an array
3466      *
3467      * @param array $data
3468      */
3469     public function write_setting($data) {
3470         if (!is_array($data)) {
3471             return ''; //ignore it
3472         }
3473         if (!$this->load_choices() or empty($this->choices)) {
3474             return '';
3475         }
3477         unset($data['xxxxx']);
3479         $save = array();
3480         foreach ($data as $value) {
3481             if (!array_key_exists($value, $this->choices)) {
3482                 continue; // ignore it
3483             }
3484             $save[] = $value;
3485         }
3487         return ($this->config_write($this->name, implode(',', $save)) ? '' : get_string('errorsetting', 'admin'));
3488     }
3490     /**
3491      * Is setting related to query text - used when searching
3492      *
3493      * @param string $query
3494      * @return bool true if related, false if not
3495      */
3496     public function is_related($query) {
3497         if (!$this->load_choices() or empty($this->choices)) {
3498             return false;
3499         }
3500         if (parent::is_related($query)) {
3501             return true;
3502         }
3504         foreach ($this->choices as $desc) {
3505             if (strpos(core_text::strtolower($desc), $query) !== false) {
3506                 return true;
3507             }
3508         }
3509         return false;
3510     }
3512     /**
3513      * Returns XHTML multi-select field
3514      *
3515      * @todo Add vartype handling to ensure $data is an array
3516      * @param array $data Array of values to select by default
3517      * @param string $query
3518      * @return string XHTML multi-select field
3519      */
3520     public function output_html($data, $query='') {
3521         global $OUTPUT;
3523         if (!$this->load_choices() or empty($this->choices)) {
3524             return '';
3525         }
3527         $default = $this->get_defaultsetting();
3528         if (is_null($default)) {
3529             $default = array();
3530         }
3531         if (is_null($data)) {
3532             $data = array();
3533         }
3535         $context = (object) [
3536             'id' => $this->get_id(),
3537             'name' => $this->get_full_name(),
3538             'size' => min(10, count($this->choices))
3539         ];
3541         $defaults = [];
3542         $options = [];
3543         $template = 'core_admin/setting_configmultiselect';
3545         if (!empty($this->optgroups)) {
3546             $optgroups = [];
3547             foreach ($this->optgroups as $label => $choices) {
3548                 $optgroup = array('label' => $label, 'options' => []);
3549                 foreach ($choices as $value => $name) {
3550                     if (in_array($value, $default)) {
3551                         $defaults[] = $name;
3552                     }
3553                     $optgroup['options'][] = [
3554                         'value' => $value,
3555                         'name' => $name,
3556                         'selected' => in_array($value, $data)
3557                     ];
3558                     unset($this->choices[$value]);
3559                 }
3560                 $optgroups[] = $optgroup;
3561             }
3562             $context->optgroups = $optgroups;
3563             $template = 'core_admin/setting_configmultiselect_optgroup';
3564         }
3566         foreach ($this->choices as $value => $name) {
3567             if (in_array($value, $default)) {
3568                 $defaults[] = $name;
3569             }
3570             $options[] = [
3571                 'value' => $value,
3572                 'name' => $name,
3573                 'selected' => in_array($value, $data)
3574             ];
3575         }
3576         $context->options = $options;
3578         if (is_null($default)) {
3579             $defaultinfo = NULL;
3580         } if (!empty($defaults)) {
3581             $defaultinfo = implode(', ', $defaults);
3582         } else {
3583             $defaultinfo = get_string('none');
3584         }
3586         $element = $OUTPUT->render_from_template($template, $context);
3588         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
3589     }
3592 /**
3593  * Time selector
3594  *
3595  * This is a liiitle bit messy. we're using two selects, but we're returning
3596  * them as an array named after $name (so we only use $name2 internally for the setting)
3597  *
3598  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3599  */
3600 class admin_setting_configtime extends admin_setting {
3601     /** @var string Used for setting second select (minutes) */
3602     public $name2;
3604     /**
3605      * Constructor
3606      * @param string $hoursname setting for hours
3607      * @param string $minutesname setting for hours
3608      * @param string $visiblename localised
3609      * @param string $description long localised info
3610      * @param array $defaultsetting array representing default time 'h'=>hours, 'm'=>minutes
3611      */
3612     public function __construct($hoursname, $minutesname, $visiblename, $description, $defaultsetting) {
3613         $this->name2 = $minutesname;
3614         parent::__construct($hoursname, $visiblename, $description, $defaultsetting);
3615     }
3617     /**
3618      * Get the selected time
3619      *
3620      * @return mixed An array containing 'h'=>xx, 'm'=>xx, or null if not set
3621      */
3622     public function get_setting() {
3623         $result1 = $this->config_read($this->name);
3624         $result2 = $this->config_read($this->name2);
3625         if (is_null($result1) or is_null($result2)) {
3626             return NULL;
3627         }
3629         return array('h' => $result1, 'm' => $result2);
3630     }
3632     /**
3633      * Store the time (hours and minutes)
3634      *
3635      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3636      * @return bool true if success, false if not
3637      */
3638     public function write_setting($data) {
3639         if (!is_array($data)) {
3640             return '';
3641         }
3643         $result = $this->config_write($this->name, (int)$data['h']) && $this->config_write($this->name2, (int)$data['m']);
3644         return ($result ? '' : get_string('errorsetting', 'admin'));
3645     }
3647     /**
3648      * Returns XHTML time select fields
3649      *
3650      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3651      * @param string $query
3652      * @return string XHTML time select fields and wrapping div(s)
3653      */
3654     public function output_html($data, $query='') {
3655         global $OUTPUT;
3657         $default = $this->get_defaultsetting();
3658         if (is_array($default)) {
3659             $defaultinfo = $default['h'].':'.$default['m'];
3660         } else {
3661             $defaultinfo = NULL;
3662         }
3664         $context = (object) [
3665             'id' => $this->get_id(),
3666             'name' => $this->get_full_name(),
3667             'hours' => array_map(function($i) use ($data) {
3668                 return [
3669                     'value' => $i,
3670                     'name' => $i,
3671                     'selected' => $i == $data['h']
3672                 ];
3673             }, range(0, 23)),
3674             'minutes' => array_map(function($i) use ($data) {
3675                 return [
3676                     'value' => $i,
3677                     'name' => $i,
3678                     'selected' => $i == $data['m']