MDL-35597 admin: categories now support localised alphabetical sorting
[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(dirname(dirname(dirname(__FILE__))).'/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);
135         if (file_exists("$base/db/subplugins.php")) {
136             $subplugins = array();
137             include("$base/db/subplugins.php");
138             foreach ($subplugins as $subplugintype=>$dir) {
139                 $instances = core_component::get_plugin_list($subplugintype);
140                 foreach ($instances as $subpluginname => $notusedpluginpath) {
141                     uninstall_plugin($subplugintype, $subpluginname);
142                 }
143             }
144         }
146     }
148     $component = $type . '_' . $name;  // eg. 'qtype_multichoice' or 'workshopgrading_accumulative' or 'mod_forum'
150     if ($type === 'mod') {
151         $pluginname = $name;  // eg. 'forum'
152         if (get_string_manager()->string_exists('modulename', $component)) {
153             $strpluginname = get_string('modulename', $component);
154         } else {
155             $strpluginname = $component;
156         }
158     } else {
159         $pluginname = $component;
160         if (get_string_manager()->string_exists('pluginname', $component)) {
161             $strpluginname = get_string('pluginname', $component);
162         } else {
163             $strpluginname = $component;
164         }
165     }
167     echo $OUTPUT->heading($pluginname);
169     // Custom plugin uninstall.
170     $plugindirectory = core_component::get_plugin_directory($type, $name);
171     $uninstalllib = $plugindirectory . '/db/uninstall.php';
172     if (file_exists($uninstalllib)) {
173         require_once($uninstalllib);
174         $uninstallfunction = 'xmldb_' . $pluginname . '_uninstall';    // eg. 'xmldb_workshop_uninstall()'
175         if (function_exists($uninstallfunction)) {
176             // Do not verify result, let plugin complain if necessary.
177             $uninstallfunction();
178         }
179     }
181     // Specific plugin type cleanup.
182     $plugininfo = core_plugin_manager::instance()->get_plugin_info($component);
183     if ($plugininfo) {
184         $plugininfo->uninstall_cleanup();
185         core_plugin_manager::reset_caches();
186     }
187     $plugininfo = null;
189     // perform clean-up task common for all the plugin/subplugin types
191     //delete the web service functions and pre-built services
192     require_once($CFG->dirroot.'/lib/externallib.php');
193     external_delete_descriptions($component);
195     // delete calendar events
196     $DB->delete_records('event', array('modulename' => $pluginname));
198     // delete all the logs
199     $DB->delete_records('log', array('module' => $pluginname));
201     // delete log_display information
202     $DB->delete_records('log_display', array('component' => $component));
204     // delete the module configuration records
205     unset_all_config_for_plugin($component);
206     if ($type === 'mod') {
207         unset_all_config_for_plugin($pluginname);
208     }
210     // delete message provider
211     message_provider_uninstall($component);
213     // delete the plugin tables
214     $xmldbfilepath = $plugindirectory . '/db/install.xml';
215     drop_plugin_tables($component, $xmldbfilepath, false);
216     if ($type === 'mod' or $type === 'block') {
217         // non-frankenstyle table prefixes
218         drop_plugin_tables($name, $xmldbfilepath, false);
219     }
221     // delete the capabilities that were defined by this module
222     capabilities_cleanup($component);
224     // remove event handlers and dequeue pending events
225     events_uninstall($component);
227     // Delete all remaining files in the filepool owned by the component.
228     $fs = get_file_storage();
229     $fs->delete_component_files($component);
231     // Finally purge all caches.
232     purge_all_caches();
234     // Invalidate the hash used for upgrade detections.
235     set_config('allversionshash', '');
237     echo $OUTPUT->notification(get_string('success'), 'notifysuccess');
240 /**
241  * Returns the version of installed component
242  *
243  * @param string $component component name
244  * @param string $source either 'disk' or 'installed' - where to get the version information from
245  * @return string|bool version number or false if the component is not found
246  */
247 function get_component_version($component, $source='installed') {
248     global $CFG, $DB;
250     list($type, $name) = core_component::normalize_component($component);
252     // moodle core or a core subsystem
253     if ($type === 'core') {
254         if ($source === 'installed') {
255             if (empty($CFG->version)) {
256                 return false;
257             } else {
258                 return $CFG->version;
259             }
260         } else {
261             if (!is_readable($CFG->dirroot.'/version.php')) {
262                 return false;
263             } else {
264                 $version = null; //initialize variable for IDEs
265                 include($CFG->dirroot.'/version.php');
266                 return $version;
267             }
268         }
269     }
271     // activity module
272     if ($type === 'mod') {
273         if ($source === 'installed') {
274             if ($CFG->version < 2013092001.02) {
275                 return $DB->get_field('modules', 'version', array('name'=>$name));
276             } else {
277                 return get_config('mod_'.$name, 'version');
278             }
280         } else {
281             $mods = core_component::get_plugin_list('mod');
282             if (empty($mods[$name]) or !is_readable($mods[$name].'/version.php')) {
283                 return false;
284             } else {
285                 $plugin = new stdClass();
286                 $plugin->version = null;
287                 $module = $plugin;
288                 include($mods[$name].'/version.php');
289                 return $plugin->version;
290             }
291         }
292     }
294     // block
295     if ($type === 'block') {
296         if ($source === 'installed') {
297             if ($CFG->version < 2013092001.02) {
298                 return $DB->get_field('block', 'version', array('name'=>$name));
299             } else {
300                 return get_config('block_'.$name, 'version');
301             }
302         } else {
303             $blocks = core_component::get_plugin_list('block');
304             if (empty($blocks[$name]) or !is_readable($blocks[$name].'/version.php')) {
305                 return false;
306             } else {
307                 $plugin = new stdclass();
308                 include($blocks[$name].'/version.php');
309                 return $plugin->version;
310             }
311         }
312     }
314     // all other plugin types
315     if ($source === 'installed') {
316         return get_config($type.'_'.$name, 'version');
317     } else {
318         $plugins = core_component::get_plugin_list($type);
319         if (empty($plugins[$name])) {
320             return false;
321         } else {
322             $plugin = new stdclass();
323             include($plugins[$name].'/version.php');
324             return $plugin->version;
325         }
326     }
329 /**
330  * Delete all plugin tables
331  *
332  * @param string $name Name of plugin, used as table prefix
333  * @param string $file Path to install.xml file
334  * @param bool $feedback defaults to true
335  * @return bool Always returns true
336  */
337 function drop_plugin_tables($name, $file, $feedback=true) {
338     global $CFG, $DB;
340     // first try normal delete
341     if (file_exists($file) and $DB->get_manager()->delete_tables_from_xmldb_file($file)) {
342         return true;
343     }
345     // then try to find all tables that start with name and are not in any xml file
346     $used_tables = get_used_table_names();
348     $tables = $DB->get_tables();
350     /// Iterate over, fixing id fields as necessary
351     foreach ($tables as $table) {
352         if (in_array($table, $used_tables)) {
353             continue;
354         }
356         if (strpos($table, $name) !== 0) {
357             continue;
358         }
360         // found orphan table --> delete it
361         if ($DB->get_manager()->table_exists($table)) {
362             $xmldb_table = new xmldb_table($table);
363             $DB->get_manager()->drop_table($xmldb_table);
364         }
365     }
367     return true;
370 /**
371  * Returns names of all known tables == tables that moodle knows about.
372  *
373  * @return array Array of lowercase table names
374  */
375 function get_used_table_names() {
376     $table_names = array();
377     $dbdirs = get_db_directories();
379     foreach ($dbdirs as $dbdir) {
380         $file = $dbdir.'/install.xml';
382         $xmldb_file = new xmldb_file($file);
384         if (!$xmldb_file->fileExists()) {
385             continue;
386         }
388         $loaded    = $xmldb_file->loadXMLStructure();
389         $structure = $xmldb_file->getStructure();
391         if ($loaded and $tables = $structure->getTables()) {
392             foreach($tables as $table) {
393                 $table_names[] = strtolower($table->getName());
394             }
395         }
396     }
398     return $table_names;
401 /**
402  * Returns list of all directories where we expect install.xml files
403  * @return array Array of paths
404  */
405 function get_db_directories() {
406     global $CFG;
408     $dbdirs = array();
410     /// First, the main one (lib/db)
411     $dbdirs[] = $CFG->libdir.'/db';
413     /// Then, all the ones defined by core_component::get_plugin_types()
414     $plugintypes = core_component::get_plugin_types();
415     foreach ($plugintypes as $plugintype => $pluginbasedir) {
416         if ($plugins = core_component::get_plugin_list($plugintype)) {
417             foreach ($plugins as $plugin => $plugindir) {
418                 $dbdirs[] = $plugindir.'/db';
419             }
420         }
421     }
423     return $dbdirs;
426 /**
427  * Try to obtain or release the cron lock.
428  * @param string  $name  name of lock
429  * @param int  $until timestamp when this lock considered stale, null means remove lock unconditionally
430  * @param bool $ignorecurrent ignore current lock state, usually extend previous lock, defaults to false
431  * @return bool true if lock obtained
432  */
433 function set_cron_lock($name, $until, $ignorecurrent=false) {
434     global $DB;
435     if (empty($name)) {
436         debugging("Tried to get a cron lock for a null fieldname");
437         return false;
438     }
440     // remove lock by force == remove from config table
441     if (is_null($until)) {
442         set_config($name, null);
443         return true;
444     }
446     if (!$ignorecurrent) {
447         // read value from db - other processes might have changed it
448         $value = $DB->get_field('config', 'value', array('name'=>$name));
450         if ($value and $value > time()) {
451             //lock active
452             return false;
453         }
454     }
456     set_config($name, $until);
457     return true;
460 /**
461  * Test if and critical warnings are present
462  * @return bool
463  */
464 function admin_critical_warnings_present() {
465     global $SESSION;
467     if (!has_capability('moodle/site:config', context_system::instance())) {
468         return 0;
469     }
471     if (!isset($SESSION->admin_critical_warning)) {
472         $SESSION->admin_critical_warning = 0;
473         if (is_dataroot_insecure(true) === INSECURE_DATAROOT_ERROR) {
474             $SESSION->admin_critical_warning = 1;
475         }
476     }
478     return $SESSION->admin_critical_warning;
481 /**
482  * Detects if float supports at least 10 decimal digits
483  *
484  * Detects if float supports at least 10 decimal digits
485  * and also if float-->string conversion works as expected.
486  *
487  * @return bool true if problem found
488  */
489 function is_float_problem() {
490     $num1 = 2009010200.01;
491     $num2 = 2009010200.02;
493     return ((string)$num1 === (string)$num2 or $num1 === $num2 or $num2 <= (string)$num1);
496 /**
497  * Try to verify that dataroot is not accessible from web.
498  *
499  * Try to verify that dataroot is not accessible from web.
500  * It is not 100% correct but might help to reduce number of vulnerable sites.
501  * Protection from httpd.conf and .htaccess is not detected properly.
502  *
503  * @uses INSECURE_DATAROOT_WARNING
504  * @uses INSECURE_DATAROOT_ERROR
505  * @param bool $fetchtest try to test public access by fetching file, default false
506  * @return mixed empty means secure, INSECURE_DATAROOT_ERROR found a critical problem, INSECURE_DATAROOT_WARNING might be problematic
507  */
508 function is_dataroot_insecure($fetchtest=false) {
509     global $CFG;
511     $siteroot = str_replace('\\', '/', strrev($CFG->dirroot.'/')); // win32 backslash workaround
513     $rp = preg_replace('|https?://[^/]+|i', '', $CFG->wwwroot, 1);
514     $rp = strrev(trim($rp, '/'));
515     $rp = explode('/', $rp);
516     foreach($rp as $r) {
517         if (strpos($siteroot, '/'.$r.'/') === 0) {
518             $siteroot = substr($siteroot, strlen($r)+1); // moodle web in subdirectory
519         } else {
520             break; // probably alias root
521         }
522     }
524     $siteroot = strrev($siteroot);
525     $dataroot = str_replace('\\', '/', $CFG->dataroot.'/');
527     if (strpos($dataroot, $siteroot) !== 0) {
528         return false;
529     }
531     if (!$fetchtest) {
532         return INSECURE_DATAROOT_WARNING;
533     }
535     // now try all methods to fetch a test file using http protocol
537     $httpdocroot = str_replace('\\', '/', strrev($CFG->dirroot.'/'));
538     preg_match('|(https?://[^/]+)|i', $CFG->wwwroot, $matches);
539     $httpdocroot = $matches[1];
540     $datarooturl = $httpdocroot.'/'. substr($dataroot, strlen($siteroot));
541     make_upload_directory('diag');
542     $testfile = $CFG->dataroot.'/diag/public.txt';
543     if (!file_exists($testfile)) {
544         file_put_contents($testfile, 'test file, do not delete');
545         @chmod($testfile, $CFG->filepermissions);
546     }
547     $teststr = trim(file_get_contents($testfile));
548     if (empty($teststr)) {
549     // hmm, strange
550         return INSECURE_DATAROOT_WARNING;
551     }
553     $testurl = $datarooturl.'/diag/public.txt';
554     if (extension_loaded('curl') and
555         !(stripos(ini_get('disable_functions'), 'curl_init') !== FALSE) and
556         !(stripos(ini_get('disable_functions'), 'curl_setop') !== FALSE) and
557         ($ch = @curl_init($testurl)) !== false) {
558         curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
559         curl_setopt($ch, CURLOPT_HEADER, false);
560         $data = curl_exec($ch);
561         if (!curl_errno($ch)) {
562             $data = trim($data);
563             if ($data === $teststr) {
564                 curl_close($ch);
565                 return INSECURE_DATAROOT_ERROR;
566             }
567         }
568         curl_close($ch);
569     }
571     if ($data = @file_get_contents($testurl)) {
572         $data = trim($data);
573         if ($data === $teststr) {
574             return INSECURE_DATAROOT_ERROR;
575         }
576     }
578     preg_match('|https?://([^/]+)|i', $testurl, $matches);
579     $sitename = $matches[1];
580     $error = 0;
581     if ($fp = @fsockopen($sitename, 80, $error)) {
582         preg_match('|https?://[^/]+(.*)|i', $testurl, $matches);
583         $localurl = $matches[1];
584         $out = "GET $localurl HTTP/1.1\r\n";
585         $out .= "Host: $sitename\r\n";
586         $out .= "Connection: Close\r\n\r\n";
587         fwrite($fp, $out);
588         $data = '';
589         $incoming = false;
590         while (!feof($fp)) {
591             if ($incoming) {
592                 $data .= fgets($fp, 1024);
593             } else if (@fgets($fp, 1024) === "\r\n") {
594                     $incoming = true;
595                 }
596         }
597         fclose($fp);
598         $data = trim($data);
599         if ($data === $teststr) {
600             return INSECURE_DATAROOT_ERROR;
601         }
602     }
604     return INSECURE_DATAROOT_WARNING;
607 /**
608  * Enables CLI maintenance mode by creating new dataroot/climaintenance.html file.
609  */
610 function enable_cli_maintenance_mode() {
611     global $CFG;
613     if (file_exists("$CFG->dataroot/climaintenance.html")) {
614         unlink("$CFG->dataroot/climaintenance.html");
615     }
617     if (isset($CFG->maintenance_message) and !html_is_blank($CFG->maintenance_message)) {
618         $data = $CFG->maintenance_message;
619         $data = bootstrap_renderer::early_error_content($data, null, null, null);
620         $data = bootstrap_renderer::plain_page(get_string('sitemaintenance', 'admin'), $data);
622     } else if (file_exists("$CFG->dataroot/climaintenance.template.html")) {
623         $data = file_get_contents("$CFG->dataroot/climaintenance.template.html");
625     } else {
626         $data = get_string('sitemaintenance', 'admin');
627         $data = bootstrap_renderer::early_error_content($data, null, null, null);
628         $data = bootstrap_renderer::plain_page(get_string('sitemaintenance', 'admin'), $data);
629     }
631     file_put_contents("$CFG->dataroot/climaintenance.html", $data);
632     chmod("$CFG->dataroot/climaintenance.html", $CFG->filepermissions);
635 /// CLASS DEFINITIONS /////////////////////////////////////////////////////////
638 /**
639  * Interface for anything appearing in the admin tree
640  *
641  * The interface that is implemented by anything that appears in the admin tree
642  * block. It forces inheriting classes to define a method for checking user permissions
643  * and methods for finding something in the admin tree.
644  *
645  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
646  */
647 interface part_of_admin_tree {
649 /**
650  * Finds a named part_of_admin_tree.
651  *
652  * Used to find a part_of_admin_tree. If a class only inherits part_of_admin_tree
653  * and not parentable_part_of_admin_tree, then this function should only check if
654  * $this->name matches $name. If it does, it should return a reference to $this,
655  * otherwise, it should return a reference to NULL.
656  *
657  * If a class inherits parentable_part_of_admin_tree, this method should be called
658  * recursively on all child objects (assuming, of course, the parent object's name
659  * doesn't match the search criterion).
660  *
661  * @param string $name The internal name of the part_of_admin_tree we're searching for.
662  * @return mixed An object reference or a NULL reference.
663  */
664     public function locate($name);
666     /**
667      * Removes named part_of_admin_tree.
668      *
669      * @param string $name The internal name of the part_of_admin_tree we want to remove.
670      * @return bool success.
671      */
672     public function prune($name);
674     /**
675      * Search using query
676      * @param string $query
677      * @return mixed array-object structure of found settings and pages
678      */
679     public function search($query);
681     /**
682      * Verifies current user's access to this part_of_admin_tree.
683      *
684      * Used to check if the current user has access to this part of the admin tree or
685      * not. If a class only inherits part_of_admin_tree and not parentable_part_of_admin_tree,
686      * then this method is usually just a call to has_capability() in the site context.
687      *
688      * If a class inherits parentable_part_of_admin_tree, this method should return the
689      * logical OR of the return of check_access() on all child objects.
690      *
691      * @return bool True if the user has access, false if she doesn't.
692      */
693     public function check_access();
695     /**
696      * Mostly useful for removing of some parts of the tree in admin tree block.
697      *
698      * @return True is hidden from normal list view
699      */
700     public function is_hidden();
702     /**
703      * Show we display Save button at the page bottom?
704      * @return bool
705      */
706     public function show_save();
710 /**
711  * Interface implemented by any part_of_admin_tree that has children.
712  *
713  * The interface implemented by any part_of_admin_tree that can be a parent
714  * to other part_of_admin_tree's. (For now, this only includes admin_category.) Apart
715  * from ensuring part_of_admin_tree compliancy, it also ensures inheriting methods
716  * include an add method for adding other part_of_admin_tree objects as children.
717  *
718  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
719  */
720 interface parentable_part_of_admin_tree extends part_of_admin_tree {
722 /**
723  * Adds a part_of_admin_tree object to the admin tree.
724  *
725  * Used to add a part_of_admin_tree object to this object or a child of this
726  * object. $something should only be added if $destinationname matches
727  * $this->name. If it doesn't, add should be called on child objects that are
728  * also parentable_part_of_admin_tree's.
729  *
730  * $something should be appended as the last child in the $destinationname. If the
731  * $beforesibling is specified, $something should be prepended to it. If the given
732  * sibling is not found, $something should be appended to the end of $destinationname
733  * and a developer debugging message should be displayed.
734  *
735  * @param string $destinationname The internal name of the new parent for $something.
736  * @param part_of_admin_tree $something The object to be added.
737  * @return bool True on success, false on failure.
738  */
739     public function add($destinationname, $something, $beforesibling = null);
744 /**
745  * The object used to represent folders (a.k.a. categories) in the admin tree block.
746  *
747  * Each admin_category object contains a number of part_of_admin_tree objects.
748  *
749  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
750  */
751 class admin_category implements parentable_part_of_admin_tree {
753     /** @var part_of_admin_tree[] An array of part_of_admin_tree objects that are this object's children */
754     protected $children;
755     /** @var string An internal name for this category. Must be unique amongst ALL part_of_admin_tree objects */
756     public $name;
757     /** @var string The displayed name for this category. Usually obtained through get_string() */
758     public $visiblename;
759     /** @var bool Should this category be hidden in admin tree block? */
760     public $hidden;
761     /** @var mixed Either a string or an array or strings */
762     public $path;
763     /** @var mixed Either a string or an array or strings */
764     public $visiblepath;
766     /** @var array fast lookup category cache, all categories of one tree point to one cache */
767     protected $category_cache;
769     /** @var bool If set to true children will be sorted when calling {@link admin_category::get_children()} */
770     protected $sort = false;
771     /** @var bool If set to true children will be sorted in ascending order. */
772     protected $sortasc = true;
773     /** @var bool If set to true sub categories and pages will be split and then sorted.. */
774     protected $sortsplit = true;
775     /** @var bool $sorted True if the children have been sorted and don't need resorting */
776     protected $sorted = false;
778     /**
779      * Constructor for an empty admin category
780      *
781      * @param string $name The internal name for this category. Must be unique amongst ALL part_of_admin_tree objects
782      * @param string $visiblename The displayed named for this category. Usually obtained through get_string()
783      * @param bool $hidden hide category in admin tree block, defaults to false
784      */
785     public function __construct($name, $visiblename, $hidden=false) {
786         $this->children    = array();
787         $this->name        = $name;
788         $this->visiblename = $visiblename;
789         $this->hidden      = $hidden;
790     }
792     /**
793      * Returns a reference to the part_of_admin_tree object with internal name $name.
794      *
795      * @param string $name The internal name of the object we want.
796      * @param bool $findpath initialize path and visiblepath arrays
797      * @return mixed A reference to the object with internal name $name if found, otherwise a reference to NULL.
798      *                  defaults to false
799      */
800     public function locate($name, $findpath=false) {
801         if (!isset($this->category_cache[$this->name])) {
802             // somebody much have purged the cache
803             $this->category_cache[$this->name] = $this;
804         }
806         if ($this->name == $name) {
807             if ($findpath) {
808                 $this->visiblepath[] = $this->visiblename;
809                 $this->path[]        = $this->name;
810             }
811             return $this;
812         }
814         // quick category lookup
815         if (!$findpath and isset($this->category_cache[$name])) {
816             return $this->category_cache[$name];
817         }
819         $return = NULL;
820         foreach($this->children as $childid=>$unused) {
821             if ($return = $this->children[$childid]->locate($name, $findpath)) {
822                 break;
823             }
824         }
826         if (!is_null($return) and $findpath) {
827             $return->visiblepath[] = $this->visiblename;
828             $return->path[]        = $this->name;
829         }
831         return $return;
832     }
834     /**
835      * Search using query
836      *
837      * @param string query
838      * @return mixed array-object structure of found settings and pages
839      */
840     public function search($query) {
841         $result = array();
842         foreach ($this->get_children() as $child) {
843             $subsearch = $child->search($query);
844             if (!is_array($subsearch)) {
845                 debugging('Incorrect search result from '.$child->name);
846                 continue;
847             }
848             $result = array_merge($result, $subsearch);
849         }
850         return $result;
851     }
853     /**
854      * Removes part_of_admin_tree object with internal name $name.
855      *
856      * @param string $name The internal name of the object we want to remove.
857      * @return bool success
858      */
859     public function prune($name) {
861         if ($this->name == $name) {
862             return false;  //can not remove itself
863         }
865         foreach($this->children as $precedence => $child) {
866             if ($child->name == $name) {
867                 // clear cache and delete self
868                 while($this->category_cache) {
869                     // delete the cache, but keep the original array address
870                     array_pop($this->category_cache);
871                 }
872                 unset($this->children[$precedence]);
873                 return true;
874             } else if ($this->children[$precedence]->prune($name)) {
875                 return true;
876             }
877         }
878         return false;
879     }
881     /**
882      * Adds a part_of_admin_tree to a child or grandchild (or great-grandchild, and so forth) of this object.
883      *
884      * By default the new part of the tree is appended as the last child of the parent. You
885      * can specify a sibling node that the new part should be prepended to. If the given
886      * sibling is not found, the part is appended to the end (as it would be by default) and
887      * a developer debugging message is displayed.
888      *
889      * @throws coding_exception if the $beforesibling is empty string or is not string at all.
890      * @param string $destinationame The internal name of the immediate parent that we want for $something.
891      * @param mixed $something A part_of_admin_tree or setting instance to be added.
892      * @param string $beforesibling The name of the parent's child the $something should be prepended to.
893      * @return bool True if successfully added, false if $something can not be added.
894      */
895     public function add($parentname, $something, $beforesibling = null) {
896         global $CFG;
898         $parent = $this->locate($parentname);
899         if (is_null($parent)) {
900             debugging('parent does not exist!');
901             return false;
902         }
904         if ($something instanceof part_of_admin_tree) {
905             if (!($parent instanceof parentable_part_of_admin_tree)) {
906                 debugging('error - parts of tree can be inserted only into parentable parts');
907                 return false;
908             }
909             if ($CFG->debugdeveloper && !is_null($this->locate($something->name))) {
910                 // The name of the node is already used, simply warn the developer that this should not happen.
911                 // It is intentional to check for the debug level before performing the check.
912                 debugging('Duplicate admin page name: ' . $something->name, DEBUG_DEVELOPER);
913             }
914             if (is_null($beforesibling)) {
915                 // Append $something as the parent's last child.
916                 $parent->children[] = $something;
917             } else {
918                 if (!is_string($beforesibling) or trim($beforesibling) === '') {
919                     throw new coding_exception('Unexpected value of the beforesibling parameter');
920                 }
921                 // Try to find the position of the sibling.
922                 $siblingposition = null;
923                 foreach ($parent->children as $childposition => $child) {
924                     if ($child->name === $beforesibling) {
925                         $siblingposition = $childposition;
926                         break;
927                     }
928                 }
929                 if (is_null($siblingposition)) {
930                     debugging('Sibling '.$beforesibling.' not found', DEBUG_DEVELOPER);
931                     $parent->children[] = $something;
932                 } else {
933                     $parent->children = array_merge(
934                         array_slice($parent->children, 0, $siblingposition),
935                         array($something),
936                         array_slice($parent->children, $siblingposition)
937                     );
938                 }
939             }
940             if ($something instanceof admin_category) {
941                 if (isset($this->category_cache[$something->name])) {
942                     debugging('Duplicate admin category name: '.$something->name);
943                 } else {
944                     $this->category_cache[$something->name] = $something;
945                     $something->category_cache =& $this->category_cache;
946                     foreach ($something->children as $child) {
947                         // just in case somebody already added subcategories
948                         if ($child instanceof admin_category) {
949                             if (isset($this->category_cache[$child->name])) {
950                                 debugging('Duplicate admin category name: '.$child->name);
951                             } else {
952                                 $this->category_cache[$child->name] = $child;
953                                 $child->category_cache =& $this->category_cache;
954                             }
955                         }
956                     }
957                 }
958             }
959             return true;
961         } else {
962             debugging('error - can not add this element');
963             return false;
964         }
966     }
968     /**
969      * Checks if the user has access to anything in this category.
970      *
971      * @return bool True if the user has access to at least one child in this category, false otherwise.
972      */
973     public function check_access() {
974         foreach ($this->children as $child) {
975             if ($child->check_access()) {
976                 return true;
977             }
978         }
979         return false;
980     }
982     /**
983      * Is this category hidden in admin tree block?
984      *
985      * @return bool True if hidden
986      */
987     public function is_hidden() {
988         return $this->hidden;
989     }
991     /**
992      * Show we display Save button at the page bottom?
993      * @return bool
994      */
995     public function show_save() {
996         foreach ($this->children as $child) {
997             if ($child->show_save()) {
998                 return true;
999             }
1000         }
1001         return false;
1002     }
1004     /**
1005      * Sets sorting on this category.
1006      *
1007      * Please note this function doesn't actually do the sorting.
1008      * It can be called anytime.
1009      * Sorting occurs when the user calls get_children.
1010      * Code using the children array directly won't see the sorted results.
1011      *
1012      * @param bool $sort If set to true children will be sorted, if false they won't be.
1013      * @param bool $asc If true sorting will be ascending, otherwise descending.
1014      * @param bool $split If true we sort pages and sub categories separately.
1015      */
1016     public function set_sorting($sort, $asc = true, $split = true) {
1017         $this->sort = (bool)$sort;
1018         $this->sortasc = (bool)$asc;
1019         $this->sortsplit = (bool)$split;
1020     }
1022     /**
1023      * Returns the children associated with this category.
1024      *
1025      * @return part_of_admin_tree[]
1026      */
1027     public function get_children() {
1028         // If we should sort and it hasn't already been sorted.
1029         if ($this->sort && !$this->sorted) {
1030             if ($this->sortsplit) {
1031                 $categories = array();
1032                 $pages = array();
1033                 foreach ($this->children as $child) {
1034                     if ($child instanceof admin_category) {
1035                         $categories[] = $child;
1036                     } else {
1037                         $pages[] = $child;
1038                     }
1039                 }
1040                 core_collator::asort_objects_by_property($categories, 'visiblename');
1041                 core_collator::asort_objects_by_property($pages, 'visiblename');
1042                 if (!$this->sortasc) {
1043                     $categories = array_reverse($categories);
1044                     $pages = array_reverse($pages);
1045                 }
1046                 $this->children = array_merge($pages, $categories);
1047             } else {
1048                 core_collator::asort_objects_by_property($this->children, 'visiblename');
1049                 if (!$this->sortasc) {
1050                     $this->children = array_reverse($this->children);
1051                 }
1052             }
1053             $this->sorted = true;
1054         }
1055         return $this->children;
1056     }
1058     /**
1059      * Magically gets a property from this object.
1060      *
1061      * @param $property
1062      * @return part_of_admin_tree[]
1063      * @throws coding_exception
1064      */
1065     public function __get($property) {
1066         if ($property === 'children') {
1067             return $this->get_children();
1068         }
1069         throw new coding_exception('Invalid property requested.');
1070     }
1072     /**
1073      * Magically sets a property against this object.
1074      *
1075      * @param string $property
1076      * @param mixed $value
1077      * @throws coding_exception
1078      */
1079     public function __set($property, $value) {
1080         if ($property === 'children') {
1081             $this->sorted = false;
1082             $this->children = $value;
1083         } else {
1084             throw new coding_exception('Invalid property requested.');
1085         }
1086     }
1088     /**
1089      * Checks if an inaccessible property is set.
1090      *
1091      * @param string $property
1092      * @return bool
1093      * @throws coding_exception
1094      */
1095     public function __isset($property) {
1096         if ($property === 'children') {
1097             return isset($this->children);
1098         }
1099         throw new coding_exception('Invalid property requested.');
1100     }
1104 /**
1105  * Root of admin settings tree, does not have any parent.
1106  *
1107  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1108  */
1109 class admin_root extends admin_category {
1110 /** @var array List of errors */
1111     public $errors;
1112     /** @var string search query */
1113     public $search;
1114     /** @var bool full tree flag - true means all settings required, false only pages required */
1115     public $fulltree;
1116     /** @var bool flag indicating loaded tree */
1117     public $loaded;
1118     /** @var mixed site custom defaults overriding defaults in settings files*/
1119     public $custom_defaults;
1121     /**
1122      * @param bool $fulltree true means all settings required,
1123      *                            false only pages required
1124      */
1125     public function __construct($fulltree) {
1126         global $CFG;
1128         parent::__construct('root', get_string('administration'), false);
1129         $this->errors   = array();
1130         $this->search   = '';
1131         $this->fulltree = $fulltree;
1132         $this->loaded   = false;
1134         $this->category_cache = array();
1136         // load custom defaults if found
1137         $this->custom_defaults = null;
1138         $defaultsfile = "$CFG->dirroot/local/defaults.php";
1139         if (is_readable($defaultsfile)) {
1140             $defaults = array();
1141             include($defaultsfile);
1142             if (is_array($defaults) and count($defaults)) {
1143                 $this->custom_defaults = $defaults;
1144             }
1145         }
1146     }
1148     /**
1149      * Empties children array, and sets loaded to false
1150      *
1151      * @param bool $requirefulltree
1152      */
1153     public function purge_children($requirefulltree) {
1154         $this->children = array();
1155         $this->fulltree = ($requirefulltree || $this->fulltree);
1156         $this->loaded   = false;
1157         //break circular dependencies - this helps PHP 5.2
1158         while($this->category_cache) {
1159             array_pop($this->category_cache);
1160         }
1161         $this->category_cache = array();
1162     }
1166 /**
1167  * Links external PHP pages into the admin tree.
1168  *
1169  * See detailed usage example at the top of this document (adminlib.php)
1170  *
1171  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1172  */
1173 class admin_externalpage implements part_of_admin_tree {
1175     /** @var string An internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects */
1176     public $name;
1178     /** @var string The displayed name for this external page. Usually obtained through get_string(). */
1179     public $visiblename;
1181     /** @var string The external URL that we should link to when someone requests this external page. */
1182     public $url;
1184     /** @var string The role capability/permission a user must have to access this external page. */
1185     public $req_capability;
1187     /** @var object The context in which capability/permission should be checked, default is site context. */
1188     public $context;
1190     /** @var bool hidden in admin tree block. */
1191     public $hidden;
1193     /** @var mixed either string or array of string */
1194     public $path;
1196     /** @var array list of visible names of page parents */
1197     public $visiblepath;
1199     /**
1200      * Constructor for adding an external page into the admin tree.
1201      *
1202      * @param string $name The internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects.
1203      * @param string $visiblename The displayed name for this external page. Usually obtained through get_string().
1204      * @param string $url The external URL that we should link to when someone requests this external page.
1205      * @param mixed $req_capability The role capability/permission a user must have to access this external page. Defaults to 'moodle/site:config'.
1206      * @param boolean $hidden Is this external page hidden in admin tree block? Default false.
1207      * @param stdClass $context The context the page relates to. Not sure what happens
1208      *      if you specify something other than system or front page. Defaults to system.
1209      */
1210     public function __construct($name, $visiblename, $url, $req_capability='moodle/site:config', $hidden=false, $context=NULL) {
1211         $this->name        = $name;
1212         $this->visiblename = $visiblename;
1213         $this->url         = $url;
1214         if (is_array($req_capability)) {
1215             $this->req_capability = $req_capability;
1216         } else {
1217             $this->req_capability = array($req_capability);
1218         }
1219         $this->hidden = $hidden;
1220         $this->context = $context;
1221     }
1223     /**
1224      * Returns a reference to the part_of_admin_tree object with internal name $name.
1225      *
1226      * @param string $name The internal name of the object we want.
1227      * @param bool $findpath defaults to false
1228      * @return mixed A reference to the object with internal name $name if found, otherwise a reference to NULL.
1229      */
1230     public function locate($name, $findpath=false) {
1231         if ($this->name == $name) {
1232             if ($findpath) {
1233                 $this->visiblepath = array($this->visiblename);
1234                 $this->path        = array($this->name);
1235             }
1236             return $this;
1237         } else {
1238             $return = NULL;
1239             return $return;
1240         }
1241     }
1243     /**
1244      * This function always returns false, required function by interface
1245      *
1246      * @param string $name
1247      * @return false
1248      */
1249     public function prune($name) {
1250         return false;
1251     }
1253     /**
1254      * Search using query
1255      *
1256      * @param string $query
1257      * @return mixed array-object structure of found settings and pages
1258      */
1259     public function search($query) {
1260         $found = false;
1261         if (strpos(strtolower($this->name), $query) !== false) {
1262             $found = true;
1263         } else if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
1264                 $found = true;
1265             }
1266         if ($found) {
1267             $result = new stdClass();
1268             $result->page     = $this;
1269             $result->settings = array();
1270             return array($this->name => $result);
1271         } else {
1272             return array();
1273         }
1274     }
1276     /**
1277      * Determines if the current user has access to this external page based on $this->req_capability.
1278      *
1279      * @return bool True if user has access, false otherwise.
1280      */
1281     public function check_access() {
1282         global $CFG;
1283         $context = empty($this->context) ? context_system::instance() : $this->context;
1284         foreach($this->req_capability as $cap) {
1285             if (has_capability($cap, $context)) {
1286                 return true;
1287             }
1288         }
1289         return false;
1290     }
1292     /**
1293      * Is this external page hidden in admin tree block?
1294      *
1295      * @return bool True if hidden
1296      */
1297     public function is_hidden() {
1298         return $this->hidden;
1299     }
1301     /**
1302      * Show we display Save button at the page bottom?
1303      * @return bool
1304      */
1305     public function show_save() {
1306         return false;
1307     }
1311 /**
1312  * Used to group a number of admin_setting objects into a page and add them to the admin tree.
1313  *
1314  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1315  */
1316 class admin_settingpage implements part_of_admin_tree {
1318     /** @var string An internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects */
1319     public $name;
1321     /** @var string The displayed name for this external page. Usually obtained through get_string(). */
1322     public $visiblename;
1324     /** @var mixed An array of admin_setting objects that are part of this setting page. */
1325     public $settings;
1327     /** @var string The role capability/permission a user must have to access this external page. */
1328     public $req_capability;
1330     /** @var object The context in which capability/permission should be checked, default is site context. */
1331     public $context;
1333     /** @var bool hidden in admin tree block. */
1334     public $hidden;
1336     /** @var mixed string of paths or array of strings of paths */
1337     public $path;
1339     /** @var array list of visible names of page parents */
1340     public $visiblepath;
1342     /**
1343      * see admin_settingpage for details of this function
1344      *
1345      * @param string $name The internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects.
1346      * @param string $visiblename The displayed name for this external page. Usually obtained through get_string().
1347      * @param mixed $req_capability The role capability/permission a user must have to access this external page. Defaults to 'moodle/site:config'.
1348      * @param boolean $hidden Is this external page hidden in admin tree block? Default false.
1349      * @param stdClass $context The context the page relates to. Not sure what happens
1350      *      if you specify something other than system or front page. Defaults to system.
1351      */
1352     public function __construct($name, $visiblename, $req_capability='moodle/site:config', $hidden=false, $context=NULL) {
1353         $this->settings    = new stdClass();
1354         $this->name        = $name;
1355         $this->visiblename = $visiblename;
1356         if (is_array($req_capability)) {
1357             $this->req_capability = $req_capability;
1358         } else {
1359             $this->req_capability = array($req_capability);
1360         }
1361         $this->hidden      = $hidden;
1362         $this->context     = $context;
1363     }
1365     /**
1366      * see admin_category
1367      *
1368      * @param string $name
1369      * @param bool $findpath
1370      * @return mixed Object (this) if name ==  this->name, else returns null
1371      */
1372     public function locate($name, $findpath=false) {
1373         if ($this->name == $name) {
1374             if ($findpath) {
1375                 $this->visiblepath = array($this->visiblename);
1376                 $this->path        = array($this->name);
1377             }
1378             return $this;
1379         } else {
1380             $return = NULL;
1381             return $return;
1382         }
1383     }
1385     /**
1386      * Search string in settings page.
1387      *
1388      * @param string $query
1389      * @return array
1390      */
1391     public function search($query) {
1392         $found = array();
1394         foreach ($this->settings as $setting) {
1395             if ($setting->is_related($query)) {
1396                 $found[] = $setting;
1397             }
1398         }
1400         if ($found) {
1401             $result = new stdClass();
1402             $result->page     = $this;
1403             $result->settings = $found;
1404             return array($this->name => $result);
1405         }
1407         $found = false;
1408         if (strpos(strtolower($this->name), $query) !== false) {
1409             $found = true;
1410         } else if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
1411                 $found = true;
1412             }
1413         if ($found) {
1414             $result = new stdClass();
1415             $result->page     = $this;
1416             $result->settings = array();
1417             return array($this->name => $result);
1418         } else {
1419             return array();
1420         }
1421     }
1423     /**
1424      * This function always returns false, required by interface
1425      *
1426      * @param string $name
1427      * @return bool Always false
1428      */
1429     public function prune($name) {
1430         return false;
1431     }
1433     /**
1434      * adds an admin_setting to this admin_settingpage
1435      *
1436      * 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
1437      * n.b. each admin_setting in an admin_settingpage must have a unique internal name
1438      *
1439      * @param object $setting is the admin_setting object you want to add
1440      * @return bool true if successful, false if not
1441      */
1442     public function add($setting) {
1443         if (!($setting instanceof admin_setting)) {
1444             debugging('error - not a setting instance');
1445             return false;
1446         }
1448         $this->settings->{$setting->name} = $setting;
1449         return true;
1450     }
1452     /**
1453      * see admin_externalpage
1454      *
1455      * @return bool Returns true for yes false for no
1456      */
1457     public function check_access() {
1458         global $CFG;
1459         $context = empty($this->context) ? context_system::instance() : $this->context;
1460         foreach($this->req_capability as $cap) {
1461             if (has_capability($cap, $context)) {
1462                 return true;
1463             }
1464         }
1465         return false;
1466     }
1468     /**
1469      * outputs this page as html in a table (suitable for inclusion in an admin pagetype)
1470      * @return string Returns an XHTML string
1471      */
1472     public function output_html() {
1473         $adminroot = admin_get_root();
1474         $return = '<fieldset>'."\n".'<div class="clearer"><!-- --></div>'."\n";
1475         foreach($this->settings as $setting) {
1476             $fullname = $setting->get_full_name();
1477             if (array_key_exists($fullname, $adminroot->errors)) {
1478                 $data = $adminroot->errors[$fullname]->data;
1479             } else {
1480                 $data = $setting->get_setting();
1481                 // do not use defaults if settings not available - upgrade settings handles the defaults!
1482             }
1483             $return .= $setting->output_html($data);
1484         }
1485         $return .= '</fieldset>';
1486         return $return;
1487     }
1489     /**
1490      * Is this settings page hidden in admin tree block?
1491      *
1492      * @return bool True if hidden
1493      */
1494     public function is_hidden() {
1495         return $this->hidden;
1496     }
1498     /**
1499      * Show we display Save button at the page bottom?
1500      * @return bool
1501      */
1502     public function show_save() {
1503         foreach($this->settings as $setting) {
1504             if (empty($setting->nosave)) {
1505                 return true;
1506             }
1507         }
1508         return false;
1509     }
1513 /**
1514  * Admin settings class. Only exists on setting pages.
1515  * Read & write happens at this level; no authentication.
1516  *
1517  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1518  */
1519 abstract class admin_setting {
1520     /** @var string unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins. */
1521     public $name;
1522     /** @var string localised name */
1523     public $visiblename;
1524     /** @var string localised long description in Markdown format */
1525     public $description;
1526     /** @var mixed Can be string or array of string */
1527     public $defaultsetting;
1528     /** @var string */
1529     public $updatedcallback;
1530     /** @var mixed can be String or Null.  Null means main config table */
1531     public $plugin; // null means main config table
1532     /** @var bool true indicates this setting does not actually save anything, just information */
1533     public $nosave = false;
1534     /** @var bool if set, indicates that a change to this setting requires rebuild course cache */
1535     public $affectsmodinfo = false;
1536     /** @var array of admin_setting_flag - These are extra checkboxes attached to a setting. */
1537     private $flags = array();
1539     /**
1540      * Constructor
1541      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
1542      *                     or 'myplugin/mysetting' for ones in config_plugins.
1543      * @param string $visiblename localised name
1544      * @param string $description localised long description
1545      * @param mixed $defaultsetting string or array depending on implementation
1546      */
1547     public function __construct($name, $visiblename, $description, $defaultsetting) {
1548         $this->parse_setting_name($name);
1549         $this->visiblename    = $visiblename;
1550         $this->description    = $description;
1551         $this->defaultsetting = $defaultsetting;
1552     }
1554     /**
1555      * Generic function to add a flag to this admin setting.
1556      *
1557      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1558      * @param bool $default - The default for the flag
1559      * @param string $shortname - The shortname for this flag. Used as a suffix for the setting name.
1560      * @param string $displayname - The display name for this flag. Used as a label next to the checkbox.
1561      */
1562     protected function set_flag_options($enabled, $default, $shortname, $displayname) {
1563         if (empty($this->flags[$shortname])) {
1564             $this->flags[$shortname] = new admin_setting_flag($enabled, $default, $shortname, $displayname);
1565         } else {
1566             $this->flags[$shortname]->set_options($enabled, $default);
1567         }
1568     }
1570     /**
1571      * Set the enabled options flag on this admin setting.
1572      *
1573      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1574      * @param bool $default - The default for the flag
1575      */
1576     public function set_enabled_flag_options($enabled, $default) {
1577         $this->set_flag_options($enabled, $default, 'enabled', new lang_string('enabled', 'core_admin'));
1578     }
1580     /**
1581      * Set the advanced options flag on this admin setting.
1582      *
1583      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1584      * @param bool $default - The default for the flag
1585      */
1586     public function set_advanced_flag_options($enabled, $default) {
1587         $this->set_flag_options($enabled, $default, 'adv', new lang_string('advanced'));
1588     }
1591     /**
1592      * Set the locked options flag on this admin setting.
1593      *
1594      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1595      * @param bool $default - The default for the flag
1596      */
1597     public function set_locked_flag_options($enabled, $default) {
1598         $this->set_flag_options($enabled, $default, 'locked', new lang_string('locked', 'core_admin'));
1599     }
1601     /**
1602      * Get the currently saved value for a setting flag
1603      *
1604      * @param admin_setting_flag $flag - One of the admin_setting_flag for this admin_setting.
1605      * @return bool
1606      */
1607     public function get_setting_flag_value(admin_setting_flag $flag) {
1608         $value = $this->config_read($this->name . '_' . $flag->get_shortname());
1609         if (!isset($value)) {
1610             $value = $flag->get_default();
1611         }
1613         return !empty($value);
1614     }
1616     /**
1617      * Get the list of defaults for the flags on this setting.
1618      *
1619      * @param array of strings describing the defaults for this setting. This is appended to by this function.
1620      */
1621     public function get_setting_flag_defaults(& $defaults) {
1622         foreach ($this->flags as $flag) {
1623             if ($flag->is_enabled() && $flag->get_default()) {
1624                 $defaults[] = $flag->get_displayname();
1625             }
1626         }
1627     }
1629     /**
1630      * Output the input fields for the advanced and locked flags on this setting.
1631      *
1632      * @param bool $adv - The current value of the advanced flag.
1633      * @param bool $locked - The current value of the locked flag.
1634      * @return string $output - The html for the flags.
1635      */
1636     public function output_setting_flags() {
1637         $output = '';
1639         foreach ($this->flags as $flag) {
1640             if ($flag->is_enabled()) {
1641                 $output .= $flag->output_setting_flag($this);
1642             }
1643         }
1645         if (!empty($output)) {
1646             return html_writer::tag('span', $output, array('class' => 'adminsettingsflags'));
1647         }
1648         return $output;
1649     }
1651     /**
1652      * Write the values of the flags for this admin setting.
1653      *
1654      * @param array $data - The data submitted from the form or null to set the default value for new installs.
1655      * @return bool - true if successful.
1656      */
1657     public function write_setting_flags($data) {
1658         $result = true;
1659         foreach ($this->flags as $flag) {
1660             $result = $result && $flag->write_setting_flag($this, $data);
1661         }
1662         return $result;
1663     }
1665     /**
1666      * Set up $this->name and potentially $this->plugin
1667      *
1668      * Set up $this->name and possibly $this->plugin based on whether $name looks
1669      * like 'settingname' or 'plugin/settingname'. Also, do some sanity checking
1670      * on the names, that is, output a developer debug warning if the name
1671      * contains anything other than [a-zA-Z0-9_]+.
1672      *
1673      * @param string $name the setting name passed in to the constructor.
1674      */
1675     private function parse_setting_name($name) {
1676         $bits = explode('/', $name);
1677         if (count($bits) > 2) {
1678             throw new moodle_exception('invalidadminsettingname', '', '', $name);
1679         }
1680         $this->name = array_pop($bits);
1681         if (!preg_match('/^[a-zA-Z0-9_]+$/', $this->name)) {
1682             throw new moodle_exception('invalidadminsettingname', '', '', $name);
1683         }
1684         if (!empty($bits)) {
1685             $this->plugin = array_pop($bits);
1686             if ($this->plugin === 'moodle') {
1687                 $this->plugin = null;
1688             } else if (!preg_match('/^[a-zA-Z0-9_]+$/', $this->plugin)) {
1689                     throw new moodle_exception('invalidadminsettingname', '', '', $name);
1690                 }
1691         }
1692     }
1694     /**
1695      * Returns the fullname prefixed by the plugin
1696      * @return string
1697      */
1698     public function get_full_name() {
1699         return 's_'.$this->plugin.'_'.$this->name;
1700     }
1702     /**
1703      * Returns the ID string based on plugin and name
1704      * @return string
1705      */
1706     public function get_id() {
1707         return 'id_s_'.$this->plugin.'_'.$this->name;
1708     }
1710     /**
1711      * @param bool $affectsmodinfo If true, changes to this setting will
1712      *   cause the course cache to be rebuilt
1713      */
1714     public function set_affects_modinfo($affectsmodinfo) {
1715         $this->affectsmodinfo = $affectsmodinfo;
1716     }
1718     /**
1719      * Returns the config if possible
1720      *
1721      * @return mixed returns config if successful else null
1722      */
1723     public function config_read($name) {
1724         global $CFG;
1725         if (!empty($this->plugin)) {
1726             $value = get_config($this->plugin, $name);
1727             return $value === false ? NULL : $value;
1729         } else {
1730             if (isset($CFG->$name)) {
1731                 return $CFG->$name;
1732             } else {
1733                 return NULL;
1734             }
1735         }
1736     }
1738     /**
1739      * Used to set a config pair and log change
1740      *
1741      * @param string $name
1742      * @param mixed $value Gets converted to string if not null
1743      * @return bool Write setting to config table
1744      */
1745     public function config_write($name, $value) {
1746         global $DB, $USER, $CFG;
1748         if ($this->nosave) {
1749             return true;
1750         }
1752         // make sure it is a real change
1753         $oldvalue = get_config($this->plugin, $name);
1754         $oldvalue = ($oldvalue === false) ? null : $oldvalue; // normalise
1755         $value = is_null($value) ? null : (string)$value;
1757         if ($oldvalue === $value) {
1758             return true;
1759         }
1761         // store change
1762         set_config($name, $value, $this->plugin);
1764         // Some admin settings affect course modinfo
1765         if ($this->affectsmodinfo) {
1766             // Clear course cache for all courses
1767             rebuild_course_cache(0, true);
1768         }
1770         $this->add_to_config_log($name, $oldvalue, $value);
1772         return true; // BC only
1773     }
1775     /**
1776      * Log config changes if necessary.
1777      * @param string $name
1778      * @param string $oldvalue
1779      * @param string $value
1780      */
1781     protected function add_to_config_log($name, $oldvalue, $value) {
1782         add_to_config_log($name, $oldvalue, $value, $this->plugin);
1783     }
1785     /**
1786      * Returns current value of this setting
1787      * @return mixed array or string depending on instance, NULL means not set yet
1788      */
1789     public abstract function get_setting();
1791     /**
1792      * Returns default setting if exists
1793      * @return mixed array or string depending on instance; NULL means no default, user must supply
1794      */
1795     public function get_defaultsetting() {
1796         $adminroot =  admin_get_root(false, false);
1797         if (!empty($adminroot->custom_defaults)) {
1798             $plugin = is_null($this->plugin) ? 'moodle' : $this->plugin;
1799             if (isset($adminroot->custom_defaults[$plugin])) {
1800                 if (array_key_exists($this->name, $adminroot->custom_defaults[$plugin])) { // null is valid value here ;-)
1801                     return $adminroot->custom_defaults[$plugin][$this->name];
1802                 }
1803             }
1804         }
1805         return $this->defaultsetting;
1806     }
1808     /**
1809      * Store new setting
1810      *
1811      * @param mixed $data string or array, must not be NULL
1812      * @return string empty string if ok, string error message otherwise
1813      */
1814     public abstract function write_setting($data);
1816     /**
1817      * Return part of form with setting
1818      * This function should always be overwritten
1819      *
1820      * @param mixed $data array or string depending on setting
1821      * @param string $query
1822      * @return string
1823      */
1824     public function output_html($data, $query='') {
1825     // should be overridden
1826         return;
1827     }
1829     /**
1830      * Function called if setting updated - cleanup, cache reset, etc.
1831      * @param string $functionname Sets the function name
1832      * @return void
1833      */
1834     public function set_updatedcallback($functionname) {
1835         $this->updatedcallback = $functionname;
1836     }
1838     /**
1839      * Execute postupdatecallback if necessary.
1840      * @param mixed $original original value before write_setting()
1841      * @return bool true if changed, false if not.
1842      */
1843     public function post_write_settings($original) {
1844         // Comparison must work for arrays too.
1845         if (serialize($original) === serialize($this->get_setting())) {
1846             return false;
1847         }
1849         $callbackfunction = $this->updatedcallback;
1850         if (!empty($callbackfunction) and function_exists($callbackfunction)) {
1851             $callbackfunction($this->get_full_name());
1852         }
1853         return true;
1854     }
1856     /**
1857      * Is setting related to query text - used when searching
1858      * @param string $query
1859      * @return bool
1860      */
1861     public function is_related($query) {
1862         if (strpos(strtolower($this->name), $query) !== false) {
1863             return true;
1864         }
1865         if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
1866             return true;
1867         }
1868         if (strpos(core_text::strtolower($this->description), $query) !== false) {
1869             return true;
1870         }
1871         $current = $this->get_setting();
1872         if (!is_null($current)) {
1873             if (is_string($current)) {
1874                 if (strpos(core_text::strtolower($current), $query) !== false) {
1875                     return true;
1876                 }
1877             }
1878         }
1879         $default = $this->get_defaultsetting();
1880         if (!is_null($default)) {
1881             if (is_string($default)) {
1882                 if (strpos(core_text::strtolower($default), $query) !== false) {
1883                     return true;
1884                 }
1885             }
1886         }
1887         return false;
1888     }
1891 /**
1892  * An additional option that can be applied to an admin setting.
1893  * The currently supported options are 'ADVANCED' and 'LOCKED'.
1894  *
1895  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1896  */
1897 class admin_setting_flag {
1898     /** @var bool Flag to indicate if this option can be toggled for this setting */
1899     private $enabled = false;
1900     /** @var bool Flag to indicate if this option defaults to true or false */
1901     private $default = false;
1902     /** @var string Short string used to create setting name - e.g. 'adv' */
1903     private $shortname = '';
1904     /** @var string String used as the label for this flag */
1905     private $displayname = '';
1906     /** @const Checkbox for this flag is displayed in admin page */
1907     const ENABLED = true;
1908     /** @const Checkbox for this flag is not displayed in admin page */
1909     const DISABLED = false;
1911     /**
1912      * Constructor
1913      *
1914      * @param bool $enabled Can this option can be toggled.
1915      *                      Should be one of admin_setting_flag::ENABLED or admin_setting_flag::DISABLED.
1916      * @param bool $default The default checked state for this setting option.
1917      * @param string $shortname The shortname of this flag. Currently supported flags are 'locked' and 'adv'
1918      * @param string $displayname The displayname of this flag. Used as a label for the flag.
1919      */
1920     public function __construct($enabled, $default, $shortname, $displayname) {
1921         $this->shortname = $shortname;
1922         $this->displayname = $displayname;
1923         $this->set_options($enabled, $default);
1924     }
1926     /**
1927      * Update the values of this setting options class
1928      *
1929      * @param bool $enabled Can this option can be toggled.
1930      *                      Should be one of admin_setting_flag::ENABLED or admin_setting_flag::DISABLED.
1931      * @param bool $default The default checked state for this setting option.
1932      */
1933     public function set_options($enabled, $default) {
1934         $this->enabled = $enabled;
1935         $this->default = $default;
1936     }
1938     /**
1939      * Should this option appear in the interface and be toggleable?
1940      *
1941      * @return bool Is it enabled?
1942      */
1943     public function is_enabled() {
1944         return $this->enabled;
1945     }
1947     /**
1948      * Should this option be checked by default?
1949      *
1950      * @return bool Is it on by default?
1951      */
1952     public function get_default() {
1953         return $this->default;
1954     }
1956     /**
1957      * Return the short name for this flag. e.g. 'adv' or 'locked'
1958      *
1959      * @return string
1960      */
1961     public function get_shortname() {
1962         return $this->shortname;
1963     }
1965     /**
1966      * Return the display name for this flag. e.g. 'Advanced' or 'Locked'
1967      *
1968      * @return string
1969      */
1970     public function get_displayname() {
1971         return $this->displayname;
1972     }
1974     /**
1975      * Save the submitted data for this flag - or set it to the default if $data is null.
1976      *
1977      * @param admin_setting $setting - The admin setting for this flag
1978      * @param array $data - The data submitted from the form or null to set the default value for new installs.
1979      * @return bool
1980      */
1981     public function write_setting_flag(admin_setting $setting, $data) {
1982         $result = true;
1983         if ($this->is_enabled()) {
1984             if (!isset($data)) {
1985                 $value = $this->get_default();
1986             } else {
1987                 $value = !empty($data[$setting->get_full_name() . '_' . $this->get_shortname()]);
1988             }
1989             $result = $setting->config_write($setting->name . '_' . $this->get_shortname(), $value);
1990         }
1992         return $result;
1994     }
1996     /**
1997      * Output the checkbox for this setting flag. Should only be called if the flag is enabled.
1998      *
1999      * @param admin_setting $setting - The admin setting for this flag
2000      * @return string - The html for the checkbox.
2001      */
2002     public function output_setting_flag(admin_setting $setting) {
2003         $value = $setting->get_setting_flag_value($this);
2004         $output = ' <input type="checkbox" class="form-checkbox" ' .
2005                         ' id="' .  $setting->get_id() . '_' . $this->get_shortname() . '" ' .
2006                         ' name="' . $setting->get_full_name() .  '_' . $this->get_shortname() . '" ' .
2007                         ' value="1" ' . ($value ? 'checked="checked"' : '') . ' />' .
2008                         ' <label for="' . $setting->get_id() . '_' . $this->get_shortname() . '">' .
2009                         $this->get_displayname() .
2010                         ' </label> ';
2011         return $output;
2012     }
2016 /**
2017  * No setting - just heading and text.
2018  *
2019  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2020  */
2021 class admin_setting_heading extends admin_setting {
2023     /**
2024      * not a setting, just text
2025      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2026      * @param string $heading heading
2027      * @param string $information text in box
2028      */
2029     public function __construct($name, $heading, $information) {
2030         $this->nosave = true;
2031         parent::__construct($name, $heading, $information, '');
2032     }
2034     /**
2035      * Always returns true
2036      * @return bool Always returns true
2037      */
2038     public function get_setting() {
2039         return true;
2040     }
2042     /**
2043      * Always returns true
2044      * @return bool Always returns true
2045      */
2046     public function get_defaultsetting() {
2047         return true;
2048     }
2050     /**
2051      * Never write settings
2052      * @return string Always returns an empty string
2053      */
2054     public function write_setting($data) {
2055     // do not write any setting
2056         return '';
2057     }
2059     /**
2060      * Returns an HTML string
2061      * @return string Returns an HTML string
2062      */
2063     public function output_html($data, $query='') {
2064         global $OUTPUT;
2065         $return = '';
2066         if ($this->visiblename != '') {
2067             $return .= $OUTPUT->heading($this->visiblename, 3, 'main');
2068         }
2069         if ($this->description != '') {
2070             $return .= $OUTPUT->box(highlight($query, markdown_to_html($this->description)), 'generalbox formsettingheading');
2071         }
2072         return $return;
2073     }
2077 /**
2078  * The most flexibly setting, user is typing text
2079  *
2080  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2081  */
2082 class admin_setting_configtext extends admin_setting {
2084     /** @var mixed int means PARAM_XXX type, string is a allowed format in regex */
2085     public $paramtype;
2086     /** @var int default field size */
2087     public $size;
2089     /**
2090      * Config text constructor
2091      *
2092      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2093      * @param string $visiblename localised
2094      * @param string $description long localised info
2095      * @param string $defaultsetting
2096      * @param mixed $paramtype int means PARAM_XXX type, string is a allowed format in regex
2097      * @param int $size default field size
2098      */
2099     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $size=null) {
2100         $this->paramtype = $paramtype;
2101         if (!is_null($size)) {
2102             $this->size  = $size;
2103         } else {
2104             $this->size  = ($paramtype === PARAM_INT) ? 5 : 30;
2105         }
2106         parent::__construct($name, $visiblename, $description, $defaultsetting);
2107     }
2109     /**
2110      * Return the setting
2111      *
2112      * @return mixed returns config if successful else null
2113      */
2114     public function get_setting() {
2115         return $this->config_read($this->name);
2116     }
2118     public function write_setting($data) {
2119         if ($this->paramtype === PARAM_INT and $data === '') {
2120         // do not complain if '' used instead of 0
2121             $data = 0;
2122         }
2123         // $data is a string
2124         $validated = $this->validate($data);
2125         if ($validated !== true) {
2126             return $validated;
2127         }
2128         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
2129     }
2131     /**
2132      * Validate data before storage
2133      * @param string data
2134      * @return mixed true if ok string if error found
2135      */
2136     public function validate($data) {
2137         // allow paramtype to be a custom regex if it is the form of /pattern/
2138         if (preg_match('#^/.*/$#', $this->paramtype)) {
2139             if (preg_match($this->paramtype, $data)) {
2140                 return true;
2141             } else {
2142                 return get_string('validateerror', 'admin');
2143             }
2145         } else if ($this->paramtype === PARAM_RAW) {
2146             return true;
2148         } else {
2149             $cleaned = clean_param($data, $this->paramtype);
2150             if ("$data" === "$cleaned") { // implicit conversion to string is needed to do exact comparison
2151                 return true;
2152             } else {
2153                 return get_string('validateerror', 'admin');
2154             }
2155         }
2156     }
2158     /**
2159      * Return an XHTML string for the setting
2160      * @return string Returns an XHTML string
2161      */
2162     public function output_html($data, $query='') {
2163         $default = $this->get_defaultsetting();
2165         return format_admin_setting($this, $this->visiblename,
2166         '<div class="form-text defaultsnext"><input type="text" size="'.$this->size.'" id="'.$this->get_id().'" name="'.$this->get_full_name().'" value="'.s($data).'" /></div>',
2167         $this->description, true, '', $default, $query);
2168     }
2172 /**
2173  * General text area without html editor.
2174  *
2175  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2176  */
2177 class admin_setting_configtextarea extends admin_setting_configtext {
2178     private $rows;
2179     private $cols;
2181     /**
2182      * @param string $name
2183      * @param string $visiblename
2184      * @param string $description
2185      * @param mixed $defaultsetting string or array
2186      * @param mixed $paramtype
2187      * @param string $cols The number of columns to make the editor
2188      * @param string $rows The number of rows to make the editor
2189      */
2190     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $cols='60', $rows='8') {
2191         $this->rows = $rows;
2192         $this->cols = $cols;
2193         parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype);
2194     }
2196     /**
2197      * Returns an XHTML string for the editor
2198      *
2199      * @param string $data
2200      * @param string $query
2201      * @return string XHTML string for the editor
2202      */
2203     public function output_html($data, $query='') {
2204         $default = $this->get_defaultsetting();
2206         $defaultinfo = $default;
2207         if (!is_null($default) and $default !== '') {
2208             $defaultinfo = "\n".$default;
2209         }
2211         return format_admin_setting($this, $this->visiblename,
2212         '<div class="form-textarea" ><textarea rows="'. $this->rows .'" cols="'. $this->cols .'" id="'. $this->get_id() .'" name="'. $this->get_full_name() .'" spellcheck="true">'. s($data) .'</textarea></div>',
2213         $this->description, true, '', $defaultinfo, $query);
2214     }
2218 /**
2219  * General text area with html editor.
2220  */
2221 class admin_setting_confightmleditor extends admin_setting_configtext {
2222     private $rows;
2223     private $cols;
2225     /**
2226      * @param string $name
2227      * @param string $visiblename
2228      * @param string $description
2229      * @param mixed $defaultsetting string or array
2230      * @param mixed $paramtype
2231      */
2232     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $cols='60', $rows='8') {
2233         $this->rows = $rows;
2234         $this->cols = $cols;
2235         parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype);
2236         editors_head_setup();
2237     }
2239     /**
2240      * Returns an XHTML string for the editor
2241      *
2242      * @param string $data
2243      * @param string $query
2244      * @return string XHTML string for the editor
2245      */
2246     public function output_html($data, $query='') {
2247         $default = $this->get_defaultsetting();
2249         $defaultinfo = $default;
2250         if (!is_null($default) and $default !== '') {
2251             $defaultinfo = "\n".$default;
2252         }
2254         $editor = editors_get_preferred_editor(FORMAT_HTML);
2255         $editor->use_editor($this->get_id(), array('noclean'=>true));
2257         return format_admin_setting($this, $this->visiblename,
2258         '<div class="form-textarea"><textarea rows="'. $this->rows .'" cols="'. $this->cols .'" id="'. $this->get_id() .'" name="'. $this->get_full_name() .'" spellcheck="true">'. s($data) .'</textarea></div>',
2259         $this->description, true, '', $defaultinfo, $query);
2260     }
2264 /**
2265  * Password field, allows unmasking of password
2266  *
2267  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2268  */
2269 class admin_setting_configpasswordunmask extends admin_setting_configtext {
2270     /**
2271      * Constructor
2272      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2273      * @param string $visiblename localised
2274      * @param string $description long localised info
2275      * @param string $defaultsetting default password
2276      */
2277     public function __construct($name, $visiblename, $description, $defaultsetting) {
2278         parent::__construct($name, $visiblename, $description, $defaultsetting, PARAM_RAW, 30);
2279     }
2281     /**
2282      * Log config changes if necessary.
2283      * @param string $name
2284      * @param string $oldvalue
2285      * @param string $value
2286      */
2287     protected function add_to_config_log($name, $oldvalue, $value) {
2288         if ($value !== '') {
2289             $value = '********';
2290         }
2291         if ($oldvalue !== '' and $oldvalue !== null) {
2292             $oldvalue = '********';
2293         }
2294         parent::add_to_config_log($name, $oldvalue, $value);
2295     }
2297     /**
2298      * Returns XHTML for the field
2299      * Writes Javascript into the HTML below right before the last div
2300      *
2301      * @todo Make javascript available through newer methods if possible
2302      * @param string $data Value for the field
2303      * @param string $query Passed as final argument for format_admin_setting
2304      * @return string XHTML field
2305      */
2306     public function output_html($data, $query='') {
2307         $id = $this->get_id();
2308         $unmask = get_string('unmaskpassword', 'form');
2309         $unmaskjs = '<script type="text/javascript">
2310 //<![CDATA[
2311 var is_ie = (navigator.userAgent.toLowerCase().indexOf("msie") != -1);
2313 document.getElementById("'.$id.'").setAttribute("autocomplete", "off");
2315 var unmaskdiv = document.getElementById("'.$id.'unmaskdiv");
2317 var unmaskchb = document.createElement("input");
2318 unmaskchb.setAttribute("type", "checkbox");
2319 unmaskchb.setAttribute("id", "'.$id.'unmask");
2320 unmaskchb.onchange = function() {unmaskPassword("'.$id.'");};
2321 unmaskdiv.appendChild(unmaskchb);
2323 var unmasklbl = document.createElement("label");
2324 unmasklbl.innerHTML = "'.addslashes_js($unmask).'";
2325 if (is_ie) {
2326   unmasklbl.setAttribute("htmlFor", "'.$id.'unmask");
2327 } else {
2328   unmasklbl.setAttribute("for", "'.$id.'unmask");
2330 unmaskdiv.appendChild(unmasklbl);
2332 if (is_ie) {
2333   // ugly hack to work around the famous onchange IE bug
2334   unmaskchb.onclick = function() {this.blur();};
2335   unmaskdiv.onclick = function() {this.blur();};
2337 //]]>
2338 </script>';
2339         return format_admin_setting($this, $this->visiblename,
2340         '<div class="form-password"><input type="password" size="'.$this->size.'" id="'.$id.'" name="'.$this->get_full_name().'" value="'.s($data).'" /><div class="unmask" id="'.$id.'unmaskdiv"></div>'.$unmaskjs.'</div>',
2341         $this->description, true, '', NULL, $query);
2342     }
2345 /**
2346  * Empty setting used to allow flags (advanced) on settings that can have no sensible default.
2347  * Note: Only advanced makes sense right now - locked does not.
2348  *
2349  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2350  */
2351 class admin_setting_configempty extends admin_setting_configtext {
2353     /**
2354      * @param string $name
2355      * @param string $visiblename
2356      * @param string $description
2357      */
2358     public function __construct($name, $visiblename, $description) {
2359         parent::__construct($name, $visiblename, $description, '', PARAM_RAW);
2360     }
2362     /**
2363      * Returns an XHTML string for the hidden field
2364      *
2365      * @param string $data
2366      * @param string $query
2367      * @return string XHTML string for the editor
2368      */
2369     public function output_html($data, $query='') {
2370         return format_admin_setting($this,
2371                                     $this->visiblename,
2372                                     '<div class="form-empty" >' .
2373                                     '<input type="hidden"' .
2374                                         ' id="'. $this->get_id() .'"' .
2375                                         ' name="'. $this->get_full_name() .'"' .
2376                                         ' value=""/></div>',
2377                                     $this->description,
2378                                     true,
2379                                     '',
2380                                     get_string('none'),
2381                                     $query);
2382     }
2386 /**
2387  * Path to directory
2388  *
2389  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2390  */
2391 class admin_setting_configfile extends admin_setting_configtext {
2392     /**
2393      * Constructor
2394      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2395      * @param string $visiblename localised
2396      * @param string $description long localised info
2397      * @param string $defaultdirectory default directory location
2398      */
2399     public function __construct($name, $visiblename, $description, $defaultdirectory) {
2400         parent::__construct($name, $visiblename, $description, $defaultdirectory, PARAM_RAW, 50);
2401     }
2403     /**
2404      * Returns XHTML for the field
2405      *
2406      * Returns XHTML for the field and also checks whether the file
2407      * specified in $data exists using file_exists()
2408      *
2409      * @param string $data File name and path to use in value attr
2410      * @param string $query
2411      * @return string XHTML field
2412      */
2413     public function output_html($data, $query='') {
2414         $default = $this->get_defaultsetting();
2416         if ($data) {
2417             if (file_exists($data)) {
2418                 $executable = '<span class="pathok">&#x2714;</span>';
2419             } else {
2420                 $executable = '<span class="patherror">&#x2718;</span>';
2421             }
2422         } else {
2423             $executable = '';
2424         }
2426         return format_admin_setting($this, $this->visiblename,
2427         '<div class="form-file defaultsnext"><input type="text" size="'.$this->size.'" id="'.$this->get_id().'" name="'.$this->get_full_name().'" value="'.s($data).'" />'.$executable.'</div>',
2428         $this->description, true, '', $default, $query);
2429     }
2430     /**
2431      * checks if execpatch has been disabled in config.php
2432      */
2433     public function write_setting($data) {
2434         global $CFG;
2435         if (!empty($CFG->preventexecpath)) {
2436             return '';
2437         }
2438         return parent::write_setting($data);
2439     }
2443 /**
2444  * Path to executable file
2445  *
2446  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2447  */
2448 class admin_setting_configexecutable extends admin_setting_configfile {
2450     /**
2451      * Returns an XHTML field
2452      *
2453      * @param string $data This is the value for the field
2454      * @param string $query
2455      * @return string XHTML field
2456      */
2457     public function output_html($data, $query='') {
2458         global $CFG;
2459         $default = $this->get_defaultsetting();
2461         if ($data) {
2462             if (file_exists($data) and is_executable($data)) {
2463                 $executable = '<span class="pathok">&#x2714;</span>';
2464             } else {
2465                 $executable = '<span class="patherror">&#x2718;</span>';
2466             }
2467         } else {
2468             $executable = '';
2469         }
2470         if (!empty($CFG->preventexecpath)) {
2471             $this->visiblename .= '<div class="form-overridden">'.get_string('execpathnotallowed', 'admin').'</div>';
2472         }
2474         return format_admin_setting($this, $this->visiblename,
2475         '<div class="form-file defaultsnext"><input type="text" size="'.$this->size.'" id="'.$this->get_id().'" name="'.$this->get_full_name().'" value="'.s($data).'" />'.$executable.'</div>',
2476         $this->description, true, '', $default, $query);
2477     }
2481 /**
2482  * Path to directory
2483  *
2484  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2485  */
2486 class admin_setting_configdirectory extends admin_setting_configfile {
2488     /**
2489      * Returns an XHTML field
2490      *
2491      * @param string $data This is the value for the field
2492      * @param string $query
2493      * @return string XHTML
2494      */
2495     public function output_html($data, $query='') {
2496         $default = $this->get_defaultsetting();
2498         if ($data) {
2499             if (file_exists($data) and is_dir($data)) {
2500                 $executable = '<span class="pathok">&#x2714;</span>';
2501             } else {
2502                 $executable = '<span class="patherror">&#x2718;</span>';
2503             }
2504         } else {
2505             $executable = '';
2506         }
2508         return format_admin_setting($this, $this->visiblename,
2509         '<div class="form-file defaultsnext"><input type="text" size="'.$this->size.'" id="'.$this->get_id().'" name="'.$this->get_full_name().'" value="'.s($data).'" />'.$executable.'</div>',
2510         $this->description, true, '', $default, $query);
2511     }
2515 /**
2516  * Checkbox
2517  *
2518  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2519  */
2520 class admin_setting_configcheckbox extends admin_setting {
2521     /** @var string Value used when checked */
2522     public $yes;
2523     /** @var string Value used when not checked */
2524     public $no;
2526     /**
2527      * Constructor
2528      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2529      * @param string $visiblename localised
2530      * @param string $description long localised info
2531      * @param string $defaultsetting
2532      * @param string $yes value used when checked
2533      * @param string $no value used when not checked
2534      */
2535     public function __construct($name, $visiblename, $description, $defaultsetting, $yes='1', $no='0') {
2536         parent::__construct($name, $visiblename, $description, $defaultsetting);
2537         $this->yes = (string)$yes;
2538         $this->no  = (string)$no;
2539     }
2541     /**
2542      * Retrieves the current setting using the objects name
2543      *
2544      * @return string
2545      */
2546     public function get_setting() {
2547         return $this->config_read($this->name);
2548     }
2550     /**
2551      * Sets the value for the setting
2552      *
2553      * Sets the value for the setting to either the yes or no values
2554      * of the object by comparing $data to yes
2555      *
2556      * @param mixed $data Gets converted to str for comparison against yes value
2557      * @return string empty string or error
2558      */
2559     public function write_setting($data) {
2560         if ((string)$data === $this->yes) { // convert to strings before comparison
2561             $data = $this->yes;
2562         } else {
2563             $data = $this->no;
2564         }
2565         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
2566     }
2568     /**
2569      * Returns an XHTML checkbox field
2570      *
2571      * @param string $data If $data matches yes then checkbox is checked
2572      * @param string $query
2573      * @return string XHTML field
2574      */
2575     public function output_html($data, $query='') {
2576         $default = $this->get_defaultsetting();
2578         if (!is_null($default)) {
2579             if ((string)$default === $this->yes) {
2580                 $defaultinfo = get_string('checkboxyes', 'admin');
2581             } else {
2582                 $defaultinfo = get_string('checkboxno', 'admin');
2583             }
2584         } else {
2585             $defaultinfo = NULL;
2586         }
2588         if ((string)$data === $this->yes) { // convert to strings before comparison
2589             $checked = 'checked="checked"';
2590         } else {
2591             $checked = '';
2592         }
2594         return format_admin_setting($this, $this->visiblename,
2595         '<div class="form-checkbox defaultsnext" ><input type="hidden" name="'.$this->get_full_name().'" value="'.s($this->no).'" /> '
2596             .'<input type="checkbox" id="'.$this->get_id().'" name="'.$this->get_full_name().'" value="'.s($this->yes).'" '.$checked.' /></div>',
2597         $this->description, true, '', $defaultinfo, $query);
2598     }
2602 /**
2603  * Multiple checkboxes, each represents different value, stored in csv format
2604  *
2605  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2606  */
2607 class admin_setting_configmulticheckbox extends admin_setting {
2608     /** @var array Array of choices value=>label */
2609     public $choices;
2611     /**
2612      * Constructor: uses parent::__construct
2613      *
2614      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2615      * @param string $visiblename localised
2616      * @param string $description long localised info
2617      * @param array $defaultsetting array of selected
2618      * @param array $choices array of $value=>$label for each checkbox
2619      */
2620     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
2621         $this->choices = $choices;
2622         parent::__construct($name, $visiblename, $description, $defaultsetting);
2623     }
2625     /**
2626      * This public function may be used in ancestors for lazy loading of choices
2627      *
2628      * @todo Check if this function is still required content commented out only returns true
2629      * @return bool true if loaded, false if error
2630      */
2631     public function load_choices() {
2632         /*
2633         if (is_array($this->choices)) {
2634             return true;
2635         }
2636         .... load choices here
2637         */
2638         return true;
2639     }
2641     /**
2642      * Is setting related to query text - used when searching
2643      *
2644      * @param string $query
2645      * @return bool true on related, false on not or failure
2646      */
2647     public function is_related($query) {
2648         if (!$this->load_choices() or empty($this->choices)) {
2649             return false;
2650         }
2651         if (parent::is_related($query)) {
2652             return true;
2653         }
2655         foreach ($this->choices as $desc) {
2656             if (strpos(core_text::strtolower($desc), $query) !== false) {
2657                 return true;
2658             }
2659         }
2660         return false;
2661     }
2663     /**
2664      * Returns the current setting if it is set
2665      *
2666      * @return mixed null if null, else an array
2667      */
2668     public function get_setting() {
2669         $result = $this->config_read($this->name);
2671         if (is_null($result)) {
2672             return NULL;
2673         }
2674         if ($result === '') {
2675             return array();
2676         }
2677         $enabled = explode(',', $result);
2678         $setting = array();
2679         foreach ($enabled as $option) {
2680             $setting[$option] = 1;
2681         }
2682         return $setting;
2683     }
2685     /**
2686      * Saves the setting(s) provided in $data
2687      *
2688      * @param array $data An array of data, if not array returns empty str
2689      * @return mixed empty string on useless data or bool true=success, false=failed
2690      */
2691     public function write_setting($data) {
2692         if (!is_array($data)) {
2693             return ''; // ignore it
2694         }
2695         if (!$this->load_choices() or empty($this->choices)) {
2696             return '';
2697         }
2698         unset($data['xxxxx']);
2699         $result = array();
2700         foreach ($data as $key => $value) {
2701             if ($value and array_key_exists($key, $this->choices)) {
2702                 $result[] = $key;
2703             }
2704         }
2705         return $this->config_write($this->name, implode(',', $result)) ? '' : get_string('errorsetting', 'admin');
2706     }
2708     /**
2709      * Returns XHTML field(s) as required by choices
2710      *
2711      * Relies on data being an array should data ever be another valid vartype with
2712      * acceptable value this may cause a warning/error
2713      * if (!is_array($data)) would fix the problem
2714      *
2715      * @todo Add vartype handling to ensure $data is an array
2716      *
2717      * @param array $data An array of checked values
2718      * @param string $query
2719      * @return string XHTML field
2720      */
2721     public function output_html($data, $query='') {
2722         if (!$this->load_choices() or empty($this->choices)) {
2723             return '';
2724         }
2725         $default = $this->get_defaultsetting();
2726         if (is_null($default)) {
2727             $default = array();
2728         }
2729         if (is_null($data)) {
2730             $data = array();
2731         }
2732         $options = array();
2733         $defaults = array();
2734         foreach ($this->choices as $key=>$description) {
2735             if (!empty($data[$key])) {
2736                 $checked = 'checked="checked"';
2737             } else {
2738                 $checked = '';
2739             }
2740             if (!empty($default[$key])) {
2741                 $defaults[] = $description;
2742             }
2744             $options[] = '<input type="checkbox" id="'.$this->get_id().'_'.$key.'" name="'.$this->get_full_name().'['.$key.']" value="1" '.$checked.' />'
2745                 .'<label for="'.$this->get_id().'_'.$key.'">'.highlightfast($query, $description).'</label>';
2746         }
2748         if (is_null($default)) {
2749             $defaultinfo = NULL;
2750         } else if (!empty($defaults)) {
2751                 $defaultinfo = implode(', ', $defaults);
2752             } else {
2753                 $defaultinfo = get_string('none');
2754             }
2756         $return = '<div class="form-multicheckbox">';
2757         $return .= '<input type="hidden" name="'.$this->get_full_name().'[xxxxx]" value="1" />'; // something must be submitted even if nothing selected
2758         if ($options) {
2759             $return .= '<ul>';
2760             foreach ($options as $option) {
2761                 $return .= '<li>'.$option.'</li>';
2762             }
2763             $return .= '</ul>';
2764         }
2765         $return .= '</div>';
2767         return format_admin_setting($this, $this->visiblename, $return, $this->description, false, '', $defaultinfo, $query);
2769     }
2773 /**
2774  * Multiple checkboxes 2, value stored as string 00101011
2775  *
2776  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2777  */
2778 class admin_setting_configmulticheckbox2 extends admin_setting_configmulticheckbox {
2780     /**
2781      * Returns the setting if set
2782      *
2783      * @return mixed null if not set, else an array of set settings
2784      */
2785     public function get_setting() {
2786         $result = $this->config_read($this->name);
2787         if (is_null($result)) {
2788             return NULL;
2789         }
2790         if (!$this->load_choices()) {
2791             return NULL;
2792         }
2793         $result = str_pad($result, count($this->choices), '0');
2794         $result = preg_split('//', $result, -1, PREG_SPLIT_NO_EMPTY);
2795         $setting = array();
2796         foreach ($this->choices as $key=>$unused) {
2797             $value = array_shift($result);
2798             if ($value) {
2799                 $setting[$key] = 1;
2800             }
2801         }
2802         return $setting;
2803     }
2805     /**
2806      * Save setting(s) provided in $data param
2807      *
2808      * @param array $data An array of settings to save
2809      * @return mixed empty string for bad data or bool true=>success, false=>error
2810      */
2811     public function write_setting($data) {
2812         if (!is_array($data)) {
2813             return ''; // ignore it
2814         }
2815         if (!$this->load_choices() or empty($this->choices)) {
2816             return '';
2817         }
2818         $result = '';
2819         foreach ($this->choices as $key=>$unused) {
2820             if (!empty($data[$key])) {
2821                 $result .= '1';
2822             } else {
2823                 $result .= '0';
2824             }
2825         }
2826         return $this->config_write($this->name, $result) ? '' : get_string('errorsetting', 'admin');
2827     }
2831 /**
2832  * Select one value from list
2833  *
2834  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2835  */
2836 class admin_setting_configselect extends admin_setting {
2837     /** @var array Array of choices value=>label */
2838     public $choices;
2840     /**
2841      * Constructor
2842      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2843      * @param string $visiblename localised
2844      * @param string $description long localised info
2845      * @param string|int $defaultsetting
2846      * @param array $choices array of $value=>$label for each selection
2847      */
2848     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
2849         $this->choices = $choices;
2850         parent::__construct($name, $visiblename, $description, $defaultsetting);
2851     }
2853     /**
2854      * This function may be used in ancestors for lazy loading of choices
2855      *
2856      * Override this method if loading of choices is expensive, such
2857      * as when it requires multiple db requests.
2858      *
2859      * @return bool true if loaded, false if error
2860      */
2861     public function load_choices() {
2862         /*
2863         if (is_array($this->choices)) {
2864             return true;
2865         }
2866         .... load choices here
2867         */
2868         return true;
2869     }
2871     /**
2872      * Check if this is $query is related to a choice
2873      *
2874      * @param string $query
2875      * @return bool true if related, false if not
2876      */
2877     public function is_related($query) {
2878         if (parent::is_related($query)) {
2879             return true;
2880         }
2881         if (!$this->load_choices()) {
2882             return false;
2883         }
2884         foreach ($this->choices as $key=>$value) {
2885             if (strpos(core_text::strtolower($key), $query) !== false) {
2886                 return true;
2887             }
2888             if (strpos(core_text::strtolower($value), $query) !== false) {
2889                 return true;
2890             }
2891         }
2892         return false;
2893     }
2895     /**
2896      * Return the setting
2897      *
2898      * @return mixed returns config if successful else null
2899      */
2900     public function get_setting() {
2901         return $this->config_read($this->name);
2902     }
2904     /**
2905      * Save a setting
2906      *
2907      * @param string $data
2908      * @return string empty of error string
2909      */
2910     public function write_setting($data) {
2911         if (!$this->load_choices() or empty($this->choices)) {
2912             return '';
2913         }
2914         if (!array_key_exists($data, $this->choices)) {
2915             return ''; // ignore it
2916         }
2918         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
2919     }
2921     /**
2922      * Returns XHTML select field
2923      *
2924      * Ensure the options are loaded, and generate the XHTML for the select
2925      * element and any warning message. Separating this out from output_html
2926      * makes it easier to subclass this class.
2927      *
2928      * @param string $data the option to show as selected.
2929      * @param string $current the currently selected option in the database, null if none.
2930      * @param string $default the default selected option.
2931      * @return array the HTML for the select element, and a warning message.
2932      */
2933     public function output_select_html($data, $current, $default, $extraname = '') {
2934         if (!$this->load_choices() or empty($this->choices)) {
2935             return array('', '');
2936         }
2938         $warning = '';
2939         if (is_null($current)) {
2940         // first run
2941         } else if (empty($current) and (array_key_exists('', $this->choices) or array_key_exists(0, $this->choices))) {
2942             // no warning
2943             } else if (!array_key_exists($current, $this->choices)) {
2944                     $warning = get_string('warningcurrentsetting', 'admin', s($current));
2945                     if (!is_null($default) and $data == $current) {
2946                         $data = $default; // use default instead of first value when showing the form
2947                     }
2948                 }
2950         $selecthtml = '<select id="'.$this->get_id().'" name="'.$this->get_full_name().$extraname.'">';
2951         foreach ($this->choices as $key => $value) {
2952         // the string cast is needed because key may be integer - 0 is equal to most strings!
2953             $selecthtml .= '<option value="'.$key.'"'.((string)$key==$data ? ' selected="selected"' : '').'>'.$value.'</option>';
2954         }
2955         $selecthtml .= '</select>';
2956         return array($selecthtml, $warning);
2957     }
2959     /**
2960      * Returns XHTML select field and wrapping div(s)
2961      *
2962      * @see output_select_html()
2963      *
2964      * @param string $data the option to show as selected
2965      * @param string $query
2966      * @return string XHTML field and wrapping div
2967      */
2968     public function output_html($data, $query='') {
2969         $default = $this->get_defaultsetting();
2970         $current = $this->get_setting();
2972         list($selecthtml, $warning) = $this->output_select_html($data, $current, $default);
2973         if (!$selecthtml) {
2974             return '';
2975         }
2977         if (!is_null($default) and array_key_exists($default, $this->choices)) {
2978             $defaultinfo = $this->choices[$default];
2979         } else {
2980             $defaultinfo = NULL;
2981         }
2983         $return = '<div class="form-select defaultsnext">' . $selecthtml . '</div>';
2985         return format_admin_setting($this, $this->visiblename, $return, $this->description, true, $warning, $defaultinfo, $query);
2986     }
2990 /**
2991  * Select multiple items from list
2992  *
2993  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2994  */
2995 class admin_setting_configmultiselect extends admin_setting_configselect {
2996     /**
2997      * Constructor
2998      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2999      * @param string $visiblename localised
3000      * @param string $description long localised info
3001      * @param array $defaultsetting array of selected items
3002      * @param array $choices array of $value=>$label for each list item
3003      */
3004     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
3005         parent::__construct($name, $visiblename, $description, $defaultsetting, $choices);
3006     }
3008     /**
3009      * Returns the select setting(s)
3010      *
3011      * @return mixed null or array. Null if no settings else array of setting(s)
3012      */
3013     public function get_setting() {
3014         $result = $this->config_read($this->name);
3015         if (is_null($result)) {
3016             return NULL;
3017         }
3018         if ($result === '') {
3019             return array();
3020         }
3021         return explode(',', $result);
3022     }
3024     /**
3025      * Saves setting(s) provided through $data
3026      *
3027      * Potential bug in the works should anyone call with this function
3028      * using a vartype that is not an array
3029      *
3030      * @param array $data
3031      */
3032     public function write_setting($data) {
3033         if (!is_array($data)) {
3034             return ''; //ignore it
3035         }
3036         if (!$this->load_choices() or empty($this->choices)) {
3037             return '';
3038         }
3040         unset($data['xxxxx']);
3042         $save = array();
3043         foreach ($data as $value) {
3044             if (!array_key_exists($value, $this->choices)) {
3045                 continue; // ignore it
3046             }
3047             $save[] = $value;
3048         }
3050         return ($this->config_write($this->name, implode(',', $save)) ? '' : get_string('errorsetting', 'admin'));
3051     }
3053     /**
3054      * Is setting related to query text - used when searching
3055      *
3056      * @param string $query
3057      * @return bool true if related, false if not
3058      */
3059     public function is_related($query) {
3060         if (!$this->load_choices() or empty($this->choices)) {
3061             return false;
3062         }
3063         if (parent::is_related($query)) {
3064             return true;
3065         }
3067         foreach ($this->choices as $desc) {
3068             if (strpos(core_text::strtolower($desc), $query) !== false) {
3069                 return true;
3070             }
3071         }
3072         return false;
3073     }
3075     /**
3076      * Returns XHTML multi-select field
3077      *
3078      * @todo Add vartype handling to ensure $data is an array
3079      * @param array $data Array of values to select by default
3080      * @param string $query
3081      * @return string XHTML multi-select field
3082      */
3083     public function output_html($data, $query='') {
3084         if (!$this->load_choices() or empty($this->choices)) {
3085             return '';
3086         }
3087         $choices = $this->choices;
3088         $default = $this->get_defaultsetting();
3089         if (is_null($default)) {
3090             $default = array();
3091         }
3092         if (is_null($data)) {
3093             $data = array();
3094         }
3096         $defaults = array();
3097         $size = min(10, count($this->choices));
3098         $return = '<div class="form-select"><input type="hidden" name="'.$this->get_full_name().'[xxxxx]" value="1" />'; // something must be submitted even if nothing selected
3099         $return .= '<select id="'.$this->get_id().'" name="'.$this->get_full_name().'[]" size="'.$size.'" multiple="multiple">';
3100         foreach ($this->choices as $key => $description) {
3101             if (in_array($key, $data)) {
3102                 $selected = 'selected="selected"';
3103             } else {
3104                 $selected = '';
3105             }
3106             if (in_array($key, $default)) {
3107                 $defaults[] = $description;
3108             }
3110             $return .= '<option value="'.s($key).'" '.$selected.'>'.$description.'</option>';
3111         }
3113         if (is_null($default)) {
3114             $defaultinfo = NULL;
3115         } if (!empty($defaults)) {
3116             $defaultinfo = implode(', ', $defaults);
3117         } else {
3118             $defaultinfo = get_string('none');
3119         }
3121         $return .= '</select></div>';
3122         return format_admin_setting($this, $this->visiblename, $return, $this->description, true, '', $defaultinfo, $query);
3123     }
3126 /**
3127  * Time selector
3128  *
3129  * This is a liiitle bit messy. we're using two selects, but we're returning
3130  * them as an array named after $name (so we only use $name2 internally for the setting)
3131  *
3132  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3133  */
3134 class admin_setting_configtime extends admin_setting {
3135     /** @var string Used for setting second select (minutes) */
3136     public $name2;
3138     /**
3139      * Constructor
3140      * @param string $hoursname setting for hours
3141      * @param string $minutesname setting for hours
3142      * @param string $visiblename localised
3143      * @param string $description long localised info
3144      * @param array $defaultsetting array representing default time 'h'=>hours, 'm'=>minutes
3145      */
3146     public function __construct($hoursname, $minutesname, $visiblename, $description, $defaultsetting) {
3147         $this->name2 = $minutesname;
3148         parent::__construct($hoursname, $visiblename, $description, $defaultsetting);
3149     }
3151     /**
3152      * Get the selected time
3153      *
3154      * @return mixed An array containing 'h'=>xx, 'm'=>xx, or null if not set
3155      */
3156     public function get_setting() {
3157         $result1 = $this->config_read($this->name);
3158         $result2 = $this->config_read($this->name2);
3159         if (is_null($result1) or is_null($result2)) {
3160             return NULL;
3161         }
3163         return array('h' => $result1, 'm' => $result2);
3164     }
3166     /**
3167      * Store the time (hours and minutes)
3168      *
3169      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3170      * @return bool true if success, false if not
3171      */
3172     public function write_setting($data) {
3173         if (!is_array($data)) {
3174             return '';
3175         }
3177         $result = $this->config_write($this->name, (int)$data['h']) && $this->config_write($this->name2, (int)$data['m']);
3178         return ($result ? '' : get_string('errorsetting', 'admin'));
3179     }
3181     /**
3182      * Returns XHTML time select fields
3183      *
3184      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3185      * @param string $query
3186      * @return string XHTML time select fields and wrapping div(s)
3187      */
3188     public function output_html($data, $query='') {
3189         $default = $this->get_defaultsetting();
3191         if (is_array($default)) {
3192             $defaultinfo = $default['h'].':'.$default['m'];
3193         } else {
3194             $defaultinfo = NULL;
3195         }
3197         $return = '<div class="form-time defaultsnext">'.
3198             '<select id="'.$this->get_id().'h" name="'.$this->get_full_name().'[h]">';
3199         for ($i = 0; $i < 24; $i++) {
3200             $return .= '<option value="'.$i.'"'.($i == $data['h'] ? ' selected="selected"' : '').'>'.$i.'</option>';
3201         }
3202         $return .= '</select>:<select id="'.$this->get_id().'m" name="'.$this->get_full_name().'[m]">';
3203         for ($i = 0; $i < 60; $i += 5) {
3204             $return .= '<option value="'.$i.'"'.($i == $data['m'] ? ' selected="selected"' : '').'>'.$i.'</option>';
3205         }
3206         $return .= '</select></div>';
3207         return format_admin_setting($this, $this->visiblename, $return, $this->description, false, '', $defaultinfo, $query);
3208     }
3213 /**
3214  * Seconds duration setting.
3215  *
3216  * @copyright 2012 Petr Skoda (http://skodak.org)
3217  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3218  */
3219 class admin_setting_configduration extends admin_setting {
3221     /** @var int default duration unit */
3222     protected $defaultunit;
3224     /**
3225      * Constructor
3226      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
3227      *                     or 'myplugin/mysetting' for ones in config_plugins.
3228      * @param string $visiblename localised name
3229      * @param string $description localised long description
3230      * @param mixed $defaultsetting string or array depending on implementation
3231      * @param int $defaultunit - day, week, etc. (in seconds)
3232      */
3233     public function __construct($name, $visiblename, $description, $defaultsetting, $defaultunit = 86400) {
3234         if (is_number($defaultsetting)) {
3235             $defaultsetting = self::parse_seconds($defaultsetting);
3236         }
3237         $units = self::get_units();
3238         if (isset($units[$defaultunit])) {
3239             $this->defaultunit = $defaultunit;
3240         } else {
3241             $this->defaultunit = 86400;
3242         }
3243         parent::__construct($name, $visiblename, $description, $defaultsetting);
3244     }
3246     /**
3247      * Returns selectable units.
3248      * @static
3249      * @return array
3250      */
3251     protected static function get_units() {
3252         return array(
3253             604800 => get_string('weeks'),
3254             86400 => get_string('days'),
3255             3600 => get_string('hours'),
3256             60 => get_string('minutes'),
3257             1 => get_string('seconds'),
3258         );
3259     }
3261     /**
3262      * Converts seconds to some more user friendly string.
3263      * @static
3264      * @param int $seconds
3265      * @return string
3266      */
3267     protected static function get_duration_text($seconds) {
3268         if (empty($seconds)) {
3269             return get_string('none');
3270         }
3271         $data = self::parse_seconds($seconds);
3272         switch ($data['u']) {
3273             case (60*60*24*7):
3274                 return get_string('numweeks', '', $data['v']);
3275             case (60*60*24):
3276                 return get_string('numdays', '', $data['v']);
3277             case (60*60):
3278                 return get_string('numhours', '', $data['v']);
3279             case (60):
3280                 return get_string('numminutes', '', $data['v']);
3281             default:
3282                 return get_string('numseconds', '', $data['v']*$data['u']);
3283         }
3284     }
3286     /**
3287      * Finds suitable units for given duration.
3288      * @static
3289      * @param int $seconds
3290      * @return array
3291      */
3292     protected static function parse_seconds($seconds) {
3293         foreach (self::get_units() as $unit => $unused) {
3294             if ($seconds % $unit === 0) {
3295                 return array('v'=>(int)($seconds/$unit), 'u'=>$unit);
3296             }
3297         }
3298         return array('v'=>(int)$seconds, 'u'=>1);
3299     }
3301     /**
3302      * Get the selected duration as array.
3303      *
3304      * @return mixed An array containing 'v'=>xx, 'u'=>xx, or null if not set
3305      */
3306     public function get_setting() {
3307         $seconds = $this->config_read($this->name);
3308         if (is_null($seconds)) {
3309             return null;
3310         }
3312         return self::parse_seconds($seconds);
3313     }
3315     /**
3316      * Store the duration as seconds.
3317      *
3318      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3319      * @return bool true if success, false if not
3320      */
3321     public function write_setting($data) {
3322         if (!is_array($data)) {
3323             return '';
3324         }
3326         $seconds = (int)($data['v']*$data['u']);
3327         if ($seconds < 0) {
3328             return get_string('errorsetting', 'admin');
3329         }
3331         $result = $this->config_write($this->name, $seconds);
3332         return ($result ? '' : get_string('errorsetting', 'admin'));
3333     }
3335     /**
3336      * Returns duration text+select fields.
3337      *
3338      * @param array $data Must be form 'v'=>xx, 'u'=>xx
3339      * @param string $query
3340      * @return string duration text+select fields and wrapping div(s)
3341      */
3342     public function output_html($data, $query='') {
3343         $default = $this->get_defaultsetting();
3345         if (is_number($default)) {
3346             $defaultinfo = self::get_duration_text($default);
3347         } else if (is_array($default)) {
3348             $defaultinfo = self::get_duration_text($default['v']*$default['u']);
3349         } else {
3350             $defaultinfo = null;
3351         }
3353         $units = self::get_units();
3355         $return = '<div class="form-duration defaultsnext">';
3356         $return .= '<input type="text" size="5" id="'.$this->get_id().'v" name="'.$this->get_full_name().'[v]" value="'.s($data['v']).'" />';
3357         $return .= '<select id="'.$this->get_id().'u" name="'.$this->get_full_name().'[u]">';
3358         foreach ($units as $val => $text) {
3359             $selected = '';
3360             if ($data['v'] == 0) {
3361                 if ($val == $this->defaultunit) {
3362                     $selected = ' selected="selected"';
3363                 }
3364             } else if ($val == $data['u']) {
3365                 $selected = ' selected="selected"';
3366             }
3367             $return .= '<option value="'.$val.'"'.$selected.'>'.$text.'</option>';
3368         }
3369         $return .= '</select></div>';
3370         return format_admin_setting($this, $this->visiblename, $return, $this->description, false, '', $defaultinfo, $query);
3371     }
3375 /**
3376  * Used to validate a textarea used for ip addresses
3377  *
3378  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3379  */
3380 class admin_setting_configiplist extends admin_setting_configtextarea {
3382     /**
3383      * Validate the contents of the textarea as IP addresses
3384      *
3385      * Used to validate a new line separated list of IP addresses collected from
3386      * a textarea control
3387      *
3388      * @param string $data A list of IP Addresses separated by new lines
3389      * @return mixed bool true for success or string:error on failure
3390      */
3391     public function validate($data) {
3392         if(!empty($data)) {
3393             $ips = explode("\n", $data);
3394         } else {
3395             return true;
3396         }
3397         $result = true;
3398         foreach($ips as $ip) {
3399             $ip = trim($ip);
3400             if (preg_match('#^(\d{1,3})(\.\d{1,3}){0,3}$#', $ip, $match) ||
3401                 preg_match('#^(\d{1,3})(\.\d{1,3}){0,3}(\/\d{1,2})$#', $ip, $match) ||
3402                 preg_match('#^(\d{1,3})(\.\d{1,3}){3}(-\d{1,3})$#', $ip, $match)) {
3403                 $result = true;
3404             } else {
3405                 $result = false;
3406                 break;
3407             }
3408         }
3409         if($result) {
3410             return true;
3411         } else {
3412             return get_string('validateerror', 'admin');
3413         }
3414     }
3418 /**
3419  * An admin setting for selecting one or more users who have a capability
3420  * in the system context
3421  *
3422  * An admin setting for selecting one or more users, who have a particular capability
3423  * in the system context. Warning, make sure the list will never be too long. There is
3424  * no paging or searching of this list.
3425  *
3426  * To correctly get a list of users from this config setting, you need to call the
3427  * get_users_from_config($CFG->mysetting, $capability); function in moodlelib.php.
3428  *
3429  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3430  */
3431 class admin_setting_users_with_capability extends admin_setting_configmultiselect {
3432     /** @var string The capabilities name */
3433     protected $capability;
3434     /** @var int include admin users too */
3435     protected $includeadmins;
3437     /**
3438      * Constructor.
3439      *
3440      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3441      * @param string $visiblename localised name
3442      * @param string $description localised long description
3443      * @param array $defaultsetting array of usernames
3444      * @param string $capability string capability name.
3445      * @param bool $includeadmins include administrators
3446      */
3447     function __construct($name, $visiblename, $description, $defaultsetting, $capability, $includeadmins = true) {
3448         $this->capability    = $capability;
3449         $this->includeadmins = $includeadmins;
3450         parent::__construct($name, $visiblename, $description, $defaultsetting, NULL);
3451     }
3453     /**
3454      * Load all of the uses who have the capability into choice array
3455      *
3456      * @return bool Always returns true
3457      */
3458     function load_choices() {
3459         if (is_array($this->choices)) {
3460             return true;
3461         }
3462         list($sort, $sortparams) = users_order_by_sql('u');
3463         if (!empty($sortparams)) {
3464             throw new coding_exception('users_order_by_sql returned some query parameters. ' .
3465                     'This is unexpected, and a problem because there is no way to pass these ' .
3466                     'parameters to get_users_by_capability. See MDL-34657.');
3467         }
3468         $userfields = 'u.id, u.username, ' . get_all_user_name_fields(true, 'u');
3469         $users = get_users_by_capability(context_system::instance(), $this->capability, $userfields, $sort);
3470         $this->choices = array(
3471             '$@NONE@$' => get_string('nobody'),
3472             '$@ALL@$' => get_string('everyonewhocan', 'admin', get_capability_string($this->capability)),
3473         );
3474         if ($this->includeadmins) {
3475             $admins = get_admins();
3476             foreach ($admins as $user) {
3477                 $this->choices[$user->id] = fullname($user);
3478             }
3479         }
3480         if (is_array($users)) {
3481             foreach ($users as $user) {
3482                 $this->choices[$user->id] = fullname($user);
3483             }
3484         }
3485         return true;
3486     }
3488     /**
3489      * Returns the default setting for class
3490      *
3491      * @return mixed Array, or string. Empty string if no default
3492      */
3493     public function get_defaultsetting() {
3494         $this->load_choices();
3495         $defaultsetting = parent::get_defaultsetting();
3496         if (empty($defaultsetting)) {
3497             return array('$@NONE@$');
3498         } else if (array_key_exists($defaultsetting, $this->choices)) {
3499                 return $defaultsetting;
3500             } else {
3501                 return '';
3502             }
3503     }
3505     /**
3506      * Returns the current setting
3507      *
3508      * @return mixed array or string
3509      */
3510     public function get_setting() {
3511         $result = parent::get_setting();
3512         if ($result === null) {
3513             // this is necessary for settings upgrade
3514             return null;
3515         }
3516         if (empty($result)) {
3517             $result = array('$@NONE@$');
3518         }
3519         return $result;
3520     }
3522     /**
3523      * Save the chosen setting provided as $data
3524      *
3525      * @param array $data
3526      * @return mixed string or array
3527      */
3528     public function write_setting($data) {
3529     // If all is selected, remove any explicit options.
3530         if (in_array('$@ALL@$', $data)) {
3531             $data = array('$@ALL@$');
3532         }
3533         // None never needs to be written to the DB.
3534         if (in_array('$@NONE@$', $data)) {
3535             unset($data[array_search('$@NONE@$', $data)]);
3536         }
3537         return parent::write_setting($data);
3538     }
3542 /**
3543  * Special checkbox for calendar - resets SESSION vars.
3544  *
3545  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3546  */
3547 class admin_setting_special_adminseesall extends admin_setting_configcheckbox {
3548     /**
3549      * Calls the parent::__construct with default values
3550      *
3551      * name =>  calendar_adminseesall
3552      * visiblename => get_string('adminseesall', 'admin')
3553      * description => get_string('helpadminseesall', 'admin')
3554      * defaultsetting => 0
3555      */
3556     public function __construct() {
3557         parent::__construct('calendar_adminseesall', get_string('adminseesall', 'admin'),
3558             get_string('helpadminseesall', 'admin'), '0');
3559     }
3561     /**
3562      * Stores the setting passed in $data
3563      *
3564      * @param mixed gets converted to string for comparison
3565      * @return string empty string or error message
3566      */
3567     public function write_setting($data) {
3568         global $SESSION;
3569         return parent::write_setting($data);
3570     }
3573 /**
3574  * Special select for settings that are altered in setup.php and can not be altered on the fly
3575  *
3576  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3577  */
3578 class admin_setting_special_selectsetup extends admin_setting_configselect {
3579     /**
3580      * Reads the setting directly from the database
3581      *
3582      * @return mixed
3583      */
3584     public function get_setting() {
3585     // read directly from db!
3586         return get_config(NULL, $this->name);
3587     }
3589     /**
3590      * Save the setting passed in $data
3591      *
3592      * @param string $data The setting to save
3593      * @return string empty or error message
3594      */
3595     public function write_setting($data) {
3596         global $CFG;
3597         // do not change active CFG setting!
3598         $current = $CFG->{$this->name};
3599         $result = parent::write_setting($data);
3600         $CFG->{$this->name} = $current;
3601         return $result;
3602     }
3606 /**
3607  * Special select for frontpage - stores data in course table
3608  *
3609  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3610  */
3611 class admin_setting_sitesetselect extends admin_setting_configselect {
3612     /**
3613      * Returns the site name for the selected site
3614      *
3615      * @see get_site()
3616      * @return string The site name of the selected site
3617      */
3618     public function get_setting() {
3619         $site = course_get_format(get_site())->get_course();
3620         return $site->{$this->name};
3621     }
3623     /**
3624      * Updates the database and save the setting
3625      *
3626      * @param string data
3627      * @return string empty or error message
3628      */
3629     public function write_setting($data) {
3630         global $DB, $SITE, $COURSE;
3631         if (!in_array($data, array_keys($this->choices))) {
3632             return get_string('errorsetting', 'admin');
3633         }
3634         $record = new stdClass();
3635         $record->id           = SITEID;
3636         $temp                 = $this->name;
3637         $record->$temp        = $data;
3638         $record->timemodified = time();
3640         course_get_format($SITE)->update_course_format_options($record);
3641         $DB->update_record('course', $record);
3643         // Reset caches.
3644         $SITE = $DB->get_record('course', array('id'=>$SITE->id), '*', MUST_EXIST);
3645         if ($SITE->id == $COURSE->id) {
3646             $COURSE = $SITE;
3647         }
3648         format_base::reset_course_cache($SITE->id);
3650         return '';
3652     }
3656 /**
3657  * Select for blog's bloglevel setting: if set to 0, will set blog_menu
3658  * block to hidden.
3659  *
3660  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3661  */
3662 class admin_setting_bloglevel extends admin_setting_configselect {
3663     /**
3664      * Updates the database and save the setting
3665      *
3666      * @param string data