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