MDL-52651 htmlpurifier: Append rel=noreferrer to links.
[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 areas, collections and instances associated with this plugin.
170     core_tag_area::uninstall($component);
172     // Custom plugin uninstall.
173     $plugindirectory = core_component::get_plugin_directory($type, $name);
174     $uninstalllib = $plugindirectory . '/db/uninstall.php';
175     if (file_exists($uninstalllib)) {
176         require_once($uninstalllib);
177         $uninstallfunction = 'xmldb_' . $pluginname . '_uninstall';    // eg. 'xmldb_workshop_uninstall()'
178         if (function_exists($uninstallfunction)) {
179             // Do not verify result, let plugin complain if necessary.
180             $uninstallfunction();
181         }
182     }
184     // Specific plugin type cleanup.
185     $plugininfo = core_plugin_manager::instance()->get_plugin_info($component);
186     if ($plugininfo) {
187         $plugininfo->uninstall_cleanup();
188         core_plugin_manager::reset_caches();
189     }
190     $plugininfo = null;
192     // perform clean-up task common for all the plugin/subplugin types
194     //delete the web service functions and pre-built services
195     require_once($CFG->dirroot.'/lib/externallib.php');
196     external_delete_descriptions($component);
198     // delete calendar events
199     $DB->delete_records('event', array('modulename' => $pluginname));
201     // Delete scheduled tasks.
202     $DB->delete_records('task_scheduled', array('component' => $pluginname));
204     // Delete Inbound Message datakeys.
205     $DB->delete_records_select('messageinbound_datakeys',
206             'handler IN (SELECT id FROM {messageinbound_handlers} WHERE component = ?)', array($pluginname));
208     // Delete Inbound Message handlers.
209     $DB->delete_records('messageinbound_handlers', array('component' => $pluginname));
211     // delete all the logs
212     $DB->delete_records('log', array('module' => $pluginname));
214     // delete log_display information
215     $DB->delete_records('log_display', array('component' => $component));
217     // delete the module configuration records
218     unset_all_config_for_plugin($component);
219     if ($type === 'mod') {
220         unset_all_config_for_plugin($pluginname);
221     }
223     // delete message provider
224     message_provider_uninstall($component);
226     // delete the plugin tables
227     $xmldbfilepath = $plugindirectory . '/db/install.xml';
228     drop_plugin_tables($component, $xmldbfilepath, false);
229     if ($type === 'mod' or $type === 'block') {
230         // non-frankenstyle table prefixes
231         drop_plugin_tables($name, $xmldbfilepath, false);
232     }
234     // delete the capabilities that were defined by this module
235     capabilities_cleanup($component);
237     // remove event handlers and dequeue pending events
238     events_uninstall($component);
240     // Delete all remaining files in the filepool owned by the component.
241     $fs = get_file_storage();
242     $fs->delete_component_files($component);
244     // Finally purge all caches.
245     purge_all_caches();
247     // Invalidate the hash used for upgrade detections.
248     set_config('allversionshash', '');
250     echo $OUTPUT->notification(get_string('success'), 'notifysuccess');
253 /**
254  * Returns the version of installed component
255  *
256  * @param string $component component name
257  * @param string $source either 'disk' or 'installed' - where to get the version information from
258  * @return string|bool version number or false if the component is not found
259  */
260 function get_component_version($component, $source='installed') {
261     global $CFG, $DB;
263     list($type, $name) = core_component::normalize_component($component);
265     // moodle core or a core subsystem
266     if ($type === 'core') {
267         if ($source === 'installed') {
268             if (empty($CFG->version)) {
269                 return false;
270             } else {
271                 return $CFG->version;
272             }
273         } else {
274             if (!is_readable($CFG->dirroot.'/version.php')) {
275                 return false;
276             } else {
277                 $version = null; //initialize variable for IDEs
278                 include($CFG->dirroot.'/version.php');
279                 return $version;
280             }
281         }
282     }
284     // activity module
285     if ($type === 'mod') {
286         if ($source === 'installed') {
287             if ($CFG->version < 2013092001.02) {
288                 return $DB->get_field('modules', 'version', array('name'=>$name));
289             } else {
290                 return get_config('mod_'.$name, 'version');
291             }
293         } else {
294             $mods = core_component::get_plugin_list('mod');
295             if (empty($mods[$name]) or !is_readable($mods[$name].'/version.php')) {
296                 return false;
297             } else {
298                 $plugin = new stdClass();
299                 $plugin->version = null;
300                 $module = $plugin;
301                 include($mods[$name].'/version.php');
302                 return $plugin->version;
303             }
304         }
305     }
307     // block
308     if ($type === 'block') {
309         if ($source === 'installed') {
310             if ($CFG->version < 2013092001.02) {
311                 return $DB->get_field('block', 'version', array('name'=>$name));
312             } else {
313                 return get_config('block_'.$name, 'version');
314             }
315         } else {
316             $blocks = core_component::get_plugin_list('block');
317             if (empty($blocks[$name]) or !is_readable($blocks[$name].'/version.php')) {
318                 return false;
319             } else {
320                 $plugin = new stdclass();
321                 include($blocks[$name].'/version.php');
322                 return $plugin->version;
323             }
324         }
325     }
327     // all other plugin types
328     if ($source === 'installed') {
329         return get_config($type.'_'.$name, 'version');
330     } else {
331         $plugins = core_component::get_plugin_list($type);
332         if (empty($plugins[$name])) {
333             return false;
334         } else {
335             $plugin = new stdclass();
336             include($plugins[$name].'/version.php');
337             return $plugin->version;
338         }
339     }
342 /**
343  * Delete all plugin tables
344  *
345  * @param string $name Name of plugin, used as table prefix
346  * @param string $file Path to install.xml file
347  * @param bool $feedback defaults to true
348  * @return bool Always returns true
349  */
350 function drop_plugin_tables($name, $file, $feedback=true) {
351     global $CFG, $DB;
353     // first try normal delete
354     if (file_exists($file) and $DB->get_manager()->delete_tables_from_xmldb_file($file)) {
355         return true;
356     }
358     // then try to find all tables that start with name and are not in any xml file
359     $used_tables = get_used_table_names();
361     $tables = $DB->get_tables();
363     /// Iterate over, fixing id fields as necessary
364     foreach ($tables as $table) {
365         if (in_array($table, $used_tables)) {
366             continue;
367         }
369         if (strpos($table, $name) !== 0) {
370             continue;
371         }
373         // found orphan table --> delete it
374         if ($DB->get_manager()->table_exists($table)) {
375             $xmldb_table = new xmldb_table($table);
376             $DB->get_manager()->drop_table($xmldb_table);
377         }
378     }
380     return true;
383 /**
384  * Returns names of all known tables == tables that moodle knows about.
385  *
386  * @return array Array of lowercase table names
387  */
388 function get_used_table_names() {
389     $table_names = array();
390     $dbdirs = get_db_directories();
392     foreach ($dbdirs as $dbdir) {
393         $file = $dbdir.'/install.xml';
395         $xmldb_file = new xmldb_file($file);
397         if (!$xmldb_file->fileExists()) {
398             continue;
399         }
401         $loaded    = $xmldb_file->loadXMLStructure();
402         $structure = $xmldb_file->getStructure();
404         if ($loaded and $tables = $structure->getTables()) {
405             foreach($tables as $table) {
406                 $table_names[] = strtolower($table->getName());
407             }
408         }
409     }
411     return $table_names;
414 /**
415  * Returns list of all directories where we expect install.xml files
416  * @return array Array of paths
417  */
418 function get_db_directories() {
419     global $CFG;
421     $dbdirs = array();
423     /// First, the main one (lib/db)
424     $dbdirs[] = $CFG->libdir.'/db';
426     /// Then, all the ones defined by core_component::get_plugin_types()
427     $plugintypes = core_component::get_plugin_types();
428     foreach ($plugintypes as $plugintype => $pluginbasedir) {
429         if ($plugins = core_component::get_plugin_list($plugintype)) {
430             foreach ($plugins as $plugin => $plugindir) {
431                 $dbdirs[] = $plugindir.'/db';
432             }
433         }
434     }
436     return $dbdirs;
439 /**
440  * Try to obtain or release the cron lock.
441  * @param string  $name  name of lock
442  * @param int  $until timestamp when this lock considered stale, null means remove lock unconditionally
443  * @param bool $ignorecurrent ignore current lock state, usually extend previous lock, defaults to false
444  * @return bool true if lock obtained
445  */
446 function set_cron_lock($name, $until, $ignorecurrent=false) {
447     global $DB;
448     if (empty($name)) {
449         debugging("Tried to get a cron lock for a null fieldname");
450         return false;
451     }
453     // remove lock by force == remove from config table
454     if (is_null($until)) {
455         set_config($name, null);
456         return true;
457     }
459     if (!$ignorecurrent) {
460         // read value from db - other processes might have changed it
461         $value = $DB->get_field('config', 'value', array('name'=>$name));
463         if ($value and $value > time()) {
464             //lock active
465             return false;
466         }
467     }
469     set_config($name, $until);
470     return true;
473 /**
474  * Test if and critical warnings are present
475  * @return bool
476  */
477 function admin_critical_warnings_present() {
478     global $SESSION;
480     if (!has_capability('moodle/site:config', context_system::instance())) {
481         return 0;
482     }
484     if (!isset($SESSION->admin_critical_warning)) {
485         $SESSION->admin_critical_warning = 0;
486         if (is_dataroot_insecure(true) === INSECURE_DATAROOT_ERROR) {
487             $SESSION->admin_critical_warning = 1;
488         }
489     }
491     return $SESSION->admin_critical_warning;
494 /**
495  * Detects if float supports at least 10 decimal digits
496  *
497  * Detects if float supports at least 10 decimal digits
498  * and also if float-->string conversion works as expected.
499  *
500  * @return bool true if problem found
501  */
502 function is_float_problem() {
503     $num1 = 2009010200.01;
504     $num2 = 2009010200.02;
506     return ((string)$num1 === (string)$num2 or $num1 === $num2 or $num2 <= (string)$num1);
509 /**
510  * Try to verify that dataroot is not accessible from web.
511  *
512  * Try to verify that dataroot is not accessible from web.
513  * It is not 100% correct but might help to reduce number of vulnerable sites.
514  * Protection from httpd.conf and .htaccess is not detected properly.
515  *
516  * @uses INSECURE_DATAROOT_WARNING
517  * @uses INSECURE_DATAROOT_ERROR
518  * @param bool $fetchtest try to test public access by fetching file, default false
519  * @return mixed empty means secure, INSECURE_DATAROOT_ERROR found a critical problem, INSECURE_DATAROOT_WARNING might be problematic
520  */
521 function is_dataroot_insecure($fetchtest=false) {
522     global $CFG;
524     $siteroot = str_replace('\\', '/', strrev($CFG->dirroot.'/')); // win32 backslash workaround
526     $rp = preg_replace('|https?://[^/]+|i', '', $CFG->wwwroot, 1);
527     $rp = strrev(trim($rp, '/'));
528     $rp = explode('/', $rp);
529     foreach($rp as $r) {
530         if (strpos($siteroot, '/'.$r.'/') === 0) {
531             $siteroot = substr($siteroot, strlen($r)+1); // moodle web in subdirectory
532         } else {
533             break; // probably alias root
534         }
535     }
537     $siteroot = strrev($siteroot);
538     $dataroot = str_replace('\\', '/', $CFG->dataroot.'/');
540     if (strpos($dataroot, $siteroot) !== 0) {
541         return false;
542     }
544     if (!$fetchtest) {
545         return INSECURE_DATAROOT_WARNING;
546     }
548     // now try all methods to fetch a test file using http protocol
550     $httpdocroot = str_replace('\\', '/', strrev($CFG->dirroot.'/'));
551     preg_match('|(https?://[^/]+)|i', $CFG->wwwroot, $matches);
552     $httpdocroot = $matches[1];
553     $datarooturl = $httpdocroot.'/'. substr($dataroot, strlen($siteroot));
554     make_upload_directory('diag');
555     $testfile = $CFG->dataroot.'/diag/public.txt';
556     if (!file_exists($testfile)) {
557         file_put_contents($testfile, 'test file, do not delete');
558         @chmod($testfile, $CFG->filepermissions);
559     }
560     $teststr = trim(file_get_contents($testfile));
561     if (empty($teststr)) {
562     // hmm, strange
563         return INSECURE_DATAROOT_WARNING;
564     }
566     $testurl = $datarooturl.'/diag/public.txt';
567     if (extension_loaded('curl') and
568         !(stripos(ini_get('disable_functions'), 'curl_init') !== FALSE) and
569         !(stripos(ini_get('disable_functions'), 'curl_setop') !== FALSE) and
570         ($ch = @curl_init($testurl)) !== false) {
571         curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
572         curl_setopt($ch, CURLOPT_HEADER, false);
573         $data = curl_exec($ch);
574         if (!curl_errno($ch)) {
575             $data = trim($data);
576             if ($data === $teststr) {
577                 curl_close($ch);
578                 return INSECURE_DATAROOT_ERROR;
579             }
580         }
581         curl_close($ch);
582     }
584     if ($data = @file_get_contents($testurl)) {
585         $data = trim($data);
586         if ($data === $teststr) {
587             return INSECURE_DATAROOT_ERROR;
588         }
589     }
591     preg_match('|https?://([^/]+)|i', $testurl, $matches);
592     $sitename = $matches[1];
593     $error = 0;
594     if ($fp = @fsockopen($sitename, 80, $error)) {
595         preg_match('|https?://[^/]+(.*)|i', $testurl, $matches);
596         $localurl = $matches[1];
597         $out = "GET $localurl HTTP/1.1\r\n";
598         $out .= "Host: $sitename\r\n";
599         $out .= "Connection: Close\r\n\r\n";
600         fwrite($fp, $out);
601         $data = '';
602         $incoming = false;
603         while (!feof($fp)) {
604             if ($incoming) {
605                 $data .= fgets($fp, 1024);
606             } else if (@fgets($fp, 1024) === "\r\n") {
607                     $incoming = true;
608                 }
609         }
610         fclose($fp);
611         $data = trim($data);
612         if ($data === $teststr) {
613             return INSECURE_DATAROOT_ERROR;
614         }
615     }
617     return INSECURE_DATAROOT_WARNING;
620 /**
621  * Enables CLI maintenance mode by creating new dataroot/climaintenance.html file.
622  */
623 function enable_cli_maintenance_mode() {
624     global $CFG;
626     if (file_exists("$CFG->dataroot/climaintenance.html")) {
627         unlink("$CFG->dataroot/climaintenance.html");
628     }
630     if (isset($CFG->maintenance_message) and !html_is_blank($CFG->maintenance_message)) {
631         $data = $CFG->maintenance_message;
632         $data = bootstrap_renderer::early_error_content($data, null, null, null);
633         $data = bootstrap_renderer::plain_page(get_string('sitemaintenance', 'admin'), $data);
635     } else if (file_exists("$CFG->dataroot/climaintenance.template.html")) {
636         $data = file_get_contents("$CFG->dataroot/climaintenance.template.html");
638     } else {
639         $data = get_string('sitemaintenance', 'admin');
640         $data = bootstrap_renderer::early_error_content($data, null, null, null);
641         $data = bootstrap_renderer::plain_page(get_string('sitemaintenance', 'admin'), $data);
642     }
644     file_put_contents("$CFG->dataroot/climaintenance.html", $data);
645     chmod("$CFG->dataroot/climaintenance.html", $CFG->filepermissions);
648 /// CLASS DEFINITIONS /////////////////////////////////////////////////////////
651 /**
652  * Interface for anything appearing in the admin tree
653  *
654  * The interface that is implemented by anything that appears in the admin tree
655  * block. It forces inheriting classes to define a method for checking user permissions
656  * and methods for finding something in the admin tree.
657  *
658  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
659  */
660 interface part_of_admin_tree {
662 /**
663  * Finds a named part_of_admin_tree.
664  *
665  * Used to find a part_of_admin_tree. If a class only inherits part_of_admin_tree
666  * and not parentable_part_of_admin_tree, then this function should only check if
667  * $this->name matches $name. If it does, it should return a reference to $this,
668  * otherwise, it should return a reference to NULL.
669  *
670  * If a class inherits parentable_part_of_admin_tree, this method should be called
671  * recursively on all child objects (assuming, of course, the parent object's name
672  * doesn't match the search criterion).
673  *
674  * @param string $name The internal name of the part_of_admin_tree we're searching for.
675  * @return mixed An object reference or a NULL reference.
676  */
677     public function locate($name);
679     /**
680      * Removes named part_of_admin_tree.
681      *
682      * @param string $name The internal name of the part_of_admin_tree we want to remove.
683      * @return bool success.
684      */
685     public function prune($name);
687     /**
688      * Search using query
689      * @param string $query
690      * @return mixed array-object structure of found settings and pages
691      */
692     public function search($query);
694     /**
695      * Verifies current user's access to this part_of_admin_tree.
696      *
697      * Used to check if the current user has access to this part of the admin tree or
698      * not. If a class only inherits part_of_admin_tree and not parentable_part_of_admin_tree,
699      * then this method is usually just a call to has_capability() in the site context.
700      *
701      * If a class inherits parentable_part_of_admin_tree, this method should return the
702      * logical OR of the return of check_access() on all child objects.
703      *
704      * @return bool True if the user has access, false if she doesn't.
705      */
706     public function check_access();
708     /**
709      * Mostly useful for removing of some parts of the tree in admin tree block.
710      *
711      * @return True is hidden from normal list view
712      */
713     public function is_hidden();
715     /**
716      * Show we display Save button at the page bottom?
717      * @return bool
718      */
719     public function show_save();
723 /**
724  * Interface implemented by any part_of_admin_tree that has children.
725  *
726  * The interface implemented by any part_of_admin_tree that can be a parent
727  * to other part_of_admin_tree's. (For now, this only includes admin_category.) Apart
728  * from ensuring part_of_admin_tree compliancy, it also ensures inheriting methods
729  * include an add method for adding other part_of_admin_tree objects as children.
730  *
731  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
732  */
733 interface parentable_part_of_admin_tree extends part_of_admin_tree {
735 /**
736  * Adds a part_of_admin_tree object to the admin tree.
737  *
738  * Used to add a part_of_admin_tree object to this object or a child of this
739  * object. $something should only be added if $destinationname matches
740  * $this->name. If it doesn't, add should be called on child objects that are
741  * also parentable_part_of_admin_tree's.
742  *
743  * $something should be appended as the last child in the $destinationname. If the
744  * $beforesibling is specified, $something should be prepended to it. If the given
745  * sibling is not found, $something should be appended to the end of $destinationname
746  * and a developer debugging message should be displayed.
747  *
748  * @param string $destinationname The internal name of the new parent for $something.
749  * @param part_of_admin_tree $something The object to be added.
750  * @return bool True on success, false on failure.
751  */
752     public function add($destinationname, $something, $beforesibling = null);
757 /**
758  * The object used to represent folders (a.k.a. categories) in the admin tree block.
759  *
760  * Each admin_category object contains a number of part_of_admin_tree objects.
761  *
762  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
763  */
764 class admin_category implements parentable_part_of_admin_tree {
766     /** @var part_of_admin_tree[] An array of part_of_admin_tree objects that are this object's children */
767     protected $children;
768     /** @var string An internal name for this category. Must be unique amongst ALL part_of_admin_tree objects */
769     public $name;
770     /** @var string The displayed name for this category. Usually obtained through get_string() */
771     public $visiblename;
772     /** @var bool Should this category be hidden in admin tree block? */
773     public $hidden;
774     /** @var mixed Either a string or an array or strings */
775     public $path;
776     /** @var mixed Either a string or an array or strings */
777     public $visiblepath;
779     /** @var array fast lookup category cache, all categories of one tree point to one cache */
780     protected $category_cache;
782     /** @var bool If set to true children will be sorted when calling {@link admin_category::get_children()} */
783     protected $sort = false;
784     /** @var bool If set to true children will be sorted in ascending order. */
785     protected $sortasc = true;
786     /** @var bool If set to true sub categories and pages will be split and then sorted.. */
787     protected $sortsplit = true;
788     /** @var bool $sorted True if the children have been sorted and don't need resorting */
789     protected $sorted = false;
791     /**
792      * Constructor for an empty admin category
793      *
794      * @param string $name The internal name for this category. Must be unique amongst ALL part_of_admin_tree objects
795      * @param string $visiblename The displayed named for this category. Usually obtained through get_string()
796      * @param bool $hidden hide category in admin tree block, defaults to false
797      */
798     public function __construct($name, $visiblename, $hidden=false) {
799         $this->children    = array();
800         $this->name        = $name;
801         $this->visiblename = $visiblename;
802         $this->hidden      = $hidden;
803     }
805     /**
806      * Returns a reference to the part_of_admin_tree object with internal name $name.
807      *
808      * @param string $name The internal name of the object we want.
809      * @param bool $findpath initialize path and visiblepath arrays
810      * @return mixed A reference to the object with internal name $name if found, otherwise a reference to NULL.
811      *                  defaults to false
812      */
813     public function locate($name, $findpath=false) {
814         if (!isset($this->category_cache[$this->name])) {
815             // somebody much have purged the cache
816             $this->category_cache[$this->name] = $this;
817         }
819         if ($this->name == $name) {
820             if ($findpath) {
821                 $this->visiblepath[] = $this->visiblename;
822                 $this->path[]        = $this->name;
823             }
824             return $this;
825         }
827         // quick category lookup
828         if (!$findpath and isset($this->category_cache[$name])) {
829             return $this->category_cache[$name];
830         }
832         $return = NULL;
833         foreach($this->children as $childid=>$unused) {
834             if ($return = $this->children[$childid]->locate($name, $findpath)) {
835                 break;
836             }
837         }
839         if (!is_null($return) and $findpath) {
840             $return->visiblepath[] = $this->visiblename;
841             $return->path[]        = $this->name;
842         }
844         return $return;
845     }
847     /**
848      * Search using query
849      *
850      * @param string query
851      * @return mixed array-object structure of found settings and pages
852      */
853     public function search($query) {
854         $result = array();
855         foreach ($this->get_children() as $child) {
856             $subsearch = $child->search($query);
857             if (!is_array($subsearch)) {
858                 debugging('Incorrect search result from '.$child->name);
859                 continue;
860             }
861             $result = array_merge($result, $subsearch);
862         }
863         return $result;
864     }
866     /**
867      * Removes part_of_admin_tree object with internal name $name.
868      *
869      * @param string $name The internal name of the object we want to remove.
870      * @return bool success
871      */
872     public function prune($name) {
874         if ($this->name == $name) {
875             return false;  //can not remove itself
876         }
878         foreach($this->children as $precedence => $child) {
879             if ($child->name == $name) {
880                 // clear cache and delete self
881                 while($this->category_cache) {
882                     // delete the cache, but keep the original array address
883                     array_pop($this->category_cache);
884                 }
885                 unset($this->children[$precedence]);
886                 return true;
887             } else if ($this->children[$precedence]->prune($name)) {
888                 return true;
889             }
890         }
891         return false;
892     }
894     /**
895      * Adds a part_of_admin_tree to a child or grandchild (or great-grandchild, and so forth) of this object.
896      *
897      * By default the new part of the tree is appended as the last child of the parent. You
898      * can specify a sibling node that the new part should be prepended to. If the given
899      * sibling is not found, the part is appended to the end (as it would be by default) and
900      * a developer debugging message is displayed.
901      *
902      * @throws coding_exception if the $beforesibling is empty string or is not string at all.
903      * @param string $destinationame The internal name of the immediate parent that we want for $something.
904      * @param mixed $something A part_of_admin_tree or setting instance to be added.
905      * @param string $beforesibling The name of the parent's child the $something should be prepended to.
906      * @return bool True if successfully added, false if $something can not be added.
907      */
908     public function add($parentname, $something, $beforesibling = null) {
909         global $CFG;
911         $parent = $this->locate($parentname);
912         if (is_null($parent)) {
913             debugging('parent does not exist!');
914             return false;
915         }
917         if ($something instanceof part_of_admin_tree) {
918             if (!($parent instanceof parentable_part_of_admin_tree)) {
919                 debugging('error - parts of tree can be inserted only into parentable parts');
920                 return false;
921             }
922             if ($CFG->debugdeveloper && !is_null($this->locate($something->name))) {
923                 // The name of the node is already used, simply warn the developer that this should not happen.
924                 // It is intentional to check for the debug level before performing the check.
925                 debugging('Duplicate admin page name: ' . $something->name, DEBUG_DEVELOPER);
926             }
927             if (is_null($beforesibling)) {
928                 // Append $something as the parent's last child.
929                 $parent->children[] = $something;
930             } else {
931                 if (!is_string($beforesibling) or trim($beforesibling) === '') {
932                     throw new coding_exception('Unexpected value of the beforesibling parameter');
933                 }
934                 // Try to find the position of the sibling.
935                 $siblingposition = null;
936                 foreach ($parent->children as $childposition => $child) {
937                     if ($child->name === $beforesibling) {
938                         $siblingposition = $childposition;
939                         break;
940                     }
941                 }
942                 if (is_null($siblingposition)) {
943                     debugging('Sibling '.$beforesibling.' not found', DEBUG_DEVELOPER);
944                     $parent->children[] = $something;
945                 } else {
946                     $parent->children = array_merge(
947                         array_slice($parent->children, 0, $siblingposition),
948                         array($something),
949                         array_slice($parent->children, $siblingposition)
950                     );
951                 }
952             }
953             if ($something instanceof admin_category) {
954                 if (isset($this->category_cache[$something->name])) {
955                     debugging('Duplicate admin category name: '.$something->name);
956                 } else {
957                     $this->category_cache[$something->name] = $something;
958                     $something->category_cache =& $this->category_cache;
959                     foreach ($something->children as $child) {
960                         // just in case somebody already added subcategories
961                         if ($child instanceof admin_category) {
962                             if (isset($this->category_cache[$child->name])) {
963                                 debugging('Duplicate admin category name: '.$child->name);
964                             } else {
965                                 $this->category_cache[$child->name] = $child;
966                                 $child->category_cache =& $this->category_cache;
967                             }
968                         }
969                     }
970                 }
971             }
972             return true;
974         } else {
975             debugging('error - can not add this element');
976             return false;
977         }
979     }
981     /**
982      * Checks if the user has access to anything in this category.
983      *
984      * @return bool True if the user has access to at least one child in this category, false otherwise.
985      */
986     public function check_access() {
987         foreach ($this->children as $child) {
988             if ($child->check_access()) {
989                 return true;
990             }
991         }
992         return false;
993     }
995     /**
996      * Is this category hidden in admin tree block?
997      *
998      * @return bool True if hidden
999      */
1000     public function is_hidden() {
1001         return $this->hidden;
1002     }
1004     /**
1005      * Show we display Save button at the page bottom?
1006      * @return bool
1007      */
1008     public function show_save() {
1009         foreach ($this->children as $child) {
1010             if ($child->show_save()) {
1011                 return true;
1012             }
1013         }
1014         return false;
1015     }
1017     /**
1018      * Sets sorting on this category.
1019      *
1020      * Please note this function doesn't actually do the sorting.
1021      * It can be called anytime.
1022      * Sorting occurs when the user calls get_children.
1023      * Code using the children array directly won't see the sorted results.
1024      *
1025      * @param bool $sort If set to true children will be sorted, if false they won't be.
1026      * @param bool $asc If true sorting will be ascending, otherwise descending.
1027      * @param bool $split If true we sort pages and sub categories separately.
1028      */
1029     public function set_sorting($sort, $asc = true, $split = true) {
1030         $this->sort = (bool)$sort;
1031         $this->sortasc = (bool)$asc;
1032         $this->sortsplit = (bool)$split;
1033     }
1035     /**
1036      * Returns the children associated with this category.
1037      *
1038      * @return part_of_admin_tree[]
1039      */
1040     public function get_children() {
1041         // If we should sort and it hasn't already been sorted.
1042         if ($this->sort && !$this->sorted) {
1043             if ($this->sortsplit) {
1044                 $categories = array();
1045                 $pages = array();
1046                 foreach ($this->children as $child) {
1047                     if ($child instanceof admin_category) {
1048                         $categories[] = $child;
1049                     } else {
1050                         $pages[] = $child;
1051                     }
1052                 }
1053                 core_collator::asort_objects_by_property($categories, 'visiblename');
1054                 core_collator::asort_objects_by_property($pages, 'visiblename');
1055                 if (!$this->sortasc) {
1056                     $categories = array_reverse($categories);
1057                     $pages = array_reverse($pages);
1058                 }
1059                 $this->children = array_merge($pages, $categories);
1060             } else {
1061                 core_collator::asort_objects_by_property($this->children, 'visiblename');
1062                 if (!$this->sortasc) {
1063                     $this->children = array_reverse($this->children);
1064                 }
1065             }
1066             $this->sorted = true;
1067         }
1068         return $this->children;
1069     }
1071     /**
1072      * Magically gets a property from this object.
1073      *
1074      * @param $property
1075      * @return part_of_admin_tree[]
1076      * @throws coding_exception
1077      */
1078     public function __get($property) {
1079         if ($property === 'children') {
1080             return $this->get_children();
1081         }
1082         throw new coding_exception('Invalid property requested.');
1083     }
1085     /**
1086      * Magically sets a property against this object.
1087      *
1088      * @param string $property
1089      * @param mixed $value
1090      * @throws coding_exception
1091      */
1092     public function __set($property, $value) {
1093         if ($property === 'children') {
1094             $this->sorted = false;
1095             $this->children = $value;
1096         } else {
1097             throw new coding_exception('Invalid property requested.');
1098         }
1099     }
1101     /**
1102      * Checks if an inaccessible property is set.
1103      *
1104      * @param string $property
1105      * @return bool
1106      * @throws coding_exception
1107      */
1108     public function __isset($property) {
1109         if ($property === 'children') {
1110             return isset($this->children);
1111         }
1112         throw new coding_exception('Invalid property requested.');
1113     }
1117 /**
1118  * Root of admin settings tree, does not have any parent.
1119  *
1120  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1121  */
1122 class admin_root extends admin_category {
1123 /** @var array List of errors */
1124     public $errors;
1125     /** @var string search query */
1126     public $search;
1127     /** @var bool full tree flag - true means all settings required, false only pages required */
1128     public $fulltree;
1129     /** @var bool flag indicating loaded tree */
1130     public $loaded;
1131     /** @var mixed site custom defaults overriding defaults in settings files*/
1132     public $custom_defaults;
1134     /**
1135      * @param bool $fulltree true means all settings required,
1136      *                            false only pages required
1137      */
1138     public function __construct($fulltree) {
1139         global $CFG;
1141         parent::__construct('root', get_string('administration'), false);
1142         $this->errors   = array();
1143         $this->search   = '';
1144         $this->fulltree = $fulltree;
1145         $this->loaded   = false;
1147         $this->category_cache = array();
1149         // load custom defaults if found
1150         $this->custom_defaults = null;
1151         $defaultsfile = "$CFG->dirroot/local/defaults.php";
1152         if (is_readable($defaultsfile)) {
1153             $defaults = array();
1154             include($defaultsfile);
1155             if (is_array($defaults) and count($defaults)) {
1156                 $this->custom_defaults = $defaults;
1157             }
1158         }
1159     }
1161     /**
1162      * Empties children array, and sets loaded to false
1163      *
1164      * @param bool $requirefulltree
1165      */
1166     public function purge_children($requirefulltree) {
1167         $this->children = array();
1168         $this->fulltree = ($requirefulltree || $this->fulltree);
1169         $this->loaded   = false;
1170         //break circular dependencies - this helps PHP 5.2
1171         while($this->category_cache) {
1172             array_pop($this->category_cache);
1173         }
1174         $this->category_cache = array();
1175     }
1179 /**
1180  * Links external PHP pages into the admin tree.
1181  *
1182  * See detailed usage example at the top of this document (adminlib.php)
1183  *
1184  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1185  */
1186 class admin_externalpage implements part_of_admin_tree {
1188     /** @var string An internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects */
1189     public $name;
1191     /** @var string The displayed name for this external page. Usually obtained through get_string(). */
1192     public $visiblename;
1194     /** @var string The external URL that we should link to when someone requests this external page. */
1195     public $url;
1197     /** @var string The role capability/permission a user must have to access this external page. */
1198     public $req_capability;
1200     /** @var object The context in which capability/permission should be checked, default is site context. */
1201     public $context;
1203     /** @var bool hidden in admin tree block. */
1204     public $hidden;
1206     /** @var mixed either string or array of string */
1207     public $path;
1209     /** @var array list of visible names of page parents */
1210     public $visiblepath;
1212     /**
1213      * Constructor for adding an external page into the admin tree.
1214      *
1215      * @param string $name The internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects.
1216      * @param string $visiblename The displayed name for this external page. Usually obtained through get_string().
1217      * @param string $url The external URL that we should link to when someone requests this external page.
1218      * @param mixed $req_capability The role capability/permission a user must have to access this external page. Defaults to 'moodle/site:config'.
1219      * @param boolean $hidden Is this external page hidden in admin tree block? Default false.
1220      * @param stdClass $context The context the page relates to. Not sure what happens
1221      *      if you specify something other than system or front page. Defaults to system.
1222      */
1223     public function __construct($name, $visiblename, $url, $req_capability='moodle/site:config', $hidden=false, $context=NULL) {
1224         $this->name        = $name;
1225         $this->visiblename = $visiblename;
1226         $this->url         = $url;
1227         if (is_array($req_capability)) {
1228             $this->req_capability = $req_capability;
1229         } else {
1230             $this->req_capability = array($req_capability);
1231         }
1232         $this->hidden = $hidden;
1233         $this->context = $context;
1234     }
1236     /**
1237      * Returns a reference to the part_of_admin_tree object with internal name $name.
1238      *
1239      * @param string $name The internal name of the object we want.
1240      * @param bool $findpath defaults to false
1241      * @return mixed A reference to the object with internal name $name if found, otherwise a reference to NULL.
1242      */
1243     public function locate($name, $findpath=false) {
1244         if ($this->name == $name) {
1245             if ($findpath) {
1246                 $this->visiblepath = array($this->visiblename);
1247                 $this->path        = array($this->name);
1248             }
1249             return $this;
1250         } else {
1251             $return = NULL;
1252             return $return;
1253         }
1254     }
1256     /**
1257      * This function always returns false, required function by interface
1258      *
1259      * @param string $name
1260      * @return false
1261      */
1262     public function prune($name) {
1263         return false;
1264     }
1266     /**
1267      * Search using query
1268      *
1269      * @param string $query
1270      * @return mixed array-object structure of found settings and pages
1271      */
1272     public function search($query) {
1273         $found = false;
1274         if (strpos(strtolower($this->name), $query) !== false) {
1275             $found = true;
1276         } else if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
1277                 $found = true;
1278             }
1279         if ($found) {
1280             $result = new stdClass();
1281             $result->page     = $this;
1282             $result->settings = array();
1283             return array($this->name => $result);
1284         } else {
1285             return array();
1286         }
1287     }
1289     /**
1290      * Determines if the current user has access to this external page based on $this->req_capability.
1291      *
1292      * @return bool True if user has access, false otherwise.
1293      */
1294     public function check_access() {
1295         global $CFG;
1296         $context = empty($this->context) ? context_system::instance() : $this->context;
1297         foreach($this->req_capability as $cap) {
1298             if (has_capability($cap, $context)) {
1299                 return true;
1300             }
1301         }
1302         return false;
1303     }
1305     /**
1306      * Is this external page hidden in admin tree block?
1307      *
1308      * @return bool True if hidden
1309      */
1310     public function is_hidden() {
1311         return $this->hidden;
1312     }
1314     /**
1315      * Show we display Save button at the page bottom?
1316      * @return bool
1317      */
1318     public function show_save() {
1319         return false;
1320     }
1324 /**
1325  * Used to group a number of admin_setting objects into a page and add them to the admin tree.
1326  *
1327  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1328  */
1329 class admin_settingpage implements part_of_admin_tree {
1331     /** @var string An internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects */
1332     public $name;
1334     /** @var string The displayed name for this external page. Usually obtained through get_string(). */
1335     public $visiblename;
1337     /** @var mixed An array of admin_setting objects that are part of this setting page. */
1338     public $settings;
1340     /** @var string The role capability/permission a user must have to access this external page. */
1341     public $req_capability;
1343     /** @var object The context in which capability/permission should be checked, default is site context. */
1344     public $context;
1346     /** @var bool hidden in admin tree block. */
1347     public $hidden;
1349     /** @var mixed string of paths or array of strings of paths */
1350     public $path;
1352     /** @var array list of visible names of page parents */
1353     public $visiblepath;
1355     /**
1356      * see admin_settingpage for details of this function
1357      *
1358      * @param string $name The internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects.
1359      * @param string $visiblename The displayed name for this external page. Usually obtained through get_string().
1360      * @param mixed $req_capability The role capability/permission a user must have to access this external page. Defaults to 'moodle/site:config'.
1361      * @param boolean $hidden Is this external page hidden in admin tree block? Default false.
1362      * @param stdClass $context The context the page relates to. Not sure what happens
1363      *      if you specify something other than system or front page. Defaults to system.
1364      */
1365     public function __construct($name, $visiblename, $req_capability='moodle/site:config', $hidden=false, $context=NULL) {
1366         $this->settings    = new stdClass();
1367         $this->name        = $name;
1368         $this->visiblename = $visiblename;
1369         if (is_array($req_capability)) {
1370             $this->req_capability = $req_capability;
1371         } else {
1372             $this->req_capability = array($req_capability);
1373         }
1374         $this->hidden      = $hidden;
1375         $this->context     = $context;
1376     }
1378     /**
1379      * see admin_category
1380      *
1381      * @param string $name
1382      * @param bool $findpath
1383      * @return mixed Object (this) if name ==  this->name, else returns null
1384      */
1385     public function locate($name, $findpath=false) {
1386         if ($this->name == $name) {
1387             if ($findpath) {
1388                 $this->visiblepath = array($this->visiblename);
1389                 $this->path        = array($this->name);
1390             }
1391             return $this;
1392         } else {
1393             $return = NULL;
1394             return $return;
1395         }
1396     }
1398     /**
1399      * Search string in settings page.
1400      *
1401      * @param string $query
1402      * @return array
1403      */
1404     public function search($query) {
1405         $found = array();
1407         foreach ($this->settings as $setting) {
1408             if ($setting->is_related($query)) {
1409                 $found[] = $setting;
1410             }
1411         }
1413         if ($found) {
1414             $result = new stdClass();
1415             $result->page     = $this;
1416             $result->settings = $found;
1417             return array($this->name => $result);
1418         }
1420         $found = false;
1421         if (strpos(strtolower($this->name), $query) !== false) {
1422             $found = true;
1423         } else if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
1424                 $found = true;
1425             }
1426         if ($found) {
1427             $result = new stdClass();
1428             $result->page     = $this;
1429             $result->settings = array();
1430             return array($this->name => $result);
1431         } else {
1432             return array();
1433         }
1434     }
1436     /**
1437      * This function always returns false, required by interface
1438      *
1439      * @param string $name
1440      * @return bool Always false
1441      */
1442     public function prune($name) {
1443         return false;
1444     }
1446     /**
1447      * adds an admin_setting to this admin_settingpage
1448      *
1449      * not the same as add for admin_category. adds an admin_setting to this admin_settingpage. settings appear (on the settingpage) in the order in which they're added
1450      * n.b. each admin_setting in an admin_settingpage must have a unique internal name
1451      *
1452      * @param object $setting is the admin_setting object you want to add
1453      * @return bool true if successful, false if not
1454      */
1455     public function add($setting) {
1456         if (!($setting instanceof admin_setting)) {
1457             debugging('error - not a setting instance');
1458             return false;
1459         }
1461         $name = $setting->name;
1462         if ($setting->plugin) {
1463             $name = $setting->plugin . $name;
1464         }
1465         $this->settings->{$name} = $setting;
1466         return true;
1467     }
1469     /**
1470      * see admin_externalpage
1471      *
1472      * @return bool Returns true for yes false for no
1473      */
1474     public function check_access() {
1475         global $CFG;
1476         $context = empty($this->context) ? context_system::instance() : $this->context;
1477         foreach($this->req_capability as $cap) {
1478             if (has_capability($cap, $context)) {
1479                 return true;
1480             }
1481         }
1482         return false;
1483     }
1485     /**
1486      * outputs this page as html in a table (suitable for inclusion in an admin pagetype)
1487      * @return string Returns an XHTML string
1488      */
1489     public function output_html() {
1490         $adminroot = admin_get_root();
1491         $return = '<fieldset>'."\n".'<div class="clearer"><!-- --></div>'."\n";
1492         foreach($this->settings as $setting) {
1493             $fullname = $setting->get_full_name();
1494             if (array_key_exists($fullname, $adminroot->errors)) {
1495                 $data = $adminroot->errors[$fullname]->data;
1496             } else {
1497                 $data = $setting->get_setting();
1498                 // do not use defaults if settings not available - upgrade settings handles the defaults!
1499             }
1500             $return .= $setting->output_html($data);
1501         }
1502         $return .= '</fieldset>';
1503         return $return;
1504     }
1506     /**
1507      * Is this settings page hidden in admin tree block?
1508      *
1509      * @return bool True if hidden
1510      */
1511     public function is_hidden() {
1512         return $this->hidden;
1513     }
1515     /**
1516      * Show we display Save button at the page bottom?
1517      * @return bool
1518      */
1519     public function show_save() {
1520         foreach($this->settings as $setting) {
1521             if (empty($setting->nosave)) {
1522                 return true;
1523             }
1524         }
1525         return false;
1526     }
1530 /**
1531  * Admin settings class. Only exists on setting pages.
1532  * Read & write happens at this level; no authentication.
1533  *
1534  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1535  */
1536 abstract class admin_setting {
1537     /** @var string unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins. */
1538     public $name;
1539     /** @var string localised name */
1540     public $visiblename;
1541     /** @var string localised long description in Markdown format */
1542     public $description;
1543     /** @var mixed Can be string or array of string */
1544     public $defaultsetting;
1545     /** @var string */
1546     public $updatedcallback;
1547     /** @var mixed can be String or Null.  Null means main config table */
1548     public $plugin; // null means main config table
1549     /** @var bool true indicates this setting does not actually save anything, just information */
1550     public $nosave = false;
1551     /** @var bool if set, indicates that a change to this setting requires rebuild course cache */
1552     public $affectsmodinfo = false;
1553     /** @var array of admin_setting_flag - These are extra checkboxes attached to a setting. */
1554     private $flags = array();
1556     /**
1557      * Constructor
1558      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
1559      *                     or 'myplugin/mysetting' for ones in config_plugins.
1560      * @param string $visiblename localised name
1561      * @param string $description localised long description
1562      * @param mixed $defaultsetting string or array depending on implementation
1563      */
1564     public function __construct($name, $visiblename, $description, $defaultsetting) {
1565         $this->parse_setting_name($name);
1566         $this->visiblename    = $visiblename;
1567         $this->description    = $description;
1568         $this->defaultsetting = $defaultsetting;
1569     }
1571     /**
1572      * Generic function to add a flag to this admin setting.
1573      *
1574      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1575      * @param bool $default - The default for the flag
1576      * @param string $shortname - The shortname for this flag. Used as a suffix for the setting name.
1577      * @param string $displayname - The display name for this flag. Used as a label next to the checkbox.
1578      */
1579     protected function set_flag_options($enabled, $default, $shortname, $displayname) {
1580         if (empty($this->flags[$shortname])) {
1581             $this->flags[$shortname] = new admin_setting_flag($enabled, $default, $shortname, $displayname);
1582         } else {
1583             $this->flags[$shortname]->set_options($enabled, $default);
1584         }
1585     }
1587     /**
1588      * Set the enabled options flag on this admin setting.
1589      *
1590      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1591      * @param bool $default - The default for the flag
1592      */
1593     public function set_enabled_flag_options($enabled, $default) {
1594         $this->set_flag_options($enabled, $default, 'enabled', new lang_string('enabled', 'core_admin'));
1595     }
1597     /**
1598      * Set the advanced options flag on this admin setting.
1599      *
1600      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1601      * @param bool $default - The default for the flag
1602      */
1603     public function set_advanced_flag_options($enabled, $default) {
1604         $this->set_flag_options($enabled, $default, 'adv', new lang_string('advanced'));
1605     }
1608     /**
1609      * Set the locked options flag on this admin setting.
1610      *
1611      * @param bool $enabled - One of self::OPTION_ENABLED or self::OPTION_DISABLED
1612      * @param bool $default - The default for the flag
1613      */
1614     public function set_locked_flag_options($enabled, $default) {
1615         $this->set_flag_options($enabled, $default, 'locked', new lang_string('locked', 'core_admin'));
1616     }
1618     /**
1619      * Get the currently saved value for a setting flag
1620      *
1621      * @param admin_setting_flag $flag - One of the admin_setting_flag for this admin_setting.
1622      * @return bool
1623      */
1624     public function get_setting_flag_value(admin_setting_flag $flag) {
1625         $value = $this->config_read($this->name . '_' . $flag->get_shortname());
1626         if (!isset($value)) {
1627             $value = $flag->get_default();
1628         }
1630         return !empty($value);
1631     }
1633     /**
1634      * Get the list of defaults for the flags on this setting.
1635      *
1636      * @param array of strings describing the defaults for this setting. This is appended to by this function.
1637      */
1638     public function get_setting_flag_defaults(& $defaults) {
1639         foreach ($this->flags as $flag) {
1640             if ($flag->is_enabled() && $flag->get_default()) {
1641                 $defaults[] = $flag->get_displayname();
1642             }
1643         }
1644     }
1646     /**
1647      * Output the input fields for the advanced and locked flags on this setting.
1648      *
1649      * @param bool $adv - The current value of the advanced flag.
1650      * @param bool $locked - The current value of the locked flag.
1651      * @return string $output - The html for the flags.
1652      */
1653     public function output_setting_flags() {
1654         $output = '';
1656         foreach ($this->flags as $flag) {
1657             if ($flag->is_enabled()) {
1658                 $output .= $flag->output_setting_flag($this);
1659             }
1660         }
1662         if (!empty($output)) {
1663             return html_writer::tag('span', $output, array('class' => 'adminsettingsflags'));
1664         }
1665         return $output;
1666     }
1668     /**
1669      * Write the values of the flags for this admin setting.
1670      *
1671      * @param array $data - The data submitted from the form or null to set the default value for new installs.
1672      * @return bool - true if successful.
1673      */
1674     public function write_setting_flags($data) {
1675         $result = true;
1676         foreach ($this->flags as $flag) {
1677             $result = $result && $flag->write_setting_flag($this, $data);
1678         }
1679         return $result;
1680     }
1682     /**
1683      * Set up $this->name and potentially $this->plugin
1684      *
1685      * Set up $this->name and possibly $this->plugin based on whether $name looks
1686      * like 'settingname' or 'plugin/settingname'. Also, do some sanity checking
1687      * on the names, that is, output a developer debug warning if the name
1688      * contains anything other than [a-zA-Z0-9_]+.
1689      *
1690      * @param string $name the setting name passed in to the constructor.
1691      */
1692     private function parse_setting_name($name) {
1693         $bits = explode('/', $name);
1694         if (count($bits) > 2) {
1695             throw new moodle_exception('invalidadminsettingname', '', '', $name);
1696         }
1697         $this->name = array_pop($bits);
1698         if (!preg_match('/^[a-zA-Z0-9_]+$/', $this->name)) {
1699             throw new moodle_exception('invalidadminsettingname', '', '', $name);
1700         }
1701         if (!empty($bits)) {
1702             $this->plugin = array_pop($bits);
1703             if ($this->plugin === 'moodle') {
1704                 $this->plugin = null;
1705             } else if (!preg_match('/^[a-zA-Z0-9_]+$/', $this->plugin)) {
1706                     throw new moodle_exception('invalidadminsettingname', '', '', $name);
1707                 }
1708         }
1709     }
1711     /**
1712      * Returns the fullname prefixed by the plugin
1713      * @return string
1714      */
1715     public function get_full_name() {
1716         return 's_'.$this->plugin.'_'.$this->name;
1717     }
1719     /**
1720      * Returns the ID string based on plugin and name
1721      * @return string
1722      */
1723     public function get_id() {
1724         return 'id_s_'.$this->plugin.'_'.$this->name;
1725     }
1727     /**
1728      * @param bool $affectsmodinfo If true, changes to this setting will
1729      *   cause the course cache to be rebuilt
1730      */
1731     public function set_affects_modinfo($affectsmodinfo) {
1732         $this->affectsmodinfo = $affectsmodinfo;
1733     }
1735     /**
1736      * Returns the config if possible
1737      *
1738      * @return mixed returns config if successful else null
1739      */
1740     public function config_read($name) {
1741         global $CFG;
1742         if (!empty($this->plugin)) {
1743             $value = get_config($this->plugin, $name);
1744             return $value === false ? NULL : $value;
1746         } else {
1747             if (isset($CFG->$name)) {
1748                 return $CFG->$name;
1749             } else {
1750                 return NULL;
1751             }
1752         }
1753     }
1755     /**
1756      * Used to set a config pair and log change
1757      *
1758      * @param string $name
1759      * @param mixed $value Gets converted to string if not null
1760      * @return bool Write setting to config table
1761      */
1762     public function config_write($name, $value) {
1763         global $DB, $USER, $CFG;
1765         if ($this->nosave) {
1766             return true;
1767         }
1769         // make sure it is a real change
1770         $oldvalue = get_config($this->plugin, $name);
1771         $oldvalue = ($oldvalue === false) ? null : $oldvalue; // normalise
1772         $value = is_null($value) ? null : (string)$value;
1774         if ($oldvalue === $value) {
1775             return true;
1776         }
1778         // store change
1779         set_config($name, $value, $this->plugin);
1781         // Some admin settings affect course modinfo
1782         if ($this->affectsmodinfo) {
1783             // Clear course cache for all courses
1784             rebuild_course_cache(0, true);
1785         }
1787         $this->add_to_config_log($name, $oldvalue, $value);
1789         return true; // BC only
1790     }
1792     /**
1793      * Log config changes if necessary.
1794      * @param string $name
1795      * @param string $oldvalue
1796      * @param string $value
1797      */
1798     protected function add_to_config_log($name, $oldvalue, $value) {
1799         add_to_config_log($name, $oldvalue, $value, $this->plugin);
1800     }
1802     /**
1803      * Returns current value of this setting
1804      * @return mixed array or string depending on instance, NULL means not set yet
1805      */
1806     public abstract function get_setting();
1808     /**
1809      * Returns default setting if exists
1810      * @return mixed array or string depending on instance; NULL means no default, user must supply
1811      */
1812     public function get_defaultsetting() {
1813         $adminroot =  admin_get_root(false, false);
1814         if (!empty($adminroot->custom_defaults)) {
1815             $plugin = is_null($this->plugin) ? 'moodle' : $this->plugin;
1816             if (isset($adminroot->custom_defaults[$plugin])) {
1817                 if (array_key_exists($this->name, $adminroot->custom_defaults[$plugin])) { // null is valid value here ;-)
1818                     return $adminroot->custom_defaults[$plugin][$this->name];
1819                 }
1820             }
1821         }
1822         return $this->defaultsetting;
1823     }
1825     /**
1826      * Store new setting
1827      *
1828      * @param mixed $data string or array, must not be NULL
1829      * @return string empty string if ok, string error message otherwise
1830      */
1831     public abstract function write_setting($data);
1833     /**
1834      * Return part of form with setting
1835      * This function should always be overwritten
1836      *
1837      * @param mixed $data array or string depending on setting
1838      * @param string $query
1839      * @return string
1840      */
1841     public function output_html($data, $query='') {
1842     // should be overridden
1843         return;
1844     }
1846     /**
1847      * Function called if setting updated - cleanup, cache reset, etc.
1848      * @param string $functionname Sets the function name
1849      * @return void
1850      */
1851     public function set_updatedcallback($functionname) {
1852         $this->updatedcallback = $functionname;
1853     }
1855     /**
1856      * Execute postupdatecallback if necessary.
1857      * @param mixed $original original value before write_setting()
1858      * @return bool true if changed, false if not.
1859      */
1860     public function post_write_settings($original) {
1861         // Comparison must work for arrays too.
1862         if (serialize($original) === serialize($this->get_setting())) {
1863             return false;
1864         }
1866         $callbackfunction = $this->updatedcallback;
1867         if (!empty($callbackfunction) and function_exists($callbackfunction)) {
1868             $callbackfunction($this->get_full_name());
1869         }
1870         return true;
1871     }
1873     /**
1874      * Is setting related to query text - used when searching
1875      * @param string $query
1876      * @return bool
1877      */
1878     public function is_related($query) {
1879         if (strpos(strtolower($this->name), $query) !== false) {
1880             return true;
1881         }
1882         if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
1883             return true;
1884         }
1885         if (strpos(core_text::strtolower($this->description), $query) !== false) {
1886             return true;
1887         }
1888         $current = $this->get_setting();
1889         if (!is_null($current)) {
1890             if (is_string($current)) {
1891                 if (strpos(core_text::strtolower($current), $query) !== false) {
1892                     return true;
1893                 }
1894             }
1895         }
1896         $default = $this->get_defaultsetting();
1897         if (!is_null($default)) {
1898             if (is_string($default)) {
1899                 if (strpos(core_text::strtolower($default), $query) !== false) {
1900                     return true;
1901                 }
1902             }
1903         }
1904         return false;
1905     }
1908 /**
1909  * An additional option that can be applied to an admin setting.
1910  * The currently supported options are 'ADVANCED' and 'LOCKED'.
1911  *
1912  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1913  */
1914 class admin_setting_flag {
1915     /** @var bool Flag to indicate if this option can be toggled for this setting */
1916     private $enabled = false;
1917     /** @var bool Flag to indicate if this option defaults to true or false */
1918     private $default = false;
1919     /** @var string Short string used to create setting name - e.g. 'adv' */
1920     private $shortname = '';
1921     /** @var string String used as the label for this flag */
1922     private $displayname = '';
1923     /** @const Checkbox for this flag is displayed in admin page */
1924     const ENABLED = true;
1925     /** @const Checkbox for this flag is not displayed in admin page */
1926     const DISABLED = false;
1928     /**
1929      * Constructor
1930      *
1931      * @param bool $enabled Can this option can be toggled.
1932      *                      Should be one of admin_setting_flag::ENABLED or admin_setting_flag::DISABLED.
1933      * @param bool $default The default checked state for this setting option.
1934      * @param string $shortname The shortname of this flag. Currently supported flags are 'locked' and 'adv'
1935      * @param string $displayname The displayname of this flag. Used as a label for the flag.
1936      */
1937     public function __construct($enabled, $default, $shortname, $displayname) {
1938         $this->shortname = $shortname;
1939         $this->displayname = $displayname;
1940         $this->set_options($enabled, $default);
1941     }
1943     /**
1944      * Update the values of this setting options class
1945      *
1946      * @param bool $enabled Can this option can be toggled.
1947      *                      Should be one of admin_setting_flag::ENABLED or admin_setting_flag::DISABLED.
1948      * @param bool $default The default checked state for this setting option.
1949      */
1950     public function set_options($enabled, $default) {
1951         $this->enabled = $enabled;
1952         $this->default = $default;
1953     }
1955     /**
1956      * Should this option appear in the interface and be toggleable?
1957      *
1958      * @return bool Is it enabled?
1959      */
1960     public function is_enabled() {
1961         return $this->enabled;
1962     }
1964     /**
1965      * Should this option be checked by default?
1966      *
1967      * @return bool Is it on by default?
1968      */
1969     public function get_default() {
1970         return $this->default;
1971     }
1973     /**
1974      * Return the short name for this flag. e.g. 'adv' or 'locked'
1975      *
1976      * @return string
1977      */
1978     public function get_shortname() {
1979         return $this->shortname;
1980     }
1982     /**
1983      * Return the display name for this flag. e.g. 'Advanced' or 'Locked'
1984      *
1985      * @return string
1986      */
1987     public function get_displayname() {
1988         return $this->displayname;
1989     }
1991     /**
1992      * Save the submitted data for this flag - or set it to the default if $data is null.
1993      *
1994      * @param admin_setting $setting - The admin setting for this flag
1995      * @param array $data - The data submitted from the form or null to set the default value for new installs.
1996      * @return bool
1997      */
1998     public function write_setting_flag(admin_setting $setting, $data) {
1999         $result = true;
2000         if ($this->is_enabled()) {
2001             if (!isset($data)) {
2002                 $value = $this->get_default();
2003             } else {
2004                 $value = !empty($data[$setting->get_full_name() . '_' . $this->get_shortname()]);
2005             }
2006             $result = $setting->config_write($setting->name . '_' . $this->get_shortname(), $value);
2007         }
2009         return $result;
2011     }
2013     /**
2014      * Output the checkbox for this setting flag. Should only be called if the flag is enabled.
2015      *
2016      * @param admin_setting $setting - The admin setting for this flag
2017      * @return string - The html for the checkbox.
2018      */
2019     public function output_setting_flag(admin_setting $setting) {
2020         $value = $setting->get_setting_flag_value($this);
2021         $output = ' <input type="checkbox" class="form-checkbox" ' .
2022                         ' id="' .  $setting->get_id() . '_' . $this->get_shortname() . '" ' .
2023                         ' name="' . $setting->get_full_name() .  '_' . $this->get_shortname() . '" ' .
2024                         ' value="1" ' . ($value ? 'checked="checked"' : '') . ' />' .
2025                         ' <label for="' . $setting->get_id() . '_' . $this->get_shortname() . '">' .
2026                         $this->get_displayname() .
2027                         ' </label> ';
2028         return $output;
2029     }
2033 /**
2034  * No setting - just heading and text.
2035  *
2036  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2037  */
2038 class admin_setting_heading extends admin_setting {
2040     /**
2041      * not a setting, just text
2042      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2043      * @param string $heading heading
2044      * @param string $information text in box
2045      */
2046     public function __construct($name, $heading, $information) {
2047         $this->nosave = true;
2048         parent::__construct($name, $heading, $information, '');
2049     }
2051     /**
2052      * Always returns true
2053      * @return bool Always returns true
2054      */
2055     public function get_setting() {
2056         return true;
2057     }
2059     /**
2060      * Always returns true
2061      * @return bool Always returns true
2062      */
2063     public function get_defaultsetting() {
2064         return true;
2065     }
2067     /**
2068      * Never write settings
2069      * @return string Always returns an empty string
2070      */
2071     public function write_setting($data) {
2072     // do not write any setting
2073         return '';
2074     }
2076     /**
2077      * Returns an HTML string
2078      * @return string Returns an HTML string
2079      */
2080     public function output_html($data, $query='') {
2081         global $OUTPUT;
2082         $return = '';
2083         if ($this->visiblename != '') {
2084             $return .= $OUTPUT->heading($this->visiblename, 3, 'main');
2085         }
2086         if ($this->description != '') {
2087             $return .= $OUTPUT->box(highlight($query, markdown_to_html($this->description)), 'generalbox formsettingheading');
2088         }
2089         return $return;
2090     }
2094 /**
2095  * The most flexibly setting, user is typing text
2096  *
2097  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2098  */
2099 class admin_setting_configtext extends admin_setting {
2101     /** @var mixed int means PARAM_XXX type, string is a allowed format in regex */
2102     public $paramtype;
2103     /** @var int default field size */
2104     public $size;
2106     /**
2107      * Config text constructor
2108      *
2109      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2110      * @param string $visiblename localised
2111      * @param string $description long localised info
2112      * @param string $defaultsetting
2113      * @param mixed $paramtype int means PARAM_XXX type, string is a allowed format in regex
2114      * @param int $size default field size
2115      */
2116     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $size=null) {
2117         $this->paramtype = $paramtype;
2118         if (!is_null($size)) {
2119             $this->size  = $size;
2120         } else {
2121             $this->size  = ($paramtype === PARAM_INT) ? 5 : 30;
2122         }
2123         parent::__construct($name, $visiblename, $description, $defaultsetting);
2124     }
2126     /**
2127      * Return the setting
2128      *
2129      * @return mixed returns config if successful else null
2130      */
2131     public function get_setting() {
2132         return $this->config_read($this->name);
2133     }
2135     public function write_setting($data) {
2136         if ($this->paramtype === PARAM_INT and $data === '') {
2137         // do not complain if '' used instead of 0
2138             $data = 0;
2139         }
2140         // $data is a string
2141         $validated = $this->validate($data);
2142         if ($validated !== true) {
2143             return $validated;
2144         }
2145         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
2146     }
2148     /**
2149      * Validate data before storage
2150      * @param string data
2151      * @return mixed true if ok string if error found
2152      */
2153     public function validate($data) {
2154         // allow paramtype to be a custom regex if it is the form of /pattern/
2155         if (preg_match('#^/.*/$#', $this->paramtype)) {
2156             if (preg_match($this->paramtype, $data)) {
2157                 return true;
2158             } else {
2159                 return get_string('validateerror', 'admin');
2160             }
2162         } else if ($this->paramtype === PARAM_RAW) {
2163             return true;
2165         } else {
2166             $cleaned = clean_param($data, $this->paramtype);
2167             if ("$data" === "$cleaned") { // implicit conversion to string is needed to do exact comparison
2168                 return true;
2169             } else {
2170                 return get_string('validateerror', 'admin');
2171             }
2172         }
2173     }
2175     /**
2176      * Return an XHTML string for the setting
2177      * @return string Returns an XHTML string
2178      */
2179     public function output_html($data, $query='') {
2180         $default = $this->get_defaultsetting();
2182         return format_admin_setting($this, $this->visiblename,
2183         '<div class="form-text defaultsnext"><input type="text" size="'.$this->size.'" id="'.$this->get_id().'" name="'.$this->get_full_name().'" value="'.s($data).'" /></div>',
2184         $this->description, true, '', $default, $query);
2185     }
2188 /**
2189  * Text input with a maximum length constraint.
2190  *
2191  * @copyright 2015 onwards Ankit Agarwal
2192  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2193  */
2194 class admin_setting_configtext_with_maxlength extends admin_setting_configtext {
2196     /** @var int maximum number of chars allowed. */
2197     protected $maxlength;
2199     /**
2200      * Config text constructor
2201      *
2202      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
2203      *                     or 'myplugin/mysetting' for ones in config_plugins.
2204      * @param string $visiblename localised
2205      * @param string $description long localised info
2206      * @param string $defaultsetting
2207      * @param mixed $paramtype int means PARAM_XXX type, string is a allowed format in regex
2208      * @param int $size default field size
2209      * @param mixed $maxlength int maxlength allowed, 0 for infinite.
2210      */
2211     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW,
2212                                 $size=null, $maxlength = 0) {
2213         $this->maxlength = $maxlength;
2214         parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype, $size);
2215     }
2217     /**
2218      * Validate data before storage
2219      *
2220      * @param string $data data
2221      * @return mixed true if ok string if error found
2222      */
2223     public function validate($data) {
2224         $parentvalidation = parent::validate($data);
2225         if ($parentvalidation === true) {
2226             if ($this->maxlength > 0) {
2227                 // Max length check.
2228                 $length = core_text::strlen($data);
2229                 if ($length > $this->maxlength) {
2230                     return get_string('maximumchars', 'moodle',  $this->maxlength);
2231                 }
2232                 return true;
2233             } else {
2234                 return true; // No max length check needed.
2235             }
2236         } else {
2237             return $parentvalidation;
2238         }
2239     }
2242 /**
2243  * General text area without html editor.
2244  *
2245  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2246  */
2247 class admin_setting_configtextarea extends admin_setting_configtext {
2248     private $rows;
2249     private $cols;
2251     /**
2252      * @param string $name
2253      * @param string $visiblename
2254      * @param string $description
2255      * @param mixed $defaultsetting string or array
2256      * @param mixed $paramtype
2257      * @param string $cols The number of columns to make the editor
2258      * @param string $rows The number of rows to make the editor
2259      */
2260     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $cols='60', $rows='8') {
2261         $this->rows = $rows;
2262         $this->cols = $cols;
2263         parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype);
2264     }
2266     /**
2267      * Returns an XHTML string for the editor
2268      *
2269      * @param string $data
2270      * @param string $query
2271      * @return string XHTML string for the editor
2272      */
2273     public function output_html($data, $query='') {
2274         $default = $this->get_defaultsetting();
2276         $defaultinfo = $default;
2277         if (!is_null($default) and $default !== '') {
2278             $defaultinfo = "\n".$default;
2279         }
2281         return format_admin_setting($this, $this->visiblename,
2282         '<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>',
2283         $this->description, true, '', $defaultinfo, $query);
2284     }
2288 /**
2289  * General text area with html editor.
2290  */
2291 class admin_setting_confightmleditor extends admin_setting_configtext {
2292     private $rows;
2293     private $cols;
2295     /**
2296      * @param string $name
2297      * @param string $visiblename
2298      * @param string $description
2299      * @param mixed $defaultsetting string or array
2300      * @param mixed $paramtype
2301      */
2302     public function __construct($name, $visiblename, $description, $defaultsetting, $paramtype=PARAM_RAW, $cols='60', $rows='8') {
2303         $this->rows = $rows;
2304         $this->cols = $cols;
2305         parent::__construct($name, $visiblename, $description, $defaultsetting, $paramtype);
2306         editors_head_setup();
2307     }
2309     /**
2310      * Returns an XHTML string for the editor
2311      *
2312      * @param string $data
2313      * @param string $query
2314      * @return string XHTML string for the editor
2315      */
2316     public function output_html($data, $query='') {
2317         $default = $this->get_defaultsetting();
2319         $defaultinfo = $default;
2320         if (!is_null($default) and $default !== '') {
2321             $defaultinfo = "\n".$default;
2322         }
2324         $editor = editors_get_preferred_editor(FORMAT_HTML);
2325         $editor->set_text($data);
2326         $editor->use_editor($this->get_id(), array('noclean'=>true));
2328         return format_admin_setting($this, $this->visiblename,
2329         '<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>',
2330         $this->description, true, '', $defaultinfo, $query);
2331     }
2335 /**
2336  * Password field, allows unmasking of password
2337  *
2338  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2339  */
2340 class admin_setting_configpasswordunmask extends admin_setting_configtext {
2341     /**
2342      * Constructor
2343      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2344      * @param string $visiblename localised
2345      * @param string $description long localised info
2346      * @param string $defaultsetting default password
2347      */
2348     public function __construct($name, $visiblename, $description, $defaultsetting) {
2349         parent::__construct($name, $visiblename, $description, $defaultsetting, PARAM_RAW, 30);
2350     }
2352     /**
2353      * Log config changes if necessary.
2354      * @param string $name
2355      * @param string $oldvalue
2356      * @param string $value
2357      */
2358     protected function add_to_config_log($name, $oldvalue, $value) {
2359         if ($value !== '') {
2360             $value = '********';
2361         }
2362         if ($oldvalue !== '' and $oldvalue !== null) {
2363             $oldvalue = '********';
2364         }
2365         parent::add_to_config_log($name, $oldvalue, $value);
2366     }
2368     /**
2369      * Returns XHTML for the field
2370      * Writes Javascript into the HTML below right before the last div
2371      *
2372      * @todo Make javascript available through newer methods if possible
2373      * @param string $data Value for the field
2374      * @param string $query Passed as final argument for format_admin_setting
2375      * @return string XHTML field
2376      */
2377     public function output_html($data, $query='') {
2378         $id = $this->get_id();
2379         $unmask = get_string('unmaskpassword', 'form');
2380         $unmaskjs = '<script type="text/javascript">
2381 //<![CDATA[
2382 var is_ie = (navigator.userAgent.toLowerCase().indexOf("msie") != -1);
2384 document.getElementById("'.$id.'").setAttribute("autocomplete", "off");
2386 var unmaskdiv = document.getElementById("'.$id.'unmaskdiv");
2388 var unmaskchb = document.createElement("input");
2389 unmaskchb.setAttribute("type", "checkbox");
2390 unmaskchb.setAttribute("id", "'.$id.'unmask");
2391 unmaskchb.onchange = function() {unmaskPassword("'.$id.'");};
2392 unmaskdiv.appendChild(unmaskchb);
2394 var unmasklbl = document.createElement("label");
2395 unmasklbl.innerHTML = "'.addslashes_js($unmask).'";
2396 if (is_ie) {
2397   unmasklbl.setAttribute("htmlFor", "'.$id.'unmask");
2398 } else {
2399   unmasklbl.setAttribute("for", "'.$id.'unmask");
2401 unmaskdiv.appendChild(unmasklbl);
2403 if (is_ie) {
2404   // ugly hack to work around the famous onchange IE bug
2405   unmaskchb.onclick = function() {this.blur();};
2406   unmaskdiv.onclick = function() {this.blur();};
2408 //]]>
2409 </script>';
2410         return format_admin_setting($this, $this->visiblename,
2411         '<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>',
2412         $this->description, true, '', NULL, $query);
2413     }
2416 /**
2417  * Empty setting used to allow flags (advanced) on settings that can have no sensible default.
2418  * Note: Only advanced makes sense right now - locked does not.
2419  *
2420  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2421  */
2422 class admin_setting_configempty extends admin_setting_configtext {
2424     /**
2425      * @param string $name
2426      * @param string $visiblename
2427      * @param string $description
2428      */
2429     public function __construct($name, $visiblename, $description) {
2430         parent::__construct($name, $visiblename, $description, '', PARAM_RAW);
2431     }
2433     /**
2434      * Returns an XHTML string for the hidden field
2435      *
2436      * @param string $data
2437      * @param string $query
2438      * @return string XHTML string for the editor
2439      */
2440     public function output_html($data, $query='') {
2441         return format_admin_setting($this,
2442                                     $this->visiblename,
2443                                     '<div class="form-empty" >' .
2444                                     '<input type="hidden"' .
2445                                         ' id="'. $this->get_id() .'"' .
2446                                         ' name="'. $this->get_full_name() .'"' .
2447                                         ' value=""/></div>',
2448                                     $this->description,
2449                                     true,
2450                                     '',
2451                                     get_string('none'),
2452                                     $query);
2453     }
2457 /**
2458  * Path to directory
2459  *
2460  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2461  */
2462 class admin_setting_configfile extends admin_setting_configtext {
2463     /**
2464      * Constructor
2465      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2466      * @param string $visiblename localised
2467      * @param string $description long localised info
2468      * @param string $defaultdirectory default directory location
2469      */
2470     public function __construct($name, $visiblename, $description, $defaultdirectory) {
2471         parent::__construct($name, $visiblename, $description, $defaultdirectory, PARAM_RAW, 50);
2472     }
2474     /**
2475      * Returns XHTML for the field
2476      *
2477      * Returns XHTML for the field and also checks whether the file
2478      * specified in $data exists using file_exists()
2479      *
2480      * @param string $data File name and path to use in value attr
2481      * @param string $query
2482      * @return string XHTML field
2483      */
2484     public function output_html($data, $query='') {
2485         global $CFG;
2486         $default = $this->get_defaultsetting();
2488         if ($data) {
2489             if (file_exists($data)) {
2490                 $executable = '<span class="pathok">&#x2714;</span>';
2491             } else {
2492                 $executable = '<span class="patherror">&#x2718;</span>';
2493             }
2494         } else {
2495             $executable = '';
2496         }
2497         $readonly = '';
2498         if (!empty($CFG->preventexecpath)) {
2499             $this->visiblename .= '<div class="form-overridden">'.get_string('execpathnotallowed', 'admin').'</div>';
2500             $readonly = 'readonly="readonly"';
2501         }
2503         return format_admin_setting($this, $this->visiblename,
2504         '<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>',
2505         $this->description, true, '', $default, $query);
2506     }
2508     /**
2509      * Checks if execpatch has been disabled in config.php
2510      */
2511     public function write_setting($data) {
2512         global $CFG;
2513         if (!empty($CFG->preventexecpath)) {
2514             if ($this->get_setting() === null) {
2515                 // Use default during installation.
2516                 $data = $this->get_defaultsetting();
2517                 if ($data === null) {
2518                     $data = '';
2519                 }
2520             } else {
2521                 return '';
2522             }
2523         }
2524         return parent::write_setting($data);
2525     }
2529 /**
2530  * Path to executable file
2531  *
2532  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2533  */
2534 class admin_setting_configexecutable extends admin_setting_configfile {
2536     /**
2537      * Returns an XHTML field
2538      *
2539      * @param string $data This is the value for the field
2540      * @param string $query
2541      * @return string XHTML field
2542      */
2543     public function output_html($data, $query='') {
2544         global $CFG;
2545         $default = $this->get_defaultsetting();
2547         if ($data) {
2548             if (file_exists($data) and !is_dir($data) and is_executable($data)) {
2549                 $executable = '<span class="pathok">&#x2714;</span>';
2550             } else {
2551                 $executable = '<span class="patherror">&#x2718;</span>';
2552             }
2553         } else {
2554             $executable = '';
2555         }
2556         $readonly = '';
2557         if (!empty($CFG->preventexecpath)) {
2558             $this->visiblename .= '<div class="form-overridden">'.get_string('execpathnotallowed', 'admin').'</div>';
2559             $readonly = 'readonly="readonly"';
2560         }
2562         return format_admin_setting($this, $this->visiblename,
2563         '<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>',
2564         $this->description, true, '', $default, $query);
2565     }
2569 /**
2570  * Path to directory
2571  *
2572  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2573  */
2574 class admin_setting_configdirectory extends admin_setting_configfile {
2576     /**
2577      * Returns an XHTML field
2578      *
2579      * @param string $data This is the value for the field
2580      * @param string $query
2581      * @return string XHTML
2582      */
2583     public function output_html($data, $query='') {
2584         global $CFG;
2585         $default = $this->get_defaultsetting();
2587         if ($data) {
2588             if (file_exists($data) and is_dir($data)) {
2589                 $executable = '<span class="pathok">&#x2714;</span>';
2590             } else {
2591                 $executable = '<span class="patherror">&#x2718;</span>';
2592             }
2593         } else {
2594             $executable = '';
2595         }
2596         $readonly = '';
2597         if (!empty($CFG->preventexecpath)) {
2598             $this->visiblename .= '<div class="form-overridden">'.get_string('execpathnotallowed', 'admin').'</div>';
2599             $readonly = 'readonly="readonly"';
2600         }
2602         return format_admin_setting($this, $this->visiblename,
2603         '<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>',
2604         $this->description, true, '', $default, $query);
2605     }
2609 /**
2610  * Checkbox
2611  *
2612  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2613  */
2614 class admin_setting_configcheckbox extends admin_setting {
2615     /** @var string Value used when checked */
2616     public $yes;
2617     /** @var string Value used when not checked */
2618     public $no;
2620     /**
2621      * Constructor
2622      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2623      * @param string $visiblename localised
2624      * @param string $description long localised info
2625      * @param string $defaultsetting
2626      * @param string $yes value used when checked
2627      * @param string $no value used when not checked
2628      */
2629     public function __construct($name, $visiblename, $description, $defaultsetting, $yes='1', $no='0') {
2630         parent::__construct($name, $visiblename, $description, $defaultsetting);
2631         $this->yes = (string)$yes;
2632         $this->no  = (string)$no;
2633     }
2635     /**
2636      * Retrieves the current setting using the objects name
2637      *
2638      * @return string
2639      */
2640     public function get_setting() {
2641         return $this->config_read($this->name);
2642     }
2644     /**
2645      * Sets the value for the setting
2646      *
2647      * Sets the value for the setting to either the yes or no values
2648      * of the object by comparing $data to yes
2649      *
2650      * @param mixed $data Gets converted to str for comparison against yes value
2651      * @return string empty string or error
2652      */
2653     public function write_setting($data) {
2654         if ((string)$data === $this->yes) { // convert to strings before comparison
2655             $data = $this->yes;
2656         } else {
2657             $data = $this->no;
2658         }
2659         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
2660     }
2662     /**
2663      * Returns an XHTML checkbox field
2664      *
2665      * @param string $data If $data matches yes then checkbox is checked
2666      * @param string $query
2667      * @return string XHTML field
2668      */
2669     public function output_html($data, $query='') {
2670         $default = $this->get_defaultsetting();
2672         if (!is_null($default)) {
2673             if ((string)$default === $this->yes) {
2674                 $defaultinfo = get_string('checkboxyes', 'admin');
2675             } else {
2676                 $defaultinfo = get_string('checkboxno', 'admin');
2677             }
2678         } else {
2679             $defaultinfo = NULL;
2680         }
2682         if ((string)$data === $this->yes) { // convert to strings before comparison
2683             $checked = 'checked="checked"';
2684         } else {
2685             $checked = '';
2686         }
2688         return format_admin_setting($this, $this->visiblename,
2689         '<div class="form-checkbox defaultsnext" ><input type="hidden" name="'.$this->get_full_name().'" value="'.s($this->no).'" /> '
2690             .'<input type="checkbox" id="'.$this->get_id().'" name="'.$this->get_full_name().'" value="'.s($this->yes).'" '.$checked.' /></div>',
2691         $this->description, true, '', $defaultinfo, $query);
2692     }
2696 /**
2697  * Multiple checkboxes, each represents different value, stored in csv format
2698  *
2699  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2700  */
2701 class admin_setting_configmulticheckbox extends admin_setting {
2702     /** @var array Array of choices value=>label */
2703     public $choices;
2705     /**
2706      * Constructor: uses parent::__construct
2707      *
2708      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2709      * @param string $visiblename localised
2710      * @param string $description long localised info
2711      * @param array $defaultsetting array of selected
2712      * @param array $choices array of $value=>$label for each checkbox
2713      */
2714     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
2715         $this->choices = $choices;
2716         parent::__construct($name, $visiblename, $description, $defaultsetting);
2717     }
2719     /**
2720      * This public function may be used in ancestors for lazy loading of choices
2721      *
2722      * @todo Check if this function is still required content commented out only returns true
2723      * @return bool true if loaded, false if error
2724      */
2725     public function load_choices() {
2726         /*
2727         if (is_array($this->choices)) {
2728             return true;
2729         }
2730         .... load choices here
2731         */
2732         return true;
2733     }
2735     /**
2736      * Is setting related to query text - used when searching
2737      *
2738      * @param string $query
2739      * @return bool true on related, false on not or failure
2740      */
2741     public function is_related($query) {
2742         if (!$this->load_choices() or empty($this->choices)) {
2743             return false;
2744         }
2745         if (parent::is_related($query)) {
2746             return true;
2747         }
2749         foreach ($this->choices as $desc) {
2750             if (strpos(core_text::strtolower($desc), $query) !== false) {
2751                 return true;
2752             }
2753         }
2754         return false;
2755     }
2757     /**
2758      * Returns the current setting if it is set
2759      *
2760      * @return mixed null if null, else an array
2761      */
2762     public function get_setting() {
2763         $result = $this->config_read($this->name);
2765         if (is_null($result)) {
2766             return NULL;
2767         }
2768         if ($result === '') {
2769             return array();
2770         }
2771         $enabled = explode(',', $result);
2772         $setting = array();
2773         foreach ($enabled as $option) {
2774             $setting[$option] = 1;
2775         }
2776         return $setting;
2777     }
2779     /**
2780      * Saves the setting(s) provided in $data
2781      *
2782      * @param array $data An array of data, if not array returns empty str
2783      * @return mixed empty string on useless data or bool true=success, false=failed
2784      */
2785     public function write_setting($data) {
2786         if (!is_array($data)) {
2787             return ''; // ignore it
2788         }
2789         if (!$this->load_choices() or empty($this->choices)) {
2790             return '';
2791         }
2792         unset($data['xxxxx']);
2793         $result = array();
2794         foreach ($data as $key => $value) {
2795             if ($value and array_key_exists($key, $this->choices)) {
2796                 $result[] = $key;
2797             }
2798         }
2799         return $this->config_write($this->name, implode(',', $result)) ? '' : get_string('errorsetting', 'admin');
2800     }
2802     /**
2803      * Returns XHTML field(s) as required by choices
2804      *
2805      * Relies on data being an array should data ever be another valid vartype with
2806      * acceptable value this may cause a warning/error
2807      * if (!is_array($data)) would fix the problem
2808      *
2809      * @todo Add vartype handling to ensure $data is an array
2810      *
2811      * @param array $data An array of checked values
2812      * @param string $query
2813      * @return string XHTML field
2814      */
2815     public function output_html($data, $query='') {
2816         if (!$this->load_choices() or empty($this->choices)) {
2817             return '';
2818         }
2819         $default = $this->get_defaultsetting();
2820         if (is_null($default)) {
2821             $default = array();
2822         }
2823         if (is_null($data)) {
2824             $data = array();
2825         }
2826         $options = array();
2827         $defaults = array();
2828         foreach ($this->choices as $key=>$description) {
2829             if (!empty($data[$key])) {
2830                 $checked = 'checked="checked"';
2831             } else {
2832                 $checked = '';
2833             }
2834             if (!empty($default[$key])) {
2835                 $defaults[] = $description;
2836             }
2838             $options[] = '<input type="checkbox" id="'.$this->get_id().'_'.$key.'" name="'.$this->get_full_name().'['.$key.']" value="1" '.$checked.' />'
2839                 .'<label for="'.$this->get_id().'_'.$key.'">'.highlightfast($query, $description).'</label>';
2840         }
2842         if (is_null($default)) {
2843             $defaultinfo = NULL;
2844         } else if (!empty($defaults)) {
2845                 $defaultinfo = implode(', ', $defaults);
2846             } else {
2847                 $defaultinfo = get_string('none');
2848             }
2850         $return = '<div class="form-multicheckbox">';
2851         $return .= '<input type="hidden" name="'.$this->get_full_name().'[xxxxx]" value="1" />'; // something must be submitted even if nothing selected
2852         if ($options) {
2853             $return .= '<ul>';
2854             foreach ($options as $option) {
2855                 $return .= '<li>'.$option.'</li>';
2856             }
2857             $return .= '</ul>';
2858         }
2859         $return .= '</div>';
2861         return format_admin_setting($this, $this->visiblename, $return, $this->description, false, '', $defaultinfo, $query);
2863     }
2867 /**
2868  * Multiple checkboxes 2, value stored as string 00101011
2869  *
2870  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2871  */
2872 class admin_setting_configmulticheckbox2 extends admin_setting_configmulticheckbox {
2874     /**
2875      * Returns the setting if set
2876      *
2877      * @return mixed null if not set, else an array of set settings
2878      */
2879     public function get_setting() {
2880         $result = $this->config_read($this->name);
2881         if (is_null($result)) {
2882             return NULL;
2883         }
2884         if (!$this->load_choices()) {
2885             return NULL;
2886         }
2887         $result = str_pad($result, count($this->choices), '0');
2888         $result = preg_split('//', $result, -1, PREG_SPLIT_NO_EMPTY);
2889         $setting = array();
2890         foreach ($this->choices as $key=>$unused) {
2891             $value = array_shift($result);
2892             if ($value) {
2893                 $setting[$key] = 1;
2894             }
2895         }
2896         return $setting;
2897     }
2899     /**
2900      * Save setting(s) provided in $data param
2901      *
2902      * @param array $data An array of settings to save
2903      * @return mixed empty string for bad data or bool true=>success, false=>error
2904      */
2905     public function write_setting($data) {
2906         if (!is_array($data)) {
2907             return ''; // ignore it
2908         }
2909         if (!$this->load_choices() or empty($this->choices)) {
2910             return '';
2911         }
2912         $result = '';
2913         foreach ($this->choices as $key=>$unused) {
2914             if (!empty($data[$key])) {
2915                 $result .= '1';
2916             } else {
2917                 $result .= '0';
2918             }
2919         }
2920         return $this->config_write($this->name, $result) ? '' : get_string('errorsetting', 'admin');
2921     }
2925 /**
2926  * Select one value from list
2927  *
2928  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2929  */
2930 class admin_setting_configselect extends admin_setting {
2931     /** @var array Array of choices value=>label */
2932     public $choices;
2934     /**
2935      * Constructor
2936      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
2937      * @param string $visiblename localised
2938      * @param string $description long localised info
2939      * @param string|int $defaultsetting
2940      * @param array $choices array of $value=>$label for each selection
2941      */
2942     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
2943         $this->choices = $choices;
2944         parent::__construct($name, $visiblename, $description, $defaultsetting);
2945     }
2947     /**
2948      * This function may be used in ancestors for lazy loading of choices
2949      *
2950      * Override this method if loading of choices is expensive, such
2951      * as when it requires multiple db requests.
2952      *
2953      * @return bool true if loaded, false if error
2954      */
2955     public function load_choices() {
2956         /*
2957         if (is_array($this->choices)) {
2958             return true;
2959         }
2960         .... load choices here
2961         */
2962         return true;
2963     }
2965     /**
2966      * Check if this is $query is related to a choice
2967      *
2968      * @param string $query
2969      * @return bool true if related, false if not
2970      */
2971     public function is_related($query) {
2972         if (parent::is_related($query)) {
2973             return true;
2974         }
2975         if (!$this->load_choices()) {
2976             return false;
2977         }
2978         foreach ($this->choices as $key=>$value) {
2979             if (strpos(core_text::strtolower($key), $query) !== false) {
2980                 return true;
2981             }
2982             if (strpos(core_text::strtolower($value), $query) !== false) {
2983                 return true;
2984             }
2985         }
2986         return false;
2987     }
2989     /**
2990      * Return the setting
2991      *
2992      * @return mixed returns config if successful else null
2993      */
2994     public function get_setting() {
2995         return $this->config_read($this->name);
2996     }
2998     /**
2999      * Save a setting
3000      *
3001      * @param string $data
3002      * @return string empty of error string
3003      */
3004     public function write_setting($data) {
3005         if (!$this->load_choices() or empty($this->choices)) {
3006             return '';
3007         }
3008         if (!array_key_exists($data, $this->choices)) {
3009             return ''; // ignore it
3010         }
3012         return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
3013     }
3015     /**
3016      * Returns XHTML select field
3017      *
3018      * Ensure the options are loaded, and generate the XHTML for the select
3019      * element and any warning message. Separating this out from output_html
3020      * makes it easier to subclass this class.
3021      *
3022      * @param string $data the option to show as selected.
3023      * @param string $current the currently selected option in the database, null if none.
3024      * @param string $default the default selected option.
3025      * @return array the HTML for the select element, and a warning message.
3026      */
3027     public function output_select_html($data, $current, $default, $extraname = '') {
3028         if (!$this->load_choices() or empty($this->choices)) {
3029             return array('', '');
3030         }
3032         $warning = '';
3033         if (is_null($current)) {
3034         // first run
3035         } else if (empty($current) and (array_key_exists('', $this->choices) or array_key_exists(0, $this->choices))) {
3036             // no warning
3037             } else if (!array_key_exists($current, $this->choices)) {
3038                     $warning = get_string('warningcurrentsetting', 'admin', s($current));
3039                     if (!is_null($default) and $data == $current) {
3040                         $data = $default; // use default instead of first value when showing the form
3041                     }
3042                 }
3044         $selecthtml = '<select id="'.$this->get_id().'" name="'.$this->get_full_name().$extraname.'">';
3045         foreach ($this->choices as $key => $value) {
3046         // the string cast is needed because key may be integer - 0 is equal to most strings!
3047             $selecthtml .= '<option value="'.$key.'"'.((string)$key==$data ? ' selected="selected"' : '').'>'.$value.'</option>';
3048         }
3049         $selecthtml .= '</select>';
3050         return array($selecthtml, $warning);
3051     }
3053     /**
3054      * Returns XHTML select field and wrapping div(s)
3055      *
3056      * @see output_select_html()
3057      *
3058      * @param string $data the option to show as selected
3059      * @param string $query
3060      * @return string XHTML field and wrapping div
3061      */
3062     public function output_html($data, $query='') {
3063         $default = $this->get_defaultsetting();
3064         $current = $this->get_setting();
3066         list($selecthtml, $warning) = $this->output_select_html($data, $current, $default);
3067         if (!$selecthtml) {
3068             return '';
3069         }
3071         if (!is_null($default) and array_key_exists($default, $this->choices)) {
3072             $defaultinfo = $this->choices[$default];
3073         } else {
3074             $defaultinfo = NULL;
3075         }
3077         $return = '<div class="form-select defaultsnext">' . $selecthtml . '</div>';
3079         return format_admin_setting($this, $this->visiblename, $return, $this->description, true, $warning, $defaultinfo, $query);
3080     }
3084 /**
3085  * Select multiple items from list
3086  *
3087  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3088  */
3089 class admin_setting_configmultiselect extends admin_setting_configselect {
3090     /**
3091      * Constructor
3092      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3093      * @param string $visiblename localised
3094      * @param string $description long localised info
3095      * @param array $defaultsetting array of selected items
3096      * @param array $choices array of $value=>$label for each list item
3097      */
3098     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
3099         parent::__construct($name, $visiblename, $description, $defaultsetting, $choices);
3100     }
3102     /**
3103      * Returns the select setting(s)
3104      *
3105      * @return mixed null or array. Null if no settings else array of setting(s)
3106      */
3107     public function get_setting() {
3108         $result = $this->config_read($this->name);
3109         if (is_null($result)) {
3110             return NULL;
3111         }
3112         if ($result === '') {
3113             return array();
3114         }
3115         return explode(',', $result);
3116     }
3118     /**
3119      * Saves setting(s) provided through $data
3120      *
3121      * Potential bug in the works should anyone call with this function
3122      * using a vartype that is not an array
3123      *
3124      * @param array $data
3125      */
3126     public function write_setting($data) {
3127         if (!is_array($data)) {
3128             return ''; //ignore it
3129         }
3130         if (!$this->load_choices() or empty($this->choices)) {
3131             return '';
3132         }
3134         unset($data['xxxxx']);
3136         $save = array();
3137         foreach ($data as $value) {
3138             if (!array_key_exists($value, $this->choices)) {
3139                 continue; // ignore it
3140             }
3141             $save[] = $value;
3142         }
3144         return ($this->config_write($this->name, implode(',', $save)) ? '' : get_string('errorsetting', 'admin'));
3145     }
3147     /**
3148      * Is setting related to query text - used when searching
3149      *
3150      * @param string $query
3151      * @return bool true if related, false if not
3152      */
3153     public function is_related($query) {
3154         if (!$this->load_choices() or empty($this->choices)) {
3155             return false;
3156         }
3157         if (parent::is_related($query)) {
3158             return true;
3159         }
3161         foreach ($this->choices as $desc) {
3162             if (strpos(core_text::strtolower($desc), $query) !== false) {
3163                 return true;
3164             }
3165         }
3166         return false;
3167     }
3169     /**
3170      * Returns XHTML multi-select field
3171      *
3172      * @todo Add vartype handling to ensure $data is an array
3173      * @param array $data Array of values to select by default
3174      * @param string $query
3175      * @return string XHTML multi-select field
3176      */
3177     public function output_html($data, $query='') {
3178         if (!$this->load_choices() or empty($this->choices)) {
3179             return '';
3180         }
3181         $choices = $this->choices;
3182         $default = $this->get_defaultsetting();
3183         if (is_null($default)) {
3184             $default = array();
3185         }
3186         if (is_null($data)) {
3187             $data = array();
3188         }
3190         $defaults = array();
3191         $size = min(10, count($this->choices));
3192         $return = '<div class="form-select"><input type="hidden" name="'.$this->get_full_name().'[xxxxx]" value="1" />'; // something must be submitted even if nothing selected
3193         $return .= '<select id="'.$this->get_id().'" name="'.$this->get_full_name().'[]" size="'.$size.'" multiple="multiple">';
3194         foreach ($this->choices as $key => $description) {
3195             if (in_array($key, $data)) {
3196                 $selected = 'selected="selected"';
3197             } else {
3198                 $selected = '';
3199             }
3200             if (in_array($key, $default)) {
3201                 $defaults[] = $description;
3202             }
3204             $return .= '<option value="'.s($key).'" '.$selected.'>'.$description.'</option>';
3205         }
3207         if (is_null($default)) {
3208             $defaultinfo = NULL;
3209         } if (!empty($defaults)) {
3210             $defaultinfo = implode(', ', $defaults);
3211         } else {
3212             $defaultinfo = get_string('none');
3213         }
3215         $return .= '</select></div>';
3216         return format_admin_setting($this, $this->visiblename, $return, $this->description, true, '', $defaultinfo, $query);
3217     }
3220 /**
3221  * Time selector
3222  *
3223  * This is a liiitle bit messy. we're using two selects, but we're returning
3224  * them as an array named after $name (so we only use $name2 internally for the setting)
3225  *
3226  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3227  */
3228 class admin_setting_configtime extends admin_setting {
3229     /** @var string Used for setting second select (minutes) */
3230     public $name2;
3232     /**
3233      * Constructor
3234      * @param string $hoursname setting for hours
3235      * @param string $minutesname setting for hours
3236      * @param string $visiblename localised
3237      * @param string $description long localised info
3238      * @param array $defaultsetting array representing default time 'h'=>hours, 'm'=>minutes
3239      */
3240     public function __construct($hoursname, $minutesname, $visiblename, $description, $defaultsetting) {
3241         $this->name2 = $minutesname;
3242         parent::__construct($hoursname, $visiblename, $description, $defaultsetting);
3243     }
3245     /**
3246      * Get the selected time
3247      *
3248      * @return mixed An array containing 'h'=>xx, 'm'=>xx, or null if not set
3249      */
3250     public function get_setting() {
3251         $result1 = $this->config_read($this->name);
3252         $result2 = $this->config_read($this->name2);
3253         if (is_null($result1) or is_null($result2)) {
3254             return NULL;
3255         }
3257         return array('h' => $result1, 'm' => $result2);
3258     }
3260     /**
3261      * Store the time (hours and minutes)
3262      *
3263      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3264      * @return bool true if success, false if not
3265      */
3266     public function write_setting($data) {
3267         if (!is_array($data)) {
3268             return '';
3269         }
3271         $result = $this->config_write($this->name, (int)$data['h']) && $this->config_write($this->name2, (int)$data['m']);
3272         return ($result ? '' : get_string('errorsetting', 'admin'));
3273     }
3275     /**
3276      * Returns XHTML time select fields
3277      *
3278      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3279      * @param string $query
3280      * @return string XHTML time select fields and wrapping div(s)
3281      */
3282     public function output_html($data, $query='') {
3283         $default = $this->get_defaultsetting();
3285         if (is_array($default)) {
3286             $defaultinfo = $default['h'].':'.$default['m'];
3287         } else {
3288             $defaultinfo = NULL;
3289         }
3291         $return  = '<div class="form-time defaultsnext">';
3292         $return .= '<label class="accesshide" for="' . $this->get_id() . 'h">' . get_string('hours') . '</label>';
3293         $return .= '<select id="' . $this->get_id() . 'h" name="' . $this->get_full_name() . '[h]">';
3294         for ($i = 0; $i < 24; $i++) {
3295             $return .= '<option value="' . $i . '"' . ($i == $data['h'] ? ' selected="selected"' : '') . '>' . $i . '</option>';
3296         }
3297         $return .= '</select>:';
3298         $return .= '<label class="accesshide" for="' . $this->get_id() . 'm">' . get_string('minutes') . '</label>';
3299         $return .= '<select id="' . $this->get_id() . 'm" name="' . $this->get_full_name() . '[m]">';
3300         for ($i = 0; $i < 60; $i += 5) {
3301             $return .= '<option value="' . $i . '"' . ($i == $data['m'] ? ' selected="selected"' : '') . '>' . $i . '</option>';
3302         }
3303         $return .= '</select>';
3304         $return .= '</div>';
3305         return format_admin_setting($this, $this->visiblename, $return, $this->description,
3306             $this->get_id() . 'h', '', $defaultinfo, $query);
3307     }
3312 /**
3313  * Seconds duration setting.
3314  *
3315  * @copyright 2012 Petr Skoda (http://skodak.org)
3316  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3317  */
3318 class admin_setting_configduration extends admin_setting {
3320     /** @var int default duration unit */
3321     protected $defaultunit;
3323     /**
3324      * Constructor
3325      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
3326      *                     or 'myplugin/mysetting' for ones in config_plugins.
3327      * @param string $visiblename localised name
3328      * @param string $description localised long description
3329      * @param mixed $defaultsetting string or array depending on implementation
3330      * @param int $defaultunit - day, week, etc. (in seconds)
3331      */
3332     public function __construct($name, $visiblename, $description, $defaultsetting, $defaultunit = 86400) {
3333         if (is_number($defaultsetting)) {
3334             $defaultsetting = self::parse_seconds($defaultsetting);
3335         }
3336         $units = self::get_units();
3337         if (isset($units[$defaultunit])) {
3338             $this->defaultunit = $defaultunit;
3339         } else {
3340             $this->defaultunit = 86400;
3341         }
3342         parent::__construct($name, $visiblename, $description, $defaultsetting);
3343     }
3345     /**
3346      * Returns selectable units.
3347      * @static
3348      * @return array
3349      */
3350     protected static function get_units() {
3351         return array(
3352             604800 => get_string('weeks'),
3353             86400 => get_string('days'),
3354             3600 => get_string('hours'),
3355             60 => get_string('minutes'),
3356             1 => get_string('seconds'),
3357         );
3358     }
3360     /**
3361      * Converts seconds to some more user friendly string.
3362      * @static
3363      * @param int $seconds
3364      * @return string
3365      */
3366     protected static function get_duration_text($seconds) {
3367         if (empty($seconds)) {
3368             return get_string('none');
3369         }
3370         $data = self::parse_seconds($seconds);
3371         switch ($data['u']) {
3372             case (60*60*24*7):
3373                 return get_string('numweeks', '', $data['v']);
3374             case (60*60*24):
3375                 return get_string('numdays', '', $data['v']);
3376             case (60*60):
3377                 return get_string('numhours', '', $data['v']);
3378             case (60):
3379                 return get_string('numminutes', '', $data['v']);
3380             default:
3381                 return get_string('numseconds', '', $data['v']*$data['u']);
3382         }
3383     }
3385     /**
3386      * Finds suitable units for given duration.
3387      * @static
3388      * @param int $seconds
3389      * @return array
3390      */
3391     protected static function parse_seconds($seconds) {
3392         foreach (self::get_units() as $unit => $unused) {
3393             if ($seconds % $unit === 0) {
3394                 return array('v'=>(int)($seconds/$unit), 'u'=>$unit);
3395             }
3396         }
3397         return array('v'=>(int)$seconds, 'u'=>1);
3398     }
3400     /**
3401      * Get the selected duration as array.
3402      *
3403      * @return mixed An array containing 'v'=>xx, 'u'=>xx, or null if not set
3404      */
3405     public function get_setting() {
3406         $seconds = $this->config_read($this->name);
3407         if (is_null($seconds)) {
3408             return null;
3409         }
3411         return self::parse_seconds($seconds);
3412     }
3414     /**
3415      * Store the duration as seconds.
3416      *
3417      * @param array $data Must be form 'h'=>xx, 'm'=>xx
3418      * @return bool true if success, false if not
3419      */
3420     public function write_setting($data) {
3421         if (!is_array($data)) {
3422             return '';
3423         }
3425         $seconds = (int)($data['v']*$data['u']);
3426         if ($seconds < 0) {
3427             return get_string('errorsetting', 'admin');
3428         }
3430         $result = $this->config_write($this->name, $seconds);
3431         return ($result ? '' : get_string('errorsetting', 'admin'));
3432     }
3434     /**
3435      * Returns duration text+select fields.
3436      *
3437      * @param array $data Must be form 'v'=>xx, 'u'=>xx
3438      * @param string $query
3439      * @return string duration text+select fields and wrapping div(s)
3440      */
3441     public function output_html($data, $query='') {
3442         $default = $this->get_defaultsetting();
3444         if (is_number($default)) {
3445             $defaultinfo = self::get_duration_text($default);
3446         } else if (is_array($default)) {
3447             $defaultinfo = self::get_duration_text($default['v']*$default['u']);
3448         } else {
3449             $defaultinfo = null;
3450         }
3452         $units = self::get_units();
3454         $inputid = $this->get_id() . 'v';
3456         $return = '<div class="form-duration defaultsnext">';
3457         $return .= '<input type="text" size="5" id="' . $inputid . '" name="' . $this->get_full_name() .
3458             '[v]" value="' . s($data['v']) . '" />';
3459         $return .= '<label for="' . $this->get_id() . 'u" class="accesshide">' .
3460             get_string('durationunits', 'admin') . '</label>';
3461         $return .= '<select id="'.$this->get_id().'u" name="'.$this->get_full_name().'[u]">';
3462         foreach ($units as $val => $text) {
3463             $selected = '';
3464             if ($data['v'] == 0) {
3465                 if ($val == $this->defaultunit) {
3466                     $selected = ' selected="selected"';
3467                 }
3468             } else if ($val == $data['u']) {
3469                 $selected = ' selected="selected"';
3470             }
3471             $return .= '<option value="'.$val.'"'.$selected.'>'.$text.'</option>';
3472         }
3473         $return .= '</select></div>';
3474         return format_admin_setting($this, $this->visiblename, $return, $this->description, $inputid, '', $defaultinfo, $query);
3475     }
3479 /**
3480  * Seconds duration setting with an advanced checkbox, that controls a additional
3481  * $name.'_adv' setting.
3482  *
3483  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3484  * @copyright 2014 The Open University
3485  */
3486 class admin_setting_configduration_with_advanced extends admin_setting_configduration {
3487     /**
3488      * Constructor
3489      * @param string $name unique ascii name, either 'mysetting' for settings that in config,
3490      *                     or 'myplugin/mysetting' for ones in config_plugins.
3491      * @param string $visiblename localised name
3492      * @param string $description localised long description
3493      * @param array  $defaultsetting array of int value, and bool whether it is
3494      *                     is advanced by default.
3495      * @param int $defaultunit - day, week, etc. (in seconds)
3496      */
3497     public function __construct($name, $visiblename, $description, $defaultsetting, $defaultunit = 86400) {
3498         parent::__construct($name, $visiblename, $description, $defaultsetting['value'], $defaultunit);
3499         $this->set_advanced_flag_options(admin_setting_flag::ENABLED, !empty($defaultsetting['adv']));
3500     }
3504 /**
3505  * Used to validate a textarea used for ip addresses
3506  *
3507  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3508  * @copyright 2011 Petr Skoda (http://skodak.org)
3509  */
3510 class admin_setting_configiplist extends admin_setting_configtextarea {
3512     /**
3513      * Validate the contents of the textarea as IP addresses
3514      *
3515      * Used to validate a new line separated list of IP addresses collected from
3516      * a textarea control
3517      *
3518      * @param string $data A list of IP Addresses separated by new lines
3519      * @return mixed bool true for success or string:error on failure
3520      */
3521     public function validate($data) {
3522         if(!empty($data)) {
3523             $ips = explode("\n", $data);
3524         } else {
3525             return true;
3526         }
3527         $result = true;
3528         foreach($ips as $ip) {
3529             $ip = trim($ip);
3530             if (preg_match('#^(\d{1,3})(\.\d{1,3}){0,3}$#', $ip, $match) ||
3531                 preg_match('#^(\d{1,3})(\.\d{1,3}){0,3}(\/\d{1,2})$#', $ip, $match) ||
3532                 preg_match('#^(\d{1,3})(\.\d{1,3}){3}(-\d{1,3})$#', $ip, $match)) {
3533                 $result = true;
3534             } else {
3535                 $result = false;
3536                 break;
3537             }
3538         }
3539         if($result) {
3540             return true;
3541         } else {
3542             return get_string('validateerror', 'admin');
3543         }
3544     }
3548 /**
3549  * An admin setting for selecting one or more users who have a capability
3550  * in the system context
3551  *
3552  * An admin setting for selecting one or more users, who have a particular capability
3553  * in the system context. Warning, make sure the list will never be too long. There is
3554  * no paging or searching of this list.
3555  *
3556  * To correctly get a list of users from this config setting, you need to call the
3557  * get_users_from_config($CFG->mysetting, $capability); function in moodlelib.php.
3558  *
3559  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3560  */
3561 class admin_setting_users_with_capability extends admin_setting_configmultiselect {
3562     /** @var string The capabilities name */
3563     protected $capability;
3564     /** @var int include admin users too */
3565     protected $includeadmins;
3567     /**
3568      * Constructor.
3569      *
3570      * @param string $name unique ascii name, either 'mysetting' for settings that in config, or 'myplugin/mysetting' for ones in config_plugins.
3571      * @param string $visiblename localised name
3572      * @param string $description localised long description
3573      * @param array $defaultsetting array of usernames
3574      * @param string $capability string capability name.
3575      * @param bool $includeadmins include administrators
3576      */
3577     function __construct($name, $visiblename, $description, $defaultsetting, $capability, $includeadmins = true) {
3578         $this->capability    = $capability;
3579         $this->includeadmins = $includeadmins;
3580         parent::__construct($name, $visiblename, $description, $defaultsetting, NULL);
3581     }
3583     /**
3584      * Load all of the uses who have the capability into choice array
3585      *
3586      * @return bool Always returns true
3587      */
3588     function load_choices() {
3589         if (is_array($this->choices)) {
3590             return true;
3591         }
3592         list($sort, $sortparams) = users_order_by_sql('u');
3593         if (!empty($sortparams)) {
3594             throw new coding_exception('users_order_by_sql returned some query parameters. ' .
3595                     'This is unexpected, and a problem because there is no way to pass these ' .
3596                     'parameters to get_users_by_capability. See MDL-34657.');
3597         }
3598         $userfields = 'u.id, u.username, ' . get_all_user_name_fields(true, 'u');
3599         $users = get_users_by_capability(context_system::instance(), $this->capability, $userfields, $sort);
3600         $this->choices = array(
3601             '$@NONE@$' => get_string('nobody'),
3602             '$@ALL@$' => get_string('everyonewhocan', 'admin', get_capability_string($this->capability)),
3603         );
3604         if ($this->includeadmins) {
3605             $admins = get_admins();
3606             foreach ($admins as $user) {
3607                 $this->choices[$user->id] = fullname($user);
3608             }
3609         }
3610         if (is_array($users)) {
3611             foreach ($users as $user) {
3612                 $this->choices[$user->id] = fullname($user);
3613             }
3614         }
3615         return true;
3616     }
3618     /**
3619      * Returns the default setting for class
3620      *
3621      * @return mixed Array, or string. Empty string if no default
3622      */
3623     public function get_defaultsetting() {
3624         $this->load_choices();
3625         $defaultsetting = parent::get_defaultsetting();
3626         if (empty($defaultsetting)) {
3627             return array('$@NONE@$');
3628         } else if (array_key_exists($defaultsetting, $this->choices)) {
3629                 return $defaultsetting;
3630             } else {
3631                 return '';
3632             }
3633     }
3635     /**
3636      * Returns the current setting
3637      *
3638      * @return mixed array or string
3639      */
3640     public function get_setting() {
3641         $result = parent::get_setting();
3642         if ($result === null) {
3643             // this is necessary for settings upgrade
3644             return null;
3645         }
3646         if (empty($result)) {
3647             $result = array('$@NONE@$');
3648         }
3649         return $result;