68e1b4821e48d88dc15e64e2ee60134145a99a3b
[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 XHTML for the field
2410      * Writes Javascript into the HTML below right before the last div
2411      *
2412      * @todo Make javascript available through newer methods if possible
2413      * @param string $data Value for the field
2414      * @param string $query Passed as final argument for format_admin_setting
2415      * @return string XHTML field
2416      */
2417     public function output_html($data, $query='') {
2418         global $OUTPUT;
2419         $context = (object) [
2420             'id' => $this->get_id(),
2421             'name' => $this->get_full_name(),
2422             'size' => $this->size,
2423             'value' => $data,
2424             'forceltr' => $this->get_force_ltr(),
2425         ];
2426         $element = $OUTPUT->render_from_template('core_admin/setting_configpasswordunmask', $context);
2427         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', null, $query);
2428     }
2432 /**
2433  * Empty setting used to allow flags (advanced) on settings that can have no sensible default.
2434  * Note: Only advanced makes sense right now - locked does not.
2435  *
2436  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2437  */
2438 class admin_setting_configempty extends admin_setting_configtext {
2440     /**
2441      * @param string $name
2442      * @param string $visiblename
2443      * @param string $description
2444      */
2445     public function __construct($name, $visiblename, $description) {
2446         parent::__construct($name, $visiblename, $description, '', PARAM_RAW);
2447     }
2449     /**
2450      * Returns an XHTML string for the hidden field
2451      *
2452      * @param string $data
2453      * @param string $query
2454      * @return string XHTML string for the editor
2455      */
2456     public function output_html($data, $query='') {
2457         global $OUTPUT;
2459         $context = (object) [
2460             'id' => $this->get_id(),
2461             'name' => $this->get_full_name()
2462         ];
2463         $element = $OUTPUT->render_from_template('core_admin/setting_configempty', $context);
2465         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', get_string('none'), $query);
2466     }
2470 /**
2471  * Path to directory
2472  *
2473  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2474  */
2475 class admin_setting_configfile extends admin_setting_configtext {
2476     /**
2477      * Constructor
2478      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2479      * @param string $visiblename localised
2480      * @param string $description long localised info
2481      * @param string $defaultdirectory default directory location
2482      */
2483     public function __construct($name, $visiblename, $description, $defaultdirectory) {
2484         parent::__construct($name, $visiblename, $description, $defaultdirectory, PARAM_RAW, 50);
2485     }
2487     /**
2488      * Returns XHTML for the field
2489      *
2490      * Returns XHTML for the field and also checks whether the file
2491      * specified in $data exists using file_exists()
2492      *
2493      * @param string $data File name and path to use in value attr
2494      * @param string $query
2495      * @return string XHTML field
2496      */
2497     public function output_html($data, $query='') {
2498         global $CFG, $OUTPUT;
2500         $default = $this->get_defaultsetting();
2501         $context = (object) [
2502             'id' => $this->get_id(),
2503             'name' => $this->get_full_name(),
2504             'size' => $this->size,
2505             'value' => $data,
2506             'showvalidity' => !empty($data),
2507             'valid' => $data && file_exists($data),
2508             'readonly' => !empty($CFG->preventexecpath),
2509             'forceltr' => $this->get_force_ltr(),
2510         ];
2512         if ($context->readonly) {
2513             $this->visiblename .= '<div class="form-overridden">'.get_string('execpathnotallowed', 'admin').'</div>';
2514         }
2516         $element = $OUTPUT->render_from_template('core_admin/setting_configfile', $context);
2518         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2519     }
2521     /**
2522      * Checks if execpatch has been disabled in config.php
2523      */
2524     public function write_setting($data) {
2525         global $CFG;
2526         if (!empty($CFG->preventexecpath)) {
2527             if ($this->get_setting() === null) {
2528                 // Use default during installation.
2529                 $data = $this->get_defaultsetting();
2530                 if ($data === null) {
2531                     $data = '';
2532                 }
2533             } else {
2534                 return '';
2535             }
2536         }
2537         return parent::write_setting($data);
2538     }
2543 /**
2544  * Path to executable file
2545  *
2546  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2547  */
2548 class admin_setting_configexecutable extends admin_setting_configfile {
2550     /**
2551      * Returns an XHTML field
2552      *
2553      * @param string $data This is the value for the field
2554      * @param string $query
2555      * @return string XHTML field
2556      */
2557     public function output_html($data, $query='') {
2558         global $CFG, $OUTPUT;
2559         $default = $this->get_defaultsetting();
2560         require_once("$CFG->libdir/filelib.php");
2562         $context = (object) [
2563             'id' => $this->get_id(),
2564             'name' => $this->get_full_name(),
2565             'size' => $this->size,
2566             'value' => $data,
2567             'showvalidity' => !empty($data),
2568             'valid' => $data && file_exists($data) && !is_dir($data) && file_is_executable($data),
2569             'readonly' => !empty($CFG->preventexecpath),
2570             'forceltr' => $this->get_force_ltr()
2571         ];
2573         if (!empty($CFG->preventexecpath)) {
2574             $this->visiblename .= '<div class="form-overridden">'.get_string('execpathnotallowed', 'admin').'</div>';
2575         }
2577         $element = $OUTPUT->render_from_template('core_admin/setting_configexecutable', $context);
2579         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2580     }
2584 /**
2585  * Path to directory
2586  *
2587  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2588  */
2589 class admin_setting_configdirectory extends admin_setting_configfile {
2591     /**
2592      * Returns an XHTML field
2593      *
2594      * @param string $data This is the value for the field
2595      * @param string $query
2596      * @return string XHTML
2597      */
2598     public function output_html($data, $query='') {
2599         global $CFG, $OUTPUT;
2600         $default = $this->get_defaultsetting();
2602         $context = (object) [
2603             'id' => $this->get_id(),
2604             'name' => $this->get_full_name(),
2605             'size' => $this->size,
2606             'value' => $data,
2607             'showvalidity' => !empty($data),
2608             'valid' => $data && file_exists($data) && is_dir($data),
2609             'readonly' => !empty($CFG->preventexecpath),
2610             'forceltr' => $this->get_force_ltr()
2611         ];
2613         if (!empty($CFG->preventexecpath)) {
2614             $this->visiblename .= '<div class="form-overridden">'.get_string('execpathnotallowed', 'admin').'</div>';
2615         }
2617         $element = $OUTPUT->render_from_template('core_admin/setting_configdirectory', $context);
2619         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2620     }
2624 /**
2625  * Checkbox
2626  *
2627  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2628  */
2629 class admin_setting_configcheckbox extends admin_setting {
2630     /** @var string Value used when checked */
2631     public $yes;
2632     /** @var string Value used when not checked */
2633     public $no;
2635     /**
2636      * Constructor
2637      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2638      * @param string $visiblename localised
2639      * @param string $description long localised info
2640      * @param string $defaultsetting
2641      * @param string $yes value used when checked
2642      * @param string $no value used when not checked
2643      */
2644     public function __construct($name, $visiblename, $description, $defaultsetting, $yes='1', $no='0') {
2645         parent::__construct($name, $visiblename, $description, $defaultsetting);
2646         $this->yes = (string)$yes;
2647         $this->no  = (string)$no;
2648     }
2650     /**
2651      * Retrieves the current setting using the objects name
2652      *
2653      * @return string
2654      */
2655     public function get_setting() {
2656         return $this->config_read($this->name);
2657     }
2659     /**
2660      * Sets the value for the setting
2661      *
2662      * Sets the value for the setting to either the yes or no values
2663      * of the object by comparing $data to yes
2664      *
2665      * @param mixed $data Gets converted to str for comparison against yes value
2666      * @return string empty string or error
2667      */
2668     public function write_setting($data) {
2669         if ((string)$data === $this->yes) { // convert to strings before comparison
2670             $data = $this->yes;
2671         } else {
2672             $data = $this->no;
2673         }
2674         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
2675     }
2677     /**
2678      * Returns an XHTML checkbox field
2679      *
2680      * @param string $data If $data matches yes then checkbox is checked
2681      * @param string $query
2682      * @return string XHTML field
2683      */
2684     public function output_html($data, $query='') {
2685         global $OUTPUT;
2687         $context = (object) [
2688             'id' => $this->get_id(),
2689             'name' => $this->get_full_name(),
2690             'no' => $this->no,
2691             'value' => $this->yes,
2692             'checked' => (string) $data === $this->yes,
2693         ];
2695         $default = $this->get_defaultsetting();
2696         if (!is_null($default)) {
2697             if ((string)$default === $this->yes) {
2698                 $defaultinfo = get_string('checkboxyes', 'admin');
2699             } else {
2700                 $defaultinfo = get_string('checkboxno', 'admin');
2701             }
2702         } else {
2703             $defaultinfo = NULL;
2704         }
2706         $element = $OUTPUT->render_from_template('core_admin/setting_configcheckbox', $context);
2708         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
2709     }
2713 /**
2714  * Multiple checkboxes, each represents different value, stored in csv format
2715  *
2716  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2717  */
2718 class admin_setting_configmulticheckbox extends admin_setting {
2719     /** @var array Array of choices value=>label */
2720     public $choices;
2722     /**
2723      * Constructor: uses parent::__construct
2724      *
2725      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2726      * @param string $visiblename localised
2727      * @param string $description long localised info
2728      * @param array $defaultsetting array of selected
2729      * @param array $choices array of $value=>$label for each checkbox
2730      */
2731     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
2732         $this->choices = $choices;
2733         parent::__construct($name, $visiblename, $description, $defaultsetting);
2734     }
2736     /**
2737      * This public function may be used in ancestors for lazy loading of choices
2738      *
2739      * @todo Check if this function is still required content commented out only returns true
2740      * @return bool true if loaded, false if error
2741      */
2742     public function load_choices() {
2743         /*
2744         if (is_array($this->choices)) {
2745             return true;
2746         }
2747         .... load choices here
2748         */
2749         return true;
2750     }
2752     /**
2753      * Is setting related to query text - used when searching
2754      *
2755      * @param string $query
2756      * @return bool true on related, false on not or failure
2757      */
2758     public function is_related($query) {
2759         if (!$this->load_choices() or empty($this->choices)) {
2760             return false;
2761         }
2762         if (parent::is_related($query)) {
2763             return true;
2764         }
2766         foreach ($this->choices as $desc) {
2767             if (strpos(core_text::strtolower($desc), $query) !== false) {
2768                 return true;
2769             }
2770         }
2771         return false;
2772     }
2774     /**
2775      * Returns the current setting if it is set
2776      *
2777      * @return mixed null if null, else an array
2778      */
2779     public function get_setting() {
2780         $result = $this->config_read($this->name);
2782         if (is_null($result)) {
2783             return NULL;
2784         }
2785         if ($result === '') {
2786             return array();
2787         }
2788         $enabled = explode(',', $result);
2789         $setting = array();
2790         foreach ($enabled as $option) {
2791             $setting[$option] = 1;
2792         }
2793         return $setting;
2794     }
2796     /**
2797      * Saves the setting(s) provided in $data
2798      *
2799      * @param array $data An array of data, if not array returns empty str
2800      * @return mixed empty string on useless data or bool true=success, false=failed
2801      */
2802     public function write_setting($data) {
2803         if (!is_array($data)) {
2804             return ''; // ignore it
2805         }
2806         if (!$this->load_choices() or empty($this->choices)) {
2807             return '';
2808         }
2809         unset($data['xxxxx']);
2810         $result = array();
2811         foreach ($data as $key => $value) {
2812             if ($value and array_key_exists($key, $this->choices)) {
2813                 $result[] = $key;
2814             }
2815         }
2816         return $this->config_write($this->name, implode(',', $result)) ? '' : get_string('errorsetting', 'admin');
2817     }
2819     /**
2820      * Returns XHTML field(s) as required by choices
2821      *
2822      * Relies on data being an array should data ever be another valid vartype with
2823      * acceptable value this may cause a warning/error
2824      * if (!is_array($data)) would fix the problem
2825      *
2826      * @todo Add vartype handling to ensure $data is an array
2827      *
2828      * @param array $data An array of checked values
2829      * @param string $query
2830      * @return string XHTML field
2831      */
2832     public function output_html($data, $query='') {
2833         global $OUTPUT;
2835         if (!$this->load_choices() or empty($this->choices)) {
2836             return '';
2837         }
2839         $default = $this->get_defaultsetting();
2840         if (is_null($default)) {
2841             $default = array();
2842         }
2843         if (is_null($data)) {
2844             $data = array();
2845         }
2847         $context = (object) [
2848             'id' => $this->get_id(),
2849             'name' => $this->get_full_name(),
2850         ];
2852         $options = array();
2853         $defaults = array();
2854         foreach ($this->choices as $key => $description) {
2855             if (!empty($default[$key])) {
2856                 $defaults[] = $description;
2857             }
2859             $options[] = [
2860                 'key' => $key,
2861                 'checked' => !empty($data[$key]),
2862                 'label' => highlightfast($query, $description)
2863             ];
2864         }
2866         if (is_null($default)) {
2867             $defaultinfo = null;
2868         } else if (!empty($defaults)) {
2869             $defaultinfo = implode(', ', $defaults);
2870         } else {
2871             $defaultinfo = get_string('none');
2872         }
2874         $context->options = $options;
2875         $context->hasoptions = !empty($options);
2877         $element = $OUTPUT->render_from_template('core_admin/setting_configmulticheckbox', $context);
2879         return format_admin_setting($this, $this->visiblename, $element, $this->description, false, '', $defaultinfo, $query);
2881     }
2885 /**
2886  * Multiple checkboxes 2, value stored as string 00101011
2887  *
2888  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2889  */
2890 class admin_setting_configmulticheckbox2 extends admin_setting_configmulticheckbox {
2892     /**
2893      * Returns the setting if set
2894      *
2895      * @return mixed null if not set, else an array of set settings
2896      */
2897     public function get_setting() {
2898         $result = $this->config_read($this->name);
2899         if (is_null($result)) {
2900             return NULL;
2901         }
2902         if (!$this->load_choices()) {
2903             return NULL;
2904         }
2905         $result = str_pad($result, count($this->choices), '0');
2906         $result = preg_split('//', $result, -1, PREG_SPLIT_NO_EMPTY);
2907         $setting = array();
2908         foreach ($this->choices as $key=>$unused) {
2909             $value = array_shift($result);
2910             if ($value) {
2911                 $setting[$key] = 1;
2912             }
2913         }
2914         return $setting;
2915     }
2917     /**
2918      * Save setting(s) provided in $data param
2919      *
2920      * @param array $data An array of settings to save
2921      * @return mixed empty string for bad data or bool true=>success, false=>error
2922      */
2923     public function write_setting($data) {
2924         if (!is_array($data)) {
2925             return ''; // ignore it
2926         }
2927         if (!$this->load_choices() or empty($this->choices)) {
2928             return '';
2929         }
2930         $result = '';
2931         foreach ($this->choices as $key=>$unused) {
2932             if (!empty($data[$key])) {
2933                 $result .= '1';
2934             } else {
2935                 $result .= '0';
2936             }
2937         }
2938         return $this->config_write($this->name, $result) ? '' : get_string('errorsetting', 'admin');
2939     }
2943 /**
2944  * Select one value from list
2945  *
2946  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2947  */
2948 class admin_setting_configselect extends admin_setting {
2949     /** @var array Array of choices value=>label */
2950     public $choices;
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         $this->choices = $choices;
2962         parent::__construct($name, $visiblename, $description, $defaultsetting);
2963     }
2965     /**
2966      * This function may be used in ancestors for lazy loading of choices
2967      *
2968      * Override this method if loading of choices is expensive, such
2969      * as when it requires multiple db requests.
2970      *
2971      * @return bool true if loaded, false if error
2972      */
2973     public function load_choices() {
2974         /*
2975         if (is_array($this->choices)) {
2976             return true;
2977         }
2978         .... load choices here
2979         */
2980         return true;
2981     }
2983     /**
2984      * Check if this is $query is related to a choice
2985      *
2986      * @param string $query
2987      * @return bool true if related, false if not
2988      */
2989     public function is_related($query) {
2990         if (parent::is_related($query)) {
2991             return true;
2992         }
2993         if (!$this->load_choices()) {
2994             return false;
2995         }
2996         foreach ($this->choices as $key=>$value) {
2997             if (strpos(core_text::strtolower($key), $query) !== false) {
2998                 return true;
2999             }
3000             if (strpos(core_text::strtolower($value), $query) !== false) {
3001                 return true;
3002             }
3003         }
3004         return false;
3005     }
3007     /**
3008      * Return the setting
3009      *
3010      * @return mixed returns config if successful else null
3011      */
3012     public function get_setting() {
3013         return $this->config_read($this->name);
3014     }
3016     /**
3017      * Save a setting
3018      *
3019      * @param string $data
3020      * @return string empty of error string
3021      */
3022     public function write_setting($data) {
3023         if (!$this->load_choices() or empty($this->choices)) {
3024             return '';
3025         }
3026         if (!array_key_exists($data, $this->choices)) {
3027             return ''; // ignore it
3028         }
3030         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
3031     }
3033     /**
3034      * Returns XHTML select field
3035      *
3036      * Ensure the options are loaded, and generate the XHTML for the select
3037      * element and any warning message. Separating this out from output_html
3038      * makes it easier to subclass this class.
3039      *
3040      * @param string $data the option to show as selected.
3041      * @param string $current the currently selected option in the database, null if none.
3042      * @param string $default the default selected option.
3043      * @return array the HTML for the select element, and a warning message.
3044      * @deprecated since Moodle 3.2
3045      */
3046     public function output_select_html($data, $current, $default, $extraname = '') {
3047         debugging('The method admin_setting_configselect::output_select_html is depreacted, do not use any more.', DEBUG_DEVELOPER);
3048     }
3050     /**
3051      * Returns XHTML select field and wrapping div(s)
3052      *
3053      * @see output_select_html()
3054      *
3055      * @param string $data the option to show as selected
3056      * @param string $query
3057      * @return string XHTML field and wrapping div
3058      */
3059     public function output_html($data, $query='') {
3060         global $OUTPUT;
3062         $default = $this->get_defaultsetting();
3063         $current = $this->get_setting();
3065         if (!$this->load_choices() || empty($this->choices)) {
3066             return '';
3067         }
3069         $context = (object) [
3070             'id' => $this->get_id(),
3071             'name' => $this->get_full_name(),
3072         ];
3074         if (!is_null($default) && array_key_exists($default, $this->choices)) {
3075             $defaultinfo = $this->choices[$default];
3076         } else {
3077             $defaultinfo = NULL;
3078         }
3080         // Warnings.
3081         $warning = '';
3082         if ($current === null) {
3083             // First run.
3084         } else if (empty($current) && (array_key_exists('', $this->choices) || array_key_exists(0, $this->choices))) {
3085             // No warning.
3086         } else if (!array_key_exists($current, $this->choices)) {
3087             $warning = get_string('warningcurrentsetting', 'admin', $current);
3088             if (!is_null($default) && $data == $current) {
3089                 $data = $default; // Use default instead of first value when showing the form.
3090             }
3091         }
3093         $options = [];
3094         foreach ($this->choices as $value => $name) {
3095             $options[] = [
3096                 'value' => $value,
3097                 'name' => $name,
3098                 'selected' => (string) $value == $data
3099             ];
3100         }
3101         $context->options = $options;
3103         $element = $OUTPUT->render_from_template('core_admin/setting_configselect', $context);
3105         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, $warning, $defaultinfo, $query);
3106     }
3110 /**
3111  * Select multiple items from list
3112  *
3113  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3114  */
3115 class admin_setting_configmultiselect extends admin_setting_configselect {
3116     /**
3117      * Constructor
3118      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3119      * @param string $visiblename localised
3120      * @param string $description long localised info
3121      * @param array $defaultsetting array of selected items
3122      * @param array $choices array of $value=>$label for each list item
3123      */
3124     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
3125         parent::__construct($name, $visiblename, $description, $defaultsetting, $choices);
3126     }
3128     /**
3129      * Returns the select setting(s)
3130      *
3131      * @return mixed null or array. Null if no settings else array of setting(s)
3132      */
3133     public function get_setting() {
3134         $result = $this->config_read($this->name);
3135         if (is_null($result)) {
3136             return NULL;
3137         }
3138         if ($result === '') {
3139             return array();
3140         }
3141         return explode(',', $result);
3142     }
3144     /**
3145      * Saves setting(s) provided through $data
3146      *
3147      * Potential bug in the works should anyone call with this function
3148      * using a vartype that is not an array
3149      *
3150      * @param array $data
3151      */
3152     public function write_setting($data) {
3153         if (!is_array($data)) {
3154             return ''; //ignore it
3155         }
3156         if (!$this->load_choices() or empty($this->choices)) {
3157             return '';
3158         }
3160         unset($data['xxxxx']);
3162         $save = array();
3163         foreach ($data as $value) {
3164             if (!array_key_exists($value, $this->choices)) {
3165                 continue; // ignore it
3166             }
3167             $save[] = $value;
3168         }
3170         return ($this->config_write($this->name, implode(',', $save)) ? '' : get_string('errorsetting', 'admin'));
3171     }
3173     /**
3174      * Is setting related to query text - used when searching
3175      *
3176      * @param string $query
3177      * @return bool true if related, false if not
3178      */
3179     public function is_related($query) {
3180         if (!$this->load_choices() or empty($this->choices)) {
3181             return false;
3182         }
3183         if (parent::is_related($query)) {
3184             return true;
3185         }
3187         foreach ($this->choices as $desc) {
3188             if (strpos(core_text::strtolower($desc), $query) !== false) {
3189                 return true;
3190             }
3191         }
3192         return false;
3193     }
3195     /**
3196      * Returns XHTML multi-select field
3197      *
3198      * @todo Add vartype handling to ensure $data is an array
3199      * @param array $data Array of values to select by default
3200      * @param string $query
3201      * @return string XHTML multi-select field
3202      */
3203     public function output_html($data, $query='') {
3204         global $OUTPUT;
3206         if (!$this->load_choices() or empty($this->choices)) {
3207             return '';
3208         }
3210         $default = $this->get_defaultsetting();
3211         if (is_null($default)) {
3212             $default = array();
3213         }
3214         if (is_null($data)) {
3215             $data = array();
3216         }
3218         $context = (object) [
3219             'id' => $this->get_id(),
3220             'name' => $this->get_full_name(),
3221             'size' => min(10, count($this->choices))
3222         ];
3224         $defaults = [];
3225         $options = [];
3226         foreach ($this->choices as $value => $name) {
3227             if (in_array($value, $default)) {
3228                 $defaults[] = $name;
3229             }
3230             $options[] = [
3231                 'value' => $value,
3232                 'name' => $name,
3233                 'selected' => in_array($value, $data)
3234             ];
3235         }
3236         $context->options = $options;
3238         if (is_null($default)) {
3239             $defaultinfo = NULL;
3240         } if (!empty($defaults)) {
3241             $defaultinfo = implode(', ', $defaults);
3242         } else {
3243             $defaultinfo = get_string('none');
3244         }
3246         $element = $OUTPUT->render_from_template('core_admin/setting_configmultiselect', $context);
3248         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
3249     }
3252 /**
3253  * Time selector
3254  *
3255  * This is a liiitle bit messy. we're using two selects, but we're returning
3256  * them as an array named after $name (so we only use $name2 internally for the setting)
3257  *
3258  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3259  */
3260 class admin_setting_configtime extends admin_setting {
3261     /** @var string Used for setting second select (minutes) */
3262     public $name2;
3264     /**
3265      * Constructor
3266      * @param string $hoursname setting for hours
3267      * @param string $minutesname setting for hours
3268      * @param string $visiblename localised
3269      * @param string $description long localised info
3270      * @param array $defaultsetting array representing default time 'h'=>hours, 'm'=>minutes
3271      */
3272     public function __construct($hoursname, $minutesname, $visiblename, $description, $defaultsetting) {
3273         $this->name2 = $minutesname;
3274         parent::__construct($hoursname, $visiblename, $description, $defaultsetting);
3275     }
3277     /**
3278      * Get the selected time
3279      *
3280      * @return mixed An array containing 'h'=>xx, 'm'=>xx, or null if not set
3281      */
3282     public function get_setting() {
3283         $result1 = $this->config_read($this->name);
3284         $result2 = $this->config_read($this->name2);
3285         if (is_null($result1) or is_null($result2)) {
3286             return NULL;
3287         }
3289         return array('h' => $result1, 'm' => $result2);
3290     }
3292     /**
3293      * Store the time (hours and minutes)
3294      *
3295      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3296      * @return bool true if success, false if not
3297      */
3298     public function write_setting($data) {
3299         if (!is_array($data)) {
3300             return '';
3301         }
3303         $result = $this->config_write($this->name, (int)$data['h']) && $this->config_write($this->name2, (int)$data['m']);
3304         return ($result ? '' : get_string('errorsetting', 'admin'));
3305     }
3307     /**
3308      * Returns XHTML time select fields
3309      *
3310      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3311      * @param string $query
3312      * @return string XHTML time select fields and wrapping div(s)
3313      */
3314     public function output_html($data, $query='') {
3315         global $OUTPUT;
3317         $default = $this->get_defaultsetting();
3318         if (is_array($default)) {
3319             $defaultinfo = $default['h'].':'.$default['m'];
3320         } else {
3321             $defaultinfo = NULL;
3322         }
3324         $context = (object) [
3325             'id' => $this->get_id(),
3326             'name' => $this->get_full_name(),
3327             'hours' => array_map(function($i) use ($data) {
3328                 return [
3329                     'value' => $i,
3330                     'name' => $i,
3331                     'selected' => $i == $data['h']
3332                 ];
3333             }, range(0, 23)),
3334             'minutes' => array_map(function($i) use ($data) {
3335                 return [
3336                     'value' => $i,
3337                     'name' => $i,
3338                     'selected' => $i == $data['m']
3339                 ];
3340             }, range(0, 59, 5))
3341         ];
3343         $element = $OUTPUT->render_from_template('core_admin/setting_configtime', $context);
3345         return format_admin_setting($this, $this->visiblename, $element, $this->description,
3346             $this->get_id() . 'h', '', $defaultinfo, $query);
3347     }
3352 /**
3353  * Seconds duration setting.
3354  *
3355  * @copyright 2012 Petr Skoda (http://skodak.org)
3356  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3357  */
3358 class admin_setting_configduration extends admin_setting {
3360     /** @var int default duration unit */
3361     protected $defaultunit;
3363     /**
3364      * Constructor
3365      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
3366      *                     or 'myplugin/mysetting' for ones in config_plugins.
3367      * @param string $visiblename localised name
3368      * @param string $description localised long description
3369      * @param mixed $defaultsetting string or array depending on implementation
3370      * @param int $defaultunit - day, week, etc. (in seconds)
3371      */
3372     public function __construct($name, $visiblename, $description, $defaultsetting, $defaultunit = 86400) {
3373         if (is_number($defaultsetting)) {
3374             $defaultsetting = self::parse_seconds($defaultsetting);
3375         }
3376         $units = self::get_units();
3377         if (isset($units[$defaultunit])) {
3378             $this->defaultunit = $defaultunit;
3379         } else {
3380             $this->defaultunit = 86400;
3381         }
3382         parent::__construct($name, $visiblename, $description, $defaultsetting);
3383     }
3385     /**
3386      * Returns selectable units.
3387      * @static
3388      * @return array
3389      */
3390     protected static function get_units() {
3391         return array(
3392             604800 => get_string('weeks'),
3393             86400 => get_string('days'),
3394             3600 => get_string('hours'),
3395             60 => get_string('minutes'),
3396             1 => get_string('seconds'),
3397         );
3398     }
3400     /**
3401      * Converts seconds to some more user friendly string.
3402      * @static
3403      * @param int $seconds
3404      * @return string
3405      */
3406     protected static function get_duration_text($seconds) {
3407         if (empty($seconds)) {
3408             return get_string('none');
3409         }
3410         $data = self::parse_seconds($seconds);
3411         switch ($data['u']) {
3412             case (60*60*24*7):
3413                 return get_string('numweeks', '', $data['v']);
3414             case (60*60*24):
3415                 return get_string('numdays', '', $data['v']);
3416             case (60*60):
3417                 return get_string('numhours', '', $data['v']);
3418             case (60):
3419                 return get_string('numminutes', '', $data['v']);
3420             default:
3421                 return get_string('numseconds', '', $data['v']*$data['u']);
3422         }
3423     }
3425     /**
3426      * Finds suitable units for given duration.
3427      * @static
3428      * @param int $seconds
3429      * @return array
3430      */
3431     protected static function parse_seconds($seconds) {
3432         foreach (self::get_units() as $unit => $unused) {
3433             if ($seconds % $unit === 0) {
3434                 return array('v'=>(int)($seconds/$unit), 'u'=>$unit);
3435             }
3436         }
3437         return array('v'=>(int)$seconds, 'u'=>1);
3438     }
3440     /**
3441      * Get the selected duration as array.
3442      *
3443      * @return mixed An array containing 'v'=>xx, 'u'=>xx, or null if not set
3444      */
3445     public function get_setting() {
3446         $seconds = $this->config_read($this->name);
3447         if (is_null($seconds)) {
3448             return null;
3449         }
3451         return self::parse_seconds($seconds);
3452     }
3454     /**
3455      * Store the duration as seconds.
3456      *
3457      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3458      * @return bool true if success, false if not
3459      */
3460     public function write_setting($data) {
3461         if (!is_array($data)) {
3462             return '';
3463         }
3465         $seconds = (int)($data['v']*$data['u']);
3466         if ($seconds < 0) {
3467             return get_string('errorsetting', 'admin');
3468         }
3470         $result = $this->config_write($this->name, $seconds);
3471         return ($result ? '' : get_string('errorsetting', 'admin'));
3472     }
3474     /**
3475      * Returns duration text+select fields.
3476      *
3477      * @param array $data Must be form 'v'=>xx, 'u'=>xx
3478      * @param string $query
3479      * @return string duration text+select fields and wrapping div(s)
3480      */
3481     public function output_html($data, $query='') {
3482         global $OUTPUT;
3484         $default = $this->get_defaultsetting();
3485         if (is_number($default)) {
3486             $defaultinfo = self::get_duration_text($default);
3487         } else if (is_array($default)) {
3488             $defaultinfo = self::get_duration_text($default['v']*$default['u']);
3489         } else {
3490             $defaultinfo = null;
3491         }
3493         $inputid = $this->get_id() . 'v';
3494         $units = self::get_units();
3495         $defaultunit = $this->defaultunit;
3497         $context = (object) [
3498             'id' => $this->get_id(),
3499             'name' => $this->get_full_name(),
3500             'value' => $data['v'],
3501             'options' => array_map(function($unit) use ($units, $data, $defaultunit) {
3502                 return [
3503                     'value' => $unit,
3504                     'name' => $units[$unit],
3505                     'selected' => ($data['v'] == 0 && $unit == $defaultunit) || $unit == $data['u']
3506                 ];
3507             }, array_keys($units))
3508         ];
3510         $element = $OUTPUT->render_from_template('core_admin/setting_configduration', $context);
3512         return format_admin_setting($this, $this->visiblename, $element, $this->description, $inputid, '', $defaultinfo, $query);
3513     }
3517 /**
3518  * Seconds duration setting with an advanced checkbox, that controls a additional
3519  * $name.'_adv' setting.
3520  *
3521  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3522  * @copyright 2014 The Open University
3523  */
3524 class admin_setting_configduration_with_advanced extends admin_setting_configduration {
3525     /**
3526      * Constructor
3527      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
3528      *                     or 'myplugin/mysetting' for ones in config_plugins.
3529      * @param string $visiblename localised name
3530      * @param string $description localised long description
3531      * @param array  $defaultsetting array of int value, and bool whether it is
3532      *                     is advanced by default.
3533      * @param int $defaultunit - day, week, etc. (in seconds)
3534      */
3535     public function __construct($name, $visiblename, $description, $defaultsetting, $defaultunit = 86400) {
3536         parent::__construct($name, $visiblename, $description, $defaultsetting['value'], $defaultunit);
3537         $this->set_advanced_flag_options(admin_setting_flag::ENABLED, !empty($defaultsetting['adv']));
3538     }
3542 /**
3543  * Used to validate a textarea used for ip addresses
3544  *
3545  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3546  * @copyright 2011 Petr Skoda (http://skodak.org)
3547  */
3548 class admin_setting_configiplist extends admin_setting_configtextarea {
3550     /**
3551      * Validate the contents of the textarea as IP addresses
3552      *
3553      * Used to validate a new line separated list of IP addresses collected from
3554      * a textarea control
3555      *
3556      * @param string $data A list of IP Addresses separated by new lines
3557      * @return mixed bool true for success or string:error on failure
3558      */
3559     public function validate($data) {
3560         if(!empty($data)) {
3561             $ips = explode("\n", $data);
3562         } else {
3563             return true;
3564         }
3565         $result = true;
3566         $badips = array();
3567         foreach($ips as $ip) {
3568             $ip = trim($ip);
3569             if (empty($ip)) {
3570                 continue;
3571             }
3572             if (preg_match('#^(\d{1,3})(\.\d{1,3}){0,3}$#', $ip, $match) ||
3573                 preg_match('#^(\d{1,3})(\.\d{1,3}){0,3}(\/\d{1,2})$#', $ip, $match) ||
3574                 preg_match('#^(\d{1,3})(\.\d{1,3}){3}(-\d{1,3})$#', $ip, $match)) {
3575             } else {
3576                 $result = false;
3577                 $badips[] = $ip;
3578             }
3579         }
3580         if($result) {
3581             return true;
3582         } else {
3583             return get_string('validateiperror', 'admin', join(', ', $badips));
3584         }
3585     }
3589 /**
3590  * An admin setting for selecting one or more users who have a capability
3591  * in the system context
3592  *
3593  * An admin setting for selecting one or more users, who have a particular capability
3594  * in the system context. Warning, make sure the list will never be too long. There is
3595  * no paging or searching of this list.
3596  *
3597  * To correctly get a list of users from this config setting, you need to call the
3598  * get_users_from_config($CFG->mysetting, $capability); function in moodlelib.php.
3599  *
3600  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3601  */
3602 class admin_setting_users_with_capability extends admin_setting_configmultiselect {
3603     /** @var string The capabilities name */
3604     protected $capability;
3605     /** @var int include admin users too */
3606     protected $includeadmins;
3608     /**
3609      * Constructor.
3610      *
3611      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3612      * @param string $visiblename localised name
3613      * @param string $description localised long description
3614      * @param array $defaultsetting array of usernames
3615      * @param string $capability string capability name.
3616      * @param bool $includeadmins include administrators
3617      */
3618     function __construct($name, $visiblename, $description, $defaultsetting, $capability, $includeadmins = true) {
3619         $this->capability    = $capability;
3620         $this->includeadmins = $includeadmins;
3621         parent::__construct($name, $visiblename, $description, $defaultsetting, NULL);
3622     }
3624     /**
3625      * Load all of the uses who have the capability into choice array
3626      *
3627      * @return bool Always returns true
3628      */
3629     function load_choices() {
3630         if (is_array($this->choices)) {
3631             return true;
3632         }
3633         list($sort, $sortparams) = users_order_by_sql('u');
3634         if (!empty($sortparams)) {
3635             throw new coding_exception('users_order_by_sql returned some query parameters. ' .
3636                     'This is unexpected, and a problem because there is no way to pass these ' .
3637                     'parameters to get_users_by_capability. See MDL-34657.');
3638         }
3639         $userfields = 'u.id, u.username, ' . get_all_user_name_fields(true, 'u');
3640         $users = get_users_by_capability(context_system::instance(), $this->capability, $userfields, $sort);
3641         $this->choices = array(
3642             '$@NONE@$' => get_string('nobody'),
3643             '$@ALL@$' => get_string('everyonewhocan', 'admin', get_capability_string($this->capability)),
3644         );
3645         if ($this->includeadmins) {
3646             $admins = get_admins();
3647             foreach ($admins as $user) {
3648                 $this->choices[$user->id] = fullname($user);
3649             }
3650         }
3651         if (is_array($users)) {
3652             foreach ($users as $user) {
3653                 $this->choices[$user->id] = fullname($user);
3654             }
3655         }
3656         return true;
3657     }
3659     /**
3660      * Returns the default setting for class
3661      *
3662      * @return mixed Array, or string. Empty string if no default
3663      */
3664     public function get_defaultsetting() {
3665         $this->load_choices();
3666         $defaultsetting = parent::get_defaultsetting();
3667         if (empty($defaultsetting)) {
3668             return array('$@NONE@$');
3669         } else if (array_key_exists($defaultsetting, $this->choices)) {
3670                 return $defaultsetting;
3671             } else {
3672                 return '';
3673             }
3674     }
3676     /**
3677      * Returns the current setting
3678      *
3679      * @return mixed array or string
3680      */
3681     public function get_setting() {
3682         $result = parent::get_setting();
3683         if ($result === null) {