MDL-62777 Administration: CLI upgrade new setting notification
[moodle.git] / lib / adminlib.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * Functions and classes used during installation, upgrades and for admin settings.
20  *
21  *  ADMIN SETTINGS TREE INTRODUCTION
22  *
23  *  This file performs the following tasks:
24  *   -it defines the necessary objects and interfaces to build the Moodle
25  *    admin hierarchy
26  *   -it defines the admin_externalpage_setup()
27  *
28  *  ADMIN_SETTING OBJECTS
29  *
30  *  Moodle settings are represented by objects that inherit from the admin_setting
31  *  class. These objects encapsulate how to read a setting, how to write a new value
32  *  to a setting, and how to appropriately display the HTML to modify the setting.
33  *
34  *  ADMIN_SETTINGPAGE OBJECTS
35  *
36  *  The admin_setting objects are then grouped into admin_settingpages. The latter
37  *  appear in the Moodle admin tree block. All interaction with admin_settingpage
38  *  objects is handled by the admin/settings.php file.
39  *
40  *  ADMIN_EXTERNALPAGE OBJECTS
41  *
42  *  There are some settings in Moodle that are too complex to (efficiently) handle
43  *  with admin_settingpages. (Consider, for example, user management and displaying
44  *  lists of users.) In this case, we use the admin_externalpage object. This object
45  *  places a link to an external PHP file in the admin tree block.
46  *
47  *  If you're using an admin_externalpage object for some settings, you can take
48  *  advantage of the admin_externalpage_* functions. For example, suppose you wanted
49  *  to add a foo.php file into admin. First off, you add the following line to
50  *  admin/settings/first.php (at the end of the file) or to some other file in
51  *  admin/settings:
52  * <code>
53  *     $ADMIN->add('userinterface', new admin_externalpage('foo', get_string('foo'),
54  *         $CFG->wwwdir . '/' . '$CFG->admin . '/foo.php', 'some_role_permission'));
55  * </code>
56  *
57  *  Next, in foo.php, your file structure would resemble the following:
58  * <code>
59  *         require(__DIR__.'/../../config.php');
60  *         require_once($CFG->libdir.'/adminlib.php');
61  *         admin_externalpage_setup('foo');
62  *         // functionality like processing form submissions goes here
63  *         echo $OUTPUT->header();
64  *         // your HTML goes here
65  *         echo $OUTPUT->footer();
66  * </code>
67  *
68  *  The admin_externalpage_setup() function call ensures the user is logged in,
69  *  and makes sure that they have the proper role permission to access the page.
70  *  It also configures all $PAGE properties needed for navigation.
71  *
72  *  ADMIN_CATEGORY OBJECTS
73  *
74  *  Above and beyond all this, we have admin_category objects. These objects
75  *  appear as folders in the admin tree block. They contain admin_settingpage's,
76  *  admin_externalpage's, and other admin_category's.
77  *
78  *  OTHER NOTES
79  *
80  *  admin_settingpage's, admin_externalpage's, and admin_category's all inherit
81  *  from part_of_admin_tree (a pseudointerface). This interface insists that
82  *  a class has a check_access method for access permissions, a locate method
83  *  used to find a specific node in the admin tree and find parent path.
84  *
85  *  admin_category's inherit from parentable_part_of_admin_tree. This pseudo-
86  *  interface ensures that the class implements a recursive add function which
87  *  accepts a part_of_admin_tree object and searches for the proper place to
88  *  put it. parentable_part_of_admin_tree implies part_of_admin_tree.
89  *
90  *  Please note that the $this->name field of any part_of_admin_tree must be
91  *  UNIQUE throughout the ENTIRE admin tree.
92  *
93  *  The $this->name field of an admin_setting object (which is *not* part_of_
94  *  admin_tree) must be unique on the respective admin_settingpage where it is
95  *  used.
96  *
97  * Original author: Vincenzo K. Marcovecchio
98  * Maintainer:      Petr Skoda
99  *
100  * @package    core
101  * @subpackage admin
102  * @copyright  1999 onwards Martin Dougiamas  http://dougiamas.com
103  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
104  */
106 defined('MOODLE_INTERNAL') || die();
108 /// Add libraries
109 require_once($CFG->libdir.'/ddllib.php');
110 require_once($CFG->libdir.'/xmlize.php');
111 require_once($CFG->libdir.'/messagelib.php');
113 define('INSECURE_DATAROOT_WARNING', 1);
114 define('INSECURE_DATAROOT_ERROR', 2);
116 /**
117  * Automatically clean-up all plugin data and remove the plugin DB tables
118  *
119  * NOTE: do not call directly, use new /admin/plugins.php?uninstall=component instead!
120  *
121  * @param string $type The plugin type, eg. 'mod', 'qtype', 'workshopgrading' etc.
122  * @param string $name The plugin name, eg. 'forum', 'multichoice', 'accumulative' etc.
123  * @uses global $OUTPUT to produce notices and other messages
124  * @return void
125  */
126 function uninstall_plugin($type, $name) {
127     global $CFG, $DB, $OUTPUT;
129     // This may take a long time.
130     core_php_time_limit::raise();
132     // Recursively uninstall all subplugins first.
133     $subplugintypes = core_component::get_plugin_types_with_subplugins();
134     if (isset($subplugintypes[$type])) {
135         $base = core_component::get_plugin_directory($type, $name);
136         if (file_exists("$base/db/subplugins.php")) {
137             $subplugins = array();
138             include("$base/db/subplugins.php");
139             foreach ($subplugins as $subplugintype=>$dir) {
140                 $instances = core_component::get_plugin_list($subplugintype);
141                 foreach ($instances as $subpluginname => $notusedpluginpath) {
142                     uninstall_plugin($subplugintype, $subpluginname);
143                 }
144             }
145         }
147     }
149     $component = $type . '_' . $name;  // eg. 'qtype_multichoice' or 'workshopgrading_accumulative' or 'mod_forum'
151     if ($type === 'mod') {
152         $pluginname = $name;  // eg. 'forum'
153         if (get_string_manager()->string_exists('modulename', $component)) {
154             $strpluginname = get_string('modulename', $component);
155         } else {
156             $strpluginname = $component;
157         }
159     } else {
160         $pluginname = $component;
161         if (get_string_manager()->string_exists('pluginname', $component)) {
162             $strpluginname = get_string('pluginname', $component);
163         } else {
164             $strpluginname = $component;
165         }
166     }
168     echo $OUTPUT->heading($pluginname);
170     // Delete all tag areas, collections and instances associated with this plugin.
171     core_tag_area::uninstall($component);
173     // Custom plugin uninstall.
174     $plugindirectory = core_component::get_plugin_directory($type, $name);
175     $uninstalllib = $plugindirectory . '/db/uninstall.php';
176     if (file_exists($uninstalllib)) {
177         require_once($uninstalllib);
178         $uninstallfunction = 'xmldb_' . $pluginname . '_uninstall';    // eg. 'xmldb_workshop_uninstall()'
179         if (function_exists($uninstallfunction)) {
180             // Do not verify result, let plugin complain if necessary.
181             $uninstallfunction();
182         }
183     }
185     // Specific plugin type cleanup.
186     $plugininfo = core_plugin_manager::instance()->get_plugin_info($component);
187     if ($plugininfo) {
188         $plugininfo->uninstall_cleanup();
189         core_plugin_manager::reset_caches();
190     }
191     $plugininfo = null;
193     // perform clean-up task common for all the plugin/subplugin types
195     //delete the web service functions and pre-built services
196     require_once($CFG->dirroot.'/lib/externallib.php');
197     external_delete_descriptions($component);
199     // delete calendar events
200     $DB->delete_records('event', array('modulename' => $pluginname));
202     // Delete scheduled tasks.
203     $DB->delete_records('task_scheduled', array('component' => $component));
205     // Delete Inbound Message datakeys.
206     $DB->delete_records_select('messageinbound_datakeys',
207             'handler IN (SELECT id FROM {messageinbound_handlers} WHERE component = ?)', array($component));
209     // Delete Inbound Message handlers.
210     $DB->delete_records('messageinbound_handlers', array('component' => $component));
212     // delete all the logs
213     $DB->delete_records('log', array('module' => $pluginname));
215     // delete log_display information
216     $DB->delete_records('log_display', array('component' => $component));
218     // delete the module configuration records
219     unset_all_config_for_plugin($component);
220     if ($type === 'mod') {
221         unset_all_config_for_plugin($pluginname);
222     }
224     // delete message provider
225     message_provider_uninstall($component);
227     // delete the plugin tables
228     $xmldbfilepath = $plugindirectory . '/db/install.xml';
229     drop_plugin_tables($component, $xmldbfilepath, false);
230     if ($type === 'mod' or $type === 'block') {
231         // non-frankenstyle table prefixes
232         drop_plugin_tables($name, $xmldbfilepath, false);
233     }
235     // delete the capabilities that were defined by this module
236     capabilities_cleanup($component);
238     // Delete all remaining files in the filepool owned by the component.
239     $fs = get_file_storage();
240     $fs->delete_component_files($component);
242     // Finally purge all caches.
243     purge_all_caches();
245     // Invalidate the hash used for upgrade detections.
246     set_config('allversionshash', '');
248     echo $OUTPUT->notification(get_string('success'), 'notifysuccess');
251 /**
252  * Returns the version of installed component
253  *
254  * @param string $component component name
255  * @param string $source either 'disk' or 'installed' - where to get the version information from
256  * @return string|bool version number or false if the component is not found
257  */
258 function get_component_version($component, $source='installed') {
259     global $CFG, $DB;
261     list($type, $name) = core_component::normalize_component($component);
263     // moodle core or a core subsystem
264     if ($type === 'core') {
265         if ($source === 'installed') {
266             if (empty($CFG->version)) {
267                 return false;
268             } else {
269                 return $CFG->version;
270             }
271         } else {
272             if (!is_readable($CFG->dirroot.'/version.php')) {
273                 return false;
274             } else {
275                 $version = null; //initialize variable for IDEs
276                 include($CFG->dirroot.'/version.php');
277                 return $version;
278             }
279         }
280     }
282     // activity module
283     if ($type === 'mod') {
284         if ($source === 'installed') {
285             if ($CFG->version < 2013092001.02) {
286                 return $DB->get_field('modules', 'version', array('name'=>$name));
287             } else {
288                 return get_config('mod_'.$name, 'version');
289             }
291         } else {
292             $mods = core_component::get_plugin_list('mod');
293             if (empty($mods[$name]) or !is_readable($mods[$name].'/version.php')) {
294                 return false;
295             } else {
296                 $plugin = new stdClass();
297                 $plugin->version = null;
298                 $module = $plugin;
299                 include($mods[$name].'/version.php');
300                 return $plugin->version;
301             }
302         }
303     }
305     // block
306     if ($type === 'block') {
307         if ($source === 'installed') {
308             if ($CFG->version < 2013092001.02) {
309                 return $DB->get_field('block', 'version', array('name'=>$name));
310             } else {
311                 return get_config('block_'.$name, 'version');
312             }
313         } else {
314             $blocks = core_component::get_plugin_list('block');
315             if (empty($blocks[$name]) or !is_readable($blocks[$name].'/version.php')) {
316                 return false;
317             } else {
318                 $plugin = new stdclass();
319                 include($blocks[$name].'/version.php');
320                 return $plugin->version;
321             }
322         }
323     }
325     // all other plugin types
326     if ($source === 'installed') {
327         return get_config($type.'_'.$name, 'version');
328     } else {
329         $plugins = core_component::get_plugin_list($type);
330         if (empty($plugins[$name])) {
331             return false;
332         } else {
333             $plugin = new stdclass();
334             include($plugins[$name].'/version.php');
335             return $plugin->version;
336         }
337     }
340 /**
341  * Delete all plugin tables
342  *
343  * @param string $name Name of plugin, used as table prefix
344  * @param string $file Path to install.xml file
345  * @param bool $feedback defaults to true
346  * @return bool Always returns true
347  */
348 function drop_plugin_tables($name, $file, $feedback=true) {
349     global $CFG, $DB;
351     // first try normal delete
352     if (file_exists($file) and $DB->get_manager()->delete_tables_from_xmldb_file($file)) {
353         return true;
354     }
356     // then try to find all tables that start with name and are not in any xml file
357     $used_tables = get_used_table_names();
359     $tables = $DB->get_tables();
361     /// Iterate over, fixing id fields as necessary
362     foreach ($tables as $table) {
363         if (in_array($table, $used_tables)) {
364             continue;
365         }
367         if (strpos($table, $name) !== 0) {
368             continue;
369         }
371         // found orphan table --> delete it
372         if ($DB->get_manager()->table_exists($table)) {
373             $xmldb_table = new xmldb_table($table);
374             $DB->get_manager()->drop_table($xmldb_table);
375         }
376     }
378     return true;
381 /**
382  * Returns names of all known tables == tables that moodle knows about.
383  *
384  * @return array Array of lowercase table names
385  */
386 function get_used_table_names() {
387     $table_names = array();
388     $dbdirs = get_db_directories();
390     foreach ($dbdirs as $dbdir) {
391         $file = $dbdir.'/install.xml';
393         $xmldb_file = new xmldb_file($file);
395         if (!$xmldb_file->fileExists()) {
396             continue;
397         }
399         $loaded    = $xmldb_file->loadXMLStructure();
400         $structure = $xmldb_file->getStructure();
402         if ($loaded and $tables = $structure->getTables()) {
403             foreach($tables as $table) {
404                 $table_names[] = strtolower($table->getName());
405             }
406         }
407     }
409     return $table_names;
412 /**
413  * Returns list of all directories where we expect install.xml files
414  * @return array Array of paths
415  */
416 function get_db_directories() {
417     global $CFG;
419     $dbdirs = array();
421     /// First, the main one (lib/db)
422     $dbdirs[] = $CFG->libdir.'/db';
424     /// Then, all the ones defined by core_component::get_plugin_types()
425     $plugintypes = core_component::get_plugin_types();
426     foreach ($plugintypes as $plugintype => $pluginbasedir) {
427         if ($plugins = core_component::get_plugin_list($plugintype)) {
428             foreach ($plugins as $plugin => $plugindir) {
429                 $dbdirs[] = $plugindir.'/db';
430             }
431         }
432     }
434     return $dbdirs;
437 /**
438  * Try to obtain or release the cron lock.
439  * @param string  $name  name of lock
440  * @param int  $until timestamp when this lock considered stale, null means remove lock unconditionally
441  * @param bool $ignorecurrent ignore current lock state, usually extend previous lock, defaults to false
442  * @return bool true if lock obtained
443  */
444 function set_cron_lock($name, $until, $ignorecurrent=false) {
445     global $DB;
446     if (empty($name)) {
447         debugging("Tried to get a cron lock for a null fieldname");
448         return false;
449     }
451     // remove lock by force == remove from config table
452     if (is_null($until)) {
453         set_config($name, null);
454         return true;
455     }
457     if (!$ignorecurrent) {
458         // read value from db - other processes might have changed it
459         $value = $DB->get_field('config', 'value', array('name'=>$name));
461         if ($value and $value > time()) {
462             //lock active
463             return false;
464         }
465     }
467     set_config($name, $until);
468     return true;
471 /**
472  * Test if and critical warnings are present
473  * @return bool
474  */
475 function admin_critical_warnings_present() {
476     global $SESSION;
478     if (!has_capability('moodle/site:config', context_system::instance())) {
479         return 0;
480     }
482     if (!isset($SESSION->admin_critical_warning)) {
483         $SESSION->admin_critical_warning = 0;
484         if (is_dataroot_insecure(true) === INSECURE_DATAROOT_ERROR) {
485             $SESSION->admin_critical_warning = 1;
486         }
487     }
489     return $SESSION->admin_critical_warning;
492 /**
493  * Detects if float supports at least 10 decimal digits
494  *
495  * Detects if float supports at least 10 decimal digits
496  * and also if float-->string conversion works as expected.
497  *
498  * @return bool true if problem found
499  */
500 function is_float_problem() {
501     $num1 = 2009010200.01;
502     $num2 = 2009010200.02;
504     return ((string)$num1 === (string)$num2 or $num1 === $num2 or $num2 <= (string)$num1);
507 /**
508  * Try to verify that dataroot is not accessible from web.
509  *
510  * Try to verify that dataroot is not accessible from web.
511  * It is not 100% correct but might help to reduce number of vulnerable sites.
512  * Protection from httpd.conf and .htaccess is not detected properly.
513  *
514  * @uses INSECURE_DATAROOT_WARNING
515  * @uses INSECURE_DATAROOT_ERROR
516  * @param bool $fetchtest try to test public access by fetching file, default false
517  * @return mixed empty means secure, INSECURE_DATAROOT_ERROR found a critical problem, INSECURE_DATAROOT_WARNING might be problematic
518  */
519 function is_dataroot_insecure($fetchtest=false) {
520     global $CFG;
522     $siteroot = str_replace('\\', '/', strrev($CFG->dirroot.'/')); // win32 backslash workaround
524     $rp = preg_replace('|https?://[^/]+|i', '', $CFG->wwwroot, 1);
525     $rp = strrev(trim($rp, '/'));
526     $rp = explode('/', $rp);
527     foreach($rp as $r) {
528         if (strpos($siteroot, '/'.$r.'/') === 0) {
529             $siteroot = substr($siteroot, strlen($r)+1); // moodle web in subdirectory
530         } else {
531             break; // probably alias root
532         }
533     }
535     $siteroot = strrev($siteroot);
536     $dataroot = str_replace('\\', '/', $CFG->dataroot.'/');
538     if (strpos($dataroot, $siteroot) !== 0) {
539         return false;
540     }
542     if (!$fetchtest) {
543         return INSECURE_DATAROOT_WARNING;
544     }
546     // now try all methods to fetch a test file using http protocol
548     $httpdocroot = str_replace('\\', '/', strrev($CFG->dirroot.'/'));
549     preg_match('|(https?://[^/]+)|i', $CFG->wwwroot, $matches);
550     $httpdocroot = $matches[1];
551     $datarooturl = $httpdocroot.'/'. substr($dataroot, strlen($siteroot));
552     make_upload_directory('diag');
553     $testfile = $CFG->dataroot.'/diag/public.txt';
554     if (!file_exists($testfile)) {
555         file_put_contents($testfile, 'test file, do not delete');
556         @chmod($testfile, $CFG->filepermissions);
557     }
558     $teststr = trim(file_get_contents($testfile));
559     if (empty($teststr)) {
560     // hmm, strange
561         return INSECURE_DATAROOT_WARNING;
562     }
564     $testurl = $datarooturl.'/diag/public.txt';
565     if (extension_loaded('curl') and
566         !(stripos(ini_get('disable_functions'), 'curl_init') !== FALSE) and
567         !(stripos(ini_get('disable_functions'), 'curl_setop') !== FALSE) and
568         ($ch = @curl_init($testurl)) !== false) {
569         curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
570         curl_setopt($ch, CURLOPT_HEADER, false);
571         $data = curl_exec($ch);
572         if (!curl_errno($ch)) {
573             $data = trim($data);
574             if ($data === $teststr) {
575                 curl_close($ch);
576                 return INSECURE_DATAROOT_ERROR;
577             }
578         }
579         curl_close($ch);
580     }
582     if ($data = @file_get_contents($testurl)) {
583         $data = trim($data);
584         if ($data === $teststr) {
585             return INSECURE_DATAROOT_ERROR;
586         }
587     }
589     preg_match('|https?://([^/]+)|i', $testurl, $matches);
590     $sitename = $matches[1];
591     $error = 0;
592     if ($fp = @fsockopen($sitename, 80, $error)) {
593         preg_match('|https?://[^/]+(.*)|i', $testurl, $matches);
594         $localurl = $matches[1];
595         $out = "GET $localurl HTTP/1.1\r\n";
596         $out .= "Host: $sitename\r\n";
597         $out .= "Connection: Close\r\n\r\n";
598         fwrite($fp, $out);
599         $data = '';
600         $incoming = false;
601         while (!feof($fp)) {
602             if ($incoming) {
603                 $data .= fgets($fp, 1024);
604             } else if (@fgets($fp, 1024) === "\r\n") {
605                     $incoming = true;
606                 }
607         }
608         fclose($fp);
609         $data = trim($data);
610         if ($data === $teststr) {
611             return INSECURE_DATAROOT_ERROR;
612         }
613     }
615     return INSECURE_DATAROOT_WARNING;
618 /**
619  * Enables CLI maintenance mode by creating new dataroot/climaintenance.html file.
620  */
621 function enable_cli_maintenance_mode() {
622     global $CFG;
624     if (file_exists("$CFG->dataroot/climaintenance.html")) {
625         unlink("$CFG->dataroot/climaintenance.html");
626     }
628     if (isset($CFG->maintenance_message) and !html_is_blank($CFG->maintenance_message)) {
629         $data = $CFG->maintenance_message;
630         $data = bootstrap_renderer::early_error_content($data, null, null, null);
631         $data = bootstrap_renderer::plain_page(get_string('sitemaintenance', 'admin'), $data);
633     } else if (file_exists("$CFG->dataroot/climaintenance.template.html")) {
634         $data = file_get_contents("$CFG->dataroot/climaintenance.template.html");
636     } else {
637         $data = get_string('sitemaintenance', 'admin');
638         $data = bootstrap_renderer::early_error_content($data, null, null, null);
639         $data = bootstrap_renderer::plain_page(get_string('sitemaintenance', 'admin'), $data);
640     }
642     file_put_contents("$CFG->dataroot/climaintenance.html", $data);
643     chmod("$CFG->dataroot/climaintenance.html", $CFG->filepermissions);
646 /// CLASS DEFINITIONS /////////////////////////////////////////////////////////
649 /**
650  * Interface for anything appearing in the admin tree
651  *
652  * The interface that is implemented by anything that appears in the admin tree
653  * block. It forces inheriting classes to define a method for checking user permissions
654  * and methods for finding something in the admin tree.
655  *
656  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
657  */
658 interface part_of_admin_tree {
660 /**
661  * Finds a named part_of_admin_tree.
662  *
663  * Used to find a part_of_admin_tree. If a class only inherits part_of_admin_tree
664  * and not parentable_part_of_admin_tree, then this function should only check if
665  * $this->name matches $name. If it does, it should return a reference to $this,
666  * otherwise, it should return a reference to NULL.
667  *
668  * If a class inherits parentable_part_of_admin_tree, this method should be called
669  * recursively on all child objects (assuming, of course, the parent object's name
670  * doesn't match the search criterion).
671  *
672  * @param string $name The internal name of the part_of_admin_tree we're searching for.
673  * @return mixed An object reference or a NULL reference.
674  */
675     public function locate($name);
677     /**
678      * Removes named part_of_admin_tree.
679      *
680      * @param string $name The internal name of the part_of_admin_tree we want to remove.
681      * @return bool success.
682      */
683     public function prune($name);
685     /**
686      * Search using query
687      * @param string $query
688      * @return mixed array-object structure of found settings and pages
689      */
690     public function search($query);
692     /**
693      * Verifies current user's access to this part_of_admin_tree.
694      *
695      * Used to check if the current user has access to this part of the admin tree or
696      * not. If a class only inherits part_of_admin_tree and not parentable_part_of_admin_tree,
697      * then this method is usually just a call to has_capability() in the site context.
698      *
699      * If a class inherits parentable_part_of_admin_tree, this method should return the
700      * logical OR of the return of check_access() on all child objects.
701      *
702      * @return bool True if the user has access, false if she doesn't.
703      */
704     public function check_access();
706     /**
707      * Mostly useful for removing of some parts of the tree in admin tree block.
708      *
709      * @return True is hidden from normal list view
710      */
711     public function is_hidden();
713     /**
714      * Show we display Save button at the page bottom?
715      * @return bool
716      */
717     public function show_save();
721 /**
722  * Interface implemented by any part_of_admin_tree that has children.
723  *
724  * The interface implemented by any part_of_admin_tree that can be a parent
725  * to other part_of_admin_tree's. (For now, this only includes admin_category.) Apart
726  * from ensuring part_of_admin_tree compliancy, it also ensures inheriting methods
727  * include an add method for adding other part_of_admin_tree objects as children.
728  *
729  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
730  */
731 interface parentable_part_of_admin_tree extends part_of_admin_tree {
733 /**
734  * Adds a part_of_admin_tree object to the admin tree.
735  *
736  * Used to add a part_of_admin_tree object to this object or a child of this
737  * object. $something should only be added if $destinationname matches
738  * $this->name. If it doesn't, add should be called on child objects that are
739  * also parentable_part_of_admin_tree's.
740  *
741  * $something should be appended as the last child in the $destinationname. If the
742  * $beforesibling is specified, $something should be prepended to it. If the given
743  * sibling is not found, $something should be appended to the end of $destinationname
744  * and a developer debugging message should be displayed.
745  *
746  * @param string $destinationname The internal name of the new parent for $something.
747  * @param part_of_admin_tree $something The object to be added.
748  * @return bool True on success, false on failure.
749  */
750     public function add($destinationname, $something, $beforesibling = null);
755 /**
756  * The object used to represent folders (a.k.a. categories) in the admin tree block.
757  *
758  * Each admin_category object contains a number of part_of_admin_tree objects.
759  *
760  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
761  */
762 class admin_category implements parentable_part_of_admin_tree {
764     /** @var part_of_admin_tree[] An array of part_of_admin_tree objects that are this object's children */
765     protected $children;
766     /** @var string An internal name for this category. Must be unique amongst ALL part_of_admin_tree objects */
767     public $name;
768     /** @var string The displayed name for this category. Usually obtained through get_string() */
769     public $visiblename;
770     /** @var bool Should this category be hidden in admin tree block? */
771     public $hidden;
772     /** @var mixed Either a string or an array or strings */
773     public $path;
774     /** @var mixed Either a string or an array or strings */
775     public $visiblepath;
777     /** @var array fast lookup category cache, all categories of one tree point to one cache */
778     protected $category_cache;
780     /** @var bool If set to true children will be sorted when calling {@link admin_category::get_children()} */
781     protected $sort = false;
782     /** @var bool If set to true children will be sorted in ascending order. */
783     protected $sortasc = true;
784     /** @var bool If set to true sub categories and pages will be split and then sorted.. */
785     protected $sortsplit = true;
786     /** @var bool $sorted True if the children have been sorted and don't need resorting */
787     protected $sorted = false;
789     /**
790      * Constructor for an empty admin category
791      *
792      * @param string $name The internal name for this category. Must be unique amongst ALL part_of_admin_tree objects
793      * @param string $visiblename The displayed named for this category. Usually obtained through get_string()
794      * @param bool $hidden hide category in admin tree block, defaults to false
795      */
796     public function __construct($name, $visiblename, $hidden=false) {
797         $this->children    = array();
798         $this->name        = $name;
799         $this->visiblename = $visiblename;
800         $this->hidden      = $hidden;
801     }
803     /**
804      * Returns a reference to the part_of_admin_tree object with internal name $name.
805      *
806      * @param string $name The internal name of the object we want.
807      * @param bool $findpath initialize path and visiblepath arrays
808      * @return mixed A reference to the object with internal name $name if found, otherwise a reference to NULL.
809      *                  defaults to false
810      */
811     public function locate($name, $findpath=false) {
812         if (!isset($this->category_cache[$this->name])) {
813             // somebody much have purged the cache
814             $this->category_cache[$this->name] = $this;
815         }
817         if ($this->name == $name) {
818             if ($findpath) {
819                 $this->visiblepath[] = $this->visiblename;
820                 $this->path[]        = $this->name;
821             }
822             return $this;
823         }
825         // quick category lookup
826         if (!$findpath and isset($this->category_cache[$name])) {
827             return $this->category_cache[$name];
828         }
830         $return = NULL;
831         foreach($this->children as $childid=>$unused) {
832             if ($return = $this->children[$childid]->locate($name, $findpath)) {
833                 break;
834             }
835         }
837         if (!is_null($return) and $findpath) {
838             $return->visiblepath[] = $this->visiblename;
839             $return->path[]        = $this->name;
840         }
842         return $return;
843     }
845     /**
846      * Search using query
847      *
848      * @param string query
849      * @return mixed array-object structure of found settings and pages
850      */
851     public function search($query) {
852         $result = array();
853         foreach ($this->get_children() as $child) {
854             $subsearch = $child->search($query);
855             if (!is_array($subsearch)) {
856                 debugging('Incorrect search result from '.$child->name);
857                 continue;
858             }
859             $result = array_merge($result, $subsearch);
860         }
861         return $result;
862     }
864     /**
865      * Removes part_of_admin_tree object with internal name $name.
866      *
867      * @param string $name The internal name of the object we want to remove.
868      * @return bool success
869      */
870     public function prune($name) {
872         if ($this->name == $name) {
873             return false;  //can not remove itself
874         }
876         foreach($this->children as $precedence => $child) {
877             if ($child->name == $name) {
878                 // clear cache and delete self
879                 while($this->category_cache) {
880                     // delete the cache, but keep the original array address
881                     array_pop($this->category_cache);
882                 }
883                 unset($this->children[$precedence]);
884                 return true;
885             } else if ($this->children[$precedence]->prune($name)) {
886                 return true;
887             }
888         }
889         return false;
890     }
892     /**
893      * Adds a part_of_admin_tree to a child or grandchild (or great-grandchild, and so forth) of this object.
894      *
895      * By default the new part of the tree is appended as the last child of the parent. You
896      * can specify a sibling node that the new part should be prepended to. If the given
897      * sibling is not found, the part is appended to the end (as it would be by default) and
898      * a developer debugging message is displayed.
899      *
900      * @throws coding_exception if the $beforesibling is empty string or is not string at all.
901      * @param string $destinationame The internal name of the immediate parent that we want for $something.
902      * @param mixed $something A part_of_admin_tree or setting instance to be added.
903      * @param string $beforesibling The name of the parent's child the $something should be prepended to.
904      * @return bool True if successfully added, false if $something can not be added.
905      */
906     public function add($parentname, $something, $beforesibling = null) {
907         global $CFG;
909         $parent = $this->locate($parentname);
910         if (is_null($parent)) {
911             debugging('parent does not exist!');
912             return false;
913         }
915         if ($something instanceof part_of_admin_tree) {
916             if (!($parent instanceof parentable_part_of_admin_tree)) {
917                 debugging('error - parts of tree can be inserted only into parentable parts');
918                 return false;
919             }
920             if ($CFG->debugdeveloper && !is_null($this->locate($something->name))) {
921                 // The name of the node is already used, simply warn the developer that this should not happen.
922                 // It is intentional to check for the debug level before performing the check.
923                 debugging('Duplicate admin page name: ' . $something->name, DEBUG_DEVELOPER);
924             }
925             if (is_null($beforesibling)) {
926                 // Append $something as the parent's last child.
927                 $parent->children[] = $something;
928             } else {
929                 if (!is_string($beforesibling) or trim($beforesibling) === '') {
930                     throw new coding_exception('Unexpected value of the beforesibling parameter');
931                 }
932                 // Try to find the position of the sibling.
933                 $siblingposition = null;
934                 foreach ($parent->children as $childposition => $child) {
935                     if ($child->name === $beforesibling) {
936                         $siblingposition = $childposition;
937                         break;
938                     }
939                 }
940                 if (is_null($siblingposition)) {
941                     debugging('Sibling '.$beforesibling.' not found', DEBUG_DEVELOPER);
942                     $parent->children[] = $something;
943                 } else {
944                     $parent->children = array_merge(
945                         array_slice($parent->children, 0, $siblingposition),
946                         array($something),
947                         array_slice($parent->children, $siblingposition)
948                     );
949                 }
950             }
951             if ($something instanceof admin_category) {
952                 if (isset($this->category_cache[$something->name])) {
953                     debugging('Duplicate admin category name: '.$something->name);
954                 } else {
955                     $this->category_cache[$something->name] = $something;
956                     $something->category_cache =& $this->category_cache;
957                     foreach ($something->children as $child) {
958                         // just in case somebody already added subcategories
959                         if ($child instanceof admin_category) {
960                             if (isset($this->category_cache[$child->name])) {
961                                 debugging('Duplicate admin category name: '.$child->name);
962                             } else {
963                                 $this->category_cache[$child->name] = $child;
964                                 $child->category_cache =& $this->category_cache;
965                             }
966                         }
967                     }
968                 }
969             }
970             return true;
972         } else {
973             debugging('error - can not add this element');
974             return false;
975         }
977     }
979     /**
980      * Checks if the user has access to anything in this category.
981      *
982      * @return bool True if the user has access to at least one child in this category, false otherwise.
983      */
984     public function check_access() {
985         foreach ($this->children as $child) {
986             if ($child->check_access()) {
987                 return true;
988             }
989         }
990         return false;
991     }
993     /**
994      * Is this category hidden in admin tree block?
995      *
996      * @return bool True if hidden
997      */
998     public function is_hidden() {
999         return $this->hidden;
1000     }
1002     /**
1003      * Show we display Save button at the page bottom?
1004      * @return bool
1005      */
1006     public function show_save() {
1007         foreach ($this->children as $child) {
1008             if ($child->show_save()) {
1009                 return true;
1010             }
1011         }
1012         return false;
1013     }
1015     /**
1016      * Sets sorting on this category.
1017      *
1018      * Please note this function doesn't actually do the sorting.
1019      * It can be called anytime.
1020      * Sorting occurs when the user calls get_children.
1021      * Code using the children array directly won't see the sorted results.
1022      *
1023      * @param bool $sort If set to true children will be sorted, if false they won't be.
1024      * @param bool $asc If true sorting will be ascending, otherwise descending.
1025      * @param bool $split If true we sort pages and sub categories separately.
1026      */
1027     public function set_sorting($sort, $asc = true, $split = true) {
1028         $this->sort = (bool)$sort;
1029         $this->sortasc = (bool)$asc;
1030         $this->sortsplit = (bool)$split;
1031     }
1033     /**
1034      * Returns the children associated with this category.
1035      *
1036      * @return part_of_admin_tree[]
1037      */
1038     public function get_children() {
1039         // If we should sort and it hasn't already been sorted.
1040         if ($this->sort && !$this->sorted) {
1041             if ($this->sortsplit) {
1042                 $categories = array();
1043                 $pages = array();
1044                 foreach ($this->children as $child) {
1045                     if ($child instanceof admin_category) {
1046                         $categories[] = $child;
1047                     } else {
1048                         $pages[] = $child;
1049                     }
1050                 }
1051                 core_collator::asort_objects_by_property($categories, 'visiblename');
1052                 core_collator::asort_objects_by_property($pages, 'visiblename');
1053                 if (!$this->sortasc) {
1054                     $categories = array_reverse($categories);
1055                     $pages = array_reverse($pages);
1056                 }
1057                 $this->children = array_merge($pages, $categories);
1058             } else {
1059                 core_collator::asort_objects_by_property($this->children, 'visiblename');
1060                 if (!$this->sortasc) {
1061                     $this->children = array_reverse($this->children);
1062                 }
1063             }
1064             $this->sorted = true;
1065         }
1066         return $this->children;
1067     }
1069     /**
1070      * Magically gets a property from this object.
1071      *
1072      * @param $property
1073      * @return part_of_admin_tree[]
1074      * @throws coding_exception
1075      */
1076     public function __get($property) {
1077         if ($property === 'children') {
1078             return $this->get_children();
1079         }
1080         throw new coding_exception('Invalid property requested.');
1081     }
1083     /**
1084      * Magically sets a property against this object.
1085      *
1086      * @param string $property
1087      * @param mixed $value
1088      * @throws coding_exception
1089      */
1090     public function __set($property, $value) {
1091         if ($property === 'children') {
1092             $this->sorted = false;
1093             $this->children = $value;
1094         } else {
1095             throw new coding_exception('Invalid property requested.');
1096         }
1097     }
1099     /**
1100      * Checks if an inaccessible property is set.
1101      *
1102      * @param string $property
1103      * @return bool
1104      * @throws coding_exception
1105      */
1106     public function __isset($property) {
1107         if ($property === 'children') {
1108             return isset($this->children);
1109         }
1110         throw new coding_exception('Invalid property requested.');
1111     }
1115 /**
1116  * Root of admin settings tree, does not have any parent.
1117  *
1118  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1119  */
1120 class admin_root extends admin_category {
1121 /** @var array List of errors */
1122     public $errors;
1123     /** @var string search query */
1124     public $search;
1125     /** @var bool full tree flag - true means all settings required, false only pages required */
1126     public $fulltree;
1127     /** @var bool flag indicating loaded tree */
1128     public $loaded;
1129     /** @var mixed site custom defaults overriding defaults in settings files*/
1130     public $custom_defaults;
1132     /**
1133      * @param bool $fulltree true means all settings required,
1134      *                            false only pages required
1135      */
1136     public function __construct($fulltree) {
1137         global $CFG;
1139         parent::__construct('root', get_string('administration'), false);
1140         $this->errors   = array();
1141         $this->search   = '';
1142         $this->fulltree = $fulltree;
1143         $this->loaded   = false;
1145         $this->category_cache = array();
1147         // load custom defaults if found
1148         $this->custom_defaults = null;
1149         $defaultsfile = "$CFG->dirroot/local/defaults.php";
1150         if (is_readable($defaultsfile)) {
1151             $defaults = array();
1152             include($defaultsfile);
1153             if (is_array($defaults) and count($defaults)) {
1154                 $this->custom_defaults = $defaults;
1155             }
1156         }
1157     }
1159     /**
1160      * Empties children array, and sets loaded to false
1161      *
1162      * @param bool $requirefulltree
1163      */
1164     public function purge_children($requirefulltree) {
1165         $this->children = array();
1166         $this->fulltree = ($requirefulltree || $this->fulltree);
1167         $this->loaded   = false;
1168         //break circular dependencies - this helps PHP 5.2
1169         while($this->category_cache) {
1170             array_pop($this->category_cache);
1171         }
1172         $this->category_cache = array();
1173     }
1177 /**
1178  * Links external PHP pages into the admin tree.
1179  *
1180  * See detailed usage example at the top of this document (adminlib.php)
1181  *
1182  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1183  */
1184 class admin_externalpage implements part_of_admin_tree {
1186     /** @var string An internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects */
1187     public $name;
1189     /** @var string The displayed name for this external page. Usually obtained through get_string(). */
1190     public $visiblename;
1192     /** @var string The external URL that we should link to when someone requests this external page. */
1193     public $url;
1195     /** @var string The role capability/permission a user must have to access this external page. */
1196     public $req_capability;
1198     /** @var object The context in which capability/permission should be checked, default is site context. */
1199     public $context;
1201     /** @var bool hidden in admin tree block. */
1202     public $hidden;
1204     /** @var mixed either string or array of string */
1205     public $path;
1207     /** @var array list of visible names of page parents */
1208     public $visiblepath;
1210     /**
1211      * Constructor for adding an external page into the admin tree.
1212      *
1213      * @param string $name The internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects.
1214      * @param string $visiblename The displayed name for this external page. Usually obtained through get_string().
1215      * @param string $url The external URL that we should link to when someone requests this external page.
1216      * @param mixed $req_capability The role capability/permission a user must have to access this external page. Defaults to 'moodle/site:config'.
1217      * @param boolean $hidden Is this external page hidden in admin tree block? Default false.
1218      * @param stdClass $context The context the page relates to. Not sure what happens
1219      *      if you specify something other than system or front page. Defaults to system.
1220      */
1221     public function __construct($name, $visiblename, $url, $req_capability='moodle/site:config', $hidden=false, $context=NULL) {
1222         $this->name        = $name;
1223         $this->visiblename = $visiblename;
1224         $this->url         = $url;
1225         if (is_array($req_capability)) {
1226             $this->req_capability = $req_capability;
1227         } else {
1228             $this->req_capability = array($req_capability);
1229         }
1230         $this->hidden = $hidden;
1231         $this->context = $context;
1232     }
1234     /**
1235      * Returns a reference to the part_of_admin_tree object with internal name $name.
1236      *
1237      * @param string $name The internal name of the object we want.
1238      * @param bool $findpath defaults to false
1239      * @return mixed A reference to the object with internal name $name if found, otherwise a reference to NULL.
1240      */
1241     public function locate($name, $findpath=false) {
1242         if ($this->name == $name) {
1243             if ($findpath) {
1244                 $this->visiblepath = array($this->visiblename);
1245                 $this->path        = array($this->name);
1246             }
1247             return $this;
1248         } else {
1249             $return = NULL;
1250             return $return;
1251         }
1252     }
1254     /**
1255      * This function always returns false, required function by interface
1256      *
1257      * @param string $name
1258      * @return false
1259      */
1260     public function prune($name) {
1261         return false;
1262     }
1264     /**
1265      * Search using query
1266      *
1267      * @param string $query
1268      * @return mixed array-object structure of found settings and pages
1269      */
1270     public function search($query) {
1271         $found = false;
1272         if (strpos(strtolower($this->name), $query) !== false) {
1273             $found = true;
1274         } else if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
1275                 $found = true;
1276             }
1277         if ($found) {
1278             $result = new stdClass();
1279             $result->page     = $this;
1280             $result->settings = array();
1281             return array($this->name => $result);
1282         } else {
1283             return array();
1284         }
1285     }
1287     /**
1288      * Determines if the current user has access to this external page based on $this->req_capability.
1289      *
1290      * @return bool True if user has access, false otherwise.
1291      */
1292     public function check_access() {
1293         global $CFG;
1294         $context = empty($this->context) ? context_system::instance() : $this->context;
1295         foreach($this->req_capability as $cap) {
1296             if (has_capability($cap, $context)) {
1297                 return true;
1298             }
1299         }
1300         return false;
1301     }
1303     /**
1304      * Is this external page hidden in admin tree block?
1305      *
1306      * @return bool True if hidden
1307      */
1308     public function is_hidden() {
1309         return $this->hidden;
1310     }
1312     /**
1313      * Show we display Save button at the page bottom?
1314      * @return bool
1315      */
1316     public function show_save() {
1317         return false;
1318     }
1322 /**
1323  * Used to group a number of admin_setting objects into a page and add them to the admin tree.
1324  *
1325  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1326  */
1327 class admin_settingpage implements part_of_admin_tree {
1329     /** @var string An internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects */
1330     public $name;
1332     /** @var string The displayed name for this external page. Usually obtained through get_string(). */
1333     public $visiblename;
1335     /** @var mixed An array of admin_setting objects that are part of this setting page. */
1336     public $settings;
1338     /** @var string The role capability/permission a user must have to access this external page. */
1339     public $req_capability;
1341     /** @var object The context in which capability/permission should be checked, default is site context. */
1342     public $context;
1344     /** @var bool hidden in admin tree block. */
1345     public $hidden;
1347     /** @var mixed string of paths or array of strings of paths */
1348     public $path;
1350     /** @var array list of visible names of page parents */
1351     public $visiblepath;
1353     /**
1354      * see admin_settingpage for details of this function
1355      *
1356      * @param string $name The internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects.
1357      * @param string $visiblename The displayed name for this external page. Usually obtained through get_string().
1358      * @param mixed $req_capability The role capability/permission a user must have to access this external page. Defaults to 'moodle/site:config'.
1359      * @param boolean $hidden Is this external page hidden in admin tree block? Default false.
1360      * @param stdClass $context The context the page relates to. Not sure what happens
1361      *      if you specify something other than system or front page. Defaults to system.
1362      */
1363     public function __construct($name, $visiblename, $req_capability='moodle/site:config', $hidden=false, $context=NULL) {
1364         $this->settings    = new stdClass();
1365         $this->name        = $name;
1366         $this->visiblename = $visiblename;
1367         if (is_array($req_capability)) {
1368             $this->req_capability = $req_capability;
1369         } else {
1370             $this->req_capability = array($req_capability);
1371         }
1372         $this->hidden      = $hidden;
1373         $this->context     = $context;
1374     }
1376     /**
1377      * see admin_category
1378      *
1379      * @param string $name
1380      * @param bool $findpath
1381      * @return mixed Object (this) if name ==  this->name, else returns null
1382      */
1383     public function locate($name, $findpath=false) {
1384         if ($this->name == $name) {
1385             if ($findpath) {
1386                 $this->visiblepath = array($this->visiblename);
1387                 $this->path        = array($this->name);
1388             }
1389             return $this;
1390         } else {
1391             $return = NULL;
1392             return $return;
1393         }
1394     }
1396     /**
1397      * Search string in settings page.
1398      *
1399      * @param string $query
1400      * @return array
1401      */
1402     public function search($query) {
1403         $found = array();
1405         foreach ($this->settings as $setting) {
1406             if ($setting->is_related($query)) {
1407                 $found[] = $setting;
1408             }
1409         }
1411         if ($found) {
1412             $result = new stdClass();
1413             $result->page     = $this;
1414             $result->settings = $found;
1415             return array($this->name => $result);
1416         }
1418         $found = false;
1419         if (strpos(strtolower($this->name), $query) !== false) {
1420             $found = true;
1421         } else if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
1422                 $found = true;
1423             }
1424         if ($found) {
1425             $result = new stdClass();
1426             $result->page     = $this;
1427             $result->settings = array();
1428             return array($this->name => $result);
1429         } else {
1430             return array();
1431         }
1432     }
1434     /**
1435      * This function always returns false, required by interface
1436      *
1437      * @param string $name
1438      * @return bool Always false
1439      */
1440     public function prune($name) {
1441         return false;
1442     }
1444     /**
1445      * adds an admin_setting to this admin_settingpage
1446      *
1447      * 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
1448      * n.b. each admin_setting in an admin_settingpage must have a unique internal name
1449      *
1450      * @param object $setting is the admin_setting object you want to add
1451      * @return bool true if successful, false if not
1452      */
1453     public function add($setting) {
1454         if (!($setting instanceof admin_setting)) {
1455             debugging('error - not a setting instance');
1456             return false;
1457         }
1459         $name = $setting->name;
1460         if ($setting->plugin) {
1461             $name = $setting->plugin . $name;
1462         }
1463         $this->settings->{$name} = $setting;
1464         return true;
1465     }
1467     /**
1468      * see admin_externalpage
1469      *
1470      * @return bool Returns true for yes false for no
1471      */
1472     public function check_access() {
1473         global $CFG;
1474         $context = empty($this->context) ? context_system::instance() : $this->context;
1475         foreach($this->req_capability as $cap) {
1476             if (has_capability($cap, $context)) {
1477                 return true;
1478             }
1479         }
1480         return false;
1481     }
1483     /**
1484      * outputs this page as html in a table (suitable for inclusion in an admin pagetype)
1485      * @return string Returns an XHTML string
1486      */
1487     public function output_html() {
1488         $adminroot = admin_get_root();
1489         $return = '<fieldset>'."\n".'<div class="clearer"><!-- --></div>'."\n";
1490         foreach($this->settings as $setting) {
1491             $fullname = $setting->get_full_name();
1492             if (array_key_exists($fullname, $adminroot->errors)) {
1493                 $data = $adminroot->errors[$fullname]->data;
1494             } else {
1495                 $data = $setting->get_setting();
1496                 // do not use defaults if settings not available - upgrade settings handles the defaults!
1497             }
1498             $return .= $setting->output_html($data);
1499         }
1500         $return .= '</fieldset>';
1501         return $return;
1502     }
1504     /**
1505      * Is this settings page hidden in admin tree block?
1506      *
1507      * @return bool True if hidden
1508      */
1509     public function is_hidden() {
1510         return $this->hidden;
1511     }
1513     /**
1514      * Show we display Save button at the page bottom?
1515      * @return bool
1516      */
1517     public function show_save() {
1518         foreach($this->settings as $setting) {
1519             if (empty($setting->nosave)) {
1520                 return true;
1521             }
1522         }
1523         return false;
1524     }
1528 /**
1529  * Admin settings class. Only exists on setting pages.
1530  * Read & write happens at this level; no authentication.
1531  *
1532  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1533  */
1534 abstract class admin_setting {
1535     /** @var string unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins. */
1536     public $name;
1537     /** @var string localised name */
1538     public $visiblename;
1539     /** @var string localised long description in Markdown format */
1540     public $description;
1541     /** @var mixed Can be string or array of string */
1542     public $defaultsetting;
1543     /** @var string */
1544     public $updatedcallback;
1545     /** @var mixed can be String or Null.  Null means main config table */
1546     public $plugin; // null means main config table
1547     /** @var bool true indicates this setting does not actually save anything, just information */
1548     public $nosave = false;
1549     /** @var bool if set, indicates that a change to this setting requires rebuild course cache */
1550     public $affectsmodinfo = false;
1551     /** @var array of admin_setting_flag - These are extra checkboxes attached to a setting. */
1552     private $flags = array();
1553     /** @var bool Whether this field must be forced LTR. */
1554     private $forceltr = null;
1556     /**
1557      * Constructor
1558      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
1559      *                     or 'myplugin/mysetting' for ones in config_plugins.
1560      * @param string $visiblename localised name
1561      * @param string $description localised long description
1562      * @param mixed $defaultsetting string or array depending on implementation
1563      */
1564     public function __construct($name, $visiblename, $description, $defaultsetting) {
1565         $this->parse_setting_name($name);
1566         $this->visiblename    = $visiblename;
1567         $this->description    = $description;
1568         $this->defaultsetting = $defaultsetting;
1569     }
1571     /**
1572      * Generic function to add a flag to this admin setting.
1573      *
1574      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1575      * @param bool $default - The default for the flag
1576      * @param string $shortname - The shortname for this flag. Used as a suffix for the setting name.
1577      * @param string $displayname - The display name for this flag. Used as a label next to the checkbox.
1578      */
1579     protected function set_flag_options($enabled, $default, $shortname, $displayname) {
1580         if (empty($this->flags[$shortname])) {
1581             $this->flags[$shortname] = new admin_setting_flag($enabled, $default, $shortname, $displayname);
1582         } else {
1583             $this->flags[$shortname]->set_options($enabled, $default);
1584         }
1585     }
1587     /**
1588      * Set the enabled options flag on this admin setting.
1589      *
1590      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1591      * @param bool $default - The default for the flag
1592      */
1593     public function set_enabled_flag_options($enabled, $default) {
1594         $this->set_flag_options($enabled, $default, 'enabled', new lang_string('enabled', 'core_admin'));
1595     }
1597     /**
1598      * Set the advanced options flag on this admin setting.
1599      *
1600      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1601      * @param bool $default - The default for the flag
1602      */
1603     public function set_advanced_flag_options($enabled, $default) {
1604         $this->set_flag_options($enabled, $default, 'adv', new lang_string('advanced'));
1605     }
1608     /**
1609      * Set the locked options flag on this admin setting.
1610      *
1611      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1612      * @param bool $default - The default for the flag
1613      */
1614     public function set_locked_flag_options($enabled, $default) {
1615         $this->set_flag_options($enabled, $default, 'locked', new lang_string('locked', 'core_admin'));
1616     }
1618     /**
1619      * Get the currently saved value for a setting flag
1620      *
1621      * @param admin_setting_flag $flag - One of the admin_setting_flag for this admin_setting.
1622      * @return bool
1623      */
1624     public function get_setting_flag_value(admin_setting_flag $flag) {
1625         $value = $this->config_read($this->name . '_' . $flag->get_shortname());
1626         if (!isset($value)) {
1627             $value = $flag->get_default();
1628         }
1630         return !empty($value);
1631     }
1633     /**
1634      * Get the list of defaults for the flags on this setting.
1635      *
1636      * @param array of strings describing the defaults for this setting. This is appended to by this function.
1637      */
1638     public function get_setting_flag_defaults(& $defaults) {
1639         foreach ($this->flags as $flag) {
1640             if ($flag->is_enabled() && $flag->get_default()) {
1641                 $defaults[] = $flag->get_displayname();
1642             }
1643         }
1644     }
1646     /**
1647      * Output the input fields for the advanced and locked flags on this setting.
1648      *
1649      * @param bool $adv - The current value of the advanced flag.
1650      * @param bool $locked - The current value of the locked flag.
1651      * @return string $output - The html for the flags.
1652      */
1653     public function output_setting_flags() {
1654         $output = '';
1656         foreach ($this->flags as $flag) {
1657             if ($flag->is_enabled()) {
1658                 $output .= $flag->output_setting_flag($this);
1659             }
1660         }
1662         if (!empty($output)) {
1663             return html_writer::tag('span', $output, array('class' => 'adminsettingsflags'));
1664         }
1665         return $output;
1666     }
1668     /**
1669      * Write the values of the flags for this admin setting.
1670      *
1671      * @param array $data - The data submitted from the form or null to set the default value for new installs.
1672      * @return bool - true if successful.
1673      */
1674     public function write_setting_flags($data) {
1675         $result = true;
1676         foreach ($this->flags as $flag) {
1677             $result = $result && $flag->write_setting_flag($this, $data);
1678         }
1679         return $result;
1680     }
1682     /**
1683      * Set up $this->name and potentially $this->plugin
1684      *
1685      * Set up $this->name and possibly $this->plugin based on whether $name looks
1686      * like 'settingname' or 'plugin/settingname'. Also, do some sanity checking
1687      * on the names, that is, output a developer debug warning if the name
1688      * contains anything other than [a-zA-Z0-9_]+.
1689      *
1690      * @param string $name the setting name passed in to the constructor.
1691      */
1692     private function parse_setting_name($name) {
1693         $bits = explode('/', $name);
1694         if (count($bits) > 2) {
1695             throw new moodle_exception('invalidadminsettingname', '', '', $name);
1696         }
1697         $this->name = array_pop($bits);
1698         if (!preg_match('/^[a-zA-Z0-9_]+$/', $this->name)) {
1699             throw new moodle_exception('invalidadminsettingname', '', '', $name);
1700         }
1701         if (!empty($bits)) {
1702             $this->plugin = array_pop($bits);
1703             if ($this->plugin === 'moodle') {
1704                 $this->plugin = null;
1705             } else if (!preg_match('/^[a-zA-Z0-9_]+$/', $this->plugin)) {
1706                     throw new moodle_exception('invalidadminsettingname', '', '', $name);
1707                 }
1708         }
1709     }
1711     /**
1712      * Returns the fullname prefixed by the plugin
1713      * @return string
1714      */
1715     public function get_full_name() {
1716         return 's_'.$this->plugin.'_'.$this->name;
1717     }
1719     /**
1720      * Returns the ID string based on plugin and name
1721      * @return string
1722      */
1723     public function get_id() {
1724         return 'id_s_'.$this->plugin.'_'.$this->name;
1725     }
1727     /**
1728      * @param bool $affectsmodinfo If true, changes to this setting will
1729      *   cause the course cache to be rebuilt
1730      */
1731     public function set_affects_modinfo($affectsmodinfo) {
1732         $this->affectsmodinfo = $affectsmodinfo;
1733     }
1735     /**
1736      * Returns the config if possible
1737      *
1738      * @return mixed returns config if successful else null
1739      */
1740     public function config_read($name) {
1741         global $CFG;
1742         if (!empty($this->plugin)) {
1743             $value = get_config($this->plugin, $name);
1744             return $value === false ? NULL : $value;
1746         } else {
1747             if (isset($CFG->$name)) {
1748                 return $CFG->$name;
1749             } else {
1750                 return NULL;
1751             }
1752         }
1753     }
1755     /**
1756      * Used to set a config pair and log change
1757      *
1758      * @param string $name
1759      * @param mixed $value Gets converted to string if not null
1760      * @return bool Write setting to config table
1761      */
1762     public function config_write($name, $value) {
1763         global $DB, $USER, $CFG;
1765         if ($this->nosave) {
1766             return true;
1767         }
1769         // make sure it is a real change
1770         $oldvalue = get_config($this->plugin, $name);
1771         $oldvalue = ($oldvalue === false) ? null : $oldvalue; // normalise
1772         $value = is_null($value) ? null : (string)$value;
1774         if ($oldvalue === $value) {
1775             return true;
1776         }
1778         // store change
1779         set_config($name, $value, $this->plugin);
1781         // Some admin settings affect course modinfo
1782         if ($this->affectsmodinfo) {
1783             // Clear course cache for all courses
1784             rebuild_course_cache(0, true);
1785         }
1787         $this->add_to_config_log($name, $oldvalue, $value);
1789         return true; // BC only
1790     }
1792     /**
1793      * Log config changes if necessary.
1794      * @param string $name
1795      * @param string $oldvalue
1796      * @param string $value
1797      */
1798     protected function add_to_config_log($name, $oldvalue, $value) {
1799         add_to_config_log($name, $oldvalue, $value, $this->plugin);
1800     }
1802     /**
1803      * Returns current value of this setting
1804      * @return mixed array or string depending on instance, NULL means not set yet
1805      */
1806     public abstract function get_setting();
1808     /**
1809      * Returns default setting if exists
1810      * @return mixed array or string depending on instance; NULL means no default, user must supply
1811      */
1812     public function get_defaultsetting() {
1813         $adminroot =  admin_get_root(false, false);
1814         if (!empty($adminroot->custom_defaults)) {
1815             $plugin = is_null($this->plugin) ? 'moodle' : $this->plugin;
1816             if (isset($adminroot->custom_defaults[$plugin])) {
1817                 if (array_key_exists($this->name, $adminroot->custom_defaults[$plugin])) { // null is valid value here ;-)
1818                     return $adminroot->custom_defaults[$plugin][$this->name];
1819                 }
1820             }
1821         }
1822         return $this->defaultsetting;
1823     }
1825     /**
1826      * Store new setting
1827      *
1828      * @param mixed $data string or array, must not be NULL
1829      * @return string empty string if ok, string error message otherwise
1830      */
1831     public abstract function write_setting($data);
1833     /**
1834      * Return part of form with setting
1835      * This function should always be overwritten
1836      *
1837      * @param mixed $data array or string depending on setting
1838      * @param string $query
1839      * @return string
1840      */
1841     public function output_html($data, $query='') {
1842     // should be overridden
1843         return;
1844     }
1846     /**
1847      * Function called if setting updated - cleanup, cache reset, etc.
1848      * @param string $functionname Sets the function name
1849      * @return void
1850      */
1851     public function set_updatedcallback($functionname) {
1852         $this->updatedcallback = $functionname;
1853     }
1855     /**
1856      * Execute postupdatecallback if necessary.
1857      * @param mixed $original original value before write_setting()
1858      * @return bool true if changed, false if not.
1859      */
1860     public function post_write_settings($original) {
1861         // Comparison must work for arrays too.
1862         if (serialize($original) === serialize($this->get_setting())) {
1863             return false;
1864         }
1866         $callbackfunction = $this->updatedcallback;
1867         if (!empty($callbackfunction) and is_callable($callbackfunction)) {
1868             $callbackfunction($this->get_full_name());
1869         }
1870         return true;
1871     }
1873     /**
1874      * Is setting related to query text - used when searching
1875      * @param string $query
1876      * @return bool
1877      */
1878     public function is_related($query) {
1879         if (strpos(strtolower($this->name), $query) !== false) {
1880             return true;
1881         }
1882         if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
1883             return true;
1884         }
1885         if (strpos(core_text::strtolower($this->description), $query) !== false) {
1886             return true;
1887         }
1888         $current = $this->get_setting();
1889         if (!is_null($current)) {
1890             if (is_string($current)) {
1891                 if (strpos(core_text::strtolower($current), $query) !== false) {
1892                     return true;
1893                 }
1894             }
1895         }
1896         $default = $this->get_defaultsetting();
1897         if (!is_null($default)) {
1898             if (is_string($default)) {
1899                 if (strpos(core_text::strtolower($default), $query) !== false) {
1900                     return true;
1901                 }
1902             }
1903         }
1904         return false;
1905     }
1907     /**
1908      * Get whether this should be displayed in LTR mode.
1909      *
1910      * @return bool|null
1911      */
1912     public function get_force_ltr() {
1913         return $this->forceltr;
1914     }
1916     /**
1917      * Set whether to force LTR or not.
1918      *
1919      * @param bool $value True when forced, false when not force, null when unknown.
1920      */
1921     public function set_force_ltr($value) {
1922         $this->forceltr = $value;
1923     }
1926 /**
1927  * An additional option that can be applied to an admin setting.
1928  * The currently supported options are 'ADVANCED' and 'LOCKED'.
1929  *
1930  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1931  */
1932 class admin_setting_flag {
1933     /** @var bool Flag to indicate if this option can be toggled for this setting */
1934     private $enabled = false;
1935     /** @var bool Flag to indicate if this option defaults to true or false */
1936     private $default = false;
1937     /** @var string Short string used to create setting name - e.g. 'adv' */
1938     private $shortname = '';
1939     /** @var string String used as the label for this flag */
1940     private $displayname = '';
1941     /** @const Checkbox for this flag is displayed in admin page */
1942     const ENABLED = true;
1943     /** @const Checkbox for this flag is not displayed in admin page */
1944     const DISABLED = false;
1946     /**
1947      * Constructor
1948      *
1949      * @param bool $enabled Can this option can be toggled.
1950      *                      Should be one of admin_setting_flag::ENABLED or admin_setting_flag::DISABLED.
1951      * @param bool $default The default checked state for this setting option.
1952      * @param string $shortname The shortname of this flag. Currently supported flags are 'locked' and 'adv'
1953      * @param string $displayname The displayname of this flag. Used as a label for the flag.
1954      */
1955     public function __construct($enabled, $default, $shortname, $displayname) {
1956         $this->shortname = $shortname;
1957         $this->displayname = $displayname;
1958         $this->set_options($enabled, $default);
1959     }
1961     /**
1962      * Update the values of this setting options class
1963      *
1964      * @param bool $enabled Can this option can be toggled.
1965      *                      Should be one of admin_setting_flag::ENABLED or admin_setting_flag::DISABLED.
1966      * @param bool $default The default checked state for this setting option.
1967      */
1968     public function set_options($enabled, $default) {
1969         $this->enabled = $enabled;
1970         $this->default = $default;
1971     }
1973     /**
1974      * Should this option appear in the interface and be toggleable?
1975      *
1976      * @return bool Is it enabled?
1977      */
1978     public function is_enabled() {
1979         return $this->enabled;
1980     }
1982     /**
1983      * Should this option be checked by default?
1984      *
1985      * @return bool Is it on by default?
1986      */
1987     public function get_default() {
1988         return $this->default;
1989     }
1991     /**
1992      * Return the short name for this flag. e.g. 'adv' or 'locked'
1993      *
1994      * @return string
1995      */
1996     public function get_shortname() {
1997         return $this->shortname;
1998     }
2000     /**
2001      * Return the display name for this flag. e.g. 'Advanced' or 'Locked'
2002      *
2003      * @return string
2004      */
2005     public function get_displayname() {
2006         return $this->displayname;
2007     }
2009     /**
2010      * Save the submitted data for this flag - or set it to the default if $data is null.
2011      *
2012      * @param admin_setting $setting - The admin setting for this flag
2013      * @param array $data - The data submitted from the form or null to set the default value for new installs.
2014      * @return bool
2015      */
2016     public function write_setting_flag(admin_setting $setting, $data) {
2017         $result = true;
2018         if ($this->is_enabled()) {
2019             if (!isset($data)) {
2020                 $value = $this->get_default();
2021             } else {
2022                 $value = !empty($data[$setting->get_full_name() . '_' . $this->get_shortname()]);
2023             }
2024             $result = $setting->config_write($setting->name . '_' . $this->get_shortname(), $value);
2025         }
2027         return $result;
2029     }
2031     /**
2032      * Output the checkbox for this setting flag. Should only be called if the flag is enabled.
2033      *
2034      * @param admin_setting $setting - The admin setting for this flag
2035      * @return string - The html for the checkbox.
2036      */
2037     public function output_setting_flag(admin_setting $setting) {
2038         global $OUTPUT;
2040         $value = $setting->get_setting_flag_value($this);
2042         $context = new stdClass();
2043         $context->id = $setting->get_id() . '_' . $this->get_shortname();
2044         $context->name = $setting->get_full_name() .  '_' . $this->get_shortname();
2045         $context->value = 1;
2046         $context->checked = $value ? true : false;
2047         $context->label = $this->get_displayname();
2049         return $OUTPUT->render_from_template('core_admin/setting_flag', $context);
2050     }
2054 /**
2055  * No setting - just heading and text.
2056  *
2057  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2058  */
2059 class admin_setting_heading extends admin_setting {
2061     /**
2062      * not a setting, just text
2063      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2064      * @param string $heading heading
2065      * @param string $information text in box
2066      */
2067     public function __construct($name, $heading, $information) {
2068         $this->nosave = true;
2069         parent::__construct($name, $heading, $information, '');
2070     }
2072     /**
2073      * Always returns true
2074      * @return bool Always returns true
2075      */
2076     public function get_setting() {
2077         return true;
2078     }
2080     /**
2081      * Always returns true
2082      * @return bool Always returns true
2083      */
2084     public function get_defaultsetting() {
2085         return true;
2086     }
2088     /**
2089      * Never write settings
2090      * @return string Always returns an empty string
2091      */
2092     public function write_setting($data) {
2093     // do not write any setting
2094         return '';
2095     }
2097     /**
2098      * Returns an HTML string
2099      * @return string Returns an HTML string
2100      */
2101     public function output_html($data, $query='') {
2102         global $OUTPUT;
2103         $context = new stdClass();
2104         $context->title = $this->visiblename;
2105         $context->description = $this->description;
2106         $context->descriptionformatted = highlight($query, markdown_to_html($this->description));
2107         return $OUTPUT->render_from_template('core_admin/setting_heading', $context);
2108     }
2112 /**
2113  * The most flexible setting, the user enters text.
2114  *
2115  * This type of field should be used for config settings which are using
2116  * English words and are not localised (passwords, database name, list of values, ...).
2117  *
2118  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2119  */
2120 class admin_setting_configtext extends admin_setting {
2122     /** @var mixed int means PARAM_XXX type, string is a allowed format in regex */
2123     public $paramtype;
2124     /** @var int default field size */
2125     public $size;
2127     /**
2128      * Config text constructor
2129      *
2130      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2131      * @param string $visiblename localised
2132      * @param string $description long localised info
2133      * @param string $defaultsetting
2134      * @param mixed $paramtype int means PARAM_XXX type, string is a allowed format in regex
2135      * @param int $size default field size
2136      */
2137     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $size=null) {
2138         $this->paramtype = $paramtype;
2139         if (!is_null($size)) {
2140             $this->size  = $size;
2141         } else {
2142             $this->size  = ($paramtype === PARAM_INT) ? 5 : 30;
2143         }
2144         parent::__construct($name, $visiblename, $description, $defaultsetting);
2145     }
2147     /**
2148      * Get whether this should be displayed in LTR mode.
2149      *
2150      * Try to guess from the PARAM type unless specifically set.
2151      */
2152     public function get_force_ltr() {
2153         $forceltr = parent::get_force_ltr();
2154         if ($forceltr === null) {
2155             return !is_rtl_compatible($this->paramtype);
2156         }
2157         return $forceltr;
2158     }
2160     /**
2161      * Return the setting
2162      *
2163      * @return mixed returns config if successful else null
2164      */
2165     public function get_setting() {
2166         return $this->config_read($this->name);
2167     }
2169     public function write_setting($data) {
2170         if ($this->paramtype === PARAM_INT and $data === '') {
2171         // do not complain if '' used instead of 0
2172             $data = 0;
2173         }
2174         // $data is a string
2175         $validated = $this->validate($data);
2176         if ($validated !== true) {
2177             return $validated;
2178         }
2179         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
2180     }
2182     /**
2183      * Validate data before storage
2184      * @param string data
2185      * @return mixed true if ok string if error found
2186      */
2187     public function validate($data) {
2188         // allow paramtype to be a custom regex if it is the form of /pattern/
2189         if (preg_match('#^/.*/$#', $this->paramtype)) {
2190             if (preg_match($this->paramtype, $data)) {
2191                 return true;
2192             } else {
2193                 return get_string('validateerror', 'admin');
2194             }
2196         } else if ($this->paramtype === PARAM_RAW) {
2197             return true;
2199         } else {
2200             $cleaned = clean_param($data, $this->paramtype);
2201             if ("$data" === "$cleaned") { // implicit conversion to string is needed to do exact comparison
2202                 return true;
2203             } else {
2204                 return get_string('validateerror', 'admin');
2205             }
2206         }
2207     }
2209     /**
2210      * Return an XHTML string for the setting
2211      * @return string Returns an XHTML string
2212      */
2213     public function output_html($data, $query='') {
2214         global $OUTPUT;
2216         $default = $this->get_defaultsetting();
2217         $context = (object) [
2218             'size' => $this->size,
2219             'id' => $this->get_id(),
2220             'name' => $this->get_full_name(),
2221             'value' => $data,
2222             'forceltr' => $this->get_force_ltr(),
2223         ];
2224         $element = $OUTPUT->render_from_template('core_admin/setting_configtext', $context);
2226         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2227     }
2230 /**
2231  * Text input with a maximum length constraint.
2232  *
2233  * @copyright 2015 onwards Ankit Agarwal
2234  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2235  */
2236 class admin_setting_configtext_with_maxlength extends admin_setting_configtext {
2238     /** @var int maximum number of chars allowed. */
2239     protected $maxlength;
2241     /**
2242      * Config text constructor
2243      *
2244      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
2245      *                     or 'myplugin/mysetting' for ones in config_plugins.
2246      * @param string $visiblename localised
2247      * @param string $description long localised info
2248      * @param string $defaultsetting
2249      * @param mixed $paramtype int means PARAM_XXX type, string is a allowed format in regex
2250      * @param int $size default field size
2251      * @param mixed $maxlength int maxlength allowed, 0 for infinite.
2252      */
2253     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW,
2254                                 $size=null, $maxlength = 0) {
2255         $this->maxlength = $maxlength;
2256         parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype, $size);
2257     }
2259     /**
2260      * Validate data before storage
2261      *
2262      * @param string $data data
2263      * @return mixed true if ok string if error found
2264      */
2265     public function validate($data) {
2266         $parentvalidation = parent::validate($data);
2267         if ($parentvalidation === true) {
2268             if ($this->maxlength > 0) {
2269                 // Max length check.
2270                 $length = core_text::strlen($data);
2271                 if ($length > $this->maxlength) {
2272                     return get_string('maximumchars', 'moodle',  $this->maxlength);
2273                 }
2274                 return true;
2275             } else {
2276                 return true; // No max length check needed.
2277             }
2278         } else {
2279             return $parentvalidation;
2280         }
2281     }
2284 /**
2285  * General text area without html editor.
2286  *
2287  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2288  */
2289 class admin_setting_configtextarea extends admin_setting_configtext {
2290     private $rows;
2291     private $cols;
2293     /**
2294      * @param string $name
2295      * @param string $visiblename
2296      * @param string $description
2297      * @param mixed $defaultsetting string or array
2298      * @param mixed $paramtype
2299      * @param string $cols The number of columns to make the editor
2300      * @param string $rows The number of rows to make the editor
2301      */
2302     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $cols='60', $rows='8') {
2303         $this->rows = $rows;
2304         $this->cols = $cols;
2305         parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype);
2306     }
2308     /**
2309      * Returns an XHTML string for the editor
2310      *
2311      * @param string $data
2312      * @param string $query
2313      * @return string XHTML string for the editor
2314      */
2315     public function output_html($data, $query='') {
2316         global $OUTPUT;
2318         $default = $this->get_defaultsetting();
2319         $defaultinfo = $default;
2320         if (!is_null($default) and $default !== '') {
2321             $defaultinfo = "\n".$default;
2322         }
2324         $context = (object) [
2325             'cols' => $this->cols,
2326             'rows' => $this->rows,
2327             'id' => $this->get_id(),
2328             'name' => $this->get_full_name(),
2329             'value' => $data,
2330             'forceltr' => $this->get_force_ltr(),
2331         ];
2332         $element = $OUTPUT->render_from_template('core_admin/setting_configtextarea', $context);
2334         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
2335     }
2338 /**
2339  * General text area with html editor.
2340  */
2341 class admin_setting_confightmleditor extends admin_setting_configtextarea {
2343     /**
2344      * @param string $name
2345      * @param string $visiblename
2346      * @param string $description
2347      * @param mixed $defaultsetting string or array
2348      * @param mixed $paramtype
2349      */
2350     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $cols='60', $rows='8') {
2351         parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype, $cols, $rows);
2352         $this->set_force_ltr(false);
2353         editors_head_setup();
2354     }
2356     /**
2357      * Returns an XHTML string for the editor
2358      *
2359      * @param string $data
2360      * @param string $query
2361      * @return string XHTML string for the editor
2362      */
2363     public function output_html($data, $query='') {
2364         $editor = editors_get_preferred_editor(FORMAT_HTML);
2365         $editor->set_text($data);
2366         $editor->use_editor($this->get_id(), array('noclean'=>true));
2367         return parent::output_html($data, $query);
2368     }
2372 /**
2373  * Password field, allows unmasking of password
2374  *
2375  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2376  */
2377 class admin_setting_configpasswordunmask extends admin_setting_configtext {
2379     /**
2380      * Constructor
2381      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2382      * @param string $visiblename localised
2383      * @param string $description long localised info
2384      * @param string $defaultsetting default password
2385      */
2386     public function __construct($name, $visiblename, $description, $defaultsetting) {
2387         parent::__construct($name, $visiblename, $description, $defaultsetting, PARAM_RAW, 30);
2388     }
2390     /**
2391      * Log config changes if necessary.
2392      * @param string $name
2393      * @param string $oldvalue
2394      * @param string $value
2395      */
2396     protected function add_to_config_log($name, $oldvalue, $value) {
2397         if ($value !== '') {
2398             $value = '********';
2399         }
2400         if ($oldvalue !== '' and $oldvalue !== null) {
2401             $oldvalue = '********';
2402         }
2403         parent::add_to_config_log($name, $oldvalue, $value);
2404     }
2406     /**
2407      * Returns HTML for the field.
2408      *
2409      * @param   string  $data       Value for the field
2410      * @param   string  $query      Passed as final argument for format_admin_setting
2411      * @return  string              Rendered HTML
2412      */
2413     public function output_html($data, $query='') {
2414         global $OUTPUT;
2415         $context = (object) [
2416             'id' => $this->get_id(),
2417             'name' => $this->get_full_name(),
2418             'size' => $this->size,
2419             'value' => $data,
2420             'forceltr' => $this->get_force_ltr(),
2421         ];
2422         $element = $OUTPUT->render_from_template('core_admin/setting_configpasswordunmask', $context);
2423         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', null, $query);
2424     }
2428 /**
2429  * Empty setting used to allow flags (advanced) on settings that can have no sensible default.
2430  * Note: Only advanced makes sense right now - locked does not.
2431  *
2432  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2433  */
2434 class admin_setting_configempty extends admin_setting_configtext {
2436     /**
2437      * @param string $name
2438      * @param string $visiblename
2439      * @param string $description
2440      */
2441     public function __construct($name, $visiblename, $description) {
2442         parent::__construct($name, $visiblename, $description, '', PARAM_RAW);
2443     }
2445     /**
2446      * Returns an XHTML string for the hidden field
2447      *
2448      * @param string $data
2449      * @param string $query
2450      * @return string XHTML string for the editor
2451      */
2452     public function output_html($data, $query='') {
2453         global $OUTPUT;
2455         $context = (object) [
2456             'id' => $this->get_id(),
2457             'name' => $this->get_full_name()
2458         ];
2459         $element = $OUTPUT->render_from_template('core_admin/setting_configempty', $context);
2461         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', get_string('none'), $query);
2462     }
2466 /**
2467  * Path to directory
2468  *
2469  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2470  */
2471 class admin_setting_configfile extends admin_setting_configtext {
2472     /**
2473      * Constructor
2474      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2475      * @param string $visiblename localised
2476      * @param string $description long localised info
2477      * @param string $defaultdirectory default directory location
2478      */
2479     public function __construct($name, $visiblename, $description, $defaultdirectory) {
2480         parent::__construct($name, $visiblename, $description, $defaultdirectory, PARAM_RAW, 50);
2481     }
2483     /**
2484      * Returns XHTML for the field
2485      *
2486      * Returns XHTML for the field and also checks whether the file
2487      * specified in $data exists using file_exists()
2488      *
2489      * @param string $data File name and path to use in value attr
2490      * @param string $query
2491      * @return string XHTML field
2492      */
2493     public function output_html($data, $query='') {
2494         global $CFG, $OUTPUT;
2496         $default = $this->get_defaultsetting();
2497         $context = (object) [
2498             'id' => $this->get_id(),
2499             'name' => $this->get_full_name(),
2500             'size' => $this->size,
2501             'value' => $data,
2502             'showvalidity' => !empty($data),
2503             'valid' => $data && file_exists($data),
2504             'readonly' => !empty($CFG->preventexecpath),
2505             'forceltr' => $this->get_force_ltr(),
2506         ];
2508         if ($context->readonly) {
2509             $this->visiblename .= '<div class="form-overridden">'.get_string('execpathnotallowed', 'admin').'</div>';
2510         }
2512         $element = $OUTPUT->render_from_template('core_admin/setting_configfile', $context);
2514         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2515     }
2517     /**
2518      * Checks if execpatch has been disabled in config.php
2519      */
2520     public function write_setting($data) {
2521         global $CFG;
2522         if (!empty($CFG->preventexecpath)) {
2523             if ($this->get_setting() === null) {
2524                 // Use default during installation.
2525                 $data = $this->get_defaultsetting();
2526                 if ($data === null) {
2527                     $data = '';
2528                 }
2529             } else {
2530                 return '';
2531             }
2532         }
2533         return parent::write_setting($data);
2534     }
2539 /**
2540  * Path to executable file
2541  *
2542  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2543  */
2544 class admin_setting_configexecutable extends admin_setting_configfile {
2546     /**
2547      * Returns an XHTML field
2548      *
2549      * @param string $data This is the value for the field
2550      * @param string $query
2551      * @return string XHTML field
2552      */
2553     public function output_html($data, $query='') {
2554         global $CFG, $OUTPUT;
2555         $default = $this->get_defaultsetting();
2556         require_once("$CFG->libdir/filelib.php");
2558         $context = (object) [
2559             'id' => $this->get_id(),
2560             'name' => $this->get_full_name(),
2561             'size' => $this->size,
2562             'value' => $data,
2563             'showvalidity' => !empty($data),
2564             'valid' => $data && file_exists($data) && !is_dir($data) && file_is_executable($data),
2565             'readonly' => !empty($CFG->preventexecpath),
2566             'forceltr' => $this->get_force_ltr()
2567         ];
2569         if (!empty($CFG->preventexecpath)) {
2570             $this->visiblename .= '<div class="form-overridden">'.get_string('execpathnotallowed', 'admin').'</div>';
2571         }
2573         $element = $OUTPUT->render_from_template('core_admin/setting_configexecutable', $context);
2575         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2576     }
2580 /**
2581  * Path to directory
2582  *
2583  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2584  */
2585 class admin_setting_configdirectory extends admin_setting_configfile {
2587     /**
2588      * Returns an XHTML field
2589      *
2590      * @param string $data This is the value for the field
2591      * @param string $query
2592      * @return string XHTML
2593      */
2594     public function output_html($data, $query='') {
2595         global $CFG, $OUTPUT;
2596         $default = $this->get_defaultsetting();
2598         $context = (object) [
2599             'id' => $this->get_id(),
2600             'name' => $this->get_full_name(),
2601             'size' => $this->size,
2602             'value' => $data,
2603             'showvalidity' => !empty($data),
2604             'valid' => $data && file_exists($data) && is_dir($data),
2605             'readonly' => !empty($CFG->preventexecpath),
2606             'forceltr' => $this->get_force_ltr()
2607         ];
2609         if (!empty($CFG->preventexecpath)) {
2610             $this->visiblename .= '<div class="form-overridden">'.get_string('execpathnotallowed', 'admin').'</div>';
2611         }
2613         $element = $OUTPUT->render_from_template('core_admin/setting_configdirectory', $context);
2615         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $default, $query);
2616     }
2620 /**
2621  * Checkbox
2622  *
2623  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2624  */
2625 class admin_setting_configcheckbox extends admin_setting {
2626     /** @var string Value used when checked */
2627     public $yes;
2628     /** @var string Value used when not checked */
2629     public $no;
2631     /**
2632      * Constructor
2633      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2634      * @param string $visiblename localised
2635      * @param string $description long localised info
2636      * @param string $defaultsetting
2637      * @param string $yes value used when checked
2638      * @param string $no value used when not checked
2639      */
2640     public function __construct($name, $visiblename, $description, $defaultsetting, $yes='1', $no='0') {
2641         parent::__construct($name, $visiblename, $description, $defaultsetting);
2642         $this->yes = (string)$yes;
2643         $this->no  = (string)$no;
2644     }
2646     /**
2647      * Retrieves the current setting using the objects name
2648      *
2649      * @return string
2650      */
2651     public function get_setting() {
2652         return $this->config_read($this->name);
2653     }
2655     /**
2656      * Sets the value for the setting
2657      *
2658      * Sets the value for the setting to either the yes or no values
2659      * of the object by comparing $data to yes
2660      *
2661      * @param mixed $data Gets converted to str for comparison against yes value
2662      * @return string empty string or error
2663      */
2664     public function write_setting($data) {
2665         if ((string)$data === $this->yes) { // convert to strings before comparison
2666             $data = $this->yes;
2667         } else {
2668             $data = $this->no;
2669         }
2670         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
2671     }
2673     /**
2674      * Returns an XHTML checkbox field
2675      *
2676      * @param string $data If $data matches yes then checkbox is checked
2677      * @param string $query
2678      * @return string XHTML field
2679      */
2680     public function output_html($data, $query='') {
2681         global $OUTPUT;
2683         $context = (object) [
2684             'id' => $this->get_id(),
2685             'name' => $this->get_full_name(),
2686             'no' => $this->no,
2687             'value' => $this->yes,
2688             'checked' => (string) $data === $this->yes,
2689         ];
2691         $default = $this->get_defaultsetting();
2692         if (!is_null($default)) {
2693             if ((string)$default === $this->yes) {
2694                 $defaultinfo = get_string('checkboxyes', 'admin');
2695             } else {
2696                 $defaultinfo = get_string('checkboxno', 'admin');
2697             }
2698         } else {
2699             $defaultinfo = NULL;
2700         }
2702         $element = $OUTPUT->render_from_template('core_admin/setting_configcheckbox', $context);
2704         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
2705     }
2709 /**
2710  * Multiple checkboxes, each represents different value, stored in csv format
2711  *
2712  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2713  */
2714 class admin_setting_configmulticheckbox extends admin_setting {
2715     /** @var array Array of choices value=>label */
2716     public $choices;
2718     /**
2719      * Constructor: uses parent::__construct
2720      *
2721      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2722      * @param string $visiblename localised
2723      * @param string $description long localised info
2724      * @param array $defaultsetting array of selected
2725      * @param array $choices array of $value=>$label for each checkbox
2726      */
2727     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
2728         $this->choices = $choices;
2729         parent::__construct($name, $visiblename, $description, $defaultsetting);
2730     }
2732     /**
2733      * This public function may be used in ancestors for lazy loading of choices
2734      *
2735      * @todo Check if this function is still required content commented out only returns true
2736      * @return bool true if loaded, false if error
2737      */
2738     public function load_choices() {
2739         /*
2740         if (is_array($this->choices)) {
2741             return true;
2742         }
2743         .... load choices here
2744         */
2745         return true;
2746     }
2748     /**
2749      * Is setting related to query text - used when searching
2750      *
2751      * @param string $query
2752      * @return bool true on related, false on not or failure
2753      */
2754     public function is_related($query) {
2755         if (!$this->load_choices() or empty($this->choices)) {
2756             return false;
2757         }
2758         if (parent::is_related($query)) {
2759             return true;
2760         }
2762         foreach ($this->choices as $desc) {
2763             if (strpos(core_text::strtolower($desc), $query) !== false) {
2764                 return true;
2765             }
2766         }
2767         return false;
2768     }
2770     /**
2771      * Returns the current setting if it is set
2772      *
2773      * @return mixed null if null, else an array
2774      */
2775     public function get_setting() {
2776         $result = $this->config_read($this->name);
2778         if (is_null($result)) {
2779             return NULL;
2780         }
2781         if ($result === '') {
2782             return array();
2783         }
2784         $enabled = explode(',', $result);
2785         $setting = array();
2786         foreach ($enabled as $option) {
2787             $setting[$option] = 1;
2788         }
2789         return $setting;
2790     }
2792     /**
2793      * Saves the setting(s) provided in $data
2794      *
2795      * @param array $data An array of data, if not array returns empty str
2796      * @return mixed empty string on useless data or bool true=success, false=failed
2797      */
2798     public function write_setting($data) {
2799         if (!is_array($data)) {
2800             return ''; // ignore it
2801         }
2802         if (!$this->load_choices() or empty($this->choices)) {
2803             return '';
2804         }
2805         unset($data['xxxxx']);
2806         $result = array();
2807         foreach ($data as $key => $value) {
2808             if ($value and array_key_exists($key, $this->choices)) {
2809                 $result[] = $key;
2810             }
2811         }
2812         return $this->config_write($this->name, implode(',', $result)) ? '' : get_string('errorsetting', 'admin');
2813     }
2815     /**
2816      * Returns XHTML field(s) as required by choices
2817      *
2818      * Relies on data being an array should data ever be another valid vartype with
2819      * acceptable value this may cause a warning/error
2820      * if (!is_array($data)) would fix the problem
2821      *
2822      * @todo Add vartype handling to ensure $data is an array
2823      *
2824      * @param array $data An array of checked values
2825      * @param string $query
2826      * @return string XHTML field
2827      */
2828     public function output_html($data, $query='') {
2829         global $OUTPUT;
2831         if (!$this->load_choices() or empty($this->choices)) {
2832             return '';
2833         }
2835         $default = $this->get_defaultsetting();
2836         if (is_null($default)) {
2837             $default = array();
2838         }
2839         if (is_null($data)) {
2840             $data = array();
2841         }
2843         $context = (object) [
2844             'id' => $this->get_id(),
2845             'name' => $this->get_full_name(),
2846         ];
2848         $options = array();
2849         $defaults = array();
2850         foreach ($this->choices as $key => $description) {
2851             if (!empty($default[$key])) {
2852                 $defaults[] = $description;
2853             }
2855             $options[] = [
2856                 'key' => $key,
2857                 'checked' => !empty($data[$key]),
2858                 'label' => highlightfast($query, $description)
2859             ];
2860         }
2862         if (is_null($default)) {
2863             $defaultinfo = null;
2864         } else if (!empty($defaults)) {
2865             $defaultinfo = implode(', ', $defaults);
2866         } else {
2867             $defaultinfo = get_string('none');
2868         }
2870         $context->options = $options;
2871         $context->hasoptions = !empty($options);
2873         $element = $OUTPUT->render_from_template('core_admin/setting_configmulticheckbox', $context);
2875         return format_admin_setting($this, $this->visiblename, $element, $this->description, false, '', $defaultinfo, $query);
2877     }
2881 /**
2882  * Multiple checkboxes 2, value stored as string 00101011
2883  *
2884  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2885  */
2886 class admin_setting_configmulticheckbox2 extends admin_setting_configmulticheckbox {
2888     /**
2889      * Returns the setting if set
2890      *
2891      * @return mixed null if not set, else an array of set settings
2892      */
2893     public function get_setting() {
2894         $result = $this->config_read($this->name);
2895         if (is_null($result)) {
2896             return NULL;
2897         }
2898         if (!$this->load_choices()) {
2899             return NULL;
2900         }
2901         $result = str_pad($result, count($this->choices), '0');
2902         $result = preg_split('//', $result, -1, PREG_SPLIT_NO_EMPTY);
2903         $setting = array();
2904         foreach ($this->choices as $key=>$unused) {
2905             $value = array_shift($result);
2906             if ($value) {
2907                 $setting[$key] = 1;
2908             }
2909         }
2910         return $setting;
2911     }
2913     /**
2914      * Save setting(s) provided in $data param
2915      *
2916      * @param array $data An array of settings to save
2917      * @return mixed empty string for bad data or bool true=>success, false=>error
2918      */
2919     public function write_setting($data) {
2920         if (!is_array($data)) {
2921             return ''; // ignore it
2922         }
2923         if (!$this->load_choices() or empty($this->choices)) {
2924             return '';
2925         }
2926         $result = '';
2927         foreach ($this->choices as $key=>$unused) {
2928             if (!empty($data[$key])) {
2929                 $result .= '1';
2930             } else {
2931                 $result .= '0';
2932             }
2933         }
2934         return $this->config_write($this->name, $result) ? '' : get_string('errorsetting', 'admin');
2935     }
2939 /**
2940  * Select one value from list
2941  *
2942  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2943  */
2944 class admin_setting_configselect extends admin_setting {
2945     /** @var array Array of choices value=>label */
2946     public $choices;
2947     /** @var array Array of choices grouped using optgroups */
2948     public $optgroups;
2950     /**
2951      * Constructor
2952      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2953      * @param string $visiblename localised
2954      * @param string $description long localised info
2955      * @param string|int $defaultsetting
2956      * @param array $choices array of $value=>$label for each selection
2957      */
2958     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
2959         // Look for optgroup and single options.
2960         if (is_array($choices)) {
2961             $this->choices = [];
2962             foreach ($choices as $key => $val) {
2963                 if (is_array($val)) {
2964                     $this->optgroups[$key] = $val;
2965                     $this->choices = array_merge($this->choices, $val);
2966                 } else {
2967                     $this->choices[$key] = $val;
2968                 }
2969             }
2970         }
2972         parent::__construct($name, $visiblename, $description, $defaultsetting);
2973     }
2975     /**
2976      * This function may be used in ancestors for lazy loading of choices
2977      *
2978      * Override this method if loading of choices is expensive, such
2979      * as when it requires multiple db requests.
2980      *
2981      * @return bool true if loaded, false if error
2982      */
2983     public function load_choices() {
2984         /*
2985         if (is_array($this->choices)) {
2986             return true;
2987         }
2988         .... load choices here
2989         */
2990         return true;
2991     }
2993     /**
2994      * Check if this is $query is related to a choice
2995      *
2996      * @param string $query
2997      * @return bool true if related, false if not
2998      */
2999     public function is_related($query) {
3000         if (parent::is_related($query)) {
3001             return true;
3002         }
3003         if (!$this->load_choices()) {
3004             return false;
3005         }
3006         foreach ($this->choices as $key=>$value) {
3007             if (strpos(core_text::strtolower($key), $query) !== false) {
3008                 return true;
3009             }
3010             if (strpos(core_text::strtolower($value), $query) !== false) {
3011                 return true;
3012             }
3013         }
3014         return false;
3015     }
3017     /**
3018      * Return the setting
3019      *
3020      * @return mixed returns config if successful else null
3021      */
3022     public function get_setting() {
3023         return $this->config_read($this->name);
3024     }
3026     /**
3027      * Save a setting
3028      *
3029      * @param string $data
3030      * @return string empty of error string
3031      */
3032     public function write_setting($data) {
3033         if (!$this->load_choices() or empty($this->choices)) {
3034             return '';
3035         }
3036         if (!array_key_exists($data, $this->choices)) {
3037             return ''; // ignore it
3038         }
3040         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
3041     }
3043     /**
3044      * Returns XHTML select field
3045      *
3046      * Ensure the options are loaded, and generate the XHTML for the select
3047      * element and any warning message. Separating this out from output_html
3048      * makes it easier to subclass this class.
3049      *
3050      * @param string $data the option to show as selected.
3051      * @param string $current the currently selected option in the database, null if none.
3052      * @param string $default the default selected option.
3053      * @return array the HTML for the select element, and a warning message.
3054      * @deprecated since Moodle 3.2
3055      */
3056     public function output_select_html($data, $current, $default, $extraname = '') {
3057         debugging('The method admin_setting_configselect::output_select_html is depreacted, do not use any more.', DEBUG_DEVELOPER);
3058     }
3060     /**
3061      * Returns XHTML select field and wrapping div(s)
3062      *
3063      * @see output_select_html()
3064      *
3065      * @param string $data the option to show as selected
3066      * @param string $query
3067      * @return string XHTML field and wrapping div
3068      */
3069     public function output_html($data, $query='') {
3070         global $OUTPUT;
3072         $default = $this->get_defaultsetting();
3073         $current = $this->get_setting();
3075         if (!$this->load_choices() || empty($this->choices)) {
3076             return '';
3077         }
3079         $context = (object) [
3080             'id' => $this->get_id(),
3081             'name' => $this->get_full_name(),
3082         ];
3084         if (!is_null($default) && array_key_exists($default, $this->choices)) {
3085             $defaultinfo = $this->choices[$default];
3086         } else {
3087             $defaultinfo = NULL;
3088         }
3090         // Warnings.
3091         $warning = '';
3092         if ($current === null) {
3093             // First run.
3094         } else if (empty($current) && (array_key_exists('', $this->choices) || array_key_exists(0, $this->choices))) {
3095             // No warning.
3096         } else if (!array_key_exists($current, $this->choices)) {
3097             $warning = get_string('warningcurrentsetting', 'admin', $current);
3098             if (!is_null($default) && $data == $current) {
3099                 $data = $default; // Use default instead of first value when showing the form.
3100             }
3101         }
3103         $options = [];
3104         $template = 'core_admin/setting_configselect';
3106         if (!empty($this->optgroups)) {
3107             $optgroups = [];
3108             foreach ($this->optgroups as $label => $choices) {
3109                 $optgroup = array('label' => $label, 'options' => []);
3110                 foreach ($choices as $value => $name) {
3111                     $optgroup['options'][] = [
3112                         'value' => $value,
3113                         'name' => $name,
3114                         'selected' => (string) $value == $data
3115                     ];
3116                     unset($this->choices[$value]);
3117                 }
3118                 $optgroups[] = $optgroup;
3119             }
3120             $context->options = $options;
3121             $context->optgroups = $optgroups;
3122             $template = 'core_admin/setting_configselect_optgroup';
3123         }
3125         foreach ($this->choices as $value => $name) {
3126             $options[] = [
3127                 'value' => $value,
3128                 'name' => $name,
3129                 'selected' => (string) $value == $data
3130             ];
3131         }
3132         $context->options = $options;
3134         $element = $OUTPUT->render_from_template($template, $context);
3136         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, $warning, $defaultinfo, $query);
3137     }
3141 /**
3142  * Select multiple items from list
3143  *
3144  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3145  */
3146 class admin_setting_configmultiselect extends admin_setting_configselect {
3147     /**
3148      * Constructor
3149      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3150      * @param string $visiblename localised
3151      * @param string $description long localised info
3152      * @param array $defaultsetting array of selected items
3153      * @param array $choices array of $value=>$label for each list item
3154      */
3155     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
3156         parent::__construct($name, $visiblename, $description, $defaultsetting, $choices);
3157     }
3159     /**
3160      * Returns the select setting(s)
3161      *
3162      * @return mixed null or array. Null if no settings else array of setting(s)
3163      */
3164     public function get_setting() {
3165         $result = $this->config_read($this->name);
3166         if (is_null($result)) {
3167             return NULL;
3168         }
3169         if ($result === '') {
3170             return array();
3171         }
3172         return explode(',', $result);
3173     }
3175     /**
3176      * Saves setting(s) provided through $data
3177      *
3178      * Potential bug in the works should anyone call with this function
3179      * using a vartype that is not an array
3180      *
3181      * @param array $data
3182      */
3183     public function write_setting($data) {
3184         if (!is_array($data)) {
3185             return ''; //ignore it
3186         }
3187         if (!$this->load_choices() or empty($this->choices)) {
3188             return '';
3189         }
3191         unset($data['xxxxx']);
3193         $save = array();
3194         foreach ($data as $value) {
3195             if (!array_key_exists($value, $this->choices)) {
3196                 continue; // ignore it
3197             }
3198             $save[] = $value;
3199         }
3201         return ($this->config_write($this->name, implode(',', $save)) ? '' : get_string('errorsetting', 'admin'));
3202     }
3204     /**
3205      * Is setting related to query text - used when searching
3206      *
3207      * @param string $query
3208      * @return bool true if related, false if not
3209      */
3210     public function is_related($query) {
3211         if (!$this->load_choices() or empty($this->choices)) {
3212             return false;
3213         }
3214         if (parent::is_related($query)) {
3215             return true;
3216         }
3218         foreach ($this->choices as $desc) {
3219             if (strpos(core_text::strtolower($desc), $query) !== false) {
3220                 return true;
3221             }
3222         }
3223         return false;
3224     }
3226     /**
3227      * Returns XHTML multi-select field
3228      *
3229      * @todo Add vartype handling to ensure $data is an array
3230      * @param array $data Array of values to select by default
3231      * @param string $query
3232      * @return string XHTML multi-select field
3233      */
3234     public function output_html($data, $query='') {
3235         global $OUTPUT;
3237         if (!$this->load_choices() or empty($this->choices)) {
3238             return '';
3239         }
3241         $default = $this->get_defaultsetting();
3242         if (is_null($default)) {
3243             $default = array();
3244         }
3245         if (is_null($data)) {
3246             $data = array();
3247         }
3249         $context = (object) [
3250             'id' => $this->get_id(),
3251             'name' => $this->get_full_name(),
3252             'size' => min(10, count($this->choices))
3253         ];
3255         $defaults = [];
3256         $options = [];
3257         $template = 'core_admin/setting_configmultiselect';
3259         if (!empty($this->optgroups)) {
3260             $optgroups = [];
3261             foreach ($this->optgroups as $label => $choices) {
3262                 $optgroup = array('label' => $label, 'options' => []);
3263                 foreach ($choices as $value => $name) {
3264                     if (in_array($value, $default)) {
3265                         $defaults[] = $name;
3266                     }
3267                     $optgroup['options'][] = [
3268                         'value' => $value,
3269                         'name' => $name,
3270                         'selected' => in_array($value, $data)
3271                     ];
3272                     unset($this->choices[$value]);
3273                 }
3274                 $optgroups[] = $optgroup;
3275             }
3276             $context->optgroups = $optgroups;
3277             $template = 'core_admin/setting_configmultiselect_optgroup';
3278         }
3280         foreach ($this->choices as $value => $name) {
3281             if (in_array($value, $default)) {
3282                 $defaults[] = $name;
3283             }
3284             $options[] = [
3285                 'value' => $value,
3286                 'name' => $name,
3287                 'selected' => in_array($value, $data)
3288             ];
3289         }
3290         $context->options = $options;
3292         if (is_null($default)) {
3293             $defaultinfo = NULL;
3294         } if (!empty($defaults)) {
3295             $defaultinfo = implode(', ', $defaults);
3296         } else {
3297             $defaultinfo = get_string('none');
3298         }
3300         $element = $OUTPUT->render_from_template($template, $context);
3302         return format_admin_setting($this, $this->visiblename, $element, $this->description, true, '', $defaultinfo, $query);
3303     }
3306 /**
3307  * Time selector
3308  *
3309  * This is a liiitle bit messy. we're using two selects, but we're returning
3310  * them as an array named after $name (so we only use $name2 internally for the setting)
3311  *
3312  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3313  */
3314 class admin_setting_configtime extends admin_setting {
3315     /** @var string Used for setting second select (minutes) */
3316     public $name2;
3318     /**
3319      * Constructor
3320      * @param string $hoursname setting for hours
3321      * @param string $minutesname setting for hours
3322      * @param string $visiblename localised
3323      * @param string $description long localised info
3324      * @param array $defaultsetting array representing default time 'h'=>hours, 'm'=>minutes
3325      */
3326     public function __construct($hoursname, $minutesname, $visiblename, $description, $defaultsetting) {
3327         $this->name2 = $minutesname;
3328         parent::__construct($hoursname, $visiblename, $description, $defaultsetting);
3329     }
3331     /**
3332      * Get the selected time
3333      *
3334      * @return mixed An array containing 'h'=>xx, 'm'=>xx, or null if not set
3335      */
3336     public function get_setting() {
3337         $result1 = $this->config_read($this->name);
3338         $result2 = $this->config_read($this->name2);
3339         if (is_null($result1) or is_null($result2)) {
3340             return NULL;
3341         }
3343         return array('h' => $result1, 'm' => $result2);
3344     }
3346     /**
3347      * Store the time (hours and minutes)
3348      *
3349      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3350      * @return bool true if success, false if not
3351      */
3352     public function write_setting($data) {
3353         if (!is_array($data)) {
3354             return '';
3355         }
3357         $result = $this->config_write($this->name, (int)$data['h']) && $this->config_write($this->name2, (int)$data['m']);
3358         return ($result ? '' : get_string('errorsetting', 'admin'));
3359     }
3361     /**
3362      * Returns XHTML time select fields
3363      *
3364      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3365      * @param string $query
3366      * @return string XHTML time select fields and wrapping div(s)
3367      */
3368     public function output_html($data, $query='') {
3369         global $OUTPUT;
3371         $default = $this->get_defaultsetting();
3372         if (is_array($default)) {
3373             $defaultinfo = $default['h'].':'.$default['m'];
3374         } else {
3375             $defaultinfo = NULL;
3376         }
3378         $context = (object) [
3379             'id' => $this->get_id(),
3380             'name' => $this->get_full_name(),
3381             'hours' => array_map(function($i) use ($data) {
3382                 return [
3383                     'value' => $i,
3384                     'name' => $i,
3385                     'selected' => $i == $data['h']
3386                 ];
3387             }, range(0, 23)),
3388             'minutes' => array_map(function($i) use ($data) {
3389                 return [
3390                     'value' => $i,
3391                     'name' => $i,
3392                     'selected' => $i == $data['m']
3393                 ];
3394             }, range(0, 59, 5))
3395         ];
3397         $element = $OUTPUT->render_from_template('core_admin/setting_configtime', $context);
3399         return format_admin_setting($this, $this->visiblename, $element, $this->description,
3400             $this->get_id() . 'h', '', $defaultinfo, $query);
3401     }
3406 /**
3407  * Seconds duration setting.
3408  *
3409  * @copyright 2012 Petr Skoda (http://skodak.org)
3410  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3411  */
3412 class admin_setting_configduration extends admin_setting {
3414     /** @var int default duration unit */
3415     protected $defaultunit;
3417     /**
3418      * Constructor
3419      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
3420      *                     or 'myplugin/mysetting' for ones in config_plugins.
3421      * @param string $visiblename localised name
3422      * @param string $description localised long description
3423      * @param mixed $defaultsetting string or array depending on implementation
3424      * @param int $defaultunit - day, week, etc. (in seconds)
3425      */
3426     public function __construct($name, $visiblename, $description, $defaultsetting, $defaultunit = 86400) {
3427         if (is_number($defaultsetting)) {
3428             $defaultsetting = self::parse_seconds($defaultsetting);
3429         }
3430         $units = self::get_units();
3431         if (isset($units[$defaultunit])) {
3432             $this->defaultunit = $defaultunit;
3433         } else {
3434             $this->defaultunit = 86400;
3435         }
3436         parent::__construct($name, $visiblename, $description, $defaultsetting);
3437     }
3439     /**
3440      * Returns selectable units.
3441      * @static
3442      * @return array
3443      */
3444     protected static function get_units() {
3445         return array(
3446             604800 => get_string('weeks'),
3447             86400 => get_string('days'),
3448             3600 => get_string('hours'),
3449             60 => get_string('minutes'),
3450             1 => get_string('seconds'),
3451         );
3452     }
3454     /**
3455      * Converts seconds to some more user friendly string.
3456      * @static
3457      * @param int $seconds
3458      * @return string
3459      */
3460     protected static function get_duration_text($seconds) {
3461         if (empty($seconds)) {
3462             return get_string('none');
3463         }
3464         $data = self::parse_seconds($seconds);
3465         switch ($data['u']) {
3466             case (60*60*24*7):
3467                 return get_string('numweeks', '', $data['v']);
3468             case (60*60*24):
3469                 return get_string('numdays', '', $data['v']);
3470             case (60*60):
3471                 return get_string('numhours', '', $data['v']);
3472             case (60):
3473                 return get_string('numminutes', '', $data['v']);
3474             default:
3475                 return get_string('numseconds', '', $data['v']*$data['u']);
3476         }
3477     }
3479     /**
3480      * Finds suitable units for given duration.
3481      * @static
3482      * @param int $seconds
3483      * @return array
3484      */
3485     protected static function parse_seconds($seconds) {
3486         foreach (self::get_units() as $unit => $unused) {
3487             if ($seconds % $unit === 0) {
3488                 return array('v'=>(int)($seconds/$unit), 'u'=>$unit);
3489             }
3490         }
3491         return array('v'=>(int)$seconds, 'u'=>1);
3492     }
3494     /**
3495      * Get the selected duration as array.
3496      *
3497      * @return mixed An array containing 'v'=>xx, 'u'=>xx, or null if not set
3498      */
3499     public function get_setting() {
3500         $seconds = $this->config_read($this->name);
3501         if (is_null($seconds)) {
3502             return null;
3503         }
3505         return self::parse_seconds($seconds);
3506     }
3508     /**
3509      * Store the duration as seconds.
3510      *
3511      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3512      * @return bool true if success, false if not
3513      */
3514     public function write_setting($data) {
3515         if (!is_array($data)) {
3516             return '';
3517         }
3519         $seconds = (int)($data['v']*$data['u']);
3520         if ($seconds < 0) {
3521             return get_string('errorsetting', 'admin');
3522         }
3524         $result = $this->config_write($this->name, $seconds);
3525         return ($result ? '' : get_string('errorsetting', 'admin'));
3526     }
3528     /**
3529      * Returns duration text+select fields.
3530      *
3531      * @param array $data Must be form 'v'=>xx, 'u'=>xx
3532      * @param string $query
3533      * @return string duration text+select fields and wrapping div(s)
3534      */
3535     public function output_html($data, $query='') {
3536         global $OUTPUT;
3538         $default = $this->get_defaultsetting();
3539         if (is_number($default)) {
3540             $defaultinfo = self::get_duration_text($default);
3541         } else if (is_array($default)) {
3542             $defaultinfo = self::get_duration_text($default['v']*$default['u']);
3543         } else {
3544             $defaultinfo = null;
3545         }
3547         $inputid = $this->get_id() . 'v';
3548         $units = self::get_units();
3549         $defaultunit = $this->defaultunit;
3551         $context = (object) [
3552             'id' => $this->get_id(),
3553             'name' => $this->get_full_name(),
3554             'value' => $data['v'],
3555             'options' => array_map(function($unit) use ($units, $data, $defaultunit) {
3556                 return [
3557                     'value' => $unit,
3558                     'name' => $units[$unit],
3559                     'selected' => ($data['v'] == 0 && $unit == $defaultunit) || $unit == $data['u']
3560                 ];
3561             }, array_keys($units))
3562         ];
3564         $element = $OUTPUT->render_from_template('core_admin/setting_configduration', $context);
3566         return format_admin_setting($this, $this->visiblename, $element, $this->description, $inputid, '', $defaultinfo, $query);
3567     }
3571 /**
3572  * Seconds duration setting with an advanced checkbox, that controls a additional
3573  * $name.'_adv' setting.
3574  *
3575  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3576  * @copyright 2014 The Open University
3577  */
3578 class admin_setting_configduration_with_advanced extends admin_setting_configduration {
3579     /**
3580      * Constructor
3581      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
3582      *                     or 'myplugin/mysetting' for ones in config_plugins.
3583      * @param string $visiblename localised name
3584      * @param string $description localised long description
3585      * @param array  $defaultsetting array of int value, and bool whether it is
3586      *                     is advanced by default.
3587      * @param int $defaultunit - day, week, etc. (in seconds)
3588      */
3589     public function __construct($name, $visiblename, $description, $defaultsetting, $defaultunit = 86400) {
3590         parent::__construct($name, $visiblename, $description, $defaultsetting['value'], $defaultunit);
3591         $this->set_advanced_flag_options(admin_setting_flag::ENABLED, !empty($defaultsetting['adv']));
3592     }
3596 /**
3597  * Used to validate a textarea used for ip addresses
3598  *
3599  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3600  * @copyright 2011 Petr Skoda (http://skodak.org)
3601  */
3602 class admin_setting_configiplist extends admin_setting_configtextarea {
3604     /**
3605      * Validate the contents of the textarea as IP addresses
3606      *
3607      * Used to validate a new line separated list of IP addresses collected from
3608      * a textarea control
3609      *
3610      * @param string $data A list of IP Addresses separated by new lines
3611      * @return mixed bool true for success or string:error on failure
3612      */
3613     public function validate($data) {
3614         if(!empty($data)) {
3615             $lines = explode("\n", $data);
3616         } else {
3617             return true;
3618         }
3619         $result = true;
3620         $badips = array();
3621         foreach ($lines as $line) {
3622             $tokens = explode('#', $line);
3623             $ip = trim($tokens[0]);
3624             if (empty($ip)) {
3625                 continue;
3626             }
3627             if (preg_match('#^(\d{1,3})(\.\d{1,3}){0,3}$#', $ip, $match) ||
3628                 preg_match('#^(\d{1,3})(\.\d{1,3}){0,3}(\/\d{1,2})$#', $ip, $match) ||
3629                 preg_match('#^(\d{1,3})(\.\d{1,3}){3}(-\d{1,3})$#', $ip, $match)) {
3630             } else {
3631                 $result = false;
3632                 $badips[] = $ip;
3633             }
3634         }
3635         if($result) {
3636             return true;
3637         } else {
3638             return get_string('validateiperror', 'admin', join(', ', $badips));
3639         }
3640     }
3643 /**
3644  * Used to validate a textarea used for domain names, wildcard domain names and IP addresses/ranges (both IPv4 and IPv6 format).
3645  *
3646  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3647  * @copyright 2016 Jake Dallimore (jrhdallimore@gmail.com)
3648  */
3649 class admin_setting_configmixedhostiplist extends admin_setting_configtextarea {
3651     /**
3652      * Validate the contents of the textarea as either IP addresses, domain name or wildcard domain name (RFC 4592).
3653      * Used to validate a new line separated list of entries collected from a textarea control.
3654      *
3655      * This setting provides support for internationalised domain names (IDNs), however, such UTF-8 names will be converted to
3656      * their ascii-compatible encoding (punycode) on save, and converted back to their UTF-8 representation when fetched
3657      * via the get_setting() method, which has been overriden.
3658      *
3659      * @param string $data A list of FQDNs, DNS wildcard format domains, and IP addresses, separated by new lines.
3660      * @return mixed bool true for success or string:error on failure
3661      */
3662     public function validate($data) {
3663         if (empty($data)) {
3664             return true;
3665         }
3666         $entries = explode("\n", $data);
3667         $badentries = [];
3669         foreach ($entries as $key => $entry) {
3670             $entry = trim($entry);
3671             if (empty($entry)) {