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