7fc7d302fca44fd47560b1133849fc0c92a6e52f
[moodle.git] / lib / adminlib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Functions and classes used during installation, upgrades and for admin settings.
19  *
20  *  ADMIN SETTINGS TREE INTRODUCTION
21  *
22  *  This file performs the following tasks:
23  *   -it defines the necessary objects and interfaces to build the Moodle
24  *    admin hierarchy
25  *   -it defines the admin_externalpage_setup()
26  *
27  *  ADMIN_SETTING OBJECTS
28  *
29  *  Moodle settings are represented by objects that inherit from the admin_setting
30  *  class. These objects encapsulate how to read a setting, how to write a new value
31  *  to a setting, and how to appropriately display the HTML to modify the setting.
32  *
33  *  ADMIN_SETTINGPAGE OBJECTS
34  *
35  *  The admin_setting objects are then grouped into admin_settingpages. The latter
36  *  appear in the Moodle admin tree block. All interaction with admin_settingpage
37  *  objects is handled by the admin/settings.php file.
38  *
39  *  ADMIN_EXTERNALPAGE OBJECTS
40  *
41  *  There are some settings in Moodle that are too complex to (efficiently) handle
42  *  with admin_settingpages. (Consider, for example, user management and displaying
43  *  lists of users.) In this case, we use the admin_externalpage object. This object
44  *  places a link to an external PHP file in the admin tree block.
45  *
46  *  If you're using an admin_externalpage object for some settings, you can take
47  *  advantage of the admin_externalpage_* functions. For example, suppose you wanted
48  *  to add a foo.php file into admin. First off, you add the following line to
49  *  admin/settings/first.php (at the end of the file) or to some other file in
50  *  admin/settings:
51  * <code>
52  *     $ADMIN->add('userinterface', new admin_externalpage('foo', get_string('foo'),
53  *         $CFG->wwwdir . '/' . '$CFG->admin . '/foo.php', 'some_role_permission'));
54  * </code>
55  *
56  *  Next, in foo.php, your file structure would resemble the following:
57  * <code>
58  *         require(__DIR__.'/../../config.php');
59  *         require_once($CFG->libdir.'/adminlib.php');
60  *         admin_externalpage_setup('foo');
61  *         // functionality like processing form submissions goes here
62  *         echo $OUTPUT->header();
63  *         // your HTML goes here
64  *         echo $OUTPUT->footer();
65  * </code>
66  *
67  *  The admin_externalpage_setup() function call ensures the user is logged in,
68  *  and makes sure that they have the proper role permission to access the page.
69  *  It also configures all $PAGE properties needed for navigation.
70  *
71  *  ADMIN_CATEGORY OBJECTS
72  *
73  *  Above and beyond all this, we have admin_category objects. These objects
74  *  appear as folders in the admin tree block. They contain admin_settingpage's,
75  *  admin_externalpage's, and other admin_category's.
76  *
77  *  OTHER NOTES
78  *
79  *  admin_settingpage's, admin_externalpage's, and admin_category's all inherit
80  *  from part_of_admin_tree (a pseudointerface). This interface insists that
81  *  a class has a check_access method for access permissions, a locate method
82  *  used to find a specific node in the admin tree and find parent path.
83  *
84  *  admin_category's inherit from parentable_part_of_admin_tree. This pseudo-
85  *  interface ensures that the class implements a recursive add function which
86  *  accepts a part_of_admin_tree object and searches for the proper place to
87  *  put it. parentable_part_of_admin_tree implies part_of_admin_tree.
88  *
89  *  Please note that the $this->name field of any part_of_admin_tree must be
90  *  UNIQUE throughout the ENTIRE admin tree.
91  *
92  *  The $this->name field of an admin_setting object (which is *not* part_of_
93  *  admin_tree) must be unique on the respective admin_settingpage where it is
94  *  used.
95  *
96  * Original author: Vincenzo K. Marcovecchio
97  * Maintainer:      Petr Skoda
98  *
99  * @package    core
100  * @subpackage admin
101  * @copyright  1999 onwards Martin Dougiamas  http://dougiamas.com
102  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
103  */
105 defined('MOODLE_INTERNAL') || die();
107 /// Add libraries
108 require_once($CFG->libdir.'/ddllib.php');
109 require_once($CFG->libdir.'/xmlize.php');
110 require_once($CFG->libdir.'/messagelib.php');
112 define('INSECURE_DATAROOT_WARNING', 1);
113 define('INSECURE_DATAROOT_ERROR', 2);
115 /**
116  * Automatically clean-up all plugin data and remove the plugin DB tables
117  *
118  * NOTE: do not call directly, use new /admin/plugins.php?uninstall=component instead!
119  *
120  * @param string $type The plugin type, eg. 'mod', 'qtype', 'workshopgrading' etc.
121  * @param string $name The plugin name, eg. 'forum', 'multichoice', 'accumulative' etc.
122  * @uses global $OUTPUT to produce notices and other messages
123  * @return void
124  */
125 function uninstall_plugin($type, $name) {
126     global $CFG, $DB, $OUTPUT;
128     // This may take a long time.
129     core_php_time_limit::raise();
131     // Recursively uninstall all subplugins first.
132     $subplugintypes = core_component::get_plugin_types_with_subplugins();
133     if (isset($subplugintypes[$type])) {
134         $base = core_component::get_plugin_directory($type, $name);
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     // Delete all tag areas, collections and instances associated with this plugin.
170     core_tag_area::uninstall($component);
172     // Custom plugin uninstall.
173     $plugindirectory = core_component::get_plugin_directory($type, $name);
174     $uninstalllib = $plugindirectory . '/db/uninstall.php';
175     if (file_exists($uninstalllib)) {
176         require_once($uninstalllib);
177         $uninstallfunction = 'xmldb_' . $pluginname . '_uninstall';    // eg. 'xmldb_workshop_uninstall()'
178         if (function_exists($uninstallfunction)) {
179             // Do not verify result, let plugin complain if necessary.
180             $uninstallfunction();
181         }
182     }
184     // Specific plugin type cleanup.
185     $plugininfo = core_plugin_manager::instance()->get_plugin_info($component);
186     if ($plugininfo) {
187         $plugininfo->uninstall_cleanup();
188         core_plugin_manager::reset_caches();
189     }
190     $plugininfo = null;
192     // perform clean-up task common for all the plugin/subplugin types
194     //delete the web service functions and pre-built services
195     require_once($CFG->dirroot.'/lib/externallib.php');
196     external_delete_descriptions($component);
198     // delete calendar events
199     $DB->delete_records('event', array('modulename' => $pluginname));
201     // Delete scheduled tasks.
202     $DB->delete_records('task_scheduled', array('component' => $component));
204     // Delete Inbound Message datakeys.
205     $DB->delete_records_select('messageinbound_datakeys',
206             'handler IN (SELECT id FROM {messageinbound_handlers} WHERE component = ?)', array($component));
208     // Delete Inbound Message handlers.
209     $DB->delete_records('messageinbound_handlers', array('component' => $component));
211     // delete all the logs
212     $DB->delete_records('log', array('module' => $pluginname));
214     // delete log_display information
215     $DB->delete_records('log_display', array('component' => $component));
217     // delete the module configuration records
218     unset_all_config_for_plugin($component);
219     if ($type === 'mod') {
220         unset_all_config_for_plugin($pluginname);
221     }
223     // delete message provider
224     message_provider_uninstall($component);
226     // delete the plugin tables
227     $xmldbfilepath = $plugindirectory . '/db/install.xml';
228     drop_plugin_tables($component, $xmldbfilepath, false);
229     if ($type === 'mod' or $type === 'block') {
230         // non-frankenstyle table prefixes
231         drop_plugin_tables($name, $xmldbfilepath, false);
232     }
234     // delete the capabilities that were defined by this module
235     capabilities_cleanup($component);
237     // remove event handlers and dequeue pending events
238     events_uninstall($component);
240     // Delete all remaining files in the filepool owned by the component.
241     $fs = get_file_storage();
242     $fs->delete_component_files($component);
244     // Finally purge all caches.
245     purge_all_caches();
247     // Invalidate the hash used for upgrade detections.
248     set_config('allversionshash', '');
250     echo $OUTPUT->notification(get_string('success'), 'notifysuccess');
253 /**
254  * Returns the version of installed component
255  *
256  * @param string $component component name
257  * @param string $source either 'disk' or 'installed' - where to get the version information from
258  * @return string|bool version number or false if the component is not found
259  */
260 function get_component_version($component, $source='installed') {
261     global $CFG, $DB;
263     list($type, $name) = core_component::normalize_component($component);
265     // moodle core or a core subsystem
266     if ($type === 'core') {
267         if ($source === 'installed') {
268             if (empty($CFG->version)) {
269                 return false;
270             } else {
271                 return $CFG->version;
272             }
273         } else {
274             if (!is_readable($CFG->dirroot.'/version.php')) {
275                 return false;
276             } else {
277                 $version = null; //initialize variable for IDEs
278                 include($CFG->dirroot.'/version.php');
279                 return $version;
280             }
281         }
282     }
284     // activity module
285     if ($type === 'mod') {
286         if ($source === 'installed') {
287             if ($CFG->version < 2013092001.02) {
288                 return $DB->get_field('modules', 'version', array('name'=>$name));
289             } else {
290                 return get_config('mod_'.$name, 'version');
291             }
293         } else {
294             $mods = core_component::get_plugin_list('mod');
295             if (empty($mods[$name]) or !is_readable($mods[$name].'/version.php')) {
296                 return false;
297             } else {
298                 $plugin = new stdClass();
299                 $plugin->version = null;
300                 $module = $plugin;
301                 include($mods[$name].'/version.php');
302                 return $plugin->version;
303             }
304         }
305     }
307     // block
308     if ($type === 'block') {
309         if ($source === 'installed') {
310             if ($CFG->version < 2013092001.02) {
311                 return $DB->get_field('block', 'version', array('name'=>$name));
312             } else {
313                 return get_config('block_'.$name, 'version');
314             }
315         } else {
316             $blocks = core_component::get_plugin_list('block');
317             if (empty($blocks[$name]) or !is_readable($blocks[$name].'/version.php')) {
318                 return false;
319             } else {
320                 $plugin = new stdclass();
321                 include($blocks[$name].'/version.php');
322                 return $plugin->version;
323             }
324         }
325     }
327     // all other plugin types
328     if ($source === 'installed') {
329         return get_config($type.'_'.$name, 'version');
330     } else {
331         $plugins = core_component::get_plugin_list($type);
332         if (empty($plugins[$name])) {
333             return false;
334         } else {
335             $plugin = new stdclass();
336             include($plugins[$name].'/version.php');
337             return $plugin->version;
338         }
339     }
342 /**
343  * Delete all plugin tables
344  *
345  * @param string $name Name of plugin, used as table prefix
346  * @param string $file Path to install.xml file
347  * @param bool $feedback defaults to true
348  * @return bool Always returns true
349  */
350 function drop_plugin_tables($name, $file, $feedback=true) {
351     global $CFG, $DB;
353     // first try normal delete
354     if (file_exists($file) and $DB->get_manager()->delete_tables_from_xmldb_file($file)) {
355         return true;
356     }
358     // then try to find all tables that start with name and are not in any xml file
359     $used_tables = get_used_table_names();
361     $tables = $DB->get_tables();
363     /// Iterate over, fixing id fields as necessary
364     foreach ($tables as $table) {
365         if (in_array($table, $used_tables)) {
366             continue;
367         }
369         if (strpos($table, $name) !== 0) {
370             continue;
371         }
373         // found orphan table --> delete it
374         if ($DB->get_manager()->table_exists($table)) {
375             $xmldb_table = new xmldb_table($table);
376             $DB->get_manager()->drop_table($xmldb_table);
377         }
378     }
380     return true;
383 /**
384  * Returns names of all known tables == tables that moodle knows about.
385  *
386  * @return array Array of lowercase table names
387  */
388 function get_used_table_names() {
389     $table_names = array();
390     $dbdirs = get_db_directories();
392     foreach ($dbdirs as $dbdir) {
393         $file = $dbdir.'/install.xml';
395         $xmldb_file = new xmldb_file($file);
397         if (!$xmldb_file->fileExists()) {
398             continue;
399         }
401         $loaded    = $xmldb_file->loadXMLStructure();
402         $structure = $xmldb_file->getStructure();
404         if ($loaded and $tables = $structure->getTables()) {
405             foreach($tables as $table) {
406                 $table_names[] = strtolower($table->getName());
407             }
408         }
409     }
411     return $table_names;
414 /**
415  * Returns list of all directories where we expect install.xml files
416  * @return array Array of paths
417  */
418 function get_db_directories() {
419     global $CFG;
421     $dbdirs = array();
423     /// First, the main one (lib/db)
424     $dbdirs[] = $CFG->libdir.'/db';
426     /// Then, all the ones defined by core_component::get_plugin_types()
427     $plugintypes = core_component::get_plugin_types();
428     foreach ($plugintypes as $plugintype => $pluginbasedir) {
429         if ($plugins = core_component::get_plugin_list($plugintype)) {
430             foreach ($plugins as $plugin => $plugindir) {
431                 $dbdirs[] = $plugindir.'/db';
432             }
433         }
434     }
436     return $dbdirs;
439 /**
440  * Try to obtain or release the cron lock.
441  * @param string  $name  name of lock
442  * @param int  $until timestamp when this lock considered stale, null means remove lock unconditionally
443  * @param bool $ignorecurrent ignore current lock state, usually extend previous lock, defaults to false
444  * @return bool true if lock obtained
445  */
446 function set_cron_lock($name, $until, $ignorecurrent=false) {
447     global $DB;
448     if (empty($name)) {
449         debugging("Tried to get a cron lock for a null fieldname");
450         return false;
451     }
453     // remove lock by force == remove from config table
454     if (is_null($until)) {
455         set_config($name, null);
456         return true;
457     }
459     if (!$ignorecurrent) {
460         // read value from db - other processes might have changed it
461         $value = $DB->get_field('config', 'value', array('name'=>$name));
463         if ($value and $value > time()) {
464             //lock active
465             return false;
466         }
467     }
469     set_config($name, $until);
470     return true;
473 /**
474  * Test if and critical warnings are present
475  * @return bool
476  */
477 function admin_critical_warnings_present() {
478     global $SESSION;
480     if (!has_capability('moodle/site:config', context_system::instance())) {
481         return 0;
482     }
484     if (!isset($SESSION->admin_critical_warning)) {
485         $SESSION->admin_critical_warning = 0;
486         if (is_dataroot_insecure(true) === INSECURE_DATAROOT_ERROR) {
487             $SESSION->admin_critical_warning = 1;
488         }
489     }
491     return $SESSION->admin_critical_warning;
494 /**
495  * Detects if float supports at least 10 decimal digits
496  *
497  * Detects if float supports at least 10 decimal digits
498  * and also if float-->string conversion works as expected.
499  *
500  * @return bool true if problem found
501  */
502 function is_float_problem() {
503     $num1 = 2009010200.01;
504     $num2 = 2009010200.02;
506     return ((string)$num1 === (string)$num2 or $num1 === $num2 or $num2 <= (string)$num1);
509 /**
510  * Try to verify that dataroot is not accessible from web.
511  *
512  * Try to verify that dataroot is not accessible from web.
513  * It is not 100% correct but might help to reduce number of vulnerable sites.
514  * Protection from httpd.conf and .htaccess is not detected properly.
515  *
516  * @uses INSECURE_DATAROOT_WARNING
517  * @uses INSECURE_DATAROOT_ERROR
518  * @param bool $fetchtest try to test public access by fetching file, default false
519  * @return mixed empty means secure, INSECURE_DATAROOT_ERROR found a critical problem, INSECURE_DATAROOT_WARNING might be problematic
520  */
521 function is_dataroot_insecure($fetchtest=false) {
522     global $CFG;
524     $siteroot = str_replace('\\', '/', strrev($CFG->dirroot.'/')); // win32 backslash workaround
526     $rp = preg_replace('|https?://[^/]+|i', '', $CFG->wwwroot, 1);
527     $rp = strrev(trim($rp, '/'));
528     $rp = explode('/', $rp);
529     foreach($rp as $r) {
530         if (strpos($siteroot, '/'.$r.'/') === 0) {
531             $siteroot = substr($siteroot, strlen($r)+1); // moodle web in subdirectory
532         } else {
533             break; // probably alias root
534         }
535     }
537     $siteroot = strrev($siteroot);
538     $dataroot = str_replace('\\', '/', $CFG->dataroot.'/');
540     if (strpos($dataroot, $siteroot) !== 0) {
541         return false;
542     }
544     if (!$fetchtest) {
545         return INSECURE_DATAROOT_WARNING;
546     }
548     // now try all methods to fetch a test file using http protocol
550     $httpdocroot = str_replace('\\', '/', strrev($CFG->dirroot.'/'));
551     preg_match('|(https?://[^/]+)|i', $CFG->wwwroot, $matches);
552     $httpdocroot = $matches[1];
553     $datarooturl = $httpdocroot.'/'. substr($dataroot, strlen($siteroot));
554     make_upload_directory('diag');
555     $testfile = $CFG->dataroot.'/diag/public.txt';
556     if (!file_exists($testfile)) {
557         file_put_contents($testfile, 'test file, do not delete');
558         @chmod($testfile, $CFG->filepermissions);
559     }
560     $teststr = trim(file_get_contents($testfile));
561     if (empty($teststr)) {
562     // hmm, strange
563         return INSECURE_DATAROOT_WARNING;
564     }
566     $testurl = $datarooturl.'/diag/public.txt';
567     if (extension_loaded('curl') and
568         !(stripos(ini_get('disable_functions'), 'curl_init') !== FALSE) and
569         !(stripos(ini_get('disable_functions'), 'curl_setop') !== FALSE) and
570         ($ch = @curl_init($testurl)) !== false) {
571         curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
572         curl_setopt($ch, CURLOPT_HEADER, false);
573         $data = curl_exec($ch);
574         if (!curl_errno($ch)) {
575             $data = trim($data);
576             if ($data === $teststr) {
577                 curl_close($ch);
578                 return INSECURE_DATAROOT_ERROR;
579             }
580         }
581         curl_close($ch);
582     }
584     if ($data = @file_get_contents($testurl)) {
585         $data = trim($data);
586         if ($data === $teststr) {
587             return INSECURE_DATAROOT_ERROR;
588         }
589     }
591     preg_match('|https?://([^/]+)|i', $testurl, $matches);
592     $sitename = $matches[1];
593     $error = 0;
594     if ($fp = @fsockopen($sitename, 80, $error)) {
595         preg_match('|https?://[^/]+(.*)|i', $testurl, $matches);
596         $localurl = $matches[1];
597         $out = "GET $localurl HTTP/1.1\r\n";
598         $out .= "Host: $sitename\r\n";
599         $out .= "Connection: Close\r\n\r\n";
600         fwrite($fp, $out);
601         $data = '';
602         $incoming = false;
603         while (!feof($fp)) {
604             if ($incoming) {
605                 $data .= fgets($fp, 1024);
606             } else if (@fgets($fp, 1024) === "\r\n") {
607                     $incoming = true;
608                 }
609         }
610         fclose($fp);
611         $data = trim($data);
612         if ($data === $teststr) {
613             return INSECURE_DATAROOT_ERROR;
614         }
615     }
617     return INSECURE_DATAROOT_WARNING;
620 /**
621  * Enables CLI maintenance mode by creating new dataroot/climaintenance.html file.
622  */
623 function enable_cli_maintenance_mode() {
624     global $CFG;
626     if (file_exists("$CFG->dataroot/climaintenance.html")) {
627         unlink("$CFG->dataroot/climaintenance.html");
628     }
630     if (isset($CFG->maintenance_message) and !html_is_blank($CFG->maintenance_message)) {
631         $data = $CFG->maintenance_message;
632         $data = bootstrap_renderer::early_error_content($data, null, null, null);
633         $data = bootstrap_renderer::plain_page(get_string('sitemaintenance', 'admin'), $data);
635     } else if (file_exists("$CFG->dataroot/climaintenance.template.html")) {
636         $data = file_get_contents("$CFG->dataroot/climaintenance.template.html");
638     } else {
639         $data = get_string('sitemaintenance', 'admin');
640         $data = bootstrap_renderer::early_error_content($data, null, null, null);
641         $data = bootstrap_renderer::plain_page(get_string('sitemaintenance', 'admin'), $data);
642     }
644     file_put_contents("$CFG->dataroot/climaintenance.html", $data);
645     chmod("$CFG->dataroot/climaintenance.html", $CFG->filepermissions);
648 /// CLASS DEFINITIONS /////////////////////////////////////////////////////////
651 /**
652  * Interface for anything appearing in the admin tree
653  *
654  * The interface that is implemented by anything that appears in the admin tree
655  * block. It forces inheriting classes to define a method for checking user permissions
656  * and methods for finding something in the admin tree.
657  *
658  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
659  */
660 interface part_of_admin_tree {
662 /**
663  * Finds a named part_of_admin_tree.
664  *
665  * Used to find a part_of_admin_tree. If a class only inherits part_of_admin_tree
666  * and not parentable_part_of_admin_tree, then this function should only check if
667  * $this->name matches $name. If it does, it should return a reference to $this,
668  * otherwise, it should return a reference to NULL.
669  *
670  * If a class inherits parentable_part_of_admin_tree, this method should be called
671  * recursively on all child objects (assuming, of course, the parent object's name
672  * doesn't match the search criterion).
673  *
674  * @param string $name The internal name of the part_of_admin_tree we're searching for.
675  * @return mixed An object reference or a NULL reference.
676  */
677     public function locate($name);
679     /**
680      * Removes named part_of_admin_tree.
681      *
682      * @param string $name The internal name of the part_of_admin_tree we want to remove.
683      * @return bool success.
684      */
685     public function prune($name);
687     /**
688      * Search using query
689      * @param string $query
690      * @return mixed array-object structure of found settings and pages
691      */
692     public function search($query);
694     /**
695      * Verifies current user's access to this part_of_admin_tree.
696      *
697      * Used to check if the current user has access to this part of the admin tree or
698      * not. If a class only inherits part_of_admin_tree and not parentable_part_of_admin_tree,
699      * then this method is usually just a call to has_capability() in the site context.
700      *
701      * If a class inherits parentable_part_of_admin_tree, this method should return the
702      * logical OR of the return of check_access() on all child objects.
703      *
704      * @return bool True if the user has access, false if she doesn't.
705      */
706     public function check_access();
708     /**
709      * Mostly useful for removing of some parts of the tree in admin tree block.
710      *
711      * @return True is hidden from normal list view
712      */
713     public function is_hidden();
715     /**
716      * Show we display Save button at the page bottom?
717      * @return bool
718      */
719     public function show_save();
723 /**
724  * Interface implemented by any part_of_admin_tree that has children.
725  *
726  * The interface implemented by any part_of_admin_tree that can be a parent
727  * to other part_of_admin_tree's. (For now, this only includes admin_category.) Apart
728  * from ensuring part_of_admin_tree compliancy, it also ensures inheriting methods
729  * include an add method for adding other part_of_admin_tree objects as children.
730  *
731  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
732  */
733 interface parentable_part_of_admin_tree extends part_of_admin_tree {
735 /**
736  * Adds a part_of_admin_tree object to the admin tree.
737  *
738  * Used to add a part_of_admin_tree object to this object or a child of this
739  * object. $something should only be added if $destinationname matches
740  * $this->name. If it doesn't, add should be called on child objects that are
741  * also parentable_part_of_admin_tree's.
742  *
743  * $something should be appended as the last child in the $destinationname. If the
744  * $beforesibling is specified, $something should be prepended to it. If the given
745  * sibling is not found, $something should be appended to the end of $destinationname
746  * and a developer debugging message should be displayed.
747  *
748  * @param string $destinationname The internal name of the new parent for $something.
749  * @param part_of_admin_tree $something The object to be added.
750  * @return bool True on success, false on failure.
751  */
752     public function add($destinationname, $something, $beforesibling = null);
757 /**
758  * The object used to represent folders (a.k.a. categories) in the admin tree block.
759  *
760  * Each admin_category object contains a number of part_of_admin_tree objects.
761  *
762  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
763  */
764 class admin_category implements parentable_part_of_admin_tree {
766     /** @var part_of_admin_tree[] An array of part_of_admin_tree objects that are this object's children */
767     protected $children;
768     /** @var string An internal name for this category. Must be unique amongst ALL part_of_admin_tree objects */
769     public $name;
770     /** @var string The displayed name for this category. Usually obtained through get_string() */
771     public $visiblename;
772     /** @var bool Should this category be hidden in admin tree block? */
773     public $hidden;
774     /** @var mixed Either a string or an array or strings */
775     public $path;
776     /** @var mixed Either a string or an array or strings */
777     public $visiblepath;
779     /** @var array fast lookup category cache, all categories of one tree point to one cache */
780     protected $category_cache;
782     /** @var bool If set to true children will be sorted when calling {@link admin_category::get_children()} */
783     protected $sort = false;
784     /** @var bool If set to true children will be sorted in ascending order. */
785     protected $sortasc = true;
786     /** @var bool If set to true sub categories and pages will be split and then sorted.. */
787     protected $sortsplit = true;
788     /** @var bool $sorted True if the children have been sorted and don't need resorting */
789     protected $sorted = false;
791     /**
792      * Constructor for an empty admin category
793      *
794      * @param string $name The internal name for this category. Must be unique amongst ALL part_of_admin_tree objects
795      * @param string $visiblename The displayed named for this category. Usually obtained through get_string()
796      * @param bool $hidden hide category in admin tree block, defaults to false
797      */
798     public function __construct($name, $visiblename, $hidden=false) {
799         $this->children    = array();
800         $this->name        = $name;
801         $this->visiblename = $visiblename;
802         $this->hidden      = $hidden;
803     }
805     /**
806      * Returns a reference to the part_of_admin_tree object with internal name $name.
807      *
808      * @param string $name The internal name of the object we want.
809      * @param bool $findpath initialize path and visiblepath arrays
810      * @return mixed A reference to the object with internal name $name if found, otherwise a reference to NULL.
811      *                  defaults to false
812      */
813     public function locate($name, $findpath=false) {
814         if (!isset($this->category_cache[$this->name])) {
815             // somebody much have purged the cache
816             $this->category_cache[$this->name] = $this;
817         }
819         if ($this->name == $name) {
820             if ($findpath) {
821                 $this->visiblepath[] = $this->visiblename;
822                 $this->path[]        = $this->name;
823             }
824             return $this;
825         }
827         // quick category lookup
828         if (!$findpath and isset($this->category_cache[$name])) {
829             return $this->category_cache[$name];
830         }
832         $return = NULL;
833         foreach($this->children as $childid=>$unused) {
834             if ($return = $this->children[$childid]->locate($name, $findpath)) {
835                 break;
836             }
837         }
839         if (!is_null($return) and $findpath) {
840             $return->visiblepath[] = $this->visiblename;
841             $return->path[]        = $this->name;
842         }
844         return $return;
845     }
847     /**
848      * Search using query
849      *
850      * @param string query
851      * @return mixed array-object structure of found settings and pages
852      */
853     public function search($query) {
854         $result = array();
855         foreach ($this->get_children() as $child) {
856             $subsearch = $child->search($query);
857             if (!is_array($subsearch)) {
858                 debugging('Incorrect search result from '.$child->name);
859                 continue;
860             }
861             $result = array_merge($result, $subsearch);
862         }
863         return $result;
864     }
866     /**
867      * Removes part_of_admin_tree object with internal name $name.
868      *
869      * @param string $name The internal name of the object we want to remove.
870      * @return bool success
871      */
872     public function prune($name) {
874         if ($this->name == $name) {
875             return false;  //can not remove itself
876         }
878         foreach($this->children as $precedence => $child) {
879             if ($child->name == $name) {
880                 // clear cache and delete self
881                 while($this->category_cache) {
882                     // delete the cache, but keep the original array address
883                     array_pop($this->category_cache);
884                 }
885                 unset($this->children[$precedence]);
886                 return true;
887             } else if ($this->children[$precedence]->prune($name)) {
888                 return true;
889             }
890         }
891         return false;
892     }
894     /**
895      * Adds a part_of_admin_tree to a child or grandchild (or great-grandchild, and so forth) of this object.
896      *
897      * By default the new part of the tree is appended as the last child of the parent. You
898      * can specify a sibling node that the new part should be prepended to. If the given
899      * sibling is not found, the part is appended to the end (as it would be by default) and
900      * a developer debugging message is displayed.
901      *
902      * @throws coding_exception if the $beforesibling is empty string or is not string at all.
903      * @param string $destinationame The internal name of the immediate parent that we want for $something.
904      * @param mixed $something A part_of_admin_tree or setting instance to be added.
905      * @param string $beforesibling The name of the parent's child the $something should be prepended to.
906      * @return bool True if successfully added, false if $something can not be added.
907      */
908     public function add($parentname, $something, $beforesibling = null) {
909         global $CFG;
911         $parent = $this->locate($parentname);
912         if (is_null($parent)) {
913             debugging('parent does not exist!');
914             return false;
915         }
917         if ($something instanceof part_of_admin_tree) {
918             if (!($parent instanceof parentable_part_of_admin_tree)) {
919                 debugging('error - parts of tree can be inserted only into parentable parts');
920                 return false;
921             }
922             if ($CFG->debugdeveloper && !is_null($this->locate($something->name))) {
923                 // The name of the node is already used, simply warn the developer that this should not happen.
924                 // It is intentional to check for the debug level before performing the check.
925                 debugging('Duplicate admin page name: ' . $something->name, DEBUG_DEVELOPER);
926             }
927             if (is_null($beforesibling)) {
928                 // Append $something as the parent's last child.
929                 $parent->children[] = $something;
930             } else {
931                 if (!is_string($beforesibling) or trim($beforesibling) === '') {
932                     throw new coding_exception('Unexpected value of the beforesibling parameter');
933                 }
934                 // Try to find the position of the sibling.
935                 $siblingposition = null;
936                 foreach ($parent->children as $childposition => $child) {
937                     if ($child->name === $beforesibling) {
938                         $siblingposition = $childposition;
939                         break;
940                     }
941                 }
942                 if (is_null($siblingposition)) {
943                     debugging('Sibling '.$beforesibling.' not found', DEBUG_DEVELOPER);
944                     $parent->children[] = $something;
945                 } else {
946                     $parent->children = array_merge(
947                         array_slice($parent->children, 0, $siblingposition),
948                         array($something),
949                         array_slice($parent->children, $siblingposition)
950                     );
951                 }
952             }
953             if ($something instanceof admin_category) {
954                 if (isset($this->category_cache[$something->name])) {
955                     debugging('Duplicate admin category name: '.$something->name);
956                 } else {
957                     $this->category_cache[$something->name] = $something;
958                     $something->category_cache =& $this->category_cache;
959                     foreach ($something->children as $child) {
960                         // just in case somebody already added subcategories
961                         if ($child instanceof admin_category) {
962                             if (isset($this->category_cache[$child->name])) {
963                                 debugging('Duplicate admin category name: '.$child->name);
964                             } else {
965                                 $this->category_cache[$child->name] = $child;
966                                 $child->category_cache =& $this->category_cache;
967                             }
968                         }
969                     }
970                 }
971             }
972             return true;
974         } else {
975             debugging('error - can not add this element');
976             return false;
977         }
979     }
981     /**
982      * Checks if the user has access to anything in this category.
983      *
984      * @return bool True if the user has access to at least one child in this category, false otherwise.
985      */
986     public function check_access() {
987         foreach ($this->children as $child) {
988             if ($child->check_access()) {
989                 return true;
990             }
991         }
992         return false;
993     }
995     /**
996      * Is this category hidden in admin tree block?
997      *
998      * @return bool True if hidden
999      */
1000     public function is_hidden() {
1001         return $this->hidden;
1002     }
1004     /**
1005      * Show we display Save button at the page bottom?
1006      * @return bool
1007      */
1008     public function show_save() {
1009         foreach ($this->children as $child) {
1010             if ($child->show_save()) {
1011                 return true;
1012             }
1013         }
1014         return false;
1015     }
1017     /**
1018      * Sets sorting on this category.
1019      *
1020      * Please note this function doesn't actually do the sorting.
1021      * It can be called anytime.
1022      * Sorting occurs when the user calls get_children.
1023      * Code using the children array directly won't see the sorted results.
1024      *
1025      * @param bool $sort If set to true children will be sorted, if false they won't be.
1026      * @param bool $asc If true sorting will be ascending, otherwise descending.
1027      * @param bool $split If true we sort pages and sub categories separately.
1028      */
1029     public function set_sorting($sort, $asc = true, $split = true) {
1030         $this->sort = (bool)$sort;
1031         $this->sortasc = (bool)$asc;
1032         $this->sortsplit = (bool)$split;
1033     }
1035     /**
1036      * Returns the children associated with this category.
1037      *
1038      * @return part_of_admin_tree[]
1039      */
1040     public function get_children() {
1041         // If we should sort and it hasn't already been sorted.
1042         if ($this->sort && !$this->sorted) {
1043             if ($this->sortsplit) {
1044                 $categories = array();
1045                 $pages = array();
1046                 foreach ($this->children as $child) {
1047                     if ($child instanceof admin_category) {
1048                         $categories[] = $child;
1049                     } else {
1050                         $pages[] = $child;
1051                     }
1052                 }
1053                 core_collator::asort_objects_by_property($categories, 'visiblename');
1054                 core_collator::asort_objects_by_property($pages, 'visiblename');
1055                 if (!$this->sortasc) {
1056                     $categories = array_reverse($categories);
1057                     $pages = array_reverse($pages);
1058                 }
1059                 $this->children = array_merge($pages, $categories);
1060             } else {
1061                 core_collator::asort_objects_by_property($this->children, 'visiblename');
1062                 if (!$this->sortasc) {
1063                     $this->children = array_reverse($this->children);
1064                 }
1065             }
1066             $this->sorted = true;
1067         }
1068         return $this->children;
1069     }
1071     /**
1072      * Magically gets a property from this object.
1073      *
1074      * @param $property
1075      * @return part_of_admin_tree[]
1076      * @throws coding_exception
1077      */
1078     public function __get($property) {
1079         if ($property === 'children') {
1080             return $this->get_children();
1081         }
1082         throw new coding_exception('Invalid property requested.');
1083     }
1085     /**
1086      * Magically sets a property against this object.
1087      *
1088      * @param string $property
1089      * @param mixed $value
1090      * @throws coding_exception
1091      */
1092     public function __set($property, $value) {
1093         if ($property === 'children') {
1094             $this->sorted = false;
1095             $this->children = $value;
1096         } else {
1097             throw new coding_exception('Invalid property requested.');
1098         }
1099     }
1101     /**
1102      * Checks if an inaccessible property is set.
1103      *
1104      * @param string $property
1105      * @return bool
1106      * @throws coding_exception
1107      */
1108     public function __isset($property) {
1109         if ($property === 'children') {
1110             return isset($this->children);
1111         }
1112         throw new coding_exception('Invalid property requested.');
1113     }
1117 /**
1118  * Root of admin settings tree, does not have any parent.
1119  *
1120  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1121  */
1122 class admin_root extends admin_category {
1123 /** @var array List of errors */
1124     public $errors;
1125     /** @var string search query */
1126     public $search;
1127     /** @var bool full tree flag - true means all settings required, false only pages required */
1128     public $fulltree;
1129     /** @var bool flag indicating loaded tree */
1130     public $loaded;
1131     /** @var mixed site custom defaults overriding defaults in settings files*/
1132     public $custom_defaults;
1134     /**
1135      * @param bool $fulltree true means all settings required,
1136      *                            false only pages required
1137      */
1138     public function __construct($fulltree) {
1139         global $CFG;
1141         parent::__construct('root', get_string('administration'), false);
1142         $this->errors   = array();
1143         $this->search   = '';
1144         $this->fulltree = $fulltree;
1145         $this->loaded   = false;
1147         $this->category_cache = array();
1149         // load custom defaults if found
1150         $this->custom_defaults = null;
1151         $defaultsfile = "$CFG->dirroot/local/defaults.php";
1152         if (is_readable($defaultsfile)) {
1153             $defaults = array();
1154             include($defaultsfile);
1155             if (is_array($defaults) and count($defaults)) {
1156                 $this->custom_defaults = $defaults;
1157             }
1158         }
1159     }
1161     /**
1162      * Empties children array, and sets loaded to false
1163      *
1164      * @param bool $requirefulltree
1165      */
1166     public function purge_children($requirefulltree) {
1167         $this->children = array();
1168         $this->fulltree = ($requirefulltree || $this->fulltree);
1169         $this->loaded   = false;
1170         //break circular dependencies - this helps PHP 5.2
1171         while($this->category_cache) {
1172             array_pop($this->category_cache);
1173         }
1174         $this->category_cache = array();
1175     }
1179 /**
1180  * Links external PHP pages into the admin tree.
1181  *
1182  * See detailed usage example at the top of this document (adminlib.php)
1183  *
1184  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1185  */
1186 class admin_externalpage implements part_of_admin_tree {
1188     /** @var string An internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects */
1189     public $name;
1191     /** @var string The displayed name for this external page. Usually obtained through get_string(). */
1192     public $visiblename;
1194     /** @var string The external URL that we should link to when someone requests this external page. */
1195     public $url;
1197     /** @var string The role capability/permission a user must have to access this external page. */
1198     public $req_capability;
1200     /** @var object The context in which capability/permission should be checked, default is site context. */
1201     public $context;
1203     /** @var bool hidden in admin tree block. */
1204     public $hidden;
1206     /** @var mixed either string or array of string */
1207     public $path;
1209     /** @var array list of visible names of page parents */
1210     public $visiblepath;
1212     /**
1213      * Constructor for adding an external page into the admin tree.
1214      *
1215      * @param string $name The internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects.
1216      * @param string $visiblename The displayed name for this external page. Usually obtained through get_string().
1217      * @param string $url The external URL that we should link to when someone requests this external page.
1218      * @param mixed $req_capability The role capability/permission a user must have to access this external page. Defaults to 'moodle/site:config'.
1219      * @param boolean $hidden Is this external page hidden in admin tree block? Default false.
1220      * @param stdClass $context The context the page relates to. Not sure what happens
1221      *      if you specify something other than system or front page. Defaults to system.
1222      */
1223     public function __construct($name, $visiblename, $url, $req_capability='moodle/site:config', $hidden=false, $context=NULL) {
1224         $this->name        = $name;
1225         $this->visiblename = $visiblename;
1226         $this->url         = $url;
1227         if (is_array($req_capability)) {
1228             $this->req_capability = $req_capability;
1229         } else {
1230             $this->req_capability = array($req_capability);
1231         }
1232         $this->hidden = $hidden;
1233         $this->context = $context;
1234     }
1236     /**
1237      * Returns a reference to the part_of_admin_tree object with internal name $name.
1238      *
1239      * @param string $name The internal name of the object we want.
1240      * @param bool $findpath defaults to false
1241      * @return mixed A reference to the object with internal name $name if found, otherwise a reference to NULL.
1242      */
1243     public function locate($name, $findpath=false) {
1244         if ($this->name == $name) {
1245             if ($findpath) {
1246                 $this->visiblepath = array($this->visiblename);
1247                 $this->path        = array($this->name);
1248             }
1249             return $this;
1250         } else {
1251             $return = NULL;
1252             return $return;
1253         }
1254     }
1256     /**
1257      * This function always returns false, required function by interface
1258      *
1259      * @param string $name
1260      * @return false
1261      */
1262     public function prune($name) {
1263         return false;
1264     }
1266     /**
1267      * Search using query
1268      *
1269      * @param string $query
1270      * @return mixed array-object structure of found settings and pages
1271      */
1272     public function search($query) {
1273         $found = false;
1274         if (strpos(strtolower($this->name), $query) !== false) {
1275             $found = true;
1276         } else if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
1277                 $found = true;
1278             }
1279         if ($found) {
1280             $result = new stdClass();
1281             $result->page     = $this;
1282             $result->settings = array();
1283             return array($this->name => $result);
1284         } else {
1285             return array();
1286         }
1287     }
1289     /**
1290      * Determines if the current user has access to this external page based on $this->req_capability.
1291      *
1292      * @return bool True if user has access, false otherwise.
1293      */
1294     public function check_access() {
1295         global $CFG;
1296         $context = empty($this->context) ? context_system::instance() : $this->context;
1297         foreach($this->req_capability as $cap) {
1298             if (has_capability($cap, $context)) {
1299                 return true;
1300             }
1301         }
1302         return false;
1303     }
1305     /**
1306      * Is this external page hidden in admin tree block?
1307      *
1308      * @return bool True if hidden
1309      */
1310     public function is_hidden() {
1311         return $this->hidden;
1312     }
1314     /**
1315      * Show we display Save button at the page bottom?
1316      * @return bool
1317      */
1318     public function show_save() {
1319         return false;
1320     }
1324 /**
1325  * Used to group a number of admin_setting objects into a page and add them to the admin tree.
1326  *
1327  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1328  */
1329 class admin_settingpage implements part_of_admin_tree {
1331     /** @var string An internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects */
1332     public $name;
1334     /** @var string The displayed name for this external page. Usually obtained through get_string(). */
1335     public $visiblename;
1337     /** @var mixed An array of admin_setting objects that are part of this setting page. */
1338     public $settings;
1340     /** @var string The role capability/permission a user must have to access this external page. */
1341     public $req_capability;
1343     /** @var object The context in which capability/permission should be checked, default is site context. */
1344     public $context;
1346     /** @var bool hidden in admin tree block. */
1347     public $hidden;
1349     /** @var mixed string of paths or array of strings of paths */
1350     public $path;
1352     /** @var array list of visible names of page parents */
1353     public $visiblepath;
1355     /**
1356      * see admin_settingpage for details of this function
1357      *
1358      * @param string $name The internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects.
1359      * @param string $visiblename The displayed name for this external page. Usually obtained through get_string().
1360      * @param mixed $req_capability The role capability/permission a user must have to access this external page. Defaults to 'moodle/site:config'.
1361      * @param boolean $hidden Is this external page hidden in admin tree block? Default false.
1362      * @param stdClass $context The context the page relates to. Not sure what happens
1363      *      if you specify something other than system or front page. Defaults to system.
1364      */
1365     public function __construct($name, $visiblename, $req_capability='moodle/site:config', $hidden=false, $context=NULL) {
1366         $this->settings    = new stdClass();
1367         $this->name        = $name;
1368         $this->visiblename = $visiblename;
1369         if (is_array($req_capability)) {
1370             $this->req_capability = $req_capability;
1371         } else {
1372             $this->req_capability = array($req_capability);
1373         }
1374         $this->hidden      = $hidden;
1375         $this->context     = $context;
1376     }
1378     /**
1379      * see admin_category
1380      *
1381      * @param string $name
1382      * @param bool $findpath
1383      * @return mixed Object (this) if name ==  this->name, else returns null
1384      */
1385     public function locate($name, $findpath=false) {
1386         if ($this->name == $name) {
1387             if ($findpath) {
1388                 $this->visiblepath = array($this->visiblename);
1389                 $this->path        = array($this->name);
1390             }
1391             return $this;
1392         } else {
1393             $return = NULL;
1394             return $return;
1395         }
1396     }
1398     /**
1399      * Search string in settings page.
1400      *
1401      * @param string $query
1402      * @return array
1403      */
1404     public function search($query) {
1405         $found = array();
1407         foreach ($this->settings as $setting) {
1408             if ($setting->is_related($query)) {
1409                 $found[] = $setting;
1410             }
1411         }
1413         if ($found) {
1414             $result = new stdClass();
1415             $result->page     = $this;
1416             $result->settings = $found;
1417             return array($this->name => $result);
1418         }
1420         $found = false;
1421         if (strpos(strtolower($this->name), $query) !== false) {
1422             $found = true;
1423         } else if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
1424                 $found = true;
1425             }
1426         if ($found) {
1427             $result = new stdClass();
1428             $result->page     = $this;
1429             $result->settings = array();
1430             return array($this->name => $result);
1431         } else {
1432             return array();
1433         }
1434     }
1436     /**
1437      * This function always returns false, required by interface
1438      *
1439      * @param string $name
1440      * @return bool Always false
1441      */
1442     public function prune($name) {
1443         return false;
1444     }
1446     /**
1447      * adds an admin_setting to this admin_settingpage
1448      *
1449      * 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
1450      * n.b. each admin_setting in an admin_settingpage must have a unique internal name
1451      *
1452      * @param object $setting is the admin_setting object you want to add
1453      * @return bool true if successful, false if not
1454      */
1455     public function add($setting) {
1456         if (!($setting instanceof admin_setting)) {
1457             debugging('error - not a setting instance');
1458             return false;
1459         }
1461         $name = $setting->name;
1462         if ($setting->plugin) {
1463             $name = $setting->plugin . $name;
1464         }
1465         $this->settings->{$name} = $setting;
1466         return true;
1467     }
1469     /**
1470      * see admin_externalpage
1471      *
1472      * @return bool Returns true for yes false for no
1473      */
1474     public function check_access() {
1475         global $CFG;
1476         $context = empty($this->context) ? context_system::instance() : $this->context;
1477         foreach($this->req_capability as $cap) {
1478             if (has_capability($cap, $context)) {
1479                 return true;
1480             }
1481         }
1482         return false;
1483     }
1485     /**
1486      * outputs this page as html in a table (suitable for inclusion in an admin pagetype)
1487      * @return string Returns an XHTML string
1488      */
1489     public function output_html() {
1490         $adminroot = admin_get_root();
1491         $return = '<fieldset>'."\n".'<div class="clearer"><!-- --></div>'."\n";
1492         foreach($this->settings as $setting) {
1493             $fullname = $setting->get_full_name();
1494             if (array_key_exists($fullname, $adminroot->errors)) {
1495                 $data = $adminroot->errors[$fullname]->data;
1496             } else {
1497                 $data = $setting->get_setting();
1498                 // do not use defaults if settings not available - upgrade settings handles the defaults!
1499             }
1500             $return .= $setting->output_html($data);
1501         }
1502         $return .= '</fieldset>';
1503         return $return;
1504     }
1506     /**
1507      * Is this settings page hidden in admin tree block?
1508      *
1509      * @return bool True if hidden
1510      */
1511     public function is_hidden() {
1512         return $this->hidden;
1513     }
1515     /**
1516      * Show we display Save button at the page bottom?
1517      * @return bool
1518      */
1519     public function show_save() {
1520         foreach($this->settings as $setting) {
1521             if (empty($setting->nosave)) {
1522                 return true;
1523             }
1524         }
1525         return false;
1526     }
1530 /**
1531  * Admin settings class. Only exists on setting pages.
1532  * Read & write happens at this level; no authentication.
1533  *
1534  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1535  */
1536 abstract class admin_setting {
1537     /** @var string unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins. */
1538     public $name;
1539     /** @var string localised name */
1540     public $visiblename;
1541     /** @var string localised long description in Markdown format */
1542     public $description;
1543     /** @var mixed Can be string or array of string */
1544     public $defaultsetting;
1545     /** @var string */
1546     public $updatedcallback;
1547     /** @var mixed can be String or Null.  Null means main config table */
1548     public $plugin; // null means main config table
1549     /** @var bool true indicates this setting does not actually save anything, just information */
1550     public $nosave = false;
1551     /** @var bool if set, indicates that a change to this setting requires rebuild course cache */
1552     public $affectsmodinfo = false;
1553     /** @var array of admin_setting_flag - These are extra checkboxes attached to a setting. */
1554     private $flags = array();
1555     /** @var bool Whether this field must be forced LTR. */
1556     private $forceltr = null;
1558     /**
1559      * Constructor
1560      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
1561      *                     or 'myplugin/mysetting' for ones in config_plugins.
1562      * @param string $visiblename localised name
1563      * @param string $description localised long description
1564      * @param mixed $defaultsetting string or array depending on implementation
1565      */
1566     public function __construct($name, $visiblename, $description, $defaultsetting) {
1567         $this->parse_setting_name($name);
1568         $this->visiblename    = $visiblename;
1569         $this->description    = $description;
1570         $this->defaultsetting = $defaultsetting;
1571     }
1573     /**
1574      * Generic function to add a flag to this admin setting.
1575      *
1576      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1577      * @param bool $default - The default for the flag
1578      * @param string $shortname - The shortname for this flag. Used as a suffix for the setting name.
1579      * @param string $displayname - The display name for this flag. Used as a label next to the checkbox.
1580      */
1581     protected function set_flag_options($enabled, $default, $shortname, $displayname) {
1582         if (empty($this->flags[$shortname])) {
1583             $this->flags[$shortname] = new admin_setting_flag($enabled, $default, $shortname, $displayname);
1584         } else {
1585             $this->flags[$shortname]->set_options($enabled, $default);
1586         }
1587     }
1589     /**
1590      * Set the enabled options flag on this admin setting.
1591      *
1592      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1593      * @param bool $default - The default for the flag
1594      */
1595     public function set_enabled_flag_options($enabled, $default) {
1596         $this->set_flag_options($enabled, $default, 'enabled', new lang_string('enabled', 'core_admin'));
1597     }
1599     /**
1600      * Set the advanced options flag on this admin setting.
1601      *
1602      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1603      * @param bool $default - The default for the flag
1604      */
1605     public function set_advanced_flag_options($enabled, $default) {
1606         $this->set_flag_options($enabled, $default, 'adv', new lang_string('advanced'));
1607     }
1610     /**
1611      * Set the locked options flag on this admin setting.
1612      *
1613      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1614      * @param bool $default - The default for the flag
1615      */
1616     public function set_locked_flag_options($enabled, $default) {
1617         $this->set_flag_options($enabled, $default, 'locked', new lang_string('locked', 'core_admin'));
1618     }
1620     /**
1621      * Get the currently saved value for a setting flag
1622      *
1623      * @param admin_setting_flag $flag - One of the admin_setting_flag for this admin_setting.
1624      * @return bool
1625      */
1626     public function get_setting_flag_value(admin_setting_flag $flag) {
1627         $value = $this->config_read($this->name . '_' . $flag->get_shortname());
1628         if (!isset($value)) {
1629             $value = $flag->get_default();
1630         }
1632         return !empty($value);
1633     }
1635     /**
1636      * Get the list of defaults for the flags on this setting.
1637      *
1638      * @param array of strings describing the defaults for this setting. This is appended to by this function.
1639      */
1640     public function get_setting_flag_defaults(& $defaults) {
1641         foreach ($this->flags as $flag) {
1642             if ($flag->is_enabled() && $flag->get_default()) {
1643                 $defaults[] = $flag->get_displayname();
1644             }
1645         }
1646     }
1648     /**
1649      * Output the input fields for the advanced and locked flags on this setting.
1650      *
1651      * @param bool $adv - The current value of the advanced flag.
1652      * @param bool $locked - The current value of the locked flag.
1653      * @return string $output - The html for the flags.
1654      */
1655     public function output_setting_flags() {
1656         $output = '';
1658         foreach ($this->flags as $flag) {
1659             if ($flag->is_enabled()) {
1660                 $output .= $flag->output_setting_flag($this);
1661             }
1662         }
1664         if (!empty($output)) {
1665             return html_writer::tag('span', $output, array('class' => 'adminsettingsflags'));
1666         }
1667         return $output;
1668     }
1670     /**
1671      * Write the values of the flags for this admin setting.
1672      *
1673      * @param array $data - The data submitted from the form or null to set the default value for new installs.
1674      * @return bool - true if successful.
1675      */
1676     public function write_setting_flags($data) {
1677         $result = true;
1678         foreach ($this->flags as $flag) {
1679             $result = $result && $flag->write_setting_flag($this, $data);
1680         }
1681         return $result;
1682     }
1684     /**
1685      * Set up $this->name and potentially $this->plugin
1686      *
1687      * Set up $this->name and possibly $this->plugin based on whether $name looks
1688      * like 'settingname' or 'plugin/settingname'. Also, do some sanity checking
1689      * on the names, that is, output a developer debug warning if the name
1690      * contains anything other than [a-zA-Z0-9_]+.
1691      *
1692      * @param string $name the setting name passed in to the constructor.
1693      */
1694     private function parse_setting_name($name) {
1695         $bits = explode('/', $name);
1696         if (count($bits) > 2) {
1697             throw new moodle_exception('invalidadminsettingname', '', '', $name);
1698         }
1699         $this->name = array_pop($bits);
1700         if (!preg_match('/^[a-zA-Z0-9_]+$/', $this->name)) {
1701             throw new moodle_exception('invalidadminsettingname', '', '', $name);
1702         }
1703         if (!empty($bits)) {
1704             $this->plugin = array_pop($bits);
1705             if ($this->plugin === 'moodle') {
1706                 $this->plugin = null;
1707             } else if (!preg_match('/^[a-zA-Z0-9_]+$/', $this->plugin)) {
1708                     throw new moodle_exception('invalidadminsettingname', '', '', $name);
1709                 }
1710         }
1711     }
1713     /**
1714      * Returns the fullname prefixed by the plugin
1715      * @return string
1716      */
1717     public function get_full_name() {
1718         return 's_'.$this->plugin.'_'.$this->name;
1719     }
1721     /**
1722      * Returns the ID string based on plugin and name
1723      * @return string
1724      */
1725     public function get_id() {
1726         return 'id_s_'.$this->plugin.'_'.$this->name;
1727     }
1729     /**
1730      * @param bool $affectsmodinfo If true, changes to this setting will
1731      *   cause the course cache to be rebuilt
1732      */
1733     public function set_affects_modinfo($affectsmodinfo) {
1734         $this->affectsmodinfo = $affectsmodinfo;
1735     }
1737     /**
1738      * Returns the config if possible
1739      *
1740      * @return mixed returns config if successful else null
1741      */
1742     public function config_read($name) {
1743         global $CFG;
1744         if (!empty($this->plugin)) {
1745             $value = get_config($this->plugin, $name);
1746             return $value === false ? NULL : $value;
1748         } else {
1749             if (isset($CFG->$name)) {
1750                 return $CFG->$name;
1751             } else {
1752                 return NULL;
1753             }
1754         }
1755     }
1757     /**
1758      * Used to set a config pair and log change
1759      *
1760      * @param string $name
1761      * @param mixed $value Gets converted to string if not null
1762      * @return bool Write setting to config table
1763      */
1764     public function config_write($name, $value) {
1765         global $DB, $USER, $CFG;
1767         if ($this->nosave) {
1768             return true;
1769         }
1771         // make sure it is a real change
1772         $oldvalue = get_config($this->plugin, $name);
1773         $oldvalue = ($oldvalue === false) ? null : $oldvalue; // normalise
1774         $value = is_null($value) ? null : (string)$value;
1776         if ($oldvalue === $value) {
1777             return true;
1778         }
1780         // store change
1781         set_config($name, $value, $this->plugin);
1783         // Some admin settings affect course modinfo
1784         if ($this->affectsmodinfo) {
1785             // Clear course cache for all courses
1786             rebuild_course_cache(0, true);
1787         }
1789         $this->add_to_config_log($name, $oldvalue, $value);
1791         return true; // BC only
1792     }
1794     /**
1795      * Log config changes if necessary.
1796      * @param string $name
1797      * @param string $oldvalue
1798      * @param string $value
1799      */
1800     protected function add_to_config_log($name, $oldvalue, $value) {
1801         add_to_config_log($name, $oldvalue, $value, $this->plugin);
1802     }
1804     /**
1805      * Returns current value of this setting
1806      * @return mixed array or string depending on instance, NULL means not set yet
1807      */
1808     public abstract function get_setting();
1810     /**
1811      * Returns default setting if exists
1812      * @return mixed array or string depending on instance; NULL means no default, user must supply
1813      */
1814     public function get_defaultsetting() {
1815         $adminroot =  admin_get_root(false, false);
1816         if (!empty($adminroot->custom_defaults)) {
1817             $plugin = is_null($this->plugin) ? 'moodle' : $this->plugin;
1818             if (isset($adminroot->custom_defaults[$plugin])) {
1819                 if (array_key_exists($this->name, $adminroot->custom_defaults[$plugin])) { // null is valid value here ;-)
1820                     return $adminroot->custom_defaults[$plugin][$this->name];
1821                 }
1822             }
1823         }
1824         return $this->defaultsetting;
1825     }
1827     /**
1828      * Store new setting
1829      *
1830      * @param mixed $data string or array, must not be NULL
1831      * @return string empty string if ok, string error message otherwise
1832      */
1833     public abstract function write_setting($data);
1835     /**
1836      * Return part of form with setting
1837      * This function should always be overwritten
1838      *
1839      * @param mixed $data array or string depending on setting
1840      * @param string $query
1841      * @return string
1842      */
1843     public function output_html($data, $query='') {
1844     // should be overridden
1845         return;
1846     }
1848     /**
1849      * Function called if setting updated - cleanup, cache reset, etc.
1850      * @param string $functionname Sets the function name
1851      * @return void
1852      */
1853     public function set_updatedcallback($functionname) {
1854         $this->updatedcallback = $functionname;
1855     }
1857     /**
1858      * Execute postupdatecallback if necessary.
1859      * @param mixed $original original value before write_setting()
1860      * @return bool true if changed, false if not.
1861      */
1862     public function post_write_settings($original) {
1863         // Comparison must work for arrays too.
1864         if (serialize($original) === serialize($this->get_setting())) {
1865             return false;
1866         }
1868         $callbackfunction = $this->updatedcallback;
1869         if (!empty($callbackfunction) and function_exists($callbackfunction)) {
1870             $callbackfunction($this->get_full_name());
1871         }
1872         return true;
1873     }
1875     /**
1876      * Is setting related to query text - used when searching
1877      * @param string $query
1878      * @return bool
1879      */
1880     public function is_related($query) {
1881         if (strpos(strtolower($this->name), $query) !== false) {
1882             return true;
1883         }
1884         if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
1885             return true;
1886         }
1887         if (strpos(core_text::strtolower($this->description), $query) !== false) {
1888             return true;
1889         }
1890         $current = $this->get_setting();
1891         if (!is_null($current)) {
1892             if (is_string($current)) {
1893                 if (strpos(core_text::strtolower($current), $query) !== false) {
1894                     return true;
1895                 }
1896             }
1897         }
1898         $default = $this->get_defaultsetting();
1899         if (!is_null($default)) {
1900             if (is_string($default)) {
1901                 if (strpos(core_text::strtolower($default), $query) !== false) {
1902                     return true;
1903                 }
1904             }
1905         }
1906         return false;
1907     }
1909     /**
1910      * Get whether this should be displayed in LTR mode.
1911      *
1912      * @return bool|null
1913      */
1914     public function get_force_ltr() {
1915         return $this->forceltr;
1916     }
1918     /**
1919      * Set whether to force LTR or not.
1920      *
1921      * @param bool $value True when forced, false when not force, null when unknown.
1922      */
1923     public function set_force_ltr($value) {
1924         $this->forceltr = $value;
1925     }
1928 /**
1929  * An additional option that can be applied to an admin setting.
1930  * The currently supported options are 'ADVANCED' and 'LOCKED'.
1931  *
1932  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1933  */
1934 class admin_setting_flag {
1935     /** @var bool Flag to indicate if this option can be toggled for this setting */
1936     private $enabled = false;
1937     /** @var bool Flag to indicate if this option defaults to true or false */
1938     private $default = false;
1939     /** @var string Short string used to create setting name - e.g. 'adv' */
1940     private $shortname = '';
1941     /** @var string String used as the label for this flag */
1942     private $displayname = '';
1943     /** @const Checkbox for this flag is displayed in admin page */
1944     const ENABLED = true;
1945     /** @const Checkbox for this flag is not displayed in admin page */
1946     const DISABLED = false;
1948     /**
1949      * Constructor
1950      *
1951      * @param bool $enabled Can this option can be toggled.
1952      *                      Should be one of admin_setting_flag::ENABLED or admin_setting_flag::DISABLED.
1953      * @param bool $default The default checked state for this setting option.
1954      * @param string $shortname The shortname of this flag. Currently supported flags are 'locked' and 'adv'
1955      * @param string $displayname The displayname of this flag. Used as a label for the flag.
1956      */
1957     public function __construct($enabled, $default, $shortname, $displayname) {
1958         $this->shortname = $shortname;
1959         $this->displayname = $displayname;
1960         $this->set_options($enabled, $default);
1961     }
1963     /**
1964      * Update the values of this setting options class
1965      *
1966      * @param bool $enabled Can this option can be toggled.
1967      *                      Should be one of admin_setting_flag::ENABLED or admin_setting_flag::DISABLED.
1968      * @param bool $default The default checked state for this setting option.
1969      */
1970     public function set_options($enabled, $default) {
1971         $this->enabled = $enabled;
1972         $this->default = $default;
1973     }
1975     /**
1976      * Should this option appear in the interface and be toggleable?
1977      *
1978      * @return bool Is it enabled?
1979      */
1980     public function is_enabled() {
1981         return $this->enabled;
1982     }
1984     /**
1985      * Should this option be checked by default?
1986      *
1987      * @return bool Is it on by default?
1988      */
1989     public function get_default() {
1990         return $this->default;
1991     }
1993     /**
1994      * Return the short name for this flag. e.g. 'adv' or 'locked'
1995      *
1996      * @return string
1997      */
1998     public function get_shortname() {
1999         return $this->shortname;
2000     }
2002     /**
2003      * Return the display name for this flag. e.g. 'Advanced' or 'Locked'
2004      *
2005      * @return string
2006      */
2007     public function get_displayname() {
2008         return $this->displayname;
2009     }
2011     /**
2012      * Save the submitted data for this flag - or set it to the default if $data is null.
2013      *
2014      * @param admin_setting $setting - The admin setting for this flag
2015      * @param array $data - The data submitted from the form or null to set the default value for new installs.
2016      * @return bool
2017      */
2018     public function write_setting_flag(admin_setting $setting, $data) {
2019         $result = true;
2020         if ($this->is_enabled()) {
2021             if (!isset($data)) {
2022                 $value = $this->get_default();
2023             } else {
2024                 $value = !empty($data[$setting->get_full_name() . '_' . $this->get_shortname()]);
2025             }
2026             $result = $setting->config_write($setting->name . '_' . $this->get_shortname(), $value);
2027         }
2029         return $result;
2031     }
2033     /**
2034      * Output the checkbox for this setting flag. Should only be called if the flag is enabled.
2035      *
2036      * @param admin_setting $setting - The admin setting for this flag
2037      * @return string - The html for the checkbox.
2038      */
2039     public function output_setting_flag(admin_setting $setting) {
2040         global $OUTPUT;
2042         $value = $setting->get_setting_flag_value($this);
2044         $context = new stdClass();
2045         $context->id = $setting->get_id() . '_' . $this->get_shortname();
2046         $context->name = $setting->get_full_name() .  '_' . $this->get_shortname();
2047         $context->value = 1;
2048         $context->checked = $value ? true : false;
2049         $context->label = $this->get_displayname();
2051         return $OUTPUT->render_from_template('core_admin/setting_flag', $context);
2052     }
2056 /**
2057  * No setting - just heading and text.
2058  *
2059  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2060  */
2061 class admin_setting_heading extends admin_setting {
2063     /**
2064      * not a setting, just text
2065      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2066      * @param string $heading heading
2067      * @param string $information text in box
2068      */
2069     public function __construct($name, $heading, $information) {
2070         $this->nosave = true;
2071         parent::__construct($name, $heading, $information, '');
2072     }
2074     /**
2075      * Always returns true
2076      * @return bool Always returns true
2077      */
2078     public function get_setting() {
2079         return true;
2080     }
2082     /**
2083      * Always returns true
2084      * @return bool Always returns true
2085      */
2086     public function get_defaultsetting() {
2087         return true;
2088     }
2090     /**
2091      * Never write settings
2092      * @return string Always returns an empty string
2093      */
2094     public function write_setting($data) {
2095     // do not write any setting
2096         return '';
2097     }
2099     /**
2100      * Returns an HTML string
2101      * @return string Returns an HTML string
2102      */
2103     public function output_html($data, $query='') {
2104         global $OUTPUT;
2105         $context = new stdClass();
2106         $context->title = $this->visiblename;
2107         $context->description = $this->description;
2108         $context->descriptionformatted = highlight($query, markdown_to_html($this->description));
2109         return $OUTPUT->render_from_template('core_admin/setting_heading', $context);
2110     }
2114 /**
2115  * The most flexible setting, the user enters text.
2116  *
2117  * This type of field should be used for config settings which are using
2118  * English words and are not localised (passwords, database name, list of values, ...).
2119  *
2120  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2121  */
2122 class admin_setting_configtext extends admin_setting {
2124     /** @var mixed int means PARAM_XXX type, string is a allowed format in regex */
2125     public $paramtype;
2126     /** @var int default field size */
2127     public $size;
2129     /**
2130      * Config text constructor
2131      *
2132      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2133      * @param string $visiblename localised
2134      * @param string $description long localised info
2135      * @param string $defaultsetting
2136      * @param mixed $paramtype int means PARAM_XXX type, string is a allowed format in regex
2137      * @param int $size default field size
2138      */
2139     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $size=null) {
2140         $this->paramtype = $paramtype;
2141         if (!is_null($size)) {
2142             $this->size  = $size;
2143         } else {
2144             $this->size  = ($paramtype === PARAM_INT) ? 5 : 30;
2145         }
2146         parent::__construct($name, $visiblename, $description, $defaultsetting);
2147     }
2149     /**
2150      * Get whether this should be displayed in LTR mode.
2151      *
2152      * Try to guess from the PARAM type unless specifically set.
2153      */
2154     public function get_force_ltr() {
2155         $forceltr = parent::get_force_ltr();
2156         if ($forceltr === null) {
2157             return !is_rtl_compatible($this->paramtype);
2158         }
2159         return $forceltr;
2160     }
2162     /**
2163      * Return the setting
2164      *
2165      * @return mixed returns config if successful else null
2166      */
2167     public function get_setting() {
2168         return $this->config_read($this->name);
2169     }
2171     public function write_setting($data) {
2172         if ($this->paramtype === PARAM_INT and $data === '') {
2173         // do not complain if '' used instead of 0
2174             $data = 0;
2175         }
2176         // $data is a string
2177         $validated = $this->validate($data);
2178         if ($validated !== true) {
2179             return $validated;
2180         }
2181         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
2182     }
2184     /**
2185      * Validate data before storage
2186      * @param string data
2187      * @return mixed true if ok string if error found
2188      */
2189     public function validate($data) {
2190         // allow paramtype to be a custom regex if it is the form of /pattern/
2191         if (preg_match('#^/.*/$#', $this->paramtype)) {
2192             if (preg_match($this->paramtype, $data)) {
2193                 return true;
2194             } else {
2195                 return get_string('validateerror', 'admin');
2196             }
2198         } else if ($this->paramtype === PARAM_RAW) {
2199             return true;
2201         } else {
2202             $cleaned = clean_param($data, $this->paramtype);
2203             if ("$data" === "$cleaned") { // implicit conversion to string is needed to do exact comparison
2204                 return true;
2205             } else {
2206                 return get_string('validateerror', 'admin');
2207             }
2208         }
2209     }
2211     /**
2212      * Return an XHTML string for the setting
2213      * @return string Returns an XHTML string
2214      */
2215     public function output_html($data, $query='') {
2216         global $OUTPUT;
2218         $default = $this->get_defaultsetting();
2219         $context = (object) [
2220             'size' => $this->size,
2221             'id' => $this->get_id(),
2222             'name' => $this->get_full_name(),
2223             'value' => $data,
2224             'forceltr' => $this->get_force_ltr(),
2225         ];
2226         $element = $OUTPUT->render_from_template('core_admin/setting_configtext', $context);
2228         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2229     }
2232 /**
2233  * Text input with a maximum length constraint.
2234  *
2235  * @copyright 2015 onwards Ankit Agarwal
2236  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2237  */
2238 class admin_setting_configtext_with_maxlength extends admin_setting_configtext {
2240     /** @var int maximum number of chars allowed. */
2241     protected $maxlength;
2243     /**
2244      * Config text constructor
2245      *
2246      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
2247      *                     or 'myplugin/mysetting' for ones in config_plugins.
2248      * @param string $visiblename localised
2249      * @param string $description long localised info
2250      * @param string $defaultsetting
2251      * @param mixed $paramtype int means PARAM_XXX type, string is a allowed format in regex
2252      * @param int $size default field size
2253      * @param mixed $maxlength int maxlength allowed, 0 for infinite.
2254      */
2255     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW,
2256                                 $size=null, $maxlength = 0) {
2257         $this->maxlength = $maxlength;
2258         parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype, $size);
2259     }
2261     /**
2262      * Validate data before storage
2263      *
2264      * @param string $data data
2265      * @return mixed true if ok string if error found
2266      */
2267     public function validate($data) {
2268         $parentvalidation = parent::validate($data);
2269         if ($parentvalidation === true) {
2270             if ($this->maxlength > 0) {
2271                 // Max length check.
2272                 $length = core_text::strlen($data);
2273                 if ($length > $this->maxlength) {
2274                     return get_string('maximumchars', 'moodle',  $this->maxlength);
2275                 }
2276                 return true;
2277             } else {
2278                 return true; // No max length check needed.
2279             }
2280         } else {
2281             return $parentvalidation;
2282         }
2283     }
2286 /**
2287  * General text area without html editor.
2288  *
2289  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2290  */
2291 class admin_setting_configtextarea extends admin_setting_configtext {
2292     private $rows;
2293     private $cols;
2295     /**
2296      * @param string $name
2297      * @param string $visiblename
2298      * @param string $description
2299      * @param mixed $defaultsetting string or array
2300      * @param mixed $paramtype
2301      * @param string $cols The number of columns to make the editor
2302      * @param string $rows The number of rows to make the editor
2303      */
2304     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $cols='60', $rows='8') {
2305         $this->rows = $rows;
2306         $this->cols = $cols;
2307         parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype);
2308     }
2310     /**
2311      * Returns an XHTML string for the editor
2312      *
2313      * @param string $data
2314      * @param string $query
2315      * @return string XHTML string for the editor
2316      */
2317     public function output_html($data, $query='') {
2318         global $OUTPUT;
2320         $default = $this->get_defaultsetting();
2321         $defaultinfo = $default;
2322         if (!is_null($default) and $default !== '') {
2323             $defaultinfo = "\n".$default;
2324         }
2326         $context = (object) [
2327             'cols' => $this->cols,
2328             'rows' => $this->rows,
2329             'id' => $this->get_id(),
2330             'name' => $this->get_full_name(),
2331             'value' => $data,
2332             'forceltr' => $this->get_force_ltr(),
2333         ];
2334         $element = $OUTPUT->render_from_template('core_admin/setting_configtextarea', $context);
2336         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
2337     }
2340 /**
2341  * General text area with html editor.
2342  */
2343 class admin_setting_confightmleditor extends admin_setting_configtextarea {
2345     /**
2346      * @param string $name
2347      * @param string $visiblename
2348      * @param string $description
2349      * @param mixed $defaultsetting string or array
2350      * @param mixed $paramtype
2351      */
2352     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $cols='60', $rows='8') {
2353         parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype, $cols, $rows);
2354         $this->set_force_ltr(false);
2355         editors_head_setup();
2356     }
2358     /**
2359      * Returns an XHTML string for the editor
2360      *
2361      * @param string $data
2362      * @param string $query
2363      * @return string XHTML string for the editor
2364      */
2365     public function output_html($data, $query='') {
2366         $editor = editors_get_preferred_editor(FORMAT_HTML);
2367         $editor->set_text($data);
2368         $editor->use_editor($this->get_id(), array('noclean'=>true));
2369         return parent::output_html($data, $query);
2370     }
2374 /**
2375  * Password field, allows unmasking of password
2376  *
2377  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2378  */
2379 class admin_setting_configpasswordunmask extends admin_setting_configtext {
2381     /**
2382      * Constructor
2383      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2384      * @param string $visiblename localised
2385      * @param string $description long localised info
2386      * @param string $defaultsetting default password
2387      */
2388     public function __construct($name, $visiblename, $description, $defaultsetting) {
2389         parent::__construct($name, $visiblename, $description, $defaultsetting, PARAM_RAW, 30);
2390     }
2392     /**
2393      * Log config changes if necessary.
2394      * @param string $name
2395      * @param string $oldvalue
2396      * @param string $value
2397      */
2398     protected function add_to_config_log($name, $oldvalue, $value) {
2399         if ($value !== '') {
2400             $value = '********';
2401         }
2402         if ($oldvalue !== '' and $oldvalue !== null) {
2403             $oldvalue = '********';
2404         }
2405         parent::add_to_config_log($name, $oldvalue, $value);
2406     }
2408     /**
2409      * Returns HTML for the field.
2410      *
2411      * @param   string  $data       Value for the field
2412      * @param   string  $query      Passed as final argument for format_admin_setting
2413      * @return  string              Rendered HTML
2414      */
2415     public function output_html($data, $query='') {
2416         global $OUTPUT;
2417         $context = (object) [
2418             'id' => $this->get_id(),
2419             'name' => $this->get_full_name(),
2420             'size' => $this->size,
2421             'value' => $data,
2422             'forceltr' => $this->get_force_ltr(),
2423         ];
2424         $element = $OUTPUT->render_from_template('core_admin/setting_configpasswordunmask', $context);
2425         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', null, $query);
2426     }
2430 /**
2431  * Empty setting used to allow flags (advanced) on settings that can have no sensible default.
2432  * Note: Only advanced makes sense right now - locked does not.
2433  *
2434  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2435  */
2436 class admin_setting_configempty extends admin_setting_configtext {
2438     /**
2439      * @param string $name
2440      * @param string $visiblename
2441      * @param string $description
2442      */
2443     public function __construct($name, $visiblename, $description) {
2444         parent::__construct($name, $visiblename, $description, '', PARAM_RAW);
2445     }
2447     /**
2448      * Returns an XHTML string for the hidden field
2449      *
2450      * @param string $data
2451      * @param string $query
2452      * @return string XHTML string for the editor
2453      */
2454     public function output_html($data, $query='') {
2455         global $OUTPUT;
2457         $context = (object) [
2458             'id' => $this->get_id(),
2459             'name' => $this->get_full_name()
2460         ];
2461         $element = $OUTPUT->render_from_template('core_admin/setting_configempty', $context);
2463         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', get_string('none'), $query);
2464     }
2468 /**
2469  * Path to directory
2470  *
2471  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2472  */
2473 class admin_setting_configfile extends admin_setting_configtext {
2474     /**
2475      * Constructor
2476      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2477      * @param string $visiblename localised
2478      * @param string $description long localised info
2479      * @param string $defaultdirectory default directory location
2480      */
2481     public function __construct($name, $visiblename, $description, $defaultdirectory) {
2482         parent::__construct($name, $visiblename, $description, $defaultdirectory, PARAM_RAW, 50);
2483     }
2485     /**
2486      * Returns XHTML for the field
2487      *
2488      * Returns XHTML for the field and also checks whether the file
2489      * specified in $data exists using file_exists()
2490      *
2491      * @param string $data File name and path to use in value attr
2492      * @param string $query
2493      * @return string XHTML field
2494      */
2495     public function output_html($data, $query='') {
2496         global $CFG, $OUTPUT;
2498         $default = $this->get_defaultsetting();
2499         $context = (object) [
2500             'id' => $this->get_id(),
2501             'name' => $this->get_full_name(),
2502             'size' => $this->size,
2503             'value' => $data,
2504             'showvalidity' => !empty($data),
2505             'valid' => $data && file_exists($data),
2506             'readonly' => !empty($CFG->preventexecpath),
2507             'forceltr' => $this->get_force_ltr(),
2508         ];
2510         if ($context->readonly) {
2511             $this->visiblename .= '<div class="form-overridden">'.get_string('execpathnotallowed', 'admin').'</div>';
2512         }
2514         $element = $OUTPUT->render_from_template('core_admin/setting_configfile', $context);
2516         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2517     }
2519     /**
2520      * Checks if execpatch has been disabled in config.php
2521      */
2522     public function write_setting($data) {
2523         global $CFG;
2524         if (!empty($CFG->preventexecpath)) {
2525             if ($this->get_setting() === null) {
2526                 // Use default during installation.
2527                 $data = $this->get_defaultsetting();
2528                 if ($data === null) {
2529                     $data = '';
2530                 }
2531             } else {
2532                 return '';
2533             }
2534         }
2535         return parent::write_setting($data);
2536     }
2541 /**
2542  * Path to executable file
2543  *
2544  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2545  */
2546 class admin_setting_configexecutable extends admin_setting_configfile {
2548     /**
2549      * Returns an XHTML field
2550      *
2551      * @param string $data This is the value for the field
2552      * @param string $query
2553      * @return string XHTML field
2554      */
2555     public function output_html($data, $query='') {
2556         global $CFG, $OUTPUT;
2557         $default = $this->get_defaultsetting();
2558         require_once("$CFG->libdir/filelib.php");
2560         $context = (object) [
2561             'id' => $this->get_id(),
2562             'name' => $this->get_full_name(),
2563             'size' => $this->size,
2564             'value' => $data,
2565             'showvalidity' => !empty($data),
2566             'valid' => $data && file_exists($data) && !is_dir($data) && file_is_executable($data),
2567             'readonly' => !empty($CFG->preventexecpath),
2568             'forceltr' => $this->get_force_ltr()
2569         ];
2571         if (!empty($CFG->preventexecpath)) {
2572             $this->visiblename .= '<div class="form-overridden">'.get_string('execpathnotallowed', 'admin').'</div>';
2573         }
2575         $element = $OUTPUT->render_from_template('core_admin/setting_configexecutable', $context);
2577         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2578     }
2582 /**
2583  * Path to directory
2584  *
2585  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2586  */
2587 class admin_setting_configdirectory extends admin_setting_configfile {
2589     /**
2590      * Returns an XHTML field
2591      *
2592      * @param string $data This is the value for the field
2593      * @param string $query
2594      * @return string XHTML
2595      */
2596     public function output_html($data, $query='') {
2597         global $CFG, $OUTPUT;
2598         $default = $this->get_defaultsetting();
2600         $context = (object) [
2601             'id' => $this->get_id(),
2602             'name' => $this->get_full_name(),
2603             'size' => $this->size,
2604             'value' => $data,
2605             'showvalidity' => !empty($data),
2606             'valid' => $data && file_exists($data) && is_dir($data),
2607             'readonly' => !empty($CFG->preventexecpath),
2608             'forceltr' => $this->get_force_ltr()
2609         ];
2611         if (!empty($CFG->preventexecpath)) {
2612             $this->visiblename .= '<div class="form-overridden">'.get_string('execpathnotallowed', 'admin').'</div>';
2613         }
2615         $element = $OUTPUT->render_from_template('core_admin/setting_configdirectory', $context);
2617         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2618     }
2622 /**
2623  * Checkbox
2624  *
2625  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2626  */
2627 class admin_setting_configcheckbox extends admin_setting {
2628     /** @var string Value used when checked */
2629     public $yes;
2630     /** @var string Value used when not checked */
2631     public $no;
2633     /**
2634      * Constructor
2635      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2636      * @param string $visiblename localised
2637      * @param string $description long localised info
2638      * @param string $defaultsetting
2639      * @param string $yes value used when checked
2640      * @param string $no value used when not checked
2641      */
2642     public function __construct($name, $visiblename, $description, $defaultsetting, $yes='1', $no='0') {
2643         parent::__construct($name, $visiblename, $description, $defaultsetting);
2644         $this->yes = (string)$yes;
2645         $this->no  = (string)$no;
2646     }
2648     /**
2649      * Retrieves the current setting using the objects name
2650      *
2651      * @return string
2652      */
2653     public function get_setting() {
2654         return $this->config_read($this->name);
2655     }
2657     /**
2658      * Sets the value for the setting
2659      *
2660      * Sets the value for the setting to either the yes or no values
2661      * of the object by comparing $data to yes
2662      *
2663      * @param mixed $data Gets converted to str for comparison against yes value
2664      * @return string empty string or error
2665      */
2666     public function write_setting($data) {
2667         if ((string)$data === $this->yes) { // convert to strings before comparison
2668             $data = $this->yes;
2669         } else {
2670             $data = $this->no;
2671         }
2672         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
2673     }
2675     /**
2676      * Returns an XHTML checkbox field
2677      *
2678      * @param string $data If $data matches yes then checkbox is checked
2679      * @param string $query
2680      * @return string XHTML field
2681      */
2682     public function output_html($data, $query='') {
2683         global $OUTPUT;
2685         $context = (object) [
2686             'id' => $this->get_id(),
2687             'name' => $this->get_full_name(),
2688             'no' => $this->no,
2689             'value' => $this->yes,
2690             'checked' => (string) $data === $this->yes,
2691         ];
2693         $default = $this->get_defaultsetting();
2694         if (!is_null($default)) {
2695             if ((string)$default === $this->yes) {
2696                 $defaultinfo = get_string('checkboxyes', 'admin');
2697             } else {
2698                 $defaultinfo = get_string('checkboxno', 'admin');
2699             }
2700         } else {
2701             $defaultinfo = NULL;
2702         }
2704         $element = $OUTPUT->render_from_template('core_admin/setting_configcheckbox', $context);
2706         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
2707     }
2711 /**
2712  * Multiple checkboxes, each represents different value, stored in csv format
2713  *
2714  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2715  */
2716 class admin_setting_configmulticheckbox extends admin_setting {
2717     /** @var array Array of choices value=>label */
2718     public $choices;
2720     /**
2721      * Constructor: uses parent::__construct
2722      *
2723      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2724      * @param string $visiblename localised
2725      * @param string $description long localised info
2726      * @param array $defaultsetting array of selected
2727      * @param array $choices array of $value=>$label for each checkbox
2728      */
2729     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
2730         $this->choices = $choices;
2731         parent::__construct($name, $visiblename, $description, $defaultsetting);
2732     }
2734     /**
2735      * This public function may be used in ancestors for lazy loading of choices
2736      *
2737      * @todo Check if this function is still required content commented out only returns true
2738      * @return bool true if loaded, false if error
2739      */
2740     public function load_choices() {
2741         /*
2742         if (is_array($this->choices)) {
2743             return true;
2744         }
2745         .... load choices here
2746         */
2747         return true;
2748     }
2750     /**
2751      * Is setting related to query text - used when searching
2752      *
2753      * @param string $query
2754      * @return bool true on related, false on not or failure
2755      */
2756     public function is_related($query) {
2757         if (!$this->load_choices() or empty($this->choices)) {
2758             return false;
2759         }
2760         if (parent::is_related($query)) {
2761             return true;
2762         }
2764         foreach ($this->choices as $desc) {
2765             if (strpos(core_text::strtolower($desc), $query) !== false) {
2766                 return true;
2767             }
2768         }
2769         return false;
2770     }
2772     /**
2773      * Returns the current setting if it is set
2774      *
2775      * @return mixed null if null, else an array
2776      */
2777     public function get_setting() {
2778         $result = $this->config_read($this->name);
2780         if (is_null($result)) {
2781             return NULL;
2782         }
2783         if ($result === '') {
2784             return array();
2785         }
2786         $enabled = explode(',', $result);
2787         $setting = array();
2788         foreach ($enabled as $option) {
2789             $setting[$option] = 1;
2790         }
2791         return $setting;
2792     }
2794     /**
2795      * Saves the setting(s) provided in $data
2796      *
2797      * @param array $data An array of data, if not array returns empty str
2798      * @return mixed empty string on useless data or bool true=success, false=failed
2799      */
2800     public function write_setting($data) {
2801         if (!is_array($data)) {
2802             return ''; // ignore it
2803         }
2804         if (!$this->load_choices() or empty($this->choices)) {
2805             return '';
2806         }
2807         unset($data['xxxxx']);
2808         $result = array();
2809         foreach ($data as $key => $value) {
2810             if ($value and array_key_exists($key, $this->choices)) {
2811                 $result[] = $key;
2812             }
2813         }
2814         return $this->config_write($this->name, implode(',', $result)) ? '' : get_string('errorsetting', 'admin');
2815     }
2817     /**
2818      * Returns XHTML field(s) as required by choices
2819      *
2820      * Relies on data being an array should data ever be another valid vartype with
2821      * acceptable value this may cause a warning/error
2822      * if (!is_array($data)) would fix the problem
2823      *
2824      * @todo Add vartype handling to ensure $data is an array
2825      *
2826      * @param array $data An array of checked values
2827      * @param string $query
2828      * @return string XHTML field
2829      */
2830     public function output_html($data, $query='') {
2831         global $OUTPUT;
2833         if (!$this->load_choices() or empty($this->choices)) {
2834             return '';
2835         }
2837         $default = $this->get_defaultsetting();
2838         if (is_null($default)) {
2839             $default = array();
2840         }
2841         if (is_null($data)) {
2842             $data = array();
2843         }
2845         $context = (object) [
2846             'id' => $this->get_id(),
2847             'name' => $this->get_full_name(),
2848         ];
2850         $options = array();
2851         $defaults = array();
2852         foreach ($this->choices as $key => $description) {
2853             if (!empty($default[$key])) {
2854                 $defaults[] = $description;
2855             }
2857             $options[] = [
2858                 'key' => $key,
2859                 'checked' => !empty($data[$key]),
2860                 'label' => highlightfast($query, $description)
2861             ];
2862         }
2864         if (is_null($default)) {
2865             $defaultinfo = null;
2866         } else if (!empty($defaults)) {
2867             $defaultinfo = implode(', ', $defaults);
2868         } else {
2869             $defaultinfo = get_string('none');
2870         }
2872         $context->options = $options;
2873         $context->hasoptions = !empty($options);
2875         $element = $OUTPUT->render_from_template('core_admin/setting_configmulticheckbox', $context);
2877         return format_admin_setting($this, $this->visiblename, $element, $this->description, false, '', $defaultinfo, $query);
2879     }
2883 /**
2884  * Multiple checkboxes 2, value stored as string 00101011
2885  *
2886  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2887  */
2888 class admin_setting_configmulticheckbox2 extends admin_setting_configmulticheckbox {
2890     /**
2891      * Returns the setting if set
2892      *
2893      * @return mixed null if not set, else an array of set settings
2894      */
2895     public function get_setting() {
2896         $result = $this->config_read($this->name);
2897         if (is_null($result)) {
2898             return NULL;
2899         }
2900         if (!$this->load_choices()) {
2901             return NULL;
2902         }
2903         $result = str_pad($result, count($this->choices), '0');
2904         $result = preg_split('//', $result, -1, PREG_SPLIT_NO_EMPTY);
2905         $setting = array();
2906         foreach ($this->choices as $key=>$unused) {
2907             $value = array_shift($result);
2908             if ($value) {
2909                 $setting[$key] = 1;
2910             }
2911         }
2912         return $setting;
2913     }
2915     /**
2916      * Save setting(s) provided in $data param
2917      *
2918      * @param array $data An array of settings to save
2919      * @return mixed empty string for bad data or bool true=>success, false=>error
2920      */
2921     public function write_setting($data) {
2922         if (!is_array($data)) {
2923             return ''; // ignore it
2924         }
2925         if (!$this->load_choices() or empty($this->choices)) {
2926             return '';
2927         }
2928         $result = '';
2929         foreach ($this->choices as $key=>$unused) {
2930             if (!empty($data[$key])) {
2931                 $result .= '1';
2932             } else {
2933                 $result .= '0';
2934             }
2935         }
2936         return $this->config_write($this->name, $result) ? '' : get_string('errorsetting', 'admin');
2937     }
2941 /**
2942  * Select one value from list
2943  *
2944  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2945  */
2946 class admin_setting_configselect extends admin_setting {
2947     /** @var array Array of choices value=>label */
2948     public $choices;
2949     /** @var array Array of choices grouped using optgroups */
2950     public $optgroups;
2952     /**
2953      * Constructor
2954      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2955      * @param string $visiblename localised
2956      * @param string $description long localised info
2957      * @param string|int $defaultsetting
2958      * @param array $choices array of $value=>$label for each selection
2959      */
2960     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
2961         // Look for optgroup and single options.
2962         if (is_array($choices)) {
2963             foreach ($choices as $key => $val) {
2964                 if (is_array($val)) {
2965                     $this->optgroups[$key] = $val;
2966                     $this->choices = array_merge($this->choices, $val);
2967                 } else {
2968                     $this->choices[$key] = $val;
2969                 }
2970             }
2971         }
2973         parent::__construct($name, $visiblename, $description, $defaultsetting);
2974     }
2976     /**
2977      * This function may be used in ancestors for lazy loading of choices
2978      *
2979      * Override this method if loading of choices is expensive, such
2980      * as when it requires multiple db requests.
2981      *
2982      * @return bool true if loaded, false if error
2983      */
2984     public function load_choices() {
2985         /*
2986         if (is_array($this->choices)) {
2987             return true;
2988         }
2989         .... load choices here
2990         */
2991         return true;
2992     }
2994     /**
2995      * Check if this is $query is related to a choice
2996      *
2997      * @param string $query
2998      * @return bool true if related, false if not
2999      */
3000     public function is_related($query) {
3001         if (parent::is_related($query)) {
3002             return true;
3003         }
3004         if (!$this->load_choices()) {
3005             return false;
3006         }
3007         foreach ($this->choices as $key=>$value) {
3008             if (strpos(core_text::strtolower($key), $query) !== false) {
3009                 return true;
3010             }
3011             if (strpos(core_text::strtolower($value), $query) !== false) {
3012                 return true;
3013             }
3014         }
3015         return false;
3016     }
3018     /**
3019      * Return the setting
3020      *
3021      * @return mixed returns config if successful else null
3022      */
3023     public function get_setting() {
3024         return $this->config_read($this->name);
3025     }
3027     /**
3028      * Save a setting
3029      *
3030      * @param string $data
3031      * @return string empty of error string
3032      */
3033     public function write_setting($data) {
3034         if (!$this->load_choices() or empty($this->choices)) {
3035             return '';
3036         }
3037         if (!array_key_exists($data, $this->choices)) {
3038             return ''; // ignore it
3039         }
3041         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
3042     }
3044     /**
3045      * Returns XHTML select field
3046      *
3047      * Ensure the options are loaded, and generate the XHTML for the select
3048      * element and any warning message. Separating this out from output_html
3049      * makes it easier to subclass this class.
3050      *
3051      * @param string $data the option to show as selected.
3052      * @param string $current the currently selected option in the database, null if none.
3053      * @param string $default the default selected option.
3054      * @return array the HTML for the select element, and a warning message.
3055      * @deprecated since Moodle 3.2
3056      */
3057     public function output_select_html($data, $current, $default, $extraname = '') {
3058         debugging('The method admin_setting_configselect::output_select_html is depreacted, do not use any more.', DEBUG_DEVELOPER);
3059     }
3061     /**
3062      * Returns XHTML select field and wrapping div(s)
3063      *
3064      * @see output_select_html()
3065      *
3066      * @param string $data the option to show as selected
3067      * @param string $query
3068      * @return string XHTML field and wrapping div
3069      */
3070     public function output_html($data, $query='') {
3071         global $OUTPUT;
3073         $default = $this->get_defaultsetting();
3074         $current = $this->get_setting();
3076         if (!$this->load_choices() || empty($this->choices)) {
3077             return '';
3078         }
3080         $context = (object) [
3081             'id' => $this->get_id(),
3082             'name' => $this->get_full_name(),
3083         ];
3085         if (!is_null($default) && array_key_exists($default, $this->choices)) {
3086             $defaultinfo = $this->choices[$default];
3087         } else {
3088             $defaultinfo = NULL;
3089         }
3091         // Warnings.
3092         $warning = '';
3093         if ($current === null) {
3094             // First run.
3095         } else if (empty($current) && (array_key_exists('', $this->choices) || array_key_exists(0, $this->choices))) {
3096             // No warning.
3097         } else if (!array_key_exists($current, $this->choices)) {
3098             $warning = get_string('warningcurrentsetting', 'admin', $current);
3099             if (!is_null($default) && $data == $current) {
3100                 $data = $default; // Use default instead of first value when showing the form.
3101             }
3102         }
3104         $options = [];
3105         $template = 'core_admin/setting_configselect';
3107         if (!empty($this->optgroups)) {
3108             $optgroups = [];
3109             foreach ($this->optgroups as $label => $choices) {
3110                 $optgroup = array('label' => $label, 'options' => []);
3111                 foreach ($choices as $value => $name) {
3112                     $optgroup['options'][] = [
3113                         'value' => $value,
3114                         'name' => $name,
3115                         'selected' => (string) $value == $data
3116                     ];
3117                     unset($this->choices[$value]);
3118                 }
3119                 $optgroups[] = $optgroup;
3120             }
3121             $context->options = $options;
3122             $context->optgroups = $optgroups;
3123             $template = 'core_admin/setting_configselect_optgroup';
3124         }
3126         foreach ($this->choices as $value => $name) {
3127             $options[] = [
3128                 'value' => $value,
3129                 'name' => $name,
3130                 'selected' => (string) $value == $data
3131             ];
3132         }
3133         $context->options = $options;
3135         $element = $OUTPUT->render_from_template($template, $context);
3137         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, $warning, $defaultinfo, $query);
3138     }
3142 /**
3143  * Select multiple items from list
3144  *
3145  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3146  */
3147 class admin_setting_configmultiselect extends admin_setting_configselect {
3148     /**
3149      * Constructor
3150      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3151      * @param string $visiblename localised
3152      * @param string $description long localised info
3153      * @param array $defaultsetting array of selected items
3154      * @param array $choices array of $value=>$label for each list item
3155      */
3156     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
3157         parent::__construct($name, $visiblename, $description, $defaultsetting, $choices);
3158     }
3160     /**
3161      * Returns the select setting(s)
3162      *
3163      * @return mixed null or array. Null if no settings else array of setting(s)
3164      */
3165     public function get_setting() {
3166         $result = $this->config_read($this->name);
3167         if (is_null($result)) {
3168             return NULL;
3169         }
3170         if ($result === '') {
3171             return array();
3172         }
3173         return explode(',', $result);
3174     }
3176     /**
3177      * Saves setting(s) provided through $data
3178      *
3179      * Potential bug in the works should anyone call with this function
3180      * using a vartype that is not an array
3181      *
3182      * @param array $data
3183      */
3184     public function write_setting($data) {
3185         if (!is_array($data)) {
3186             return ''; //ignore it
3187         }
3188         if (!$this->load_choices() or empty($this->choices)) {
3189             return '';
3190         }
3192         unset($data['xxxxx']);
3194         $save = array();
3195         foreach ($data as $value) {
3196             if (!array_key_exists($value, $this->choices)) {
3197                 continue; // ignore it
3198             }
3199             $save[] = $value;
3200         }
3202         return ($this->config_write($this->name, implode(',', $save)) ? '' : get_string('errorsetting', 'admin'));
3203     }
3205     /**
3206      * Is setting related to query text - used when searching
3207      *
3208      * @param string $query
3209      * @return bool true if related, false if not
3210      */
3211     public function is_related($query) {
3212         if (!$this->load_choices() or empty($this->choices)) {
3213             return false;
3214         }
3215         if (parent::is_related($query)) {
3216             return true;
3217         }
3219         foreach ($this->choices as $desc) {
3220             if (strpos(core_text::strtolower($desc), $query) !== false) {
3221                 return true;
3222             }
3223         }
3224         return false;
3225     }
3227     /**
3228      * Returns XHTML multi-select field
3229      *
3230      * @todo Add vartype handling to ensure $data is an array
3231      * @param array $data Array of values to select by default
3232      * @param string $query
3233      * @return string XHTML multi-select field
3234      */
3235     public function output_html($data, $query='') {
3236         global $OUTPUT;
3238         if (!$this->load_choices() or empty($this->choices)) {
3239             return '';
3240         }
3242         $default = $this->get_defaultsetting();
3243         if (is_null($default)) {
3244             $default = array();
3245         }
3246         if (is_null($data)) {
3247             $data = array();
3248         }
3250         $context = (object) [
3251             'id' => $this->get_id(),
3252             'name' => $this->get_full_name(),
3253             'size' => min(10, count($this->choices))
3254         ];
3256         $defaults = [];
3257         $options = [];
3258         $template = 'core_admin/setting_configmultiselect';
3260         if (!empty($this->optgroups)) {
3261             $optgroups = [];
3262             foreach ($this->optgroups as $label => $choices) {
3263                 $optgroup = array('label' => $label, 'options' => []);
3264                 foreach ($choices as $value => $name) {
3265                     if (in_array($value, $default)) {
3266                         $defaults[] = $name;
3267                     }
3268                     $optgroup['options'][] = [
3269                         'value' => $value,
3270                         'name' => $name,
3271                         'selected' => in_array($value, $data)
3272                     ];
3273                     unset($this->choices[$value]);
3274                 }
3275                 $optgroups[] = $optgroup;
3276             }
3277             $context->optgroups = $optgroups;
3278             $template = 'core_admin/setting_configmultiselect_optgroup';
3279         }
3281         foreach ($this->choices as $value => $name) {
3282             if (in_array($value, $default)) {
3283                 $defaults[] = $name;
3284             }
3285             $options[] = [
3286                 'value' => $value,
3287                 'name' => $name,
3288                 'selected' => in_array($value, $data)
3289             ];
3290         }
3291         $context->options = $options;
3293         if (is_null($default)) {
3294             $defaultinfo = NULL;
3295         } if (!empty($defaults)) {
3296             $defaultinfo = implode(', ', $defaults);
3297         } else {
3298             $defaultinfo = get_string('none');
3299         }
3301         $element = $OUTPUT->render_from_template($template, $context);
3303         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
3304     }
3307 /**
3308  * Time selector
3309  *
3310  * This is a liiitle bit messy. we're using two selects, but we're returning
3311  * them as an array named after $name (so we only use $name2 internally for the setting)
3312  *
3313  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3314  */
3315 class admin_setting_configtime extends admin_setting {
3316     /** @var string Used for setting second select (minutes) */
3317     public $name2;
3319     /**
3320      * Constructor
3321      * @param string $hoursname setting for hours
3322      * @param string $minutesname setting for hours
3323      * @param string $visiblename localised
3324      * @param string $description long localised info
3325      * @param array $defaultsetting array representing default time 'h'=>hours, 'm'=>minutes
3326      */
3327     public function __construct($hoursname, $minutesname, $visiblename, $description, $defaultsetting) {
3328         $this->name2 = $minutesname;
3329         parent::__construct($hoursname, $visiblename, $description, $defaultsetting);
3330     }
3332     /**
3333      * Get the selected time
3334      *
3335      * @return mixed An array containing 'h'=>xx, 'm'=>xx, or null if not set
3336      */
3337     public function get_setting() {
3338         $result1 = $this->config_read($this->name);
3339         $result2 = $this->config_read($this->name2);
3340         if (is_null($result1) or is_null($result2)) {
3341             return NULL;
3342         }
3344         return array('h' => $result1, 'm' => $result2);
3345     }
3347     /**
3348      * Store the time (hours and minutes)
3349      *
3350      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3351      * @return bool true if success, false if not
3352      */
3353     public function write_setting($data) {
3354         if (!is_array($data)) {
3355             return '';
3356         }
3358         $result = $this->config_write($this->name, (int)$data['h']) && $this->config_write($this->name2, (int)$data['m']);
3359         return ($result ? '' : get_string('errorsetting', 'admin'));
3360     }
3362     /**
3363      * Returns XHTML time select fields
3364      *
3365      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3366      * @param string $query
3367      * @return string XHTML time select fields and wrapping div(s)
3368      */
3369     public function output_html($data, $query='') {
3370         global $OUTPUT;
3372         $default = $this->get_defaultsetting();
3373         if (is_array($default)) {
3374             $defaultinfo = $default['h'].':'.$default['m'];
3375         } else {
3376             $defaultinfo = NULL;
3377         }
3379         $context = (object) [
3380             'id' => $this->get_id(),
3381             'name' => $this->get_full_name(),
3382             'hours' => array_map(function($i) use ($data) {
3383                 return [
3384                     'value' => $i,
3385                     'name' => $i,
3386                     'selected' => $i == $data['h']
3387                 ];
3388             }, range(0, 23)),
3389             'minutes' => array_map(function($i) use ($data) {
3390                 return [
3391                     'value' => $i,
3392                     'name' => $i,
3393                     'selected' => $i == $data['m']
3394                 ];
3395             }, range(0, 59, 5))
3396         ];
3398         $element = $OUTPUT->render_from_template('core_admin/setting_configtime', $context);
3400         return format_admin_setting($this, $this->visiblename, $element, $this->description,
3401             $this->get_id() . 'h', '', $defaultinfo, $query);
3402     }
3407 /**
3408  * Seconds duration setting.
3409  *
3410  * @copyright 2012 Petr Skoda (http://skodak.org)
3411  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3412  */
3413 class admin_setting_configduration extends admin_setting {
3415     /** @var int default duration unit */
3416     protected $defaultunit;
3418     /**
3419      * Constructor
3420      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
3421      *                     or 'myplugin/mysetting' for ones in config_plugins.
3422      * @param string $visiblename localised name
3423      * @param string $description localised long description
3424      * @param mixed $defaultsetting string or array depending on implementation
3425      * @param int $defaultunit - day, week, etc. (in seconds)
3426      */
3427     public function __construct($name, $visiblename, $description, $defaultsetting, $defaultunit = 86400) {
3428         if (is_number($defaultsetting)) {
3429             $defaultsetting = self::parse_seconds($defaultsetting);
3430         }
3431         $units = self::get_units();
3432         if (isset($units[$defaultunit])) {
3433             $this->defaultunit = $defaultunit;
3434         } else {
3435             $this->defaultunit = 86400;
3436         }
3437         parent::__construct($name, $visiblename, $description, $defaultsetting);
3438     }
3440     /**
3441      * Returns selectable units.
3442      * @static
3443      * @return array
3444      */
3445     protected static function get_units() {
3446         return array(
3447             604800 => get_string('weeks'),
3448             86400 => get_string('days'),
3449             3600 => get_string('hours'),
3450             60 => get_string('minutes'),
3451             1 => get_string('seconds'),
3452         );
3453     }
3455     /**
3456      * Converts seconds to some more user friendly string.
3457      * @static
3458      * @param int $seconds
3459      * @return string
3460      */
3461     protected static function get_duration_text($seconds) {
3462         if (empty($seconds)) {
3463             return get_string('none');
3464         }
3465         $data = self::parse_seconds($seconds);
3466         switch ($data['u']) {
3467             case (60*60*24*7):
3468                 return get_string('numweeks', '', $data['v']);
3469             case (60*60*24):
3470                 return get_string('numdays', '', $data['v']);
3471             case (60*60):
3472                 return get_string('numhours', '', $data['v']);
3473             case (60):
3474                 return get_string('numminutes', '', $data['v']);
3475             default:
3476                 return get_string('numseconds', '', $data['v']*$data['u']);
3477         }
3478     }
3480     /**
3481      * Finds suitable units for given duration.
3482      * @static
3483      * @param int $seconds
3484      * @return array
3485      */
3486     protected static function parse_seconds($seconds) {
3487         foreach (self::get_units() as $unit => $unused) {
3488             if ($seconds % $unit === 0) {
3489                 return array('v'=>(int)($seconds/$unit), 'u'=>$unit);
3490             }
3491         }
3492         return array('v'=>(int)$seconds, 'u'=>1);
3493     }
3495     /**
3496      * Get the selected duration as array.
3497      *
3498      * @return mixed An array containing 'v'=>xx, 'u'=>xx, or null if not set
3499      */
3500     public function get_setting() {
3501         $seconds = $this->config_read($this->name);
3502         if (is_null($seconds)) {
3503             return null;
3504         }
3506         return self::parse_seconds($seconds);
3507     }
3509     /**
3510      * Store the duration as seconds.
3511      *
3512      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3513      * @return bool true if success, false if not
3514      */
3515     public function write_setting($data) {
3516         if (!is_array($data)) {
3517             return '';
3518         }
3520         $seconds = (int)($data['v']*$data['u']);
3521         if ($seconds < 0) {
3522             return get_string('errorsetting', 'admin');
3523         }
3525         $result = $this->config_write($this->name, $seconds);
3526         return ($result ? '' : get_string('errorsetting', 'admin'));
3527     }
3529     /**
3530      * Returns duration text+select fields.
3531      *
3532      * @param array $data Must be form 'v'=>xx, 'u'=>xx
3533      * @param string $query
3534      * @return string duration text+select fields and wrapping div(s)
3535      */
3536     public function output_html($data, $query='') {
3537         global $OUTPUT;
3539         $default = $this->get_defaultsetting();
3540         if (is_number($default)) {
3541             $defaultinfo = self::get_duration_text($default);
3542         } else if (is_array($default)) {
3543             $defaultinfo = self::get_duration_text($default['v']*$default['u']);
3544         } else {
3545             $defaultinfo = null;
3546         }
3548         $inputid = $this->get_id() . 'v';
3549         $units = self::get_units();
3550         $defaultunit = $this->defaultunit;
3552         $context = (object) [
3553             'id' => $this->get_id(),
3554             'name' => $this->get_full_name(),
3555             'value' => $data['v'],
3556             'options' => array_map(function($unit) use ($units, $data, $defaultunit) {
3557                 return [
3558                     'value' => $unit,
3559                     'name' => $units[$unit],
3560                     'selected' => ($data['v'] == 0 && $unit == $defaultunit) || $unit == $data['u']
3561                 ];
3562             }, array_keys($units))
3563         ];
3565         $element = $OUTPUT->render_from_template('core_admin/setting_configduration', $context);
3567         return format_admin_setting($this, $this->visiblename, $element, $this->description, $inputid, '', $defaultinfo, $query);
3568     }
3572 /**
3573  * Seconds duration setting with an advanced checkbox, that controls a additional
3574  * $name.'_adv' setting.
3575  *
3576  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3577  * @copyright 2014 The Open University
3578  */
3579 class admin_setting_configduration_with_advanced extends admin_setting_configduration {
3580     /**
3581      * Constructor
3582      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
3583      *                     or 'myplugin/mysetting' for ones in config_plugins.
3584      * @param string $visiblename localised name
3585      * @param string $description localised long description
3586      * @param array  $defaultsetting array of int value, and bool whether it is
3587      *                     is advanced by default.
3588      * @param int $defaultunit - day, week, etc. (in seconds)
3589      */
3590     public function __construct($name, $visiblename, $description, $defaultsetting, $defaultunit = 86400) {
3591         parent::__construct($name, $visiblename, $description, $defaultsetting['value'], $defaultunit);
3592         $this->set_advanced_flag_options(admin_setting_flag::ENABLED, !empty($defaultsetting['adv']));
3593     }
3597 /**
3598  * Used to validate a textarea used for ip addresses
3599  *
3600  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3601  * @copyright 2011 Petr Skoda (http://skodak.org)
3602  */
3603 class admin_setting_configiplist extends admin_setting_configtextarea {
3605     /**
3606      * Validate the contents of the textarea as IP addresses
3607      *
3608      * Used to validate a new line separated list of IP addresses collected from
3609      * a textarea control
3610      *
3611      * @param string $data A list of IP Addresses separated by new lines
3612      * @return mixed bool true for success or string:error on failure
3613      */
3614     public function validate($data) {
3615         if(!empty($data)) {
3616             $ips = explode("\n", $data);
3617         } else {
3618             return true;
3619         }
3620         $result = true;
3621         $badips = array();
3622         foreach($ips as $ip) {
3623             $ip = trim($ip);
3624             if (empty($ip)) {
3625                 continue;
3626             }
3627             if (preg_match('#^(\d{1,3})(\.\d{1,3}){0,3}$#', $ip, $match) ||
3628                 preg_match('#^(\d{1,3})(\.\d{1,3}){0,3}(\/\d{1,2})$#', $ip, $match) ||
3629                 preg_match('#^(\d{1,3})(\.\d{1,3}){3}(-\d{1,3})$#', $ip, $match)) {
3630             } else {
3631                 $result = false;
3632                 $badips[] = $ip;
3633             }
3634         }
3635         if($result) {
3636             return true;
3637         } else {
3638             return get_string('validateiperror', 'admin', join(', ', $badips));
3639         }
3640     }
3643 /**
3644  * Used to validate a textarea used for domain names, wildcard domain names and IP addresses/ranges (both IPv4 and IPv6 format).
3645  *
3646  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3647  * @copyright 2016 Jake Dallimore (jrhdallimore@gmail.com)
3648  */
3649 class admin_setting_configmixedhostiplist extends admin_setting_configtextarea {
3651     /**
3652      * Validate the contents of the textarea as either IP addresses, domain name or wildcard domain name (RFC 4592).
3653      * Used to validate a new line separated list of entries collected from a textarea control.
3654      *
3655      * This setting provides support for internationalised domain names (IDNs), however, such UTF-8 names will be converted to
3656      * their ascii-compatible encoding (punycode) on save, and converted back to their UTF-8 representation when fetched
3657      * via the get_setting() method, which has been overriden.
3658      *
3659      * @param string $data A list of FQDNs, DNS wildcard format domains, and IP addresses, separated by new lines.
3660      * @return mixed bool true for success or string:error on failure
3661      */
3662     public function validate($data) {
3663         if (empty($data)) {
3664             return true;
3665         }
3666         $entries = explode("\n", $data);
3667         $badentries = [];
3669         foreach ($entries as $key => $entry) {
3670             $entry = trim($entry);
3671             if (empty($entry)) {
3672                 return get_string('validateemptylineerror', 'admin');
3673             }
3675             // Validate each string entry against the supported formats.
3676             if (\core\ip_utils::is_ip_address($entry) || \core\ip_utils::is_ipv6_range($entry)
3677                     || \core\ip_utils::is_ipv4_range($entry) || \core\ip_utils::is_domain_name($entry)
3678                     || \core\ip_utils::is_domain_matching_pattern($entry)) {
3679                 continue;
3680             }
3682             // Otherwise, the entry is invalid.
3683             $badentries[] = $entry;
3684         }
3686         if ($badentries) {
3687             return get_string('validateerrorlist', 'admin', join(', ', $badentries));
3688         }
3689         return true;
3690     }
3692     /**
3693      * Convert any lines containing international domain names (IDNs) to their ascii-compatible encoding (ACE).
3694      *
3695      * @param string $data the setting data, as sent from the web form.
3696      * @return string $data the setting data, with all IDNs converted (using punycode) to their ascii encoded version.
3697      */
3698     protected function ace_encode($data) {
3699         if (empty($data)) {
3700             return $data;
3701         }
3702         $entries = explode("\n", $data);
3703         foreach ($entries as $key => $entry) {
3704             $entry = trim($entry);
3705             // This regex matches any string which:
3706             // a) contains at least one non-ascii unicode character AND
3707             // b) starts with a-zA-Z0-9 or any non-ascii unicode character AND
3708             // c) ends with a-zA-Z0-9 or any non-ascii unicode character
3709             // d) contains a-zA-Z0-9, hyphen, dot or any non-ascii unicode characters in the middle.
3710             if (preg_match('/^(?=[^\x00-\x7f])([^\x00-\x7f]|[a-zA-Z0-9])([^\x00-\x7f]|[a-zA-Z0-9-.])*([^\x00-\x7f]|[a-zA-Z0-9])$/',
3711                 $entry)) {
3712                 // If we can convert the unicode string to an idn, do so.
3713                 // Otherwise, leave the original unicode string alone and let the validation function handle it (it will fail).
3714                 $val = idn_to_ascii($entry);
3715                 $entries[$key] = $val ? $val : $entry;
3716             }
3717         }
3718         return implode("\n", $entries);
3719     }
3721     /**
3722      * Decode any ascii-encoded domain names back to their utf-8 representation for display.
3723      *
3724      * @param string $data the setting data, as found in the database.
3725      * @return string $data the setting data, with all ascii-encoded IDNs decoded back to their utf-8 representation.
3726      */
3727     protected function ace_decode($data) {
3728         $entries = explode("\n", $data);
3729         foreach ($entries as $key => $entry) {
3730             $entry = trim($entry);
3731             if (strpos($entry, 'xn--') !== false) {
3732                 $entries[$key] = idn_to_utf8($entry);
3733             }
3734         }
3735         return implode("\n", $entries);
3736     }
3738     /**
3739      * Override, providing utf8-decoding for ascii-encoded IDN strings.
3740      *
3741      * @return mixed returns punycode-converted setting string if successful, else null.
3742      */
3743     public function get_setting() {
3744         // Here, we need to decode any ascii-encoded IDNs back to their native, utf-8 representation.
3745         $data = $this->config_read($this->name);
3746         if (function_exists('idn_to_utf8') && !is_null($data)) {
3747             $data = $this->ace_decode($data);
3748         }
3749         return $data;
3750     }
3752     /**
3753      * Override, providing ascii-encoding for utf8 (native) IDN strings.
3754      *
3755      * @param string $data
3756      * @return string
3757      */
3758     public function write_setting($data) {
3759         if ($this->paramtype === PARAM_INT and $data === '') {
3760             // Do not complain if '' used instead of 0.
3761             $data = 0;
3762         }
3764         // Try to convert any non-ascii domains to ACE prior to validation - we can't modify anything in validate!
3765         if (function_exists('idn_to_ascii')) {
3766             $data = $this->ace_encode($data);
3767         }
3769         $validated = $this->validate($data);
3770         if ($validated !== true) {
3771             return $validated;
3772         }
3773         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
3774     }
3777 /**
3778  * Used to validate a textarea used for port numbers.
3779  *
3780  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3781  * @copyright 2016 Jake Dallimore (jrhdallimore@gmail.com)
3782  */
3783 class admin_setting_configportlist extends admin_setting_configtextarea {
3785     /**
3786      * Validate the contents of the textarea as port numbers.
3787      * Used to validate a new line separated list of ports collected from a textarea control.
3788      *
3789      * @param string $data A list of ports separated by new lines
3790      * @return mixed bool true for success or string:error on failure
3791      */
3792     public function validate($data) {
3793         if (empty($data)) {
3794             return true;
3795         }
3796         $ports = explode("\n", $data);
3797         $badentries = [];
3798         foreach ($ports as $port) {
3799             $port = trim($port);
3800             if (empty($port)) {
3801                 return get_string('validateemptylineerror', 'admin');
3802             }
3804             // Is the string a valid integer number?
3805             if (strval(intval($port)) !== $port || intval($port) <= 0) {
3806                 $badentries[] = $port;
3807             }
3808         }
3809         if ($badentries) {
3810             return get_string('validateerrorlist', 'admin', $badentries);
3811         }
3812         return true;
3813     }
3817 /**
3818  * An admin setting for selecting one or more users who have a capability
3819  * in the system context
3820  *
3821  * An admin setting for selecting one or more users, who have a particular capability
3822  * in the system context. Warning, make sure the list will never be too long. There is
3823  * no paging or searching of this list.
3824  *
3825  * To correctly get a list of users from this config setting, you need to call the
3826  * get_users_from_config($CFG->mysetting, $capability); function in moodlelib.php.
3827  *
3828  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3829  */
3830 class admin_setting_users_with_capability extends admin_setting_configmultiselect {
3831     /** @var string The capabilities name */
3832     protected $capability;
3833     /** @var int include admin users too */
3834     protected $includeadmins;
3836     /**
3837      * Constructor.
3838      *
3839      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3840      * @param string $visiblename localised name
3841      * @param string $description localised long description
3842      * @param array $defaultsetting array of usernames
3843      * @param string $capability string capability name.
3844      * @param bool $includeadmins include administrators
3845      */
3846     function __construct($name, $visiblename, $description, $defaultsetting, $capability, $includeadmins = true) {
3847         $this->capability    = $capability;
3848         $this->includeadmins = $includeadmins;
3849         parent::__construct($name, $visiblename, $description, $defaultsetting, NULL);
3850     }
3852     /**
3853      * Load all of the uses who have the capability into choice array
3854      *
3855      * @return bool Always returns true
3856      */
3857     function load_choices() {
3858         if (is_array($this->choices)) {
3859             return true;
3860         }
3861         list($sort, $sortparams) = users_order_by_sql('u');
3862         if (!empty($sortparams)) {
3863             throw new coding_exception('users_order_by_sql returned some query parameters. ' .
3864                     'This is unexpected, and a problem because there is no way to pass these ' .
3865                     'parameters to get_users_by_capability. See MDL-34657.');
3866         }
3867         $userfields = 'u.id, u.username, ' . get_all_user_name_fields(true, 'u');
3868         $users = get_users_by_capability(context_system::instance(), $this->capability, $userfields, $sort);
3869         $this->choices = array(
3870             '$@NONE@$' => get_string('nobody'),
3871             '$@ALL@$' => get_string('everyonewhocan', 'admin', get_capability_string($this->capability)),
3872         );
3873         if ($this->includeadmins) {
3874             $admins = get_admins();
3875             foreach ($admins as $user) {
3876                 $this->choices[$user->id] = fullname($user);
3877             }
3878         }
3879         if (is_array($users)) {
3880             foreach ($users as $user) {
3881                 $this->choices[$user->id] = fullname($user);
3882             }
3883         }
3884         return true;
3885     }
3887     /**
3888      * Returns the default setting for class
3889      *
3890      * @return mixed Array, or string. Empty string if no default
3891      */
3892     public function get_defaultsetting() {
3893         $this->load_choices();
3894         $defaultsetting = parent::get_defaultsetting();
3895         if (empty($defaultsetting)) {
3896             return array('$@NONE@$');
3897         } else if (array_key_exists($defaultsetting, $this->choices)) {
3898                 return $defaultsetting;
3899             } else {
3900                 return '';
3901             }
3902     }
3904     /**
3905      * Returns the current setting
3906      *
3907      * @return mixed array or string
3908      */
3909     public function get_setting() {
3910         $result = parent::get_setting();
3911         if ($result === null) {
3912             // this is necessary for settings upgrade
3913             return null;
3914         }
3915         if (empty($result)) {
3916             $result = array('$@NONE@$');
3917         }
3918         return $result;
3919     }
3921     /**
3922      * Save the chosen setting provided as $data
3923      *
3924      * @param array $data
3925      * @return mixed string or array
3926      */
3927     public function write_setting($data) {
3928     // If all is selected, remove any explicit options.
3929         if (in_array('$@ALL@$', $data)) {
3930             $data = array('$@ALL@$');
3931         }
3932         // None never needs to be written to the DB.
3933         if (in_array('$@NONE@$', $data)) {
3934             unset($data[array_search('$@NONE@$', $data)]);
3935         }
3936         return parent::write_setting($data);
3937     }
3941 /**
3942  * Special checkbox for calendar - resets SESSION vars.
3943  *
3944  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3945  */
3946 class admin_setting_special_adminseesall extends admin_setting_configcheckbox {
3947     /**
3948      * Calls the parent::__construct with default values
3949      *
3950      * name =>  calendar_adminseesall
3951      * visiblename => get_string('adminseesall', 'admin')
3952      * description => get_string('helpadminseesall', 'admin')
3953      * defaultsetting => 0
3954      */
3955     public function __construct() {
3956         parent::__construct('calendar_adminseesall', get_string('adminseesall', 'admin'),
3957             get_string('helpadminseesall', 'admin'), '0');
3958     }
3960     /**
3961      * Stores the setting passed in $data
3962      *
3963      * @param mixed gets converted to string for comparison
3964      * @return string empty string or error message
3965      */
3966     public function write_setting($data) {
3967         global $SESSION;
3968         return parent::write_setting($data);
3969     }
3972 /**
3973  * Special select for settings that are altered in setup.php and can not be altered on the fly
3974  *
3975  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3976  */
3977 class admin_setting_special_selectsetup extends admin_setting_configselect {
3978     /**
3979      * Reads the setting directly from the database
3980      *
3981      * @return mixed
3982      */
3983     public function get_setting() {
3984     // read directly from db!
3985         return get_config(NULL, $this->name);
3986     }
3988     /**
3989      * Save the setting passed in $data
3990      *
3991      * @param string $data The setting to save
3992      * @return string empty or error message
3993      */
3994     public function write_setting($data) {
3995         global $CFG;
3996         // do not change active CFG setting!
3997         $current = $CFG->{$this->name};
3998         $result = parent::write_setting($data);
3999         $CFG->{$this->name} = $current;
4000         return $result;
4001     }
4005 /**
4006  * Special select for frontpage - stores data in course table
4007  *
4008  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4009  */
4010 class admin_setting_sitesetselect extends admin_setting_configselect {
4011     /**
4012      * Returns the site name for the selected site
4013      *
4014      * @see get_site()
4015      * @return string The site name of the selected site
4016      */
4017     public function get_setting() {
4018         $site = course_get_format(get_site())->get_course();
4019         return $site->{$this->name};
4020     }
4022     /**
4023      * Updates the database and save the setting
4024      *
4025      * @param string data
4026      * @return string empty or error message
4027      */
4028     public function write_setting($data) {
4029         global $DB, $SITE, $COURSE;
4030         if (!in_array($data, array_keys($this->choices))) {
4031             return get_string('errorsetting', 'admin');
4032         }
4033         $record = new stdClass();
4034         $record->id           = SITEID;
4035         $temp                 = $this->name;
4036         $record->$temp        = $data;
4037         $record->timemodified = time();
4039         course_get_format($SITE)->update_course_format_options($record);
4040         $DB->update_record('course', $record);
4042         // Reset caches.
4043         $SITE = $DB->get_record('course', array('id'=>$SITE->id), '*', MUST_EXIST);
4044         if ($SITE->id == $COURSE->id) {
4045             $COURSE = $SITE;
4046         }
4047         format_base::reset_course_cache($SITE->id);
4049         return '';
4051     }
4055 /**
4056  * Select for blog's bloglevel setting: if set to 0, will set blog_menu
4057  * block to hidden.
4058  *
4059  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4060  */
4061 class admin_setting_bloglevel extends admin_setting_configselect {
4062     /**
4063      * Updates the database and save the setting
4064      *
4065      * @param string data
4066      * @return string empty or error message
4067      */
4068     public function write_setting($data) {
4069         global $DB, $CFG;
4070         if ($data == 0) {
4071             $blogblocks = $DB->get_records_select('block', "name LIKE 'blog_%' AND visible = 1");
4072             foreach ($blogblocks as $block) {
4073                 $DB->set_field('block', 'visible', 0, array('id' => $block->id));
4074             }
4075         } else {
4076             // reenable all blocks only when switching from disabled blogs
4077             if (isset($CFG->bloglevel) and $CFG->bloglevel == 0) {
4078                 $blogblocks = $DB->get_records_select('block', "name LIKE 'blog_%' AND visible = 0");
4079                 foreach ($blogblocks as $block) {
4080                     $DB->set_field('block', 'visible', 1, array('id' => $block->id));
4081                 }
4082             }
4083         }
4084         return parent::write_setting($data);
4085     }
4089 /**
4090  * Special select - lists on the frontpage - hacky
4091  *
4092  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4093  */
4094 class admin_setting_courselist_frontpage extends admin_setting {
4095     /** @var array Array of choices value=>label */
4096     public $choices;
4098     /**
4099      * Construct override, requires one param
4100      *
4101      * @param bool $loggedin Is the user logged in
4102      */
4103     public function __construct($loggedin) {
4104         global $CFG;
4105         require_once($CFG->dirroot.'/course/lib.php');
4106         $name        = 'frontpage'.($loggedin ? 'loggedin' : '');
4107         $visiblename = get_string('frontpage'.($loggedin ? 'loggedin' : ''),'admin');
4108         $description = get_string('configfrontpage'.($loggedin ? 'loggedin' : ''),'admin');
4109         $defaults    = array(FRONTPAGEALLCOURSELIST);
4110         parent::__construct($name, $visiblename, $description, $defaults);
4111     }
4113     /**
4114      * Loads the choices available
4115      *
4116      * @return bool always returns true
4117      */
4118     public function load_choices() {
4119         if (is_array($this->choices)) {
4120             return true;
4121         }
4122         $this->choices = array(FRONTPAGENEWS          => get_string('frontpagenews'),
4123             FRONTPAGEALLCOURSELIST => get_string('frontpagecourselist'),
4124             FRONTPAGEENROLLEDCOURSELIST => get_string('frontpageenrolledcourselist'),
4125             FRONTPAGECATEGORYNAMES => get_string('frontpagecategorynames'),
4126             FRONTPAGECATEGORYCOMBO => get_string('frontpagecategorycombo'),
4127             FRONTPAGECOURSESEARCH  => get_string('frontpagecoursesearch'),
4128             'none'                 => get_string('none'));
4129         if ($this->name === 'frontpage') {
4130             unset($this->choices[FRONTPAGEENROLLEDCOURSELIST]);
4131         }
4132         return true;
4133     }
4135     /**
4136      * Returns the selected settings
4137      *
4138      * @param mixed array or setting or null
4139      */
4140     public function get_setting() {
4141         $result = $this->config_read($this->name);
4142         if (is_null($result)) {
4143             return NULL;
4144         }
4145         if ($result === '') {
4146             return array();
4147         }
4148         return explode(',', $result);
4149     }
4151     /**
4152      * Save the selected options
4153      *
4154      * @param array $data
4155      * @return mixed empty string (data is not an array) or bool true=success false=failure
4156      */
4157     public function write_setting($data) {
4158         if (!is_array($data)) {
4159             return '';
4160         }
4161         $this->load_choices();
4162         $save = array();
4163         foreach($data as $datum) {
4164             if ($datum == 'none' or !array_key_exists($datum, $this->choices)) {
4165                 continue;
4166             }
4167             $save[$datum] = $datum; // no duplicates
4168         }
4169         return ($this->config_write($this->name, implode(',', $save)) ? '' : get_string('errorsetting', 'admin'));
4170     }
4172     /**
4173      * Return XHTML select field and wrapping div
4174      *
4175      * @todo Add vartype handling to make sure $data is an array
4176      * @param array $data Array of elements to select by default
4177      * @return string XHTML select field and wrapping div
4178      */
4179     public function output_html($data, $query='') {
4180         global $OUTPUT;
4182         $this->load_choices();
4183         $currentsetting = array();
4184         foreach ($data as $key) {
4185             if ($key != 'none' and array_key_exists($key, $this->choices)) {
4186                 $currentsetting[] = $key; // already selected first
4187             }
4188         }
4190         $context = (object) [
4191             'id' => $this->get_id(),
4192             'name' => $this->get_full_name(),
4193         ];
4195         $options = $this->choices;
4196         $selects = [];
4197         for ($i = 0; $i < count($this->choices) - 1; $i++) {
4198             if (!array_key_exists($i, $currentsetting)) {
4199                 $currentsetting[$i] = 'none';
4200             }
4201             $selects[] = [
4202                 'key' => $i,
4203                 'options' => array_map(function($option) use ($options, $currentsetting, $i) {
4204                     return [
4205                         'name' => $options[$option],
4206                         'value' => $option,
4207                         'selected' => $currentsetting[$i] == $option
4208                     ];
4209                 }, array_keys($options))
4210             ];
4211         }
4212         $context->selects = $selects;
4214         $element = $OUTPUT->render_from_template('core_admin/setting_courselist_frontpage', $context);
4216         return format_admin_setting($this, $this->visiblename, $element, $this->description, false, '', null, $query);
4217     }
4221 /**
4222  * Special checkbox for frontpage - stores data in course table
4223  *
4224  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4225  */
4226 class admin_setting_sitesetcheckbox extends admin_setting_configcheckbox {
4227     /**
4228      * Returns the current sites name
4229      *
4230      * @return string
4231      */
4232     public function get_setting() {
4233         $site = course_get_format(get_site())->get_course();
4234         return $site->{$this->name};
4235     }
4237     /**
4238      * Save the selected setting
4239      *
4240      * @param string $data The selected site
4241      * @return string empty string or error message
4242      */
4243     public function write_setting($data) {
4244         global $DB, $SITE, $COURSE;
4245         $record = new stdClass();
4246         $record->id            = $SITE->id;
4247         $record->{$this->name} = ($data == '1' ? 1 : 0);
4248         $record->timemodified  = time();
4250         course_get_format($SITE)->update_course_format_options($record);
4251         $DB->update_record('course', $record);
4253         // Reset caches.
4254         $SITE = $DB->get_record('course', array('id'=>$SITE->id), '*', MUST_EXIST);
4255         if ($SITE->id == $COURSE->id) {
4256             $COURSE = $SITE;
4257         }
4258         format_base::reset_course_cache($SITE->id);
4260         return '';
4261     }
4264 /**
4265  * Special text for frontpage - stores data in course table.
4266  * Empty string means not set here. Manual setting is required.
4267  *
4268  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4269  */
4270 class admin_setting_sitesettext extends admin_setting_configtext {
4272     /**
4273      * Constructor.
4274      */
4275     public function __construct() {
4276         call_user_func_array(['parent', '__construct'], func_get_args());
4277         $this->set_force_ltr(false);
4278     }
4280     /**
4281      * Return the current setting
4282      *
4283      * @return mixed string or null
4284      */
4285     public function get_setting() {
4286         $site = course_get_format(get_site())->get_course();
4287         return $site->{$this->name} != '' ? $site->{$this->name} : NULL;
4288     }
4290     /**
4291      * Validate the selected data
4292      *
4293      * @param string $data The selected value to validate
4294      * @return mixed true or message string
4295      */
4296     public function validate($data) {
4297         global $DB, $SITE;
4298         $cleaned = clean_param($data, PARAM_TEXT);
4299         if ($cleaned === '') {
4300             return get_string('required');
4301         }
4302         if ($this->name ==='shortname' &&
4303                 $DB->record_exists_sql('SELECT id from {course} WHERE shortname = ? AND id <> ?', array($data, $SITE->id))) {
4304             return get_string('shortnametaken', 'error', $data);
4305         }
4306         if ("$data" == "$cleaned") { // implicit conversion to string is needed to do exact comparison
4307             return true;
4308         } else {
4309             return get_string('validateerror', 'admin');
4310         }
4311     }
4313     /**
4314      * Save the selected setting
4315      *
4316      * @param string $data The selected value
4317      * @return string empty or error message
4318      */
4319     public function write_setting($data) {
4320         global $DB, $SITE, $COURSE;
4321         $data = trim($data);
4322         $validated = $this->validate($data);
4323         if ($validated !== true) {
4324             return $validated;
4325         }
4327         $record = new stdClass();
4328         $record->id            = $SITE->id;
4329         $record->{$this->name} = $data;
4330         $record->timemodified  = time();
4332         course_get_format($SITE)->update_course_format_options($record);
4333         $DB->update_record('course', $record);
4335         // Reset caches.
4336         $SITE = $DB->get_record('course', array('id'=>$SITE->id), '*', MUST_EXIST);
4337         if ($SITE->id == $COURSE->id) {
4338             $COURSE = $SITE;
4339         }
4340         format_base::reset_course_cache($SITE->id);
4342         return '';
4343     }
4347 /**