MDL-55188 events: First deprecation of eventslib.php
[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     // Delete all remaining files in the filepool owned by the component.
238     $fs = get_file_storage();
239     $fs->delete_component_files($component);
241     // Finally purge all caches.
242     purge_all_caches();
244     // Invalidate the hash used for upgrade detections.
245     set_config('allversionshash', '');
247     echo $OUTPUT->notification(get_string('success'), 'notifysuccess');
250 /**
251  * Returns the version of installed component
252  *
253  * @param string $component component name
254  * @param string $source either 'disk' or 'installed' - where to get the version information from
255  * @return string|bool version number or false if the component is not found
256  */
257 function get_component_version($component, $source='installed') {
258     global $CFG, $DB;
260     list($type, $name) = core_component::normalize_component($component);
262     // moodle core or a core subsystem
263     if ($type === 'core') {
264         if ($source === 'installed') {
265             if (empty($CFG->version)) {
266                 return false;
267             } else {
268                 return $CFG->version;
269             }
270         } else {
271             if (!is_readable($CFG->dirroot.'/version.php')) {
272                 return false;
273             } else {
274                 $version = null; //initialize variable for IDEs
275                 include($CFG->dirroot.'/version.php');
276                 return $version;
277             }
278         }
279     }
281     // activity module
282     if ($type === 'mod') {
283         if ($source === 'installed') {
284             if ($CFG->version < 2013092001.02) {
285                 return $DB->get_field('modules', 'version', array('name'=>$name));
286             } else {
287                 return get_config('mod_'.$name, 'version');
288             }
290         } else {
291             $mods = core_component::get_plugin_list('mod');
292             if (empty($mods[$name]) or !is_readable($mods[$name].'/version.php')) {
293                 return false;
294             } else {
295                 $plugin = new stdClass();
296                 $plugin->version = null;
297                 $module = $plugin;
298                 include($mods[$name].'/version.php');
299                 return $plugin->version;
300             }
301         }
302     }
304     // block
305     if ($type === 'block') {
306         if ($source === 'installed') {
307             if ($CFG->version < 2013092001.02) {
308                 return $DB->get_field('block', 'version', array('name'=>$name));
309             } else {
310                 return get_config('block_'.$name, 'version');
311             }
312         } else {
313             $blocks = core_component::get_plugin_list('block');
314             if (empty($blocks[$name]) or !is_readable($blocks[$name].'/version.php')) {
315                 return false;
316             } else {
317                 $plugin = new stdclass();
318                 include($blocks[$name].'/version.php');
319                 return $plugin->version;
320             }
321         }
322     }
324     // all other plugin types
325     if ($source === 'installed') {
326         return get_config($type.'_'.$name, 'version');
327     } else {
328         $plugins = core_component::get_plugin_list($type);
329         if (empty($plugins[$name])) {
330             return false;
331         } else {
332             $plugin = new stdclass();
333             include($plugins[$name].'/version.php');
334             return $plugin->version;
335         }
336     }
339 /**
340  * Delete all plugin tables
341  *
342  * @param string $name Name of plugin, used as table prefix
343  * @param string $file Path to install.xml file
344  * @param bool $feedback defaults to true
345  * @return bool Always returns true
346  */
347 function drop_plugin_tables($name, $file, $feedback=true) {
348     global $CFG, $DB;
350     // first try normal delete
351     if (file_exists($file) and $DB->get_manager()->delete_tables_from_xmldb_file($file)) {
352         return true;
353     }
355     // then try to find all tables that start with name and are not in any xml file
356     $used_tables = get_used_table_names();
358     $tables = $DB->get_tables();
360     /// Iterate over, fixing id fields as necessary
361     foreach ($tables as $table) {
362         if (in_array($table, $used_tables)) {
363             continue;
364         }
366         if (strpos($table, $name) !== 0) {
367             continue;
368         }
370         // found orphan table --> delete it
371         if ($DB->get_manager()->table_exists($table)) {
372             $xmldb_table = new xmldb_table($table);
373             $DB->get_manager()->drop_table($xmldb_table);
374         }
375     }
377     return true;
380 /**
381  * Returns names of all known tables == tables that moodle knows about.
382  *
383  * @return array Array of lowercase table names
384  */
385 function get_used_table_names() {
386     $table_names = array();
387     $dbdirs = get_db_directories();
389     foreach ($dbdirs as $dbdir) {
390         $file = $dbdir.'/install.xml';
392         $xmldb_file = new xmldb_file($file);
394         if (!$xmldb_file->fileExists()) {
395             continue;
396         }
398         $loaded    = $xmldb_file->loadXMLStructure();
399         $structure = $xmldb_file->getStructure();
401         if ($loaded and $tables = $structure->getTables()) {
402             foreach($tables as $table) {
403                 $table_names[] = strtolower($table->getName());
404             }
405         }
406     }
408     return $table_names;
411 /**
412  * Returns list of all directories where we expect install.xml files
413  * @return array Array of paths
414  */
415 function get_db_directories() {
416     global $CFG;
418     $dbdirs = array();
420     /// First, the main one (lib/db)
421     $dbdirs[] = $CFG->libdir.'/db';
423     /// Then, all the ones defined by core_component::get_plugin_types()
424     $plugintypes = core_component::get_plugin_types();
425     foreach ($plugintypes as $plugintype => $pluginbasedir) {
426         if ($plugins = core_component::get_plugin_list($plugintype)) {
427             foreach ($plugins as $plugin => $plugindir) {
428                 $dbdirs[] = $plugindir.'/db';
429             }
430         }
431     }
433     return $dbdirs;
436 /**
437  * Try to obtain or release the cron lock.
438  * @param string  $name  name of lock
439  * @param int  $until timestamp when this lock considered stale, null means remove lock unconditionally
440  * @param bool $ignorecurrent ignore current lock state, usually extend previous lock, defaults to false
441  * @return bool true if lock obtained
442  */
443 function set_cron_lock($name, $until, $ignorecurrent=false) {
444     global $DB;
445     if (empty($name)) {
446         debugging("Tried to get a cron lock for a null fieldname");
447         return false;
448     }
450     // remove lock by force == remove from config table
451     if (is_null($until)) {
452         set_config($name, null);
453         return true;
454     }
456     if (!$ignorecurrent) {
457         // read value from db - other processes might have changed it
458         $value = $DB->get_field('config', 'value', array('name'=>$name));
460         if ($value and $value > time()) {
461             //lock active
462             return false;
463         }
464     }
466     set_config($name, $until);
467     return true;
470 /**
471  * Test if and critical warnings are present
472  * @return bool
473  */
474 function admin_critical_warnings_present() {
475     global $SESSION;
477     if (!has_capability('moodle/site:config', context_system::instance())) {
478         return 0;
479     }
481     if (!isset($SESSION->admin_critical_warning)) {
482         $SESSION->admin_critical_warning = 0;
483         if (is_dataroot_insecure(true) === INSECURE_DATAROOT_ERROR) {
484             $SESSION->admin_critical_warning = 1;
485         }
486     }
488     return $SESSION->admin_critical_warning;
491 /**
492  * Detects if float supports at least 10 decimal digits
493  *
494  * Detects if float supports at least 10 decimal digits
495  * and also if float-->string conversion works as expected.
496  *
497  * @return bool true if problem found
498  */
499 function is_float_problem() {
500     $num1 = 2009010200.01;
501     $num2 = 2009010200.02;
503     return ((string)$num1 === (string)$num2 or $num1 === $num2 or $num2 <= (string)$num1);
506 /**
507  * Try to verify that dataroot is not accessible from web.
508  *
509  * Try to verify that dataroot is not accessible from web.
510  * It is not 100% correct but might help to reduce number of vulnerable sites.
511  * Protection from httpd.conf and .htaccess is not detected properly.
512  *
513  * @uses INSECURE_DATAROOT_WARNING
514  * @uses INSECURE_DATAROOT_ERROR
515  * @param bool $fetchtest try to test public access by fetching file, default false
516  * @return mixed empty means secure, INSECURE_DATAROOT_ERROR found a critical problem, INSECURE_DATAROOT_WARNING might be problematic
517  */
518 function is_dataroot_insecure($fetchtest=false) {
519     global $CFG;
521     $siteroot = str_replace('\\', '/', strrev($CFG->dirroot.'/')); // win32 backslash workaround
523     $rp = preg_replace('|https?://[^/]+|i', '', $CFG->wwwroot, 1);
524     $rp = strrev(trim($rp, '/'));
525     $rp = explode('/', $rp);
526     foreach($rp as $r) {
527         if (strpos($siteroot, '/'.$r.'/') === 0) {
528             $siteroot = substr($siteroot, strlen($r)+1); // moodle web in subdirectory
529         } else {
530             break; // probably alias root
531         }
532     }
534     $siteroot = strrev($siteroot);
535     $dataroot = str_replace('\\', '/', $CFG->dataroot.'/');
537     if (strpos($dataroot, $siteroot) !== 0) {
538         return false;
539     }
541     if (!$fetchtest) {
542         return INSECURE_DATAROOT_WARNING;
543     }
545     // now try all methods to fetch a test file using http protocol
547     $httpdocroot = str_replace('\\', '/', strrev($CFG->dirroot.'/'));
548     preg_match('|(https?://[^/]+)|i', $CFG->wwwroot, $matches);
549     $httpdocroot = $matches[1];
550     $datarooturl = $httpdocroot.'/'. substr($dataroot, strlen($siteroot));
551     make_upload_directory('diag');
552     $testfile = $CFG->dataroot.'/diag/public.txt';
553     if (!file_exists($testfile)) {
554         file_put_contents($testfile, 'test file, do not delete');
555         @chmod($testfile, $CFG->filepermissions);
556     }
557     $teststr = trim(file_get_contents($testfile));
558     if (empty($teststr)) {
559     // hmm, strange
560         return INSECURE_DATAROOT_WARNING;
561     }
563     $testurl = $datarooturl.'/diag/public.txt';
564     if (extension_loaded('curl') and
565         !(stripos(ini_get('disable_functions'), 'curl_init') !== FALSE) and
566         !(stripos(ini_get('disable_functions'), 'curl_setop') !== FALSE) and
567         ($ch = @curl_init($testurl)) !== false) {
568         curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
569         curl_setopt($ch, CURLOPT_HEADER, false);
570         $data = curl_exec($ch);
571         if (!curl_errno($ch)) {
572             $data = trim($data);
573             if ($data === $teststr) {
574                 curl_close($ch);
575                 return INSECURE_DATAROOT_ERROR;
576             }
577         }
578         curl_close($ch);
579     }
581     if ($data = @file_get_contents($testurl)) {
582         $data = trim($data);
583         if ($data === $teststr) {
584             return INSECURE_DATAROOT_ERROR;
585         }
586     }
588     preg_match('|https?://([^/]+)|i', $testurl, $matches);
589     $sitename = $matches[1];
590     $error = 0;
591     if ($fp = @fsockopen($sitename, 80, $error)) {
592         preg_match('|https?://[^/]+(.*)|i', $testurl, $matches);
593         $localurl = $matches[1];
594         $out = "GET $localurl HTTP/1.1\r\n";
595         $out .= "Host: $sitename\r\n";
596         $out .= "Connection: Close\r\n\r\n";
597         fwrite($fp, $out);
598         $data = '';
599         $incoming = false;
600         while (!feof($fp)) {
601             if ($incoming) {
602                 $data .= fgets($fp, 1024);
603             } else if (@fgets($fp, 1024) === "\r\n") {
604                     $incoming = true;
605                 }
606         }
607         fclose($fp);
608         $data = trim($data);
609         if ($data === $teststr) {
610             return INSECURE_DATAROOT_ERROR;
611         }
612     }
614     return INSECURE_DATAROOT_WARNING;
617 /**
618  * Enables CLI maintenance mode by creating new dataroot/climaintenance.html file.
619  */
620 function enable_cli_maintenance_mode() {
621     global $CFG;
623     if (file_exists("$CFG->dataroot/climaintenance.html")) {
624         unlink("$CFG->dataroot/climaintenance.html");
625     }
627     if (isset($CFG->maintenance_message) and !html_is_blank($CFG->maintenance_message)) {
628         $data = $CFG->maintenance_message;
629         $data = bootstrap_renderer::early_error_content($data, null, null, null);
630         $data = bootstrap_renderer::plain_page(get_string('sitemaintenance', 'admin'), $data);
632     } else if (file_exists("$CFG->dataroot/climaintenance.template.html")) {
633         $data = file_get_contents("$CFG->dataroot/climaintenance.template.html");
635     } else {
636         $data = get_string('sitemaintenance', 'admin');
637         $data = bootstrap_renderer::early_error_content($data, null, null, null);
638         $data = bootstrap_renderer::plain_page(get_string('sitemaintenance', 'admin'), $data);
639     }
641     file_put_contents("$CFG->dataroot/climaintenance.html", $data);
642     chmod("$CFG->dataroot/climaintenance.html", $CFG->filepermissions);
645 /// CLASS DEFINITIONS /////////////////////////////////////////////////////////
648 /**
649  * Interface for anything appearing in the admin tree
650  *
651  * The interface that is implemented by anything that appears in the admin tree
652  * block. It forces inheriting classes to define a method for checking user permissions
653  * and methods for finding something in the admin tree.
654  *
655  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
656  */
657 interface part_of_admin_tree {
659 /**
660  * Finds a named part_of_admin_tree.
661  *
662  * Used to find a part_of_admin_tree. If a class only inherits part_of_admin_tree
663  * and not parentable_part_of_admin_tree, then this function should only check if
664  * $this->name matches $name. If it does, it should return a reference to $this,
665  * otherwise, it should return a reference to NULL.
666  *
667  * If a class inherits parentable_part_of_admin_tree, this method should be called
668  * recursively on all child objects (assuming, of course, the parent object's name
669  * doesn't match the search criterion).
670  *
671  * @param string $name The internal name of the part_of_admin_tree we're searching for.
672  * @return mixed An object reference or a NULL reference.
673  */
674     public function locate($name);
676     /**
677      * Removes named part_of_admin_tree.
678      *
679      * @param string $name The internal name of the part_of_admin_tree we want to remove.
680      * @return bool success.
681      */
682     public function prune($name);
684     /**
685      * Search using query
686      * @param string $query
687      * @return mixed array-object structure of found settings and pages
688      */
689     public function search($query);
691     /**
692      * Verifies current user's access to this part_of_admin_tree.
693      *
694      * Used to check if the current user has access to this part of the admin tree or
695      * not. If a class only inherits part_of_admin_tree and not parentable_part_of_admin_tree,
696      * then this method is usually just a call to has_capability() in the site context.
697      *
698      * If a class inherits parentable_part_of_admin_tree, this method should return the
699      * logical OR of the return of check_access() on all child objects.
700      *
701      * @return bool True if the user has access, false if she doesn't.
702      */
703     public function check_access();
705     /**
706      * Mostly useful for removing of some parts of the tree in admin tree block.
707      *
708      * @return True is hidden from normal list view
709      */
710     public function is_hidden();
712     /**
713      * Show we display Save button at the page bottom?
714      * @return bool
715      */
716     public function show_save();
720 /**
721  * Interface implemented by any part_of_admin_tree that has children.
722  *
723  * The interface implemented by any part_of_admin_tree that can be a parent
724  * to other part_of_admin_tree's. (For now, this only includes admin_category.) Apart
725  * from ensuring part_of_admin_tree compliancy, it also ensures inheriting methods
726  * include an add method for adding other part_of_admin_tree objects as children.
727  *
728  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
729  */
730 interface parentable_part_of_admin_tree extends part_of_admin_tree {
732 /**
733  * Adds a part_of_admin_tree object to the admin tree.
734  *
735  * Used to add a part_of_admin_tree object to this object or a child of this
736  * object. $something should only be added if $destinationname matches
737  * $this->name. If it doesn't, add should be called on child objects that are
738  * also parentable_part_of_admin_tree's.
739  *
740  * $something should be appended as the last child in the $destinationname. If the
741  * $beforesibling is specified, $something should be prepended to it. If the given
742  * sibling is not found, $something should be appended to the end of $destinationname
743  * and a developer debugging message should be displayed.
744  *
745  * @param string $destinationname The internal name of the new parent for $something.
746  * @param part_of_admin_tree $something The object to be added.
747  * @return bool True on success, false on failure.
748  */
749     public function add($destinationname, $something, $beforesibling = null);
754 /**
755  * The object used to represent folders (a.k.a. categories) in the admin tree block.
756  *
757  * Each admin_category object contains a number of part_of_admin_tree objects.
758  *
759  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
760  */
761 class admin_category implements parentable_part_of_admin_tree {
763     /** @var part_of_admin_tree[] An array of part_of_admin_tree objects that are this object's children */
764     protected $children;
765     /** @var string An internal name for this category. Must be unique amongst ALL part_of_admin_tree objects */
766     public $name;
767     /** @var string The displayed name for this category. Usually obtained through get_string() */
768     public $visiblename;
769     /** @var bool Should this category be hidden in admin tree block? */
770     public $hidden;
771     /** @var mixed Either a string or an array or strings */
772     public $path;
773     /** @var mixed Either a string or an array or strings */
774     public $visiblepath;
776     /** @var array fast lookup category cache, all categories of one tree point to one cache */
777     protected $category_cache;
779     /** @var bool If set to true children will be sorted when calling {@link admin_category::get_children()} */
780     protected $sort = false;
781     /** @var bool If set to true children will be sorted in ascending order. */
782     protected $sortasc = true;
783     /** @var bool If set to true sub categories and pages will be split and then sorted.. */
784     protected $sortsplit = true;
785     /** @var bool $sorted True if the children have been sorted and don't need resorting */
786     protected $sorted = false;
788     /**
789      * Constructor for an empty admin category
790      *
791      * @param string $name The internal name for this category. Must be unique amongst ALL part_of_admin_tree objects
792      * @param string $visiblename The displayed named for this category. Usually obtained through get_string()
793      * @param bool $hidden hide category in admin tree block, defaults to false
794      */
795     public function __construct($name, $visiblename, $hidden=false) {
796         $this->children    = array();
797         $this->name        = $name;
798         $this->visiblename = $visiblename;
799         $this->hidden      = $hidden;
800     }
802     /**
803      * Returns a reference to the part_of_admin_tree object with internal name $name.
804      *
805      * @param string $name The internal name of the object we want.
806      * @param bool $findpath initialize path and visiblepath arrays
807      * @return mixed A reference to the object with internal name $name if found, otherwise a reference to NULL.
808      *                  defaults to false
809      */
810     public function locate($name, $findpath=false) {
811         if (!isset($this->category_cache[$this->name])) {
812             // somebody much have purged the cache
813             $this->category_cache[$this->name] = $this;
814         }
816         if ($this->name == $name) {
817             if ($findpath) {
818                 $this->visiblepath[] = $this->visiblename;
819                 $this->path[]        = $this->name;
820             }
821             return $this;
822         }
824         // quick category lookup
825         if (!$findpath and isset($this->category_cache[$name])) {
826             return $this->category_cache[$name];
827         }
829         $return = NULL;
830         foreach($this->children as $childid=>$unused) {
831             if ($return = $this->children[$childid]->locate($name, $findpath)) {
832                 break;
833             }
834         }
836         if (!is_null($return) and $findpath) {
837             $return->visiblepath[] = $this->visiblename;
838             $return->path[]        = $this->name;
839         }
841         return $return;
842     }
844     /**
845      * Search using query
846      *
847      * @param string query
848      * @return mixed array-object structure of found settings and pages
849      */
850     public function search($query) {
851         $result = array();
852         foreach ($this->get_children() as $child) {
853             $subsearch = $child->search($query);
854             if (!is_array($subsearch)) {
855                 debugging('Incorrect search result from '.$child->name);
856                 continue;
857             }
858             $result = array_merge($result, $subsearch);
859         }
860         return $result;
861     }
863     /**
864      * Removes part_of_admin_tree object with internal name $name.
865      *
866      * @param string $name The internal name of the object we want to remove.
867      * @return bool success
868      */
869     public function prune($name) {
871         if ($this->name == $name) {
872             return false;  //can not remove itself
873         }
875         foreach($this->children as $precedence => $child) {
876             if ($child->name == $name) {
877                 // clear cache and delete self
878                 while($this->category_cache) {
879                     // delete the cache, but keep the original array address
880                     array_pop($this->category_cache);
881                 }
882                 unset($this->children[$precedence]);
883                 return true;
884             } else if ($this->children[$precedence]->prune($name)) {
885                 return true;
886             }
887         }
888         return false;
889     }
891     /**
892      * Adds a part_of_admin_tree to a child or grandchild (or great-grandchild, and so forth) of this object.
893      *
894      * By default the new part of the tree is appended as the last child of the parent. You
895      * can specify a sibling node that the new part should be prepended to. If the given
896      * sibling is not found, the part is appended to the end (as it would be by default) and
897      * a developer debugging message is displayed.
898      *
899      * @throws coding_exception if the $beforesibling is empty string or is not string at all.
900      * @param string $destinationame The internal name of the immediate parent that we want for $something.
901      * @param mixed $something A part_of_admin_tree or setting instance to be added.
902      * @param string $beforesibling The name of the parent's child the $something should be prepended to.
903      * @return bool True if successfully added, false if $something can not be added.
904      */
905     public function add($parentname, $something, $beforesibling = null) {
906         global $CFG;
908         $parent = $this->locate($parentname);
909         if (is_null($parent)) {
910             debugging('parent does not exist!');
911             return false;
912         }
914         if ($something instanceof part_of_admin_tree) {
915             if (!($parent instanceof parentable_part_of_admin_tree)) {
916                 debugging('error - parts of tree can be inserted only into parentable parts');
917                 return false;
918             }
919             if ($CFG->debugdeveloper && !is_null($this->locate($something->name))) {
920                 // The name of the node is already used, simply warn the developer that this should not happen.
921                 // It is intentional to check for the debug level before performing the check.
922                 debugging('Duplicate admin page name: ' . $something->name, DEBUG_DEVELOPER);
923             }
924             if (is_null($beforesibling)) {
925                 // Append $something as the parent's last child.
926                 $parent->children[] = $something;
927             } else {
928                 if (!is_string($beforesibling) or trim($beforesibling) === '') {
929                     throw new coding_exception('Unexpected value of the beforesibling parameter');
930                 }
931                 // Try to find the position of the sibling.
932                 $siblingposition = null;
933                 foreach ($parent->children as $childposition => $child) {
934                     if ($child->name === $beforesibling) {
935                         $siblingposition = $childposition;
936                         break;
937                     }
938                 }
939                 if (is_null($siblingposition)) {
940                     debugging('Sibling '.$beforesibling.' not found', DEBUG_DEVELOPER);
941                     $parent->children[] = $something;
942                 } else {
943                     $parent->children = array_merge(
944                         array_slice($parent->children, 0, $siblingposition),
945                         array($something),
946                         array_slice($parent->children, $siblingposition)
947                     );
948                 }
949             }
950             if ($something instanceof admin_category) {
951                 if (isset($this->category_cache[$something->name])) {
952                     debugging('Duplicate admin category name: '.$something->name);
953                 } else {
954                     $this->category_cache[$something->name] = $something;
955                     $something->category_cache =& $this->category_cache;
956                     foreach ($something->children as $child) {
957                         // just in case somebody already added subcategories
958                         if ($child instanceof admin_category) {
959                             if (isset($this->category_cache[$child->name])) {
960                                 debugging('Duplicate admin category name: '.$child->name);
961                             } else {
962                                 $this->category_cache[$child->name] = $child;
963                                 $child->category_cache =& $this->category_cache;
964                             }
965                         }
966                     }
967                 }
968             }
969             return true;
971         } else {
972             debugging('error - can not add this element');
973             return false;
974         }
976     }
978     /**
979      * Checks if the user has access to anything in this category.
980      *
981      * @return bool True if the user has access to at least one child in this category, false otherwise.
982      */
983     public function check_access() {
984         foreach ($this->children as $child) {
985             if ($child->check_access()) {
986                 return true;
987             }
988         }
989         return false;
990     }
992     /**
993      * Is this category hidden in admin tree block?
994      *
995      * @return bool True if hidden
996      */
997     public function is_hidden() {
998         return $this->hidden;
999     }
1001     /**
1002      * Show we display Save button at the page bottom?
1003      * @return bool
1004      */
1005     public function show_save() {
1006         foreach ($this->children as $child) {
1007             if ($child->show_save()) {
1008                 return true;
1009             }
1010         }
1011         return false;
1012     }
1014     /**
1015      * Sets sorting on this category.
1016      *
1017      * Please note this function doesn't actually do the sorting.
1018      * It can be called anytime.
1019      * Sorting occurs when the user calls get_children.
1020      * Code using the children array directly won't see the sorted results.
1021      *
1022      * @param bool $sort If set to true children will be sorted, if false they won't be.
1023      * @param bool $asc If true sorting will be ascending, otherwise descending.
1024      * @param bool $split If true we sort pages and sub categories separately.
1025      */
1026     public function set_sorting($sort, $asc = true, $split = true) {
1027         $this->sort = (bool)$sort;
1028         $this->sortasc = (bool)$asc;
1029         $this->sortsplit = (bool)$split;
1030     }
1032     /**
1033      * Returns the children associated with this category.
1034      *
1035      * @return part_of_admin_tree[]
1036      */
1037     public function get_children() {
1038         // If we should sort and it hasn't already been sorted.
1039         if ($this->sort && !$this->sorted) {
1040             if ($this->sortsplit) {
1041                 $categories = array();
1042                 $pages = array();
1043                 foreach ($this->children as $child) {
1044                     if ($child instanceof admin_category) {
1045                         $categories[] = $child;
1046                     } else {
1047                         $pages[] = $child;
1048                     }
1049                 }
1050                 core_collator::asort_objects_by_property($categories, 'visiblename');
1051                 core_collator::asort_objects_by_property($pages, 'visiblename');
1052                 if (!$this->sortasc) {
1053                     $categories = array_reverse($categories);
1054                     $pages = array_reverse($pages);
1055                 }
1056                 $this->children = array_merge($pages, $categories);
1057             } else {
1058                 core_collator::asort_objects_by_property($this->children, 'visiblename');
1059                 if (!$this->sortasc) {
1060                     $this->children = array_reverse($this->children);
1061                 }
1062             }
1063             $this->sorted = true;
1064         }
1065         return $this->children;
1066     }
1068     /**
1069      * Magically gets a property from this object.
1070      *
1071      * @param $property
1072      * @return part_of_admin_tree[]
1073      * @throws coding_exception
1074      */
1075     public function __get($property) {
1076         if ($property === 'children') {
1077             return $this->get_children();
1078         }
1079         throw new coding_exception('Invalid property requested.');
1080     }
1082     /**
1083      * Magically sets a property against this object.
1084      *
1085      * @param string $property
1086      * @param mixed $value
1087      * @throws coding_exception
1088      */
1089     public function __set($property, $value) {
1090         if ($property === 'children') {
1091             $this->sorted = false;
1092             $this->children = $value;
1093         } else {
1094             throw new coding_exception('Invalid property requested.');
1095         }
1096     }
1098     /**
1099      * Checks if an inaccessible property is set.
1100      *
1101      * @param string $property
1102      * @return bool
1103      * @throws coding_exception
1104      */
1105     public function __isset($property) {
1106         if ($property === 'children') {
1107             return isset($this->children);
1108         }
1109         throw new coding_exception('Invalid property requested.');
1110     }
1114 /**
1115  * Root of admin settings tree, does not have any parent.
1116  *
1117  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1118  */
1119 class admin_root extends admin_category {
1120 /** @var array List of errors */
1121     public $errors;
1122     /** @var string search query */
1123     public $search;
1124     /** @var bool full tree flag - true means all settings required, false only pages required */
1125     public $fulltree;
1126     /** @var bool flag indicating loaded tree */
1127     public $loaded;
1128     /** @var mixed site custom defaults overriding defaults in settings files*/
1129     public $custom_defaults;
1131     /**
1132      * @param bool $fulltree true means all settings required,
1133      *                            false only pages required
1134      */
1135     public function __construct($fulltree) {
1136         global $CFG;
1138         parent::__construct('root', get_string('administration'), false);
1139         $this->errors   = array();
1140         $this->search   = '';
1141         $this->fulltree = $fulltree;
1142         $this->loaded   = false;
1144         $this->category_cache = array();
1146         // load custom defaults if found
1147         $this->custom_defaults = null;
1148         $defaultsfile = "$CFG->dirroot/local/defaults.php";
1149         if (is_readable($defaultsfile)) {
1150             $defaults = array();
1151             include($defaultsfile);
1152             if (is_array($defaults) and count($defaults)) {
1153                 $this->custom_defaults = $defaults;
1154             }
1155         }
1156     }
1158     /**
1159      * Empties children array, and sets loaded to false
1160      *
1161      * @param bool $requirefulltree
1162      */
1163     public function purge_children($requirefulltree) {
1164         $this->children = array();
1165         $this->fulltree = ($requirefulltree || $this->fulltree);
1166         $this->loaded   = false;
1167         //break circular dependencies - this helps PHP 5.2
1168         while($this->category_cache) {
1169             array_pop($this->category_cache);
1170         }
1171         $this->category_cache = array();
1172     }
1176 /**
1177  * Links external PHP pages into the admin tree.
1178  *
1179  * See detailed usage example at the top of this document (adminlib.php)
1180  *
1181  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1182  */
1183 class admin_externalpage implements part_of_admin_tree {
1185     /** @var string An internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects */
1186     public $name;
1188     /** @var string The displayed name for this external page. Usually obtained through get_string(). */
1189     public $visiblename;
1191     /** @var string The external URL that we should link to when someone requests this external page. */
1192     public $url;
1194     /** @var string The role capability/permission a user must have to access this external page. */
1195     public $req_capability;
1197     /** @var object The context in which capability/permission should be checked, default is site context. */
1198     public $context;
1200     /** @var bool hidden in admin tree block. */
1201     public $hidden;
1203     /** @var mixed either string or array of string */
1204     public $path;
1206     /** @var array list of visible names of page parents */
1207     public $visiblepath;
1209     /**
1210      * Constructor for adding an external page into the admin tree.
1211      *
1212      * @param string $name The internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects.
1213      * @param string $visiblename The displayed name for this external page. Usually obtained through get_string().
1214      * @param string $url The external URL that we should link to when someone requests this external page.
1215      * @param mixed $req_capability The role capability/permission a user must have to access this external page. Defaults to 'moodle/site:config'.
1216      * @param boolean $hidden Is this external page hidden in admin tree block? Default false.
1217      * @param stdClass $context The context the page relates to. Not sure what happens
1218      *      if you specify something other than system or front page. Defaults to system.
1219      */
1220     public function __construct($name, $visiblename, $url, $req_capability='moodle/site:config', $hidden=false, $context=NULL) {
1221         $this->name        = $name;
1222         $this->visiblename = $visiblename;
1223         $this->url         = $url;
1224         if (is_array($req_capability)) {
1225             $this->req_capability = $req_capability;
1226         } else {
1227             $this->req_capability = array($req_capability);
1228         }
1229         $this->hidden = $hidden;
1230         $this->context = $context;
1231     }
1233     /**
1234      * Returns a reference to the part_of_admin_tree object with internal name $name.
1235      *
1236      * @param string $name The internal name of the object we want.
1237      * @param bool $findpath defaults to false
1238      * @return mixed A reference to the object with internal name $name if found, otherwise a reference to NULL.
1239      */
1240     public function locate($name, $findpath=false) {
1241         if ($this->name == $name) {
1242             if ($findpath) {
1243                 $this->visiblepath = array($this->visiblename);
1244                 $this->path        = array($this->name);
1245             }
1246             return $this;
1247         } else {
1248             $return = NULL;
1249             return $return;
1250         }
1251     }
1253     /**
1254      * This function always returns false, required function by interface
1255      *
1256      * @param string $name
1257      * @return false
1258      */
1259     public function prune($name) {
1260         return false;
1261     }
1263     /**
1264      * Search using query
1265      *
1266      * @param string $query
1267      * @return mixed array-object structure of found settings and pages
1268      */
1269     public function search($query) {
1270         $found = false;
1271         if (strpos(strtolower($this->name), $query) !== false) {
1272             $found = true;
1273         } else if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
1274                 $found = true;
1275             }
1276         if ($found) {
1277             $result = new stdClass();
1278             $result->page     = $this;
1279             $result->settings = array();
1280             return array($this->name => $result);
1281         } else {
1282             return array();
1283         }
1284     }
1286     /**
1287      * Determines if the current user has access to this external page based on $this->req_capability.
1288      *
1289      * @return bool True if user has access, false otherwise.
1290      */
1291     public function check_access() {
1292         global $CFG;
1293         $context = empty($this->context) ? context_system::instance() : $this->context;
1294         foreach($this->req_capability as $cap) {
1295             if (has_capability($cap, $context)) {
1296                 return true;
1297             }
1298         }
1299         return false;
1300     }
1302     /**
1303      * Is this external page hidden in admin tree block?
1304      *
1305      * @return bool True if hidden
1306      */
1307     public function is_hidden() {
1308         return $this->hidden;
1309     }
1311     /**
1312      * Show we display Save button at the page bottom?
1313      * @return bool
1314      */
1315     public function show_save() {
1316         return false;
1317     }
1321 /**
1322  * Used to group a number of admin_setting objects into a page and add them to the admin tree.
1323  *
1324  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1325  */
1326 class admin_settingpage implements part_of_admin_tree {
1328     /** @var string An internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects */
1329     public $name;
1331     /** @var string The displayed name for this external page. Usually obtained through get_string(). */
1332     public $visiblename;
1334     /** @var mixed An array of admin_setting objects that are part of this setting page. */
1335     public $settings;
1337     /** @var string The role capability/permission a user must have to access this external page. */
1338     public $req_capability;
1340     /** @var object The context in which capability/permission should be checked, default is site context. */
1341     public $context;
1343     /** @var bool hidden in admin tree block. */
1344     public $hidden;
1346     /** @var mixed string of paths or array of strings of paths */
1347     public $path;
1349     /** @var array list of visible names of page parents */
1350     public $visiblepath;
1352     /**
1353      * see admin_settingpage for details of this function
1354      *
1355      * @param string $name The internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects.
1356      * @param string $visiblename The displayed name for this external page. Usually obtained through get_string().
1357      * @param mixed $req_capability The role capability/permission a user must have to access this external page. Defaults to 'moodle/site:config'.
1358      * @param boolean $hidden Is this external page hidden in admin tree block? Default false.
1359      * @param stdClass $context The context the page relates to. Not sure what happens
1360      *      if you specify something other than system or front page. Defaults to system.
1361      */
1362     public function __construct($name, $visiblename, $req_capability='moodle/site:config', $hidden=false, $context=NULL) {
1363         $this->settings    = new stdClass();
1364         $this->name        = $name;
1365         $this->visiblename = $visiblename;
1366         if (is_array($req_capability)) {
1367             $this->req_capability = $req_capability;
1368         } else {
1369             $this->req_capability = array($req_capability);
1370         }
1371         $this->hidden      = $hidden;
1372         $this->context     = $context;
1373     }
1375     /**
1376      * see admin_category
1377      *
1378      * @param string $name
1379      * @param bool $findpath
1380      * @return mixed Object (this) if name ==  this->name, else returns null
1381      */
1382     public function locate($name, $findpath=false) {
1383         if ($this->name == $name) {
1384             if ($findpath) {
1385                 $this->visiblepath = array($this->visiblename);
1386                 $this->path        = array($this->name);
1387             }
1388             return $this;
1389         } else {
1390             $return = NULL;
1391             return $return;
1392         }
1393     }
1395     /**
1396      * Search string in settings page.
1397      *
1398      * @param string $query
1399      * @return array
1400      */
1401     public function search($query) {
1402         $found = array();
1404         foreach ($this->settings as $setting) {
1405             if ($setting->is_related($query)) {
1406                 $found[] = $setting;
1407             }
1408         }
1410         if ($found) {
1411             $result = new stdClass();
1412             $result->page     = $this;
1413             $result->settings = $found;
1414             return array($this->name => $result);
1415         }
1417         $found = false;
1418         if (strpos(strtolower($this->name), $query) !== false) {
1419             $found = true;
1420         } else if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
1421                 $found = true;
1422             }
1423         if ($found) {
1424             $result = new stdClass();
1425             $result->page     = $this;
1426             $result->settings = array();
1427             return array($this->name => $result);
1428         } else {
1429             return array();
1430         }
1431     }
1433     /**
1434      * This function always returns false, required by interface
1435      *
1436      * @param string $name
1437      * @return bool Always false
1438      */
1439     public function prune($name) {
1440         return false;
1441     }
1443     /**
1444      * adds an admin_setting to this admin_settingpage
1445      *
1446      * 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
1447      * n.b. each admin_setting in an admin_settingpage must have a unique internal name
1448      *
1449      * @param object $setting is the admin_setting object you want to add
1450      * @return bool true if successful, false if not
1451      */
1452     public function add($setting) {
1453         if (!($setting instanceof admin_setting)) {
1454             debugging('error - not a setting instance');
1455             return false;
1456         }
1458         $name = $setting->name;
1459         if ($setting->plugin) {
1460             $name = $setting->plugin . $name;
1461         }
1462         $this->settings->{$name} = $setting;
1463         return true;
1464     }
1466     /**
1467      * see admin_externalpage
1468      *
1469      * @return bool Returns true for yes false for no
1470      */
1471     public function check_access() {
1472         global $CFG;
1473         $context = empty($this->context) ? context_system::instance() : $this->context;
1474         foreach($this->req_capability as $cap) {
1475             if (has_capability($cap, $context)) {
1476                 return true;
1477             }
1478         }
1479         return false;
1480     }
1482     /**
1483      * outputs this page as html in a table (suitable for inclusion in an admin pagetype)
1484      * @return string Returns an XHTML string
1485      */
1486     public function output_html() {
1487         $adminroot = admin_get_root();
1488         $return = '<fieldset>'."\n".'<div class="clearer"><!-- --></div>'."\n";
1489         foreach($this->settings as $setting) {
1490             $fullname = $setting->get_full_name();
1491             if (array_key_exists($fullname, $adminroot->errors)) {
1492                 $data = $adminroot->errors[$fullname]->data;
1493             } else {
1494                 $data = $setting->get_setting();
1495                 // do not use defaults if settings not available - upgrade settings handles the defaults!
1496             }
1497             $return .= $setting->output_html($data);
1498         }
1499         $return .= '</fieldset>';
1500         return $return;
1501     }
1503     /**
1504      * Is this settings page hidden in admin tree block?
1505      *
1506      * @return bool True if hidden
1507      */
1508     public function is_hidden() {
1509         return $this->hidden;
1510     }
1512     /**
1513      * Show we display Save button at the page bottom?
1514      * @return bool
1515      */
1516     public function show_save() {
1517         foreach($this->settings as $setting) {
1518             if (empty($setting->nosave)) {
1519                 return true;
1520             }
1521         }
1522         return false;
1523     }
1527 /**
1528  * Admin settings class. Only exists on setting pages.
1529  * Read & write happens at this level; no authentication.
1530  *
1531  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1532  */
1533 abstract class admin_setting {
1534     /** @var string unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins. */
1535     public $name;
1536     /** @var string localised name */
1537     public $visiblename;
1538     /** @var string localised long description in Markdown format */
1539     public $description;
1540     /** @var mixed Can be string or array of string */
1541     public $defaultsetting;
1542     /** @var string */
1543     public $updatedcallback;
1544     /** @var mixed can be String or Null.  Null means main config table */
1545     public $plugin; // null means main config table
1546     /** @var bool true indicates this setting does not actually save anything, just information */
1547     public $nosave = false;
1548     /** @var bool if set, indicates that a change to this setting requires rebuild course cache */
1549     public $affectsmodinfo = false;
1550     /** @var array of admin_setting_flag - These are extra checkboxes attached to a setting. */
1551     private $flags = array();
1552     /** @var bool Whether this field must be forced LTR. */
1553     private $forceltr = null;
1555     /**
1556      * Constructor
1557      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
1558      *                     or 'myplugin/mysetting' for ones in config_plugins.
1559      * @param string $visiblename localised name
1560      * @param string $description localised long description
1561      * @param mixed $defaultsetting string or array depending on implementation
1562      */
1563     public function __construct($name, $visiblename, $description, $defaultsetting) {
1564         $this->parse_setting_name($name);
1565         $this->visiblename    = $visiblename;
1566         $this->description    = $description;
1567         $this->defaultsetting = $defaultsetting;
1568     }
1570     /**
1571      * Generic function to add a flag to this admin setting.
1572      *
1573      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1574      * @param bool $default - The default for the flag
1575      * @param string $shortname - The shortname for this flag. Used as a suffix for the setting name.
1576      * @param string $displayname - The display name for this flag. Used as a label next to the checkbox.
1577      */
1578     protected function set_flag_options($enabled, $default, $shortname, $displayname) {
1579         if (empty($this->flags[$shortname])) {
1580             $this->flags[$shortname] = new admin_setting_flag($enabled, $default, $shortname, $displayname);
1581         } else {
1582             $this->flags[$shortname]->set_options($enabled, $default);
1583         }
1584     }
1586     /**
1587      * Set the enabled options flag on this admin setting.
1588      *
1589      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1590      * @param bool $default - The default for the flag
1591      */
1592     public function set_enabled_flag_options($enabled, $default) {
1593         $this->set_flag_options($enabled, $default, 'enabled', new lang_string('enabled', 'core_admin'));
1594     }
1596     /**
1597      * Set the advanced options flag on this admin setting.
1598      *
1599      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1600      * @param bool $default - The default for the flag
1601      */
1602     public function set_advanced_flag_options($enabled, $default) {
1603         $this->set_flag_options($enabled, $default, 'adv', new lang_string('advanced'));
1604     }
1607     /**
1608      * Set the locked options flag on this admin setting.
1609      *
1610      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1611      * @param bool $default - The default for the flag
1612      */
1613     public function set_locked_flag_options($enabled, $default) {
1614         $this->set_flag_options($enabled, $default, 'locked', new lang_string('locked', 'core_admin'));
1615     }
1617     /**
1618      * Get the currently saved value for a setting flag
1619      *
1620      * @param admin_setting_flag $flag - One of the admin_setting_flag for this admin_setting.
1621      * @return bool
1622      */
1623     public function get_setting_flag_value(admin_setting_flag $flag) {
1624         $value = $this->config_read($this->name . '_' . $flag->get_shortname());
1625         if (!isset($value)) {
1626             $value = $flag->get_default();
1627         }
1629         return !empty($value);
1630     }
1632     /**
1633      * Get the list of defaults for the flags on this setting.
1634      *
1635      * @param array of strings describing the defaults for this setting. This is appended to by this function.
1636      */
1637     public function get_setting_flag_defaults(& $defaults) {
1638         foreach ($this->flags as $flag) {
1639             if ($flag->is_enabled() && $flag->get_default()) {
1640                 $defaults[] = $flag->get_displayname();
1641             }
1642         }
1643     }
1645     /**
1646      * Output the input fields for the advanced and locked flags on this setting.
1647      *
1648      * @param bool $adv - The current value of the advanced flag.
1649      * @param bool $locked - The current value of the locked flag.
1650      * @return string $output - The html for the flags.
1651      */
1652     public function output_setting_flags() {
1653         $output = '';
1655         foreach ($this->flags as $flag) {
1656             if ($flag->is_enabled()) {
1657                 $output .= $flag->output_setting_flag($this);
1658             }
1659         }
1661         if (!empty($output)) {
1662             return html_writer::tag('span', $output, array('class' => 'adminsettingsflags'));
1663         }
1664         return $output;
1665     }
1667     /**
1668      * Write the values of the flags for this admin setting.
1669      *
1670      * @param array $data - The data submitted from the form or null to set the default value for new installs.
1671      * @return bool - true if successful.
1672      */
1673     public function write_setting_flags($data) {
1674         $result = true;
1675         foreach ($this->flags as $flag) {
1676             $result = $result && $flag->write_setting_flag($this, $data);
1677         }
1678         return $result;
1679     }
1681     /**
1682      * Set up $this->name and potentially $this->plugin
1683      *
1684      * Set up $this->name and possibly $this->plugin based on whether $name looks
1685      * like 'settingname' or 'plugin/settingname'. Also, do some sanity checking
1686      * on the names, that is, output a developer debug warning if the name
1687      * contains anything other than [a-zA-Z0-9_]+.
1688      *
1689      * @param string $name the setting name passed in to the constructor.
1690      */
1691     private function parse_setting_name($name) {
1692         $bits = explode('/', $name);
1693         if (count($bits) > 2) {
1694             throw new moodle_exception('invalidadminsettingname', '', '', $name);
1695         }
1696         $this->name = array_pop($bits);
1697         if (!preg_match('/^[a-zA-Z0-9_]+$/', $this->name)) {
1698             throw new moodle_exception('invalidadminsettingname', '', '', $name);
1699         }
1700         if (!empty($bits)) {
1701             $this->plugin = array_pop($bits);
1702             if ($this->plugin === 'moodle') {
1703                 $this->plugin = null;
1704             } else if (!preg_match('/^[a-zA-Z0-9_]+$/', $this->plugin)) {
1705                     throw new moodle_exception('invalidadminsettingname', '', '', $name);
1706                 }
1707         }
1708     }
1710     /**
1711      * Returns the fullname prefixed by the plugin
1712      * @return string
1713      */
1714     public function get_full_name() {
1715         return 's_'.$this->plugin.'_'.$this->name;
1716     }
1718     /**
1719      * Returns the ID string based on plugin and name
1720      * @return string
1721      */
1722     public function get_id() {
1723         return 'id_s_'.$this->plugin.'_'.$this->name;
1724     }
1726     /**
1727      * @param bool $affectsmodinfo If true, changes to this setting will
1728      *   cause the course cache to be rebuilt
1729      */
1730     public function set_affects_modinfo($affectsmodinfo) {
1731         $this->affectsmodinfo = $affectsmodinfo;
1732     }
1734     /**
1735      * Returns the config if possible
1736      *
1737      * @return mixed returns config if successful else null
1738      */
1739     public function config_read($name) {
1740         global $CFG;
1741         if (!empty($this->plugin)) {
1742             $value = get_config($this->plugin, $name);
1743             return $value === false ? NULL : $value;
1745         } else {
1746             if (isset($CFG->$name)) {
1747                 return $CFG->$name;
1748             } else {
1749                 return NULL;
1750             }
1751         }
1752     }
1754     /**
1755      * Used to set a config pair and log change
1756      *
1757      * @param string $name
1758      * @param mixed $value Gets converted to string if not null
1759      * @return bool Write setting to config table
1760      */
1761     public function config_write($name, $value) {
1762         global $DB, $USER, $CFG;
1764         if ($this->nosave) {
1765             return true;
1766         }
1768         // make sure it is a real change
1769         $oldvalue = get_config($this->plugin, $name);
1770         $oldvalue = ($oldvalue === false) ? null : $oldvalue; // normalise
1771         $value = is_null($value) ? null : (string)$value;
1773         if ($oldvalue === $value) {
1774             return true;
1775         }
1777         // store change
1778         set_config($name, $value, $this->plugin);
1780         // Some admin settings affect course modinfo
1781         if ($this->affectsmodinfo) {
1782             // Clear course cache for all courses
1783             rebuild_course_cache(0, true);
1784         }
1786         $this->add_to_config_log($name, $oldvalue, $value);
1788         return true; // BC only
1789     }
1791     /**
1792      * Log config changes if necessary.
1793      * @param string $name
1794      * @param string $oldvalue
1795      * @param string $value
1796      */
1797     protected function add_to_config_log($name, $oldvalue, $value) {
1798         add_to_config_log($name, $oldvalue, $value, $this->plugin);
1799     }
1801     /**
1802      * Returns current value of this setting
1803      * @return mixed array or string depending on instance, NULL means not set yet
1804      */
1805     public abstract function get_setting();
1807     /**
1808      * Returns default setting if exists
1809      * @return mixed array or string depending on instance; NULL means no default, user must supply
1810      */
1811     public function get_defaultsetting() {
1812         $adminroot =  admin_get_root(false, false);
1813         if (!empty($adminroot->custom_defaults)) {
1814             $plugin = is_null($this->plugin) ? 'moodle' : $this->plugin;
1815             if (isset($adminroot->custom_defaults[$plugin])) {
1816                 if (array_key_exists($this->name, $adminroot->custom_defaults[$plugin])) { // null is valid value here ;-)
1817                     return $adminroot->custom_defaults[$plugin][$this->name];
1818                 }
1819             }
1820         }
1821         return $this->defaultsetting;
1822     }
1824     /**
1825      * Store new setting
1826      *
1827      * @param mixed $data string or array, must not be NULL
1828      * @return string empty string if ok, string error message otherwise
1829      */
1830     public abstract function write_setting($data);
1832     /**
1833      * Return part of form with setting
1834      * This function should always be overwritten
1835      *
1836      * @param mixed $data array or string depending on setting
1837      * @param string $query
1838      * @return string
1839      */
1840     public function output_html($data, $query='') {
1841     // should be overridden
1842         return;
1843     }
1845     /**
1846      * Function called if setting updated - cleanup, cache reset, etc.
1847      * @param string $functionname Sets the function name
1848      * @return void
1849      */
1850     public function set_updatedcallback($functionname) {
1851         $this->updatedcallback = $functionname;
1852     }
1854     /**
1855      * Execute postupdatecallback if necessary.
1856      * @param mixed $original original value before write_setting()
1857      * @return bool true if changed, false if not.
1858      */
1859     public function post_write_settings($original) {
1860         // Comparison must work for arrays too.
1861         if (serialize($original) === serialize($this->get_setting())) {
1862             return false;
1863         }
1865         $callbackfunction = $this->updatedcallback;
1866         if (!empty($callbackfunction) and is_callable($callbackfunction)) {
1867             $callbackfunction($this->get_full_name());
1868         }
1869         return true;
1870     }
1872     /**
1873      * Is setting related to query text - used when searching
1874      * @param string $query
1875      * @return bool
1876      */
1877     public function is_related($query) {
1878         if (strpos(strtolower($this->name), $query) !== false) {
1879             return true;
1880         }
1881         if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
1882             return true;
1883         }
1884         if (strpos(core_text::strtolower($this->description), $query) !== false) {
1885             return true;
1886         }
1887         $current = $this->get_setting();
1888         if (!is_null($current)) {
1889             if (is_string($current)) {
1890                 if (strpos(core_text::strtolower($current), $query) !== false) {
1891                     return true;
1892                 }
1893             }
1894         }
1895         $default = $this->get_defaultsetting();
1896         if (!is_null($default)) {
1897             if (is_string($default)) {
1898                 if (strpos(core_text::strtolower($default), $query) !== false) {
1899                     return true;
1900                 }
1901             }
1902         }
1903         return false;
1904     }
1906     /**
1907      * Get whether this should be displayed in LTR mode.
1908      *
1909      * @return bool|null
1910      */
1911     public function get_force_ltr() {
1912         return $this->forceltr;
1913     }
1915     /**
1916      * Set whether to force LTR or not.
1917      *
1918      * @param bool $value True when forced, false when not force, null when unknown.
1919      */
1920     public function set_force_ltr($value) {
1921         $this->forceltr = $value;
1922     }
1925 /**
1926  * An additional option that can be applied to an admin setting.
1927  * The currently supported options are 'ADVANCED' and 'LOCKED'.
1928  *
1929  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1930  */
1931 class admin_setting_flag {
1932     /** @var bool Flag to indicate if this option can be toggled for this setting */
1933     private $enabled = false;
1934     /** @var bool Flag to indicate if this option defaults to true or false */
1935     private $default = false;
1936     /** @var string Short string used to create setting name - e.g. 'adv' */
1937     private $shortname = '';
1938     /** @var string String used as the label for this flag */
1939     private $displayname = '';
1940     /** @const Checkbox for this flag is displayed in admin page */
1941     const ENABLED = true;
1942     /** @const Checkbox for this flag is not displayed in admin page */
1943     const DISABLED = false;
1945     /**
1946      * Constructor
1947      *
1948      * @param bool $enabled Can this option can be toggled.
1949      *                      Should be one of admin_setting_flag::ENABLED or admin_setting_flag::DISABLED.
1950      * @param bool $default The default checked state for this setting option.
1951      * @param string $shortname The shortname of this flag. Currently supported flags are 'locked' and 'adv'
1952      * @param string $displayname The displayname of this flag. Used as a label for the flag.
1953      */
1954     public function __construct($enabled, $default, $shortname, $displayname) {
1955         $this->shortname = $shortname;
1956         $this->displayname = $displayname;
1957         $this->set_options($enabled, $default);
1958     }
1960     /**
1961      * Update the values of this setting options class
1962      *
1963      * @param bool $enabled Can this option can be toggled.
1964      *                      Should be one of admin_setting_flag::ENABLED or admin_setting_flag::DISABLED.
1965      * @param bool $default The default checked state for this setting option.
1966      */
1967     public function set_options($enabled, $default) {
1968         $this->enabled = $enabled;
1969         $this->default = $default;
1970     }
1972     /**
1973      * Should this option appear in the interface and be toggleable?
1974      *
1975      * @return bool Is it enabled?
1976      */
1977     public function is_enabled() {
1978         return $this->enabled;
1979     }
1981     /**
1982      * Should this option be checked by default?
1983      *
1984      * @return bool Is it on by default?
1985      */
1986     public function get_default() {
1987         return $this->default;
1988     }
1990     /**
1991      * Return the short name for this flag. e.g. 'adv' or 'locked'
1992      *
1993      * @return string
1994      */
1995     public function get_shortname() {
1996         return $this->shortname;
1997     }
1999     /**
2000      * Return the display name for this flag. e.g. 'Advanced' or 'Locked'
2001      *
2002      * @return string
2003      */
2004     public function get_displayname() {
2005         return $this->displayname;
2006     }
2008     /**
2009      * Save the submitted data for this flag - or set it to the default if $data is null.
2010      *
2011      * @param admin_setting $setting - The admin setting for this flag
2012      * @param array $data - The data submitted from the form or null to set the default value for new installs.
2013      * @return bool
2014      */
2015     public function write_setting_flag(admin_setting $setting, $data) {
2016         $result = true;
2017         if ($this->is_enabled()) {
2018             if (!isset($data)) {
2019                 $value = $this->get_default();
2020             } else {
2021                 $value = !empty($data[$setting->get_full_name() . '_' . $this->get_shortname()]);
2022             }
2023             $result = $setting->config_write($setting->name . '_' . $this->get_shortname(), $value);
2024         }
2026         return $result;
2028     }
2030     /**
2031      * Output the checkbox for this setting flag. Should only be called if the flag is enabled.
2032      *
2033      * @param admin_setting $setting - The admin setting for this flag
2034      * @return string - The html for the checkbox.
2035      */
2036     public function output_setting_flag(admin_setting $setting) {
2037         global $OUTPUT;
2039         $value = $setting->get_setting_flag_value($this);
2041         $context = new stdClass();
2042         $context->id = $setting->get_id() . '_' . $this->get_shortname();
2043         $context->name = $setting->get_full_name() .  '_' . $this->get_shortname();
2044         $context->value = 1;
2045         $context->checked = $value ? true : false;
2046         $context->label = $this->get_displayname();
2048         return $OUTPUT->render_from_template('core_admin/setting_flag', $context);
2049     }
2053 /**
2054  * No setting - just heading and text.
2055  *
2056  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2057  */
2058 class admin_setting_heading extends admin_setting {
2060     /**
2061      * not a setting, just text
2062      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2063      * @param string $heading heading
2064      * @param string $information text in box
2065      */
2066     public function __construct($name, $heading, $information) {
2067         $this->nosave = true;
2068         parent::__construct($name, $heading, $information, '');
2069     }
2071     /**
2072      * Always returns true
2073      * @return bool Always returns true
2074      */
2075     public function get_setting() {
2076         return true;
2077     }
2079     /**
2080      * Always returns true
2081      * @return bool Always returns true
2082      */
2083     public function get_defaultsetting() {
2084         return true;
2085     }
2087     /**
2088      * Never write settings
2089      * @return string Always returns an empty string
2090      */
2091     public function write_setting($data) {
2092     // do not write any setting
2093         return '';
2094     }
2096     /**
2097      * Returns an HTML string
2098      * @return string Returns an HTML string
2099      */
2100     public function output_html($data, $query='') {
2101         global $OUTPUT;
2102         $context = new stdClass();
2103         $context->title = $this->visiblename;
2104         $context->description = $this->description;
2105         $context->descriptionformatted = highlight($query, markdown_to_html($this->description));
2106         return $OUTPUT->render_from_template('core_admin/setting_heading', $context);
2107     }
2111 /**
2112  * The most flexible setting, the user enters text.
2113  *
2114  * This type of field should be used for config settings which are using
2115  * English words and are not localised (passwords, database name, list of values, ...).
2116  *
2117  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2118  */
2119 class admin_setting_configtext extends admin_setting {
2121     /** @var mixed int means PARAM_XXX type, string is a allowed format in regex */
2122     public $paramtype;
2123     /** @var int default field size */
2124     public $size;
2126     /**
2127      * Config text constructor
2128      *
2129      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2130      * @param string $visiblename localised
2131      * @param string $description long localised info
2132      * @param string $defaultsetting
2133      * @param mixed $paramtype int means PARAM_XXX type, string is a allowed format in regex
2134      * @param int $size default field size
2135      */
2136     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $size=null) {
2137         $this->paramtype = $paramtype;
2138         if (!is_null($size)) {
2139             $this->size  = $size;
2140         } else {
2141             $this->size  = ($paramtype === PARAM_INT) ? 5 : 30;
2142         }
2143         parent::__construct($name, $visiblename, $description, $defaultsetting);
2144     }
2146     /**
2147      * Get whether this should be displayed in LTR mode.
2148      *
2149      * Try to guess from the PARAM type unless specifically set.
2150      */
2151     public function get_force_ltr() {
2152         $forceltr = parent::get_force_ltr();
2153         if ($forceltr === null) {
2154             return !is_rtl_compatible($this->paramtype);
2155         }
2156         return $forceltr;
2157     }
2159     /**
2160      * Return the setting
2161      *
2162      * @return mixed returns config if successful else null
2163      */
2164     public function get_setting() {
2165         return $this->config_read($this->name);
2166     }
2168     public function write_setting($data) {
2169         if ($this->paramtype === PARAM_INT and $data === '') {
2170         // do not complain if '' used instead of 0
2171             $data = 0;
2172         }
2173         // $data is a string
2174         $validated = $this->validate($data);
2175         if ($validated !== true) {
2176             return $validated;
2177         }
2178         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
2179     }
2181     /**
2182      * Validate data before storage
2183      * @param string data
2184      * @return mixed true if ok string if error found
2185      */
2186     public function validate($data) {
2187         // allow paramtype to be a custom regex if it is the form of /pattern/
2188         if (preg_match('#^/.*/$#', $this->paramtype)) {
2189             if (preg_match($this->paramtype, $data)) {
2190                 return true;
2191             } else {
2192                 return get_string('validateerror', 'admin');
2193             }
2195         } else if ($this->paramtype === PARAM_RAW) {
2196             return true;
2198         } else {
2199             $cleaned = clean_param($data, $this->paramtype);
2200             if ("$data" === "$cleaned") { // implicit conversion to string is needed to do exact comparison
2201                 return true;
2202             } else {
2203                 return get_string('validateerror', 'admin');
2204             }
2205         }
2206     }
2208     /**
2209      * Return an XHTML string for the setting
2210      * @return string Returns an XHTML string
2211      */
2212     public function output_html($data, $query='') {
2213         global $OUTPUT;
2215         $default = $this->get_defaultsetting();
2216         $context = (object) [
2217             'size' => $this->size,
2218             'id' => $this->get_id(),
2219             'name' => $this->get_full_name(),
2220             'value' => $data,
2221             'forceltr' => $this->get_force_ltr(),
2222         ];
2223         $element = $OUTPUT->render_from_template('core_admin/setting_configtext', $context);
2225         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2226     }
2229 /**
2230  * Text input with a maximum length constraint.
2231  *
2232  * @copyright 2015 onwards Ankit Agarwal
2233  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2234  */
2235 class admin_setting_configtext_with_maxlength extends admin_setting_configtext {
2237     /** @var int maximum number of chars allowed. */
2238     protected $maxlength;
2240     /**
2241      * Config text constructor
2242      *
2243      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
2244      *                     or 'myplugin/mysetting' for ones in config_plugins.
2245      * @param string $visiblename localised
2246      * @param string $description long localised info
2247      * @param string $defaultsetting
2248      * @param mixed $paramtype int means PARAM_XXX type, string is a allowed format in regex
2249      * @param int $size default field size
2250      * @param mixed $maxlength int maxlength allowed, 0 for infinite.
2251      */
2252     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW,
2253                                 $size=null, $maxlength = 0) {
2254         $this->maxlength = $maxlength;
2255         parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype, $size);
2256     }
2258     /**
2259      * Validate data before storage
2260      *
2261      * @param string $data data
2262      * @return mixed true if ok string if error found
2263      */
2264     public function validate($data) {
2265         $parentvalidation = parent::validate($data);
2266         if ($parentvalidation === true) {
2267             if ($this->maxlength > 0) {
2268                 // Max length check.
2269                 $length = core_text::strlen($data);
2270                 if ($length > $this->maxlength) {
2271                     return get_string('maximumchars', 'moodle',  $this->maxlength);
2272                 }
2273                 return true;
2274             } else {
2275                 return true; // No max length check needed.
2276             }
2277         } else {
2278             return $parentvalidation;
2279         }
2280     }
2283 /**
2284  * General text area without html editor.
2285  *
2286  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2287  */
2288 class admin_setting_configtextarea extends admin_setting_configtext {
2289     private $rows;
2290     private $cols;
2292     /**
2293      * @param string $name
2294      * @param string $visiblename
2295      * @param string $description
2296      * @param mixed $defaultsetting string or array
2297      * @param mixed $paramtype
2298      * @param string $cols The number of columns to make the editor
2299      * @param string $rows The number of rows to make the editor
2300      */
2301     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $cols='60', $rows='8') {
2302         $this->rows = $rows;
2303         $this->cols = $cols;
2304         parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype);
2305     }
2307     /**
2308      * Returns an XHTML string for the editor
2309      *
2310      * @param string $data
2311      * @param string $query
2312      * @return string XHTML string for the editor
2313      */
2314     public function output_html($data, $query='') {
2315         global $OUTPUT;
2317         $default = $this->get_defaultsetting();
2318         $defaultinfo = $default;
2319         if (!is_null($default) and $default !== '') {
2320             $defaultinfo = "\n".$default;
2321         }
2323         $context = (object) [
2324             'cols' => $this->cols,
2325             'rows' => $this->rows,
2326             'id' => $this->get_id(),
2327             'name' => $this->get_full_name(),
2328             'value' => $data,
2329             'forceltr' => $this->get_force_ltr(),
2330         ];
2331         $element = $OUTPUT->render_from_template('core_admin/setting_configtextarea', $context);
2333         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
2334     }
2337 /**
2338  * General text area with html editor.
2339  */
2340 class admin_setting_confightmleditor extends admin_setting_configtextarea {
2342     /**
2343      * @param string $name
2344      * @param string $visiblename
2345      * @param string $description
2346      * @param mixed $defaultsetting string or array
2347      * @param mixed $paramtype
2348      */
2349     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $cols='60', $rows='8') {
2350         parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype, $cols, $rows);
2351         $this->set_force_ltr(false);
2352         editors_head_setup();
2353     }
2355     /**
2356      * Returns an XHTML string for the editor
2357      *
2358      * @param string $data
2359      * @param string $query
2360      * @return string XHTML string for the editor
2361      */
2362     public function output_html($data, $query='') {
2363         $editor = editors_get_preferred_editor(FORMAT_HTML);
2364         $editor->set_text($data);
2365         $editor->use_editor($this->get_id(), array('noclean'=>true));
2366         return parent::output_html($data, $query);
2367     }
2371 /**
2372  * Password field, allows unmasking of password
2373  *
2374  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2375  */
2376 class admin_setting_configpasswordunmask extends admin_setting_configtext {
2378     /**
2379      * Constructor
2380      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2381      * @param string $visiblename localised
2382      * @param string $description long localised info
2383      * @param string $defaultsetting default password
2384      */
2385     public function __construct($name, $visiblename, $description, $defaultsetting) {
2386         parent::__construct($name, $visiblename, $description, $defaultsetting, PARAM_RAW, 30);
2387     }
2389     /**
2390      * Log config changes if necessary.
2391      * @param string $name
2392      * @param string $oldvalue
2393      * @param string $value
2394      */
2395     protected function add_to_config_log($name, $oldvalue, $value) {
2396         if ($value !== '') {
2397             $value = '********';
2398         }
2399         if ($oldvalue !== '' and $oldvalue !== null) {
2400             $oldvalue = '********';
2401         }
2402         parent::add_to_config_log($name, $oldvalue, $value);
2403     }
2405     /**
2406      * Returns HTML for the field.
2407      *
2408      * @param   string  $data       Value for the field
2409      * @param   string  $query      Passed as final argument for format_admin_setting
2410      * @return  string              Rendered HTML
2411      */
2412     public function output_html($data, $query='') {
2413         global $OUTPUT;
2414         $context = (object) [
2415             'id' => $this->get_id(),
2416             'name' => $this->get_full_name(),
2417             'size' => $this->size,
2418             'value' => $data,
2419             'forceltr' => $this->get_force_ltr(),
2420         ];
2421         $element = $OUTPUT->render_from_template('core_admin/setting_configpasswordunmask', $context);
2422         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', null, $query);
2423     }
2427 /**
2428  * Empty setting used to allow flags (advanced) on settings that can have no sensible default.
2429  * Note: Only advanced makes sense right now - locked does not.
2430  *
2431  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2432  */
2433 class admin_setting_configempty extends admin_setting_configtext {
2435     /**
2436      * @param string $name
2437      * @param string $visiblename
2438      * @param string $description
2439      */
2440     public function __construct($name, $visiblename, $description) {
2441         parent::__construct($name, $visiblename, $description, '', PARAM_RAW);
2442     }
2444     /**
2445      * Returns an XHTML string for the hidden field
2446      *
2447      * @param string $data
2448      * @param string $query
2449      * @return string XHTML string for the editor
2450      */
2451     public function output_html($data, $query='') {
2452         global $OUTPUT;
2454         $context = (object) [
2455             'id' => $this->get_id(),
2456             'name' => $this->get_full_name()
2457         ];
2458         $element = $OUTPUT->render_from_template('core_admin/setting_configempty', $context);
2460         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', get_string('none'), $query);
2461     }
2465 /**
2466  * Path to directory
2467  *
2468  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2469  */
2470 class admin_setting_configfile extends admin_setting_configtext {
2471     /**
2472      * Constructor
2473      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2474      * @param string $visiblename localised
2475      * @param string $description long localised info
2476      * @param string $defaultdirectory default directory location
2477      */
2478     public function __construct($name, $visiblename, $description, $defaultdirectory) {
2479         parent::__construct($name, $visiblename, $description, $defaultdirectory, PARAM_RAW, 50);
2480     }
2482     /**
2483      * Returns XHTML for the field
2484      *
2485      * Returns XHTML for the field and also checks whether the file
2486      * specified in $data exists using file_exists()
2487      *
2488      * @param string $data File name and path to use in value attr
2489      * @param string $query
2490      * @return string XHTML field
2491      */
2492     public function output_html($data, $query='') {
2493         global $CFG, $OUTPUT;
2495         $default = $this->get_defaultsetting();
2496         $context = (object) [
2497             'id' => $this->get_id(),
2498             'name' => $this->get_full_name(),
2499             'size' => $this->size,
2500             'value' => $data,
2501             'showvalidity' => !empty($data),
2502             'valid' => $data && file_exists($data),
2503             'readonly' => !empty($CFG->preventexecpath),
2504             'forceltr' => $this->get_force_ltr(),
2505         ];
2507         if ($context->readonly) {
2508             $this->visiblename .= '<div class="form-overridden">'.get_string('execpathnotallowed', 'admin').'</div>';
2509         }
2511         $element = $OUTPUT->render_from_template('core_admin/setting_configfile', $context);
2513         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2514     }
2516     /**
2517      * Checks if execpatch has been disabled in config.php
2518      */
2519     public function write_setting($data) {
2520         global $CFG;
2521         if (!empty($CFG->preventexecpath)) {
2522             if ($this->get_setting() === null) {
2523                 // Use default during installation.
2524                 $data = $this->get_defaultsetting();
2525                 if ($data === null) {
2526                     $data = '';
2527                 }
2528             } else {
2529                 return '';
2530             }
2531         }
2532         return parent::write_setting($data);
2533     }
2538 /**
2539  * Path to executable file
2540  *
2541  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2542  */
2543 class admin_setting_configexecutable extends admin_setting_configfile {
2545     /**
2546      * Returns an XHTML field
2547      *
2548      * @param string $data This is the value for the field
2549      * @param string $query
2550      * @return string XHTML field
2551      */
2552     public function output_html($data, $query='') {
2553         global $CFG, $OUTPUT;
2554         $default = $this->get_defaultsetting();
2555         require_once("$CFG->libdir/filelib.php");
2557         $context = (object) [
2558             'id' => $this->get_id(),
2559             'name' => $this->get_full_name(),
2560             'size' => $this->size,
2561             'value' => $data,
2562             'showvalidity' => !empty($data),
2563             'valid' => $data && file_exists($data) && !is_dir($data) && file_is_executable($data),
2564             'readonly' => !empty($CFG->preventexecpath),
2565             'forceltr' => $this->get_force_ltr()
2566         ];
2568         if (!empty($CFG->preventexecpath)) {
2569             $this->visiblename .= '<div class="form-overridden">'.get_string('execpathnotallowed', 'admin').'</div>';
2570         }
2572         $element = $OUTPUT->render_from_template('core_admin/setting_configexecutable', $context);
2574         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2575     }
2579 /**
2580  * Path to directory
2581  *
2582  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2583  */
2584 class admin_setting_configdirectory extends admin_setting_configfile {
2586     /**
2587      * Returns an XHTML field
2588      *
2589      * @param string $data This is the value for the field
2590      * @param string $query
2591      * @return string XHTML
2592      */
2593     public function output_html($data, $query='') {
2594         global $CFG, $OUTPUT;
2595         $default = $this->get_defaultsetting();
2597         $context = (object) [
2598             'id' => $this->get_id(),
2599             'name' => $this->get_full_name(),
2600             'size' => $this->size,
2601             'value' => $data,
2602             'showvalidity' => !empty($data),
2603             'valid' => $data && file_exists($data) && is_dir($data),
2604             'readonly' => !empty($CFG->preventexecpath),
2605             'forceltr' => $this->get_force_ltr()
2606         ];
2608         if (!empty($CFG->preventexecpath)) {
2609             $this->visiblename .= '<div class="form-overridden">'.get_string('execpathnotallowed', 'admin').'</div>';
2610         }
2612         $element = $OUTPUT->render_from_template('core_admin/setting_configdirectory', $context);
2614         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2615     }
2619 /**
2620  * Checkbox
2621  *
2622  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2623  */
2624 class admin_setting_configcheckbox extends admin_setting {
2625     /** @var string Value used when checked */
2626     public $yes;
2627     /** @var string Value used when not checked */
2628     public $no;
2630     /**
2631      * Constructor
2632      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2633      * @param string $visiblename localised
2634      * @param string $description long localised info
2635      * @param string $defaultsetting
2636      * @param string $yes value used when checked
2637      * @param string $no value used when not checked
2638      */
2639     public function __construct($name, $visiblename, $description, $defaultsetting, $yes='1', $no='0') {
2640         parent::__construct($name, $visiblename, $description, $defaultsetting);
2641         $this->yes = (string)$yes;
2642         $this->no  = (string)$no;
2643     }
2645     /**
2646      * Retrieves the current setting using the objects name
2647      *
2648      * @return string
2649      */
2650     public function get_setting() {
2651         return $this->config_read($this->name);
2652     }
2654     /**
2655      * Sets the value for the setting
2656      *
2657      * Sets the value for the setting to either the yes or no values
2658      * of the object by comparing $data to yes
2659      *
2660      * @param mixed $data Gets converted to str for comparison against yes value
2661      * @return string empty string or error
2662      */
2663     public function write_setting($data) {
2664         if ((string)$data === $this->yes) { // convert to strings before comparison
2665             $data = $this->yes;
2666         } else {
2667             $data = $this->no;
2668         }
2669         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
2670     }
2672     /**
2673      * Returns an XHTML checkbox field
2674      *
2675      * @param string $data If $data matches yes then checkbox is checked
2676      * @param string $query
2677      * @return string XHTML field
2678      */
2679     public function output_html($data, $query='') {
2680         global $OUTPUT;
2682         $context = (object) [
2683             'id' => $this->get_id(),
2684             'name' => $this->get_full_name(),
2685             'no' => $this->no,
2686             'value' => $this->yes,
2687             'checked' => (string) $data === $this->yes,
2688         ];
2690         $default = $this->get_defaultsetting();
2691         if (!is_null($default)) {
2692             if ((string)$default === $this->yes) {
2693                 $defaultinfo = get_string('checkboxyes', 'admin');
2694             } else {
2695                 $defaultinfo = get_string('checkboxno', 'admin');
2696             }
2697         } else {
2698             $defaultinfo = NULL;
2699         }
2701         $element = $OUTPUT->render_from_template('core_admin/setting_configcheckbox', $context);
2703         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
2704     }
2708 /**
2709  * Multiple checkboxes, each represents different value, stored in csv format
2710  *
2711  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2712  */
2713 class admin_setting_configmulticheckbox extends admin_setting {
2714     /** @var array Array of choices value=>label */
2715     public $choices;
2717     /**
2718      * Constructor: uses parent::__construct
2719      *
2720      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2721      * @param string $visiblename localised
2722      * @param string $description long localised info
2723      * @param array $defaultsetting array of selected
2724      * @param array $choices array of $value=>$label for each checkbox
2725      */
2726     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
2727         $this->choices = $choices;
2728         parent::__construct($name, $visiblename, $description, $defaultsetting);
2729     }
2731     /**
2732      * This public function may be used in ancestors for lazy loading of choices
2733      *
2734      * @todo Check if this function is still required content commented out only returns true
2735      * @return bool true if loaded, false if error
2736      */
2737     public function load_choices() {
2738         /*
2739         if (is_array($this->choices)) {
2740             return true;
2741         }
2742         .... load choices here
2743         */
2744         return true;
2745     }
2747     /**
2748      * Is setting related to query text - used when searching
2749      *
2750      * @param string $query
2751      * @return bool true on related, false on not or failure
2752      */
2753     public function is_related($query) {
2754         if (!$this->load_choices() or empty($this->choices)) {
2755             return false;
2756         }
2757         if (parent::is_related($query)) {
2758             return true;
2759         }
2761         foreach ($this->choices as $desc) {
2762             if (strpos(core_text::strtolower($desc), $query) !== false) {
2763                 return true;
2764             }
2765         }
2766         return false;
2767     }
2769     /**
2770      * Returns the current setting if it is set
2771      *
2772      * @return mixed null if null, else an array
2773      */
2774     public function get_setting() {
2775         $result = $this->config_read($this->name);
2777         if (is_null($result)) {
2778             return NULL;
2779         }
2780         if ($result === '') {
2781             return array();
2782         }
2783         $enabled = explode(',', $result);
2784         $setting = array();
2785         foreach ($enabled as $option) {
2786             $setting[$option] = 1;
2787         }
2788         return $setting;
2789     }
2791     /**
2792      * Saves the setting(s) provided in $data
2793      *
2794      * @param array $data An array of data, if not array returns empty str
2795      * @return mixed empty string on useless data or bool true=success, false=failed
2796      */
2797     public function write_setting($data) {
2798         if (!is_array($data)) {
2799             return ''; // ignore it
2800         }
2801         if (!$this->load_choices() or empty($this->choices)) {
2802             return '';
2803         }
2804         unset($data['xxxxx']);
2805         $result = array();
2806         foreach ($data as $key => $value) {
2807             if ($value and array_key_exists($key, $this->choices)) {
2808                 $result[] = $key;
2809             }
2810         }
2811         return $this->config_write($this->name, implode(',', $result)) ? '' : get_string('errorsetting', 'admin');
2812     }
2814     /**
2815      * Returns XHTML field(s) as required by choices
2816      *
2817      * Relies on data being an array should data ever be another valid vartype with
2818      * acceptable value this may cause a warning/error
2819      * if (!is_array($data)) would fix the problem
2820      *
2821      * @todo Add vartype handling to ensure $data is an array
2822      *
2823      * @param array $data An array of checked values
2824      * @param string $query
2825      * @return string XHTML field
2826      */
2827     public function output_html($data, $query='') {
2828         global $OUTPUT;
2830         if (!$this->load_choices() or empty($this->choices)) {
2831             return '';
2832         }
2834         $default = $this->get_defaultsetting();
2835         if (is_null($default)) {
2836             $default = array();
2837         }
2838         if (is_null($data)) {
2839             $data = array();
2840         }
2842         $context = (object) [
2843             'id' => $this->get_id(),
2844             'name' => $this->get_full_name(),
2845         ];
2847         $options = array();
2848         $defaults = array();
2849         foreach ($this->choices as $key => $description) {
2850             if (!empty($default[$key])) {
2851                 $defaults[] = $description;
2852             }
2854             $options[] = [
2855                 'key' => $key,
2856                 'checked' => !empty($data[$key]),
2857                 'label' => highlightfast($query, $description)
2858             ];
2859         }
2861         if (is_null($default)) {
2862             $defaultinfo = null;
2863         } else if (!empty($defaults)) {
2864             $defaultinfo = implode(', ', $defaults);
2865         } else {
2866             $defaultinfo = get_string('none');
2867         }
2869         $context->options = $options;
2870         $context->hasoptions = !empty($options);
2872         $element = $OUTPUT->render_from_template('core_admin/setting_configmulticheckbox', $context);
2874         return format_admin_setting($this, $this->visiblename, $element, $this->description, false, '', $defaultinfo, $query);
2876     }
2880 /**
2881  * Multiple checkboxes 2, value stored as string 00101011
2882  *
2883  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2884  */
2885 class admin_setting_configmulticheckbox2 extends admin_setting_configmulticheckbox {
2887     /**
2888      * Returns the setting if set
2889      *
2890      * @return mixed null if not set, else an array of set settings
2891      */
2892     public function get_setting() {
2893         $result = $this->config_read($this->name);
2894         if (is_null($result)) {
2895             return NULL;
2896         }
2897         if (!$this->load_choices()) {
2898             return NULL;
2899         }
2900         $result = str_pad($result, count($this->choices), '0');
2901         $result = preg_split('//', $result, -1, PREG_SPLIT_NO_EMPTY);
2902         $setting = array();
2903         foreach ($this->choices as $key=>$unused) {
2904             $value = array_shift($result);
2905             if ($value) {
2906                 $setting[$key] = 1;
2907             }
2908         }
2909         return $setting;
2910     }
2912     /**
2913      * Save setting(s) provided in $data param
2914      *
2915      * @param array $data An array of settings to save
2916      * @return mixed empty string for bad data or bool true=>success, false=>error
2917      */
2918     public function write_setting($data) {
2919         if (!is_array($data)) {
2920             return ''; // ignore it
2921         }
2922         if (!$this->load_choices() or empty($this->choices)) {
2923             return '';
2924         }
2925         $result = '';
2926         foreach ($this->choices as $key=>$unused) {
2927             if (!empty($data[$key])) {
2928                 $result .= '1';
2929             } else {
2930                 $result .= '0';
2931             }
2932         }
2933         return $this->config_write($this->name, $result) ? '' : get_string('errorsetting', 'admin');
2934     }
2938 /**
2939  * Select one value from list
2940  *
2941  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2942  */
2943 class admin_setting_configselect extends admin_setting {
2944     /** @var array Array of choices value=>label */
2945     public $choices;
2946     /** @var array Array of choices grouped using optgroups */
2947     public $optgroups;
2949     /**
2950      * Constructor
2951      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2952      * @param string $visiblename localised
2953      * @param string $description long localised info
2954      * @param string|int $defaultsetting
2955      * @param array $choices array of $value=>$label for each selection
2956      */
2957     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
2958         // Look for optgroup and single options.
2959         if (is_array($choices)) {
2960             $this->choices = [];
2961             foreach ($choices as $key => $val) {
2962                 if (is_array($val)) {
2963                     $this->optgroups[$key] = $val;
2964                     $this->choices = array_merge($this->choices, $val);
2965                 } else {
2966                     $this->choices[$key] = $val;
2967                 }
2968             }
2969         }
2971         parent::__construct($name, $visiblename, $description, $defaultsetting);
2972     }
2974     /**
2975      * This function may be used in ancestors for lazy loading of choices
2976      *
2977      * Override this method if loading of choices is expensive, such
2978      * as when it requires multiple db requests.
2979      *
2980      * @return bool true if loaded, false if error
2981      */
2982     public function load_choices() {
2983         /*
2984         if (is_array($this->choices)) {
2985             return true;
2986         }
2987         .... load choices here
2988         */
2989         return true;
2990     }
2992     /**
2993      * Check if this is $query is related to a choice
2994      *
2995      * @param string $query
2996      * @return bool true if related, false if not
2997      */
2998     public function is_related($query) {
2999         if (parent::is_related($query)) {
3000             return true;
3001         }
3002         if (!$this->load_choices()) {
3003             return false;
3004         }
3005         foreach ($this->choices as $key=>$value) {
3006             if (strpos(core_text::strtolower($key), $query) !== false) {
3007                 return true;
3008             }
3009             if (strpos(core_text::strtolower($value), $query) !== false) {
3010                 return true;
3011             }
3012         }
3013         return false;
3014     }
3016     /**
3017      * Return the setting
3018      *
3019      * @return mixed returns config if successful else null
3020      */
3021     public function get_setting() {
3022         return $this->config_read($this->name);
3023     }
3025     /**
3026      * Save a setting
3027      *
3028      * @param string $data
3029      * @return string empty of error string
3030      */
3031     public function write_setting($data) {
3032         if (!$this->load_choices() or empty($this->choices)) {
3033             return '';
3034         }
3035         if (!array_key_exists($data, $this->choices)) {
3036             return ''; // ignore it
3037         }
3039         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
3040     }
3042     /**
3043      * Returns XHTML select field
3044      *
3045      * Ensure the options are loaded, and generate the XHTML for the select
3046      * element and any warning message. Separating this out from output_html
3047      * makes it easier to subclass this class.
3048      *
3049      * @param string $data the option to show as selected.
3050      * @param string $current the currently selected option in the database, null if none.
3051      * @param string $default the default selected option.
3052      * @return array the HTML for the select element, and a warning message.
3053      * @deprecated since Moodle 3.2
3054      */
3055     public function output_select_html($data, $current, $default, $extraname = '') {
3056         debugging('The method admin_setting_configselect::output_select_html is depreacted, do not use any more.', DEBUG_DEVELOPER);
3057     }
3059     /**
3060      * Returns XHTML select field and wrapping div(s)
3061      *
3062      * @see output_select_html()
3063      *
3064      * @param string $data the option to show as selected
3065      * @param string $query
3066      * @return string XHTML field and wrapping div
3067      */
3068     public function output_html($data, $query='') {
3069         global $OUTPUT;
3071         $default = $this->get_defaultsetting();
3072         $current = $this->get_setting();
3074         if (!$this->load_choices() || empty($this->choices)) {
3075             return '';
3076         }
3078         $context = (object) [
3079             'id' => $this->get_id(),
3080             'name' => $this->get_full_name(),
3081         ];
3083         if (!is_null($default) && array_key_exists($default, $this->choices)) {
3084             $defaultinfo = $this->choices[$default];
3085         } else {
3086             $defaultinfo = NULL;
3087         }
3089         // Warnings.
3090         $warning = '';
3091         if ($current === null) {
3092             // First run.
3093         } else if (empty($current) && (array_key_exists('', $this->choices) || array_key_exists(0, $this->choices))) {
3094             // No warning.
3095         } else if (!array_key_exists($current, $this->choices)) {
3096             $warning = get_string('warningcurrentsetting', 'admin', $current);
3097             if (!is_null($default) && $data == $current) {
3098                 $data = $default; // Use default instead of first value when showing the form.
3099             }
3100         }
3102         $options = [];
3103         $template = 'core_admin/setting_configselect';
3105         if (!empty($this->optgroups)) {
3106             $optgroups = [];
3107             foreach ($this->optgroups as $label => $choices) {
3108                 $optgroup = array('label' => $label, 'options' => []);
3109                 foreach ($choices as $value => $name) {
3110                     $optgroup['options'][] = [
3111                         'value' => $value,
3112                         'name' => $name,
3113                         'selected' => (string) $value == $data
3114                     ];
3115                     unset($this->choices[$value]);
3116                 }
3117                 $optgroups[] = $optgroup;
3118             }
3119             $context->options = $options;
3120             $context->optgroups = $optgroups;
3121             $template = 'core_admin/setting_configselect_optgroup';
3122         }
3124         foreach ($this->choices as $value => $name) {
3125             $options[] = [
3126                 'value' => $value,
3127                 'name' => $name,
3128                 'selected' => (string) $value == $data
3129             ];
3130         }
3131         $context->options = $options;
3133         $element = $OUTPUT->render_from_template($template, $context);
3135         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, $warning, $defaultinfo, $query);
3136     }
3140 /**
3141  * Select multiple items from list
3142  *
3143  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3144  */
3145 class admin_setting_configmultiselect extends admin_setting_configselect {
3146     /**
3147      * Constructor
3148      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3149      * @param string $visiblename localised
3150      * @param string $description long localised info
3151      * @param array $defaultsetting array of selected items
3152      * @param array $choices array of $value=>$label for each list item
3153      */
3154     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
3155         parent::__construct($name, $visiblename, $description, $defaultsetting, $choices);
3156     }
3158     /**
3159      * Returns the select setting(s)
3160      *
3161      * @return mixed null or array. Null if no settings else array of setting(s)
3162      */
3163     public function get_setting() {
3164         $result = $this->config_read($this->name);
3165         if (is_null($result)) {
3166             return NULL;
3167         }
3168         if ($result === '') {
3169             return array();
3170         }
3171         return explode(',', $result);
3172     }
3174     /**
3175      * Saves setting(s) provided through $data
3176      *
3177      * Potential bug in the works should anyone call with this function
3178      * using a vartype that is not an array
3179      *
3180      * @param array $data
3181      */
3182     public function write_setting($data) {
3183         if (!is_array($data)) {
3184             return ''; //ignore it
3185         }
3186         if (!$this->load_choices() or empty($this->choices)) {
3187             return '';
3188         }
3190         unset($data['xxxxx']);
3192         $save = array();
3193         foreach ($data as $value) {
3194             if (!array_key_exists($value, $this->choices)) {
3195                 continue; // ignore it
3196             }
3197             $save[] = $value;
3198         }
3200         return ($this->config_write($this->name, implode(',', $save)) ? '' : get_string('errorsetting', 'admin'));
3201     }
3203     /**
3204      * Is setting related to query text - used when searching
3205      *
3206      * @param string $query
3207      * @return bool true if related, false if not
3208      */
3209     public function is_related($query) {
3210         if (!$this->load_choices() or empty($this->choices)) {
3211             return false;
3212         }
3213         if (parent::is_related($query)) {
3214             return true;
3215         }
3217         foreach ($this->choices as $desc) {
3218             if (strpos(core_text::strtolower($desc), $query) !== false) {
3219                 return true;
3220             }
3221         }
3222         return false;
3223     }
3225     /**
3226      * Returns XHTML multi-select field
3227      *
3228      * @todo Add vartype handling to ensure $data is an array
3229      * @param array $data Array of values to select by default
3230      * @param string $query
3231      * @return string XHTML multi-select field
3232      */
3233     public function output_html($data, $query='') {
3234         global $OUTPUT;
3236         if (!$this->load_choices() or empty($this->choices)) {
3237             return '';
3238         }
3240         $default = $this->get_defaultsetting();
3241         if (is_null($default)) {
3242             $default = array();
3243         }
3244         if (is_null($data)) {
3245             $data = array();
3246         }
3248         $context = (object) [
3249             'id' => $this->get_id(),
3250             'name' => $this->get_full_name(),
3251             'size' => min(10, count($this->choices))
3252         ];
3254         $defaults = [];
3255         $options = [];
3256         $template = 'core_admin/setting_configmultiselect';
3258         if (!empty($this->optgroups)) {
3259             $optgroups = [];
3260             foreach ($this->optgroups as $label => $choices) {
3261                 $optgroup = array('label' => $label, 'options' => []);
3262                 foreach ($choices as $value => $name) {
3263                     if (in_array($value, $default)) {
3264                         $defaults[] = $name;
3265                     }
3266                     $optgroup['options'][] = [
3267                         'value' => $value,
3268                         'name' => $name,
3269                         'selected' => in_array($value, $data)
3270                     ];
3271                     unset($this->choices[$value]);
3272                 }
3273                 $optgroups[] = $optgroup;
3274             }
3275             $context->optgroups = $optgroups;
3276             $template = 'core_admin/setting_configmultiselect_optgroup';
3277         }
3279         foreach ($this->choices as $value => $name) {
3280             if (in_array($value, $default)) {
3281                 $defaults[] = $name;
3282             }
3283             $options[] = [
3284                 'value' => $value,
3285                 'name' => $name,
3286                 'selected' => in_array($value, $data)
3287             ];
3288         }
3289         $context->options = $options;
3291         if (is_null($default)) {
3292             $defaultinfo = NULL;
3293         } if (!empty($defaults)) {
3294             $defaultinfo = implode(', ', $defaults);
3295         } else {
3296             $defaultinfo = get_string('none');
3297         }
3299         $element = $OUTPUT->render_from_template($template, $context);
3301         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
3302     }
3305 /**
3306  * Time selector
3307  *
3308  * This is a liiitle bit messy. we're using two selects, but we're returning
3309  * them as an array named after $name (so we only use $name2 internally for the setting)
3310  *
3311  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3312  */
3313 class admin_setting_configtime extends admin_setting {
3314     /** @var string Used for setting second select (minutes) */
3315     public $name2;
3317     /**
3318      * Constructor
3319      * @param string $hoursname setting for hours
3320      * @param string $minutesname setting for hours
3321      * @param string $visiblename localised
3322      * @param string $description long localised info
3323      * @param array $defaultsetting array representing default time 'h'=>hours, 'm'=>minutes
3324      */
3325     public function __construct($hoursname, $minutesname, $visiblename, $description, $defaultsetting) {
3326         $this->name2 = $minutesname;
3327         parent::__construct($hoursname, $visiblename, $description, $defaultsetting);
3328     }
3330     /**
3331      * Get the selected time
3332      *
3333      * @return mixed An array containing 'h'=>xx, 'm'=>xx, or null if not set
3334      */
3335     public function get_setting() {
3336         $result1 = $this->config_read($this->name);
3337         $result2 = $this->config_read($this->name2);
3338         if (is_null($result1) or is_null($result2)) {
3339             return NULL;
3340         }
3342         return array('h' => $result1, 'm' => $result2);
3343     }
3345     /**
3346      * Store the time (hours and minutes)
3347      *
3348      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3349      * @return bool true if success, false if not
3350      */
3351     public function write_setting($data) {
3352         if (!is_array($data)) {
3353             return '';
3354         }
3356         $result = $this->config_write($this->name, (int)$data['h']) && $this->config_write($this->name2, (int)$data['m']);
3357         return ($result ? '' : get_string('errorsetting', 'admin'));
3358     }
3360     /**
3361      * Returns XHTML time select fields
3362      *
3363      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3364      * @param string $query
3365      * @return string XHTML time select fields and wrapping div(s)
3366      */
3367     public function output_html($data, $query='') {
3368         global $OUTPUT;
3370         $default = $this->get_defaultsetting();
3371         if (is_array($default)) {
3372             $defaultinfo = $default['h'].':'.$default['m'];
3373         } else {
3374             $defaultinfo = NULL;
3375         }
3377         $context = (object) [
3378             'id' => $this->get_id(),
3379             'name' => $this->get_full_name(),
3380             'hours' => array_map(function($i) use ($data) {
3381                 return [
3382                     'value' => $i,
3383                     'name' => $i,
3384                     'selected' => $i == $data['h']
3385                 ];
3386             }, range(0, 23)),
3387             'minutes' => array_map(function($i) use ($data) {
3388                 return [
3389                     'value' => $i,
3390                     'name' => $i,
3391                     'selected' => $i == $data['m']
3392                 ];
3393             }, range(0, 59, 5))
3394         ];
3396         $element = $OUTPUT->render_from_template('core_admin/setting_configtime', $context);
3398         return format_admin_setting($this, $this->visiblename, $element, $this->description,
3399             $this->get_id() . 'h', '', $defaultinfo, $query);
3400     }
3405 /**
3406  * Seconds duration setting.
3407  *
3408  * @copyright 2012 Petr Skoda (http://skodak.org)
3409  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3410  */
3411 class admin_setting_configduration extends admin_setting {
3413     /** @var int default duration unit */
3414     protected $defaultunit;
3416     /**
3417      * Constructor
3418      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
3419      *                     or 'myplugin/mysetting' for ones in config_plugins.
3420      * @param string $visiblename localised name
3421      * @param string $description localised long description
3422      * @param mixed $defaultsetting string or array depending on implementation
3423      * @param int $defaultunit - day, week, etc. (in seconds)
3424      */
3425     public function __construct($name, $visiblename, $description, $defaultsetting, $defaultunit = 86400) {
3426         if (is_number($defaultsetting)) {
3427             $defaultsetting = self::parse_seconds($defaultsetting);
3428         }
3429         $units = self::get_units();
3430         if (isset($units[$defaultunit])) {
3431             $this->defaultunit = $defaultunit;
3432         } else {
3433             $this->defaultunit = 86400;
3434         }
3435         parent::__construct($name, $visiblename, $description, $defaultsetting);
3436     }
3438     /**
3439      * Returns selectable units.
3440      * @static
3441      * @return array
3442      */
3443     protected static function get_units() {
3444         return array(
3445             604800 => get_string('weeks'),
3446             86400 => get_string('days'),
3447             3600 => get_string('hours'),
3448             60 => get_string('minutes'),
3449             1 => get_string('seconds'),
3450         );
3451     }
3453     /**
3454      * Converts seconds to some more user friendly string.
3455      * @static
3456      * @param int $seconds
3457      * @return string
3458      */
3459     protected static function get_duration_text($seconds) {
3460         if (empty($seconds)) {
3461             return get_string('none');
3462         }
3463         $data = self::parse_seconds($seconds);
3464         switch ($data['u']) {
3465             case (60*60*24*7):
3466                 return get_string('numweeks', '', $data['v']);
3467             case (60*60*24):
3468                 return get_string('numdays', '', $data['v']);
3469             case (60*60):
3470                 return get_string('numhours', '', $data['v']);
3471             case (60):
3472                 return get_string('numminutes', '', $data['v']);
3473             default:
3474                 return get_string('numseconds', '', $data['v']*$data['u']);
3475         }
3476     }
3478     /**
3479      * Finds suitable units for given duration.
3480      * @static
3481      * @param int $seconds
3482      * @return array
3483      */
3484     protected static function parse_seconds($seconds) {
3485         foreach (self::get_units() as $unit => $unused) {
3486             if ($seconds % $unit === 0) {
3487                 return array('v'=>(int)($seconds/$unit), 'u'=>$unit);
3488             }
3489         }
3490         return array('v'=>(int)$seconds, 'u'=>1);
3491     }
3493     /**
3494      * Get the selected duration as array.
3495      *
3496      * @return mixed An array containing 'v'=>xx, 'u'=>xx, or null if not set
3497      */
3498     public function get_setting() {
3499         $seconds = $this->config_read($this->name);
3500         if (is_null($seconds)) {
3501             return null;
3502         }
3504         return self::parse_seconds($seconds);
3505     }
3507     /**
3508      * Store the duration as seconds.
3509      *
3510      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3511      * @return bool true if success, false if not
3512      */
3513     public function write_setting($data) {
3514         if (!is_array($data)) {
3515             return '';
3516         }
3518         $seconds = (int)($data['v']*$data['u']);
3519         if ($seconds < 0) {
3520             return get_string('errorsetting', 'admin');
3521         }
3523         $result = $this->config_write($this->name, $seconds);
3524         return ($result ? '' : get_string('errorsetting', 'admin'));
3525     }
3527     /**
3528      * Returns duration text+select fields.
3529      *
3530      * @param array $data Must be form 'v'=>xx, 'u'=>xx
3531      * @param string $query
3532      * @return string duration text+select fields and wrapping div(s)
3533      */
3534     public function output_html($data, $query='') {
3535         global $OUTPUT;
3537         $default = $this->get_defaultsetting();
3538         if (is_number($default)) {
3539             $defaultinfo = self::get_duration_text($default);
3540         } else if (is_array($default)) {
3541             $defaultinfo = self::get_duration_text($default['v']*$default['u']);
3542         } else {
3543             $defaultinfo = null;
3544         }
3546         $inputid = $this->get_id() . 'v';
3547         $units = self::get_units();
3548         $defaultunit = $this->defaultunit;
3550         $context = (object) [
3551             'id' => $this->get_id(),
3552             'name' => $this->get_full_name(),
3553             'value' => $data['v'],
3554             'options' => array_map(function($unit) use ($units, $data, $defaultunit) {
3555                 return [
3556                     'value' => $unit,
3557                     'name' => $units[$unit],
3558                     'selected' => ($data['v'] == 0 && $unit == $defaultunit) || $unit == $data['u']
3559                 ];
3560             }, array_keys($units))
3561         ];
3563         $element = $OUTPUT->render_from_template('core_admin/setting_configduration', $context);
3565         return format_admin_setting($this, $this->visiblename, $element, $this->description, $inputid, '', $defaultinfo, $query);
3566     }
3570 /**
3571  * Seconds duration setting with an advanced checkbox, that controls a additional
3572  * $name.'_adv' setting.
3573  *
3574  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3575  * @copyright 2014 The Open University
3576  */
3577 class admin_setting_configduration_with_advanced extends admin_setting_configduration {
3578     /**
3579      * Constructor
3580      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
3581      *                     or 'myplugin/mysetting' for ones in config_plugins.
3582      * @param string $visiblename localised name
3583      * @param string $description localised long description
3584      * @param array  $defaultsetting array of int value, and bool whether it is
3585      *                     is advanced by default.
3586      * @param int $defaultunit - day, week, etc. (in seconds)
3587      */
3588     public function __construct($name, $visiblename, $description, $defaultsetting, $defaultunit = 86400) {
3589         parent::__construct($name, $visiblename, $description, $defaultsetting['value'], $defaultunit);
3590         $this->set_advanced_flag_options(admin_setting_flag::ENABLED, !empty($defaultsetting['adv']));
3591     }
3595 /**
3596  * Used to validate a textarea used for ip addresses
3597  *
3598  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3599  * @copyright 2011 Petr Skoda (http://skodak.org)
3600  */
3601 class admin_setting_configiplist extends admin_setting_configtextarea {
3603     /**
3604      * Validate the contents of the textarea as IP addresses
3605      *
3606      * Used to validate a new line separated list of IP addresses collected from
3607      * a textarea control
3608      *
3609      * @param string $data A list of IP Addresses separated by new lines
3610      * @return mixed bool true for success or string:error on failure
3611      */
3612     public function validate($data) {
3613         if(!empty($data)) {
3614             $lines = explode("\n", $data);
3615         } else {
3616             return true;
3617         }
3618         $result = true;
3619         $badips = array();
3620         foreach ($lines as $line) {
3621             $tokens = explode('#', $line);
3622             $ip = trim($tokens[0]);
3623             if (empty($ip)) {
3624                 continue;
3625             }
3626             if (preg_match('#^(\d{1,3})(\.\d{1,3}){0,3}$#', $ip, $match) ||
3627                 preg_match('#^(\d{1,3})(\.\d{1,3}){0,3}(\/\d{1,2})$#', $ip, $match) ||
3628                 preg_match('#^(\d{1,3})(\.\d{1,3}){3}(-\d{1,3})$#', $ip, $match)) {
3629             } else {
3630                 $result = false;
3631                 $badips[] = $ip;
3632             }
3633         }
3634         if($result) {
3635             return true;
3636         } else {
3637             return get_string('validateiperror', 'admin', join(', ', $badips));
3638         }
3639     }
3642 /**
3643  * Used to validate a textarea used for domain names, wildcard domain names and IP addresses/ranges (both IPv4 and IPv6 format).
3644  *
3645  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3646  * @copyright 2016 Jake Dallimore (jrhdallimore@gmail.com)
3647  */
3648 class admin_setting_configmixedhostiplist extends admin_setting_configtextarea {
3650     /**
3651      * Validate the contents of the textarea as either IP addresses, domain name or wildcard domain name (RFC 4592).
3652      * Used to validate a new line separated list of entries collected from a textarea control.
3653      *
3654      * This setting provides support for internationalised domain names (IDNs), however, such UTF-8 names will be converted to
3655      * their ascii-compatible encoding (punycode) on save, and converted back to their UTF-8 representation when fetched
3656      * via the get_setting() method, which has been overriden.
3657      *
3658      * @param string $data A list of FQDNs, DNS wildcard format domains, and IP addresses, separated by new lines.
3659      * @return mixed bool true for success or string:error on failure
3660      */
3661     public function validate($data) {
3662         if (empty($data)) {
3663             return true;
3664         }
3665         $entries = explode("\n", $data);
3666         $badentries = [];
3668         foreach ($entries as $key => $entry) {
3669             $entry = trim($entry);
3670             if (empty($entry)) {
3671                 return