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