PageRenderTime 68ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 2ms

/lib/adminlib.php

https://bitbucket.org/moodle/moodle
PHP | 11709 lines | 6516 code | 1364 blank | 3829 comment | 1002 complexity | 716b0648d4355c6bc628990edd7e7ab0 MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0

Large files files are truncated, but you can click here to view the full file

  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/>.
  16. /**
  17. * Functions and classes used during installation, upgrades and for admin settings.
  18. *
  19. * ADMIN SETTINGS TREE INTRODUCTION
  20. *
  21. * This file performs the following tasks:
  22. * -it defines the necessary objects and interfaces to build the Moodle
  23. * admin hierarchy
  24. * -it defines the admin_externalpage_setup()
  25. *
  26. * ADMIN_SETTING OBJECTS
  27. *
  28. * Moodle settings are represented by objects that inherit from the admin_setting
  29. * class. These objects encapsulate how to read a setting, how to write a new value
  30. * to a setting, and how to appropriately display the HTML to modify the setting.
  31. *
  32. * ADMIN_SETTINGPAGE OBJECTS
  33. *
  34. * The admin_setting objects are then grouped into admin_settingpages. The latter
  35. * appear in the Moodle admin tree block. All interaction with admin_settingpage
  36. * objects is handled by the admin/settings.php file.
  37. *
  38. * ADMIN_EXTERNALPAGE OBJECTS
  39. *
  40. * There are some settings in Moodle that are too complex to (efficiently) handle
  41. * with admin_settingpages. (Consider, for example, user management and displaying
  42. * lists of users.) In this case, we use the admin_externalpage object. This object
  43. * places a link to an external PHP file in the admin tree block.
  44. *
  45. * If you're using an admin_externalpage object for some settings, you can take
  46. * advantage of the admin_externalpage_* functions. For example, suppose you wanted
  47. * to add a foo.php file into admin. First off, you add the following line to
  48. * admin/settings/first.php (at the end of the file) or to some other file in
  49. * admin/settings:
  50. * <code>
  51. * $ADMIN->add('userinterface', new admin_externalpage('foo', get_string('foo'),
  52. * $CFG->wwwdir . '/' . '$CFG->admin . '/foo.php', 'some_role_permission'));
  53. * </code>
  54. *
  55. * Next, in foo.php, your file structure would resemble the following:
  56. * <code>
  57. * require(__DIR__.'/../../config.php');
  58. * require_once($CFG->libdir.'/adminlib.php');
  59. * admin_externalpage_setup('foo');
  60. * // functionality like processing form submissions goes here
  61. * echo $OUTPUT->header();
  62. * // your HTML goes here
  63. * echo $OUTPUT->footer();
  64. * </code>
  65. *
  66. * The admin_externalpage_setup() function call ensures the user is logged in,
  67. * and makes sure that they have the proper role permission to access the page.
  68. * It also configures all $PAGE properties needed for navigation.
  69. *
  70. * ADMIN_CATEGORY OBJECTS
  71. *
  72. * Above and beyond all this, we have admin_category objects. These objects
  73. * appear as folders in the admin tree block. They contain admin_settingpage's,
  74. * admin_externalpage's, and other admin_category's.
  75. *
  76. * OTHER NOTES
  77. *
  78. * admin_settingpage's, admin_externalpage's, and admin_category's all inherit
  79. * from part_of_admin_tree (a pseudointerface). This interface insists that
  80. * a class has a check_access method for access permissions, a locate method
  81. * used to find a specific node in the admin tree and find parent path.
  82. *
  83. * admin_category's inherit from parentable_part_of_admin_tree. This pseudo-
  84. * interface ensures that the class implements a recursive add function which
  85. * accepts a part_of_admin_tree object and searches for the proper place to
  86. * put it. parentable_part_of_admin_tree implies part_of_admin_tree.
  87. *
  88. * Please note that the $this->name field of any part_of_admin_tree must be
  89. * UNIQUE throughout the ENTIRE admin tree.
  90. *
  91. * The $this->name field of an admin_setting object (which is *not* part_of_
  92. * admin_tree) must be unique on the respective admin_settingpage where it is
  93. * used.
  94. *
  95. * Original author: Vincenzo K. Marcovecchio
  96. * Maintainer: Petr Skoda
  97. *
  98. * @package core
  99. * @subpackage admin
  100. * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com
  101. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  102. */
  103. use core_admin\local\settings\linkable_settings_page;
  104. defined('MOODLE_INTERNAL') || die();
  105. /// Add libraries
  106. require_once($CFG->libdir.'/ddllib.php');
  107. require_once($CFG->libdir.'/xmlize.php');
  108. require_once($CFG->libdir.'/messagelib.php');
  109. // Add classes, traits, and interfaces which should be autoloaded.
  110. // The autoloader is configured late in setup.php, after ABORT_AFTER_CONFIG.
  111. // This is also required where the setup system is not included at all.
  112. require_once($CFG->dirroot.'/admin/classes/local/settings/linkable_settings_page.php');
  113. define('INSECURE_DATAROOT_WARNING', 1);
  114. 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. // This may take a long time.
  128. core_php_time_limit::raise();
  129. // Recursively uninstall all subplugins first.
  130. $subplugintypes = core_component::get_plugin_types_with_subplugins();
  131. if (isset($subplugintypes[$type])) {
  132. $base = core_component::get_plugin_directory($type, $name);
  133. $subpluginsfile = "{$base}/db/subplugins.json";
  134. if (file_exists($subpluginsfile)) {
  135. $subplugins = (array) json_decode(file_get_contents($subpluginsfile))->plugintypes;
  136. } else if (file_exists("{$base}/db/subplugins.php")) {
  137. debugging('Use of subplugins.php has been deprecated. ' .
  138. 'Please update your plugin to provide a subplugins.json file instead.',
  139. DEBUG_DEVELOPER);
  140. $subplugins = [];
  141. include("{$base}/db/subplugins.php");
  142. }
  143. if (!empty($subplugins)) {
  144. foreach (array_keys($subplugins) as $subplugintype) {
  145. $instances = core_component::get_plugin_list($subplugintype);
  146. foreach ($instances as $subpluginname => $notusedpluginpath) {
  147. uninstall_plugin($subplugintype, $subpluginname);
  148. }
  149. }
  150. }
  151. }
  152. $component = $type . '_' . $name; // eg. 'qtype_multichoice' or 'workshopgrading_accumulative' or 'mod_forum'
  153. if ($type === 'mod') {
  154. $pluginname = $name; // eg. 'forum'
  155. if (get_string_manager()->string_exists('modulename', $component)) {
  156. $strpluginname = get_string('modulename', $component);
  157. } else {
  158. $strpluginname = $component;
  159. }
  160. } else {
  161. $pluginname = $component;
  162. if (get_string_manager()->string_exists('pluginname', $component)) {
  163. $strpluginname = get_string('pluginname', $component);
  164. } else {
  165. $strpluginname = $component;
  166. }
  167. }
  168. echo $OUTPUT->heading($pluginname);
  169. // Delete all tag areas, collections and instances associated with this plugin.
  170. core_tag_area::uninstall($component);
  171. // Custom plugin uninstall.
  172. $plugindirectory = core_component::get_plugin_directory($type, $name);
  173. $uninstalllib = $plugindirectory . '/db/uninstall.php';
  174. if (file_exists($uninstalllib)) {
  175. require_once($uninstalllib);
  176. $uninstallfunction = 'xmldb_' . $pluginname . '_uninstall'; // eg. 'xmldb_workshop_uninstall()'
  177. if (function_exists($uninstallfunction)) {
  178. // Do not verify result, let plugin complain if necessary.
  179. $uninstallfunction();
  180. }
  181. }
  182. // Specific plugin type cleanup.
  183. $plugininfo = core_plugin_manager::instance()->get_plugin_info($component);
  184. if ($plugininfo) {
  185. $plugininfo->uninstall_cleanup();
  186. core_plugin_manager::reset_caches();
  187. }
  188. $plugininfo = null;
  189. // perform clean-up task common for all the plugin/subplugin types
  190. //delete the web service functions and pre-built services
  191. require_once($CFG->dirroot.'/lib/externallib.php');
  192. external_delete_descriptions($component);
  193. // delete calendar events
  194. $DB->delete_records('event', array('modulename' => $pluginname));
  195. $DB->delete_records('event', ['component' => $component]);
  196. // Delete scheduled tasks.
  197. $DB->delete_records('task_adhoc', ['component' => $component]);
  198. $DB->delete_records('task_scheduled', array('component' => $component));
  199. // Delete Inbound Message datakeys.
  200. $DB->delete_records_select('messageinbound_datakeys',
  201. 'handler IN (SELECT id FROM {messageinbound_handlers} WHERE component = ?)', array($component));
  202. // Delete Inbound Message handlers.
  203. $DB->delete_records('messageinbound_handlers', array('component' => $component));
  204. // delete all the logs
  205. $DB->delete_records('log', array('module' => $pluginname));
  206. // delete log_display information
  207. $DB->delete_records('log_display', array('component' => $component));
  208. // delete the module configuration records
  209. unset_all_config_for_plugin($component);
  210. if ($type === 'mod') {
  211. unset_all_config_for_plugin($pluginname);
  212. }
  213. // delete message provider
  214. message_provider_uninstall($component);
  215. // delete the plugin tables
  216. $xmldbfilepath = $plugindirectory . '/db/install.xml';
  217. drop_plugin_tables($component, $xmldbfilepath, false);
  218. if ($type === 'mod' or $type === 'block') {
  219. // non-frankenstyle table prefixes
  220. drop_plugin_tables($name, $xmldbfilepath, false);
  221. }
  222. // delete the capabilities that were defined by this module
  223. capabilities_cleanup($component);
  224. // Delete all remaining files in the filepool owned by the component.
  225. $fs = get_file_storage();
  226. $fs->delete_component_files($component);
  227. // Finally purge all caches.
  228. purge_all_caches();
  229. // Invalidate the hash used for upgrade detections.
  230. set_config('allversionshash', '');
  231. echo $OUTPUT->notification(get_string('success'), 'notifysuccess');
  232. }
  233. /**
  234. * Returns the version of installed component
  235. *
  236. * @param string $component component name
  237. * @param string $source either 'disk' or 'installed' - where to get the version information from
  238. * @return string|bool version number or false if the component is not found
  239. */
  240. function get_component_version($component, $source='installed') {
  241. global $CFG, $DB;
  242. list($type, $name) = core_component::normalize_component($component);
  243. // moodle core or a core subsystem
  244. if ($type === 'core') {
  245. if ($source === 'installed') {
  246. if (empty($CFG->version)) {
  247. return false;
  248. } else {
  249. return $CFG->version;
  250. }
  251. } else {
  252. if (!is_readable($CFG->dirroot.'/version.php')) {
  253. return false;
  254. } else {
  255. $version = null; //initialize variable for IDEs
  256. include($CFG->dirroot.'/version.php');
  257. return $version;
  258. }
  259. }
  260. }
  261. // activity module
  262. if ($type === 'mod') {
  263. if ($source === 'installed') {
  264. if ($CFG->version < 2013092001.02) {
  265. return $DB->get_field('modules', 'version', array('name'=>$name));
  266. } else {
  267. return get_config('mod_'.$name, 'version');
  268. }
  269. } else {
  270. $mods = core_component::get_plugin_list('mod');
  271. if (empty($mods[$name]) or !is_readable($mods[$name].'/version.php')) {
  272. return false;
  273. } else {
  274. $plugin = new stdClass();
  275. $plugin->version = null;
  276. $module = $plugin;
  277. include($mods[$name].'/version.php');
  278. return $plugin->version;
  279. }
  280. }
  281. }
  282. // block
  283. if ($type === 'block') {
  284. if ($source === 'installed') {
  285. if ($CFG->version < 2013092001.02) {
  286. return $DB->get_field('block', 'version', array('name'=>$name));
  287. } else {
  288. return get_config('block_'.$name, 'version');
  289. }
  290. } else {
  291. $blocks = core_component::get_plugin_list('block');
  292. if (empty($blocks[$name]) or !is_readable($blocks[$name].'/version.php')) {
  293. return false;
  294. } else {
  295. $plugin = new stdclass();
  296. include($blocks[$name].'/version.php');
  297. return $plugin->version;
  298. }
  299. }
  300. }
  301. // all other plugin types
  302. if ($source === 'installed') {
  303. return get_config($type.'_'.$name, 'version');
  304. } else {
  305. $plugins = core_component::get_plugin_list($type);
  306. if (empty($plugins[$name])) {
  307. return false;
  308. } else {
  309. $plugin = new stdclass();
  310. include($plugins[$name].'/version.php');
  311. return $plugin->version;
  312. }
  313. }
  314. }
  315. /**
  316. * Delete all plugin tables
  317. *
  318. * @param string $name Name of plugin, used as table prefix
  319. * @param string $file Path to install.xml file
  320. * @param bool $feedback defaults to true
  321. * @return bool Always returns true
  322. */
  323. function drop_plugin_tables($name, $file, $feedback=true) {
  324. global $CFG, $DB;
  325. // first try normal delete
  326. if (file_exists($file) and $DB->get_manager()->delete_tables_from_xmldb_file($file)) {
  327. return true;
  328. }
  329. // then try to find all tables that start with name and are not in any xml file
  330. $used_tables = get_used_table_names();
  331. $tables = $DB->get_tables();
  332. /// Iterate over, fixing id fields as necessary
  333. foreach ($tables as $table) {
  334. if (in_array($table, $used_tables)) {
  335. continue;
  336. }
  337. if (strpos($table, $name) !== 0) {
  338. continue;
  339. }
  340. // found orphan table --> delete it
  341. if ($DB->get_manager()->table_exists($table)) {
  342. $xmldb_table = new xmldb_table($table);
  343. $DB->get_manager()->drop_table($xmldb_table);
  344. }
  345. }
  346. return true;
  347. }
  348. /**
  349. * Returns names of all known tables == tables that moodle knows about.
  350. *
  351. * @return array Array of lowercase table names
  352. */
  353. function get_used_table_names() {
  354. $table_names = array();
  355. $dbdirs = get_db_directories();
  356. foreach ($dbdirs as $dbdir) {
  357. $file = $dbdir.'/install.xml';
  358. $xmldb_file = new xmldb_file($file);
  359. if (!$xmldb_file->fileExists()) {
  360. continue;
  361. }
  362. $loaded = $xmldb_file->loadXMLStructure();
  363. $structure = $xmldb_file->getStructure();
  364. if ($loaded and $tables = $structure->getTables()) {
  365. foreach($tables as $table) {
  366. $table_names[] = strtolower($table->getName());
  367. }
  368. }
  369. }
  370. return $table_names;
  371. }
  372. /**
  373. * Returns list of all directories where we expect install.xml files
  374. * @return array Array of paths
  375. */
  376. function get_db_directories() {
  377. global $CFG;
  378. $dbdirs = array();
  379. /// First, the main one (lib/db)
  380. $dbdirs[] = $CFG->libdir.'/db';
  381. /// Then, all the ones defined by core_component::get_plugin_types()
  382. $plugintypes = core_component::get_plugin_types();
  383. foreach ($plugintypes as $plugintype => $pluginbasedir) {
  384. if ($plugins = core_component::get_plugin_list($plugintype)) {
  385. foreach ($plugins as $plugin => $plugindir) {
  386. $dbdirs[] = $plugindir.'/db';
  387. }
  388. }
  389. }
  390. return $dbdirs;
  391. }
  392. /**
  393. * Try to obtain or release the cron lock.
  394. * @param string $name name of lock
  395. * @param int $until timestamp when this lock considered stale, null means remove lock unconditionally
  396. * @param bool $ignorecurrent ignore current lock state, usually extend previous lock, defaults to false
  397. * @return bool true if lock obtained
  398. */
  399. function set_cron_lock($name, $until, $ignorecurrent=false) {
  400. global $DB;
  401. if (empty($name)) {
  402. debugging("Tried to get a cron lock for a null fieldname");
  403. return false;
  404. }
  405. // remove lock by force == remove from config table
  406. if (is_null($until)) {
  407. set_config($name, null);
  408. return true;
  409. }
  410. if (!$ignorecurrent) {
  411. // read value from db - other processes might have changed it
  412. $value = $DB->get_field('config', 'value', array('name'=>$name));
  413. if ($value and $value > time()) {
  414. //lock active
  415. return false;
  416. }
  417. }
  418. set_config($name, $until);
  419. return true;
  420. }
  421. /**
  422. * Test if and critical warnings are present
  423. * @return bool
  424. */
  425. function admin_critical_warnings_present() {
  426. global $SESSION;
  427. if (!has_capability('moodle/site:config', context_system::instance())) {
  428. return 0;
  429. }
  430. if (!isset($SESSION->admin_critical_warning)) {
  431. $SESSION->admin_critical_warning = 0;
  432. if (is_dataroot_insecure(true) === INSECURE_DATAROOT_ERROR) {
  433. $SESSION->admin_critical_warning = 1;
  434. }
  435. }
  436. return $SESSION->admin_critical_warning;
  437. }
  438. /**
  439. * Detects if float supports at least 10 decimal digits
  440. *
  441. * Detects if float supports at least 10 decimal digits
  442. * and also if float-->string conversion works as expected.
  443. *
  444. * @return bool true if problem found
  445. */
  446. function is_float_problem() {
  447. $num1 = 2009010200.01;
  448. $num2 = 2009010200.02;
  449. return ((string)$num1 === (string)$num2 or $num1 === $num2 or $num2 <= (string)$num1);
  450. }
  451. /**
  452. * Try to verify that dataroot is not accessible from web.
  453. *
  454. * Try to verify that dataroot is not accessible from web.
  455. * It is not 100% correct but might help to reduce number of vulnerable sites.
  456. * Protection from httpd.conf and .htaccess is not detected properly.
  457. *
  458. * @uses INSECURE_DATAROOT_WARNING
  459. * @uses INSECURE_DATAROOT_ERROR
  460. * @param bool $fetchtest try to test public access by fetching file, default false
  461. * @return mixed empty means secure, INSECURE_DATAROOT_ERROR found a critical problem, INSECURE_DATAROOT_WARNING might be problematic
  462. */
  463. function is_dataroot_insecure($fetchtest=false) {
  464. global $CFG;
  465. $siteroot = str_replace('\\', '/', strrev($CFG->dirroot.'/')); // win32 backslash workaround
  466. $rp = preg_replace('|https?://[^/]+|i', '', $CFG->wwwroot, 1);
  467. $rp = strrev(trim($rp, '/'));
  468. $rp = explode('/', $rp);
  469. foreach($rp as $r) {
  470. if (strpos($siteroot, '/'.$r.'/') === 0) {
  471. $siteroot = substr($siteroot, strlen($r)+1); // moodle web in subdirectory
  472. } else {
  473. break; // probably alias root
  474. }
  475. }
  476. $siteroot = strrev($siteroot);
  477. $dataroot = str_replace('\\', '/', $CFG->dataroot.'/');
  478. if (strpos($dataroot, $siteroot) !== 0) {
  479. return false;
  480. }
  481. if (!$fetchtest) {
  482. return INSECURE_DATAROOT_WARNING;
  483. }
  484. // now try all methods to fetch a test file using http protocol
  485. $httpdocroot = str_replace('\\', '/', strrev($CFG->dirroot.'/'));
  486. preg_match('|(https?://[^/]+)|i', $CFG->wwwroot, $matches);
  487. $httpdocroot = $matches[1];
  488. $datarooturl = $httpdocroot.'/'. substr($dataroot, strlen($siteroot));
  489. make_upload_directory('diag');
  490. $testfile = $CFG->dataroot.'/diag/public.txt';
  491. if (!file_exists($testfile)) {
  492. file_put_contents($testfile, 'test file, do not delete');
  493. @chmod($testfile, $CFG->filepermissions);
  494. }
  495. $teststr = trim(file_get_contents($testfile));
  496. if (empty($teststr)) {
  497. // hmm, strange
  498. return INSECURE_DATAROOT_WARNING;
  499. }
  500. $testurl = $datarooturl.'/diag/public.txt';
  501. if (extension_loaded('curl') and
  502. !(stripos(ini_get('disable_functions'), 'curl_init') !== FALSE) and
  503. !(stripos(ini_get('disable_functions'), 'curl_setop') !== FALSE) and
  504. ($ch = @curl_init($testurl)) !== false) {
  505. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  506. curl_setopt($ch, CURLOPT_HEADER, false);
  507. $data = curl_exec($ch);
  508. if (!curl_errno($ch)) {
  509. $data = trim($data);
  510. if ($data === $teststr) {
  511. curl_close($ch);
  512. return INSECURE_DATAROOT_ERROR;
  513. }
  514. }
  515. curl_close($ch);
  516. }
  517. if ($data = @file_get_contents($testurl)) {
  518. $data = trim($data);
  519. if ($data === $teststr) {
  520. return INSECURE_DATAROOT_ERROR;
  521. }
  522. }
  523. preg_match('|https?://([^/]+)|i', $testurl, $matches);
  524. $sitename = $matches[1];
  525. $error = 0;
  526. if ($fp = @fsockopen($sitename, 80, $error)) {
  527. preg_match('|https?://[^/]+(.*)|i', $testurl, $matches);
  528. $localurl = $matches[1];
  529. $out = "GET $localurl HTTP/1.1\r\n";
  530. $out .= "Host: $sitename\r\n";
  531. $out .= "Connection: Close\r\n\r\n";
  532. fwrite($fp, $out);
  533. $data = '';
  534. $incoming = false;
  535. while (!feof($fp)) {
  536. if ($incoming) {
  537. $data .= fgets($fp, 1024);
  538. } else if (@fgets($fp, 1024) === "\r\n") {
  539. $incoming = true;
  540. }
  541. }
  542. fclose($fp);
  543. $data = trim($data);
  544. if ($data === $teststr) {
  545. return INSECURE_DATAROOT_ERROR;
  546. }
  547. }
  548. return INSECURE_DATAROOT_WARNING;
  549. }
  550. /**
  551. * Enables CLI maintenance mode by creating new dataroot/climaintenance.html file.
  552. */
  553. function enable_cli_maintenance_mode() {
  554. global $CFG, $SITE;
  555. if (file_exists("$CFG->dataroot/climaintenance.html")) {
  556. unlink("$CFG->dataroot/climaintenance.html");
  557. }
  558. if (isset($CFG->maintenance_message) and !html_is_blank($CFG->maintenance_message)) {
  559. $data = $CFG->maintenance_message;
  560. $data = bootstrap_renderer::early_error_content($data, null, null, null);
  561. $data = bootstrap_renderer::plain_page(get_string('sitemaintenance', 'admin'), $data);
  562. } else if (file_exists("$CFG->dataroot/climaintenance.template.html")) {
  563. $data = file_get_contents("$CFG->dataroot/climaintenance.template.html");
  564. } else {
  565. $data = get_string('sitemaintenance', 'admin');
  566. $data = bootstrap_renderer::early_error_content($data, null, null, null);
  567. $data = bootstrap_renderer::plain_page(get_string('sitemaintenancetitle', 'admin',
  568. format_string($SITE->fullname, true, ['context' => context_system::instance()])), $data);
  569. }
  570. file_put_contents("$CFG->dataroot/climaintenance.html", $data);
  571. chmod("$CFG->dataroot/climaintenance.html", $CFG->filepermissions);
  572. }
  573. /// CLASS DEFINITIONS /////////////////////////////////////////////////////////
  574. /**
  575. * Interface for anything appearing in the admin tree
  576. *
  577. * The interface that is implemented by anything that appears in the admin tree
  578. * block. It forces inheriting classes to define a method for checking user permissions
  579. * and methods for finding something in the admin tree.
  580. *
  581. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  582. */
  583. interface part_of_admin_tree {
  584. /**
  585. * Finds a named part_of_admin_tree.
  586. *
  587. * Used to find a part_of_admin_tree. If a class only inherits part_of_admin_tree
  588. * and not parentable_part_of_admin_tree, then this function should only check if
  589. * $this->name matches $name. If it does, it should return a reference to $this,
  590. * otherwise, it should return a reference to NULL.
  591. *
  592. * If a class inherits parentable_part_of_admin_tree, this method should be called
  593. * recursively on all child objects (assuming, of course, the parent object's name
  594. * doesn't match the search criterion).
  595. *
  596. * @param string $name The internal name of the part_of_admin_tree we're searching for.
  597. * @return mixed An object reference or a NULL reference.
  598. */
  599. public function locate($name);
  600. /**
  601. * Removes named part_of_admin_tree.
  602. *
  603. * @param string $name The internal name of the part_of_admin_tree we want to remove.
  604. * @return bool success.
  605. */
  606. public function prune($name);
  607. /**
  608. * Search using query
  609. * @param string $query
  610. * @return mixed array-object structure of found settings and pages
  611. */
  612. public function search($query);
  613. /**
  614. * Verifies current user's access to this part_of_admin_tree.
  615. *
  616. * Used to check if the current user has access to this part of the admin tree or
  617. * not. If a class only inherits part_of_admin_tree and not parentable_part_of_admin_tree,
  618. * then this method is usually just a call to has_capability() in the site context.
  619. *
  620. * If a class inherits parentable_part_of_admin_tree, this method should return the
  621. * logical OR of the return of check_access() on all child objects.
  622. *
  623. * @return bool True if the user has access, false if she doesn't.
  624. */
  625. public function check_access();
  626. /**
  627. * Mostly useful for removing of some parts of the tree in admin tree block.
  628. *
  629. * @return True is hidden from normal list view
  630. */
  631. public function is_hidden();
  632. /**
  633. * Show we display Save button at the page bottom?
  634. * @return bool
  635. */
  636. public function show_save();
  637. }
  638. /**
  639. * Interface implemented by any part_of_admin_tree that has children.
  640. *
  641. * The interface implemented by any part_of_admin_tree that can be a parent
  642. * to other part_of_admin_tree's. (For now, this only includes admin_category.) Apart
  643. * from ensuring part_of_admin_tree compliancy, it also ensures inheriting methods
  644. * include an add method for adding other part_of_admin_tree objects as children.
  645. *
  646. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  647. */
  648. interface parentable_part_of_admin_tree extends part_of_admin_tree {
  649. /**
  650. * Adds a part_of_admin_tree object to the admin tree.
  651. *
  652. * Used to add a part_of_admin_tree object to this object or a child of this
  653. * object. $something should only be added if $destinationname matches
  654. * $this->name. If it doesn't, add should be called on child objects that are
  655. * also parentable_part_of_admin_tree's.
  656. *
  657. * $something should be appended as the last child in the $destinationname. If the
  658. * $beforesibling is specified, $something should be prepended to it. If the given
  659. * sibling is not found, $something should be appended to the end of $destinationname
  660. * and a developer debugging message should be displayed.
  661. *
  662. * @param string $destinationname The internal name of the new parent for $something.
  663. * @param part_of_admin_tree $something The object to be added.
  664. * @return bool True on success, false on failure.
  665. */
  666. public function add($destinationname, $something, $beforesibling = null);
  667. }
  668. /**
  669. * The object used to represent folders (a.k.a. categories) in the admin tree block.
  670. *
  671. * Each admin_category object contains a number of part_of_admin_tree objects.
  672. *
  673. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  674. */
  675. class admin_category implements parentable_part_of_admin_tree, linkable_settings_page {
  676. /** @var part_of_admin_tree[] An array of part_of_admin_tree objects that are this object's children */
  677. protected $children;
  678. /** @var string An internal name for this category. Must be unique amongst ALL part_of_admin_tree objects */
  679. public $name;
  680. /** @var string The displayed name for this category. Usually obtained through get_string() */
  681. public $visiblename;
  682. /** @var bool Should this category be hidden in admin tree block? */
  683. public $hidden;
  684. /** @var mixed Either a string or an array or strings */
  685. public $path;
  686. /** @var mixed Either a string or an array or strings */
  687. public $visiblepath;
  688. /** @var array fast lookup category cache, all categories of one tree point to one cache */
  689. protected $category_cache;
  690. /** @var bool If set to true children will be sorted when calling {@link admin_category::get_children()} */
  691. protected $sort = false;
  692. /** @var bool If set to true children will be sorted in ascending order. */
  693. protected $sortasc = true;
  694. /** @var bool If set to true sub categories and pages will be split and then sorted.. */
  695. protected $sortsplit = true;
  696. /** @var bool $sorted True if the children have been sorted and don't need resorting */
  697. protected $sorted = false;
  698. /**
  699. * Constructor for an empty admin category
  700. *
  701. * @param string $name The internal name for this category. Must be unique amongst ALL part_of_admin_tree objects
  702. * @param string $visiblename The displayed named for this category. Usually obtained through get_string()
  703. * @param bool $hidden hide category in admin tree block, defaults to false
  704. */
  705. public function __construct($name, $visiblename, $hidden=false) {
  706. $this->children = array();
  707. $this->name = $name;
  708. $this->visiblename = $visiblename;
  709. $this->hidden = $hidden;
  710. }
  711. /**
  712. * Get the URL to view this settings page.
  713. *
  714. * @return moodle_url
  715. */
  716. public function get_settings_page_url(): moodle_url {
  717. return new moodle_url(
  718. '/admin/category.php',
  719. [
  720. 'category' => $this->name,
  721. ]
  722. );
  723. }
  724. /**
  725. * Returns a reference to the part_of_admin_tree object with internal name $name.
  726. *
  727. * @param string $name The internal name of the object we want.
  728. * @param bool $findpath initialize path and visiblepath arrays
  729. * @return mixed A reference to the object with internal name $name if found, otherwise a reference to NULL.
  730. * defaults to false
  731. */
  732. public function locate($name, $findpath=false) {
  733. if (!isset($this->category_cache[$this->name])) {
  734. // somebody much have purged the cache
  735. $this->category_cache[$this->name] = $this;
  736. }
  737. if ($this->name == $name) {
  738. if ($findpath) {
  739. $this->visiblepath[] = $this->visiblename;
  740. $this->path[] = $this->name;
  741. }
  742. return $this;
  743. }
  744. // quick category lookup
  745. if (!$findpath and isset($this->category_cache[$name])) {
  746. return $this->category_cache[$name];
  747. }
  748. $return = NULL;
  749. foreach($this->children as $childid=>$unused) {
  750. if ($return = $this->children[$childid]->locate($name, $findpath)) {
  751. break;
  752. }
  753. }
  754. if (!is_null($return) and $findpath) {
  755. $return->visiblepath[] = $this->visiblename;
  756. $return->path[] = $this->name;
  757. }
  758. return $return;
  759. }
  760. /**
  761. * Search using query
  762. *
  763. * @param string query
  764. * @return mixed array-object structure of found settings and pages
  765. */
  766. public function search($query) {
  767. $result = array();
  768. foreach ($this->get_children() as $child) {
  769. $subsearch = $child->search($query);
  770. if (!is_array($subsearch)) {
  771. debugging('Incorrect search result from '.$child->name);
  772. continue;
  773. }
  774. $result = array_merge($result, $subsearch);
  775. }
  776. return $result;
  777. }
  778. /**
  779. * Removes part_of_admin_tree object with internal name $name.
  780. *
  781. * @param string $name The internal name of the object we want to remove.
  782. * @return bool success
  783. */
  784. public function prune($name) {
  785. if ($this->name == $name) {
  786. return false; //can not remove itself
  787. }
  788. foreach($this->children as $precedence => $child) {
  789. if ($child->name == $name) {
  790. // clear cache and delete self
  791. while($this->category_cache) {
  792. // delete the cache, but keep the original array address
  793. array_pop($this->category_cache);
  794. }
  795. unset($this->children[$precedence]);
  796. return true;
  797. } else if ($this->children[$precedence]->prune($name)) {
  798. return true;
  799. }
  800. }
  801. return false;
  802. }
  803. /**
  804. * Adds a part_of_admin_tree to a child or grandchild (or great-grandchild, and so forth) of this object.
  805. *
  806. * By default the new part of the tree is appended as the last child of the parent. You
  807. * can specify a sibling node that the new part should be prepended to. If the given
  808. * sibling is not found, the part is appended to the end (as it would be by default) and
  809. * a developer debugging message is displayed.
  810. *
  811. * @throws coding_exception if the $beforesibling is empty string or is not string at all.
  812. * @param string $destinationame The internal name of the immediate parent that we want for $something.
  813. * @param mixed $something A part_of_admin_tree or setting instance to be added.
  814. * @param string $beforesibling The name of the parent's child the $something should be prepended to.
  815. * @return bool True if successfully added, false if $something can not be added.
  816. */
  817. public function add($parentname, $something, $beforesibling = null) {
  818. global $CFG;
  819. $parent = $this->locate($parentname);
  820. if (is_null($parent)) {
  821. debugging('parent does not exist!');
  822. return false;
  823. }
  824. if ($something instanceof part_of_admin_tree) {
  825. if (!($parent instanceof parentable_part_of_admin_tree)) {
  826. debugging('error - parts of tree can be inserted only into parentable parts');
  827. return false;
  828. }
  829. if ($CFG->debugdeveloper && !is_null($this->locate($something->name))) {
  830. // The name of the node is already used, simply warn the developer that this should not happen.
  831. // It is intentional to check for the debug level before performing the check.
  832. debugging('Duplicate admin page name: ' . $something->name, DEBUG_DEVELOPER);
  833. }
  834. if (is_null($beforesibling)) {
  835. // Append $something as the parent's last child.
  836. $parent->children[] = $something;
  837. } else {
  838. if (!is_string($beforesibling) or trim($beforesibling) === '') {
  839. throw new coding_exception('Unexpected value of the beforesibling parameter');
  840. }
  841. // Try to find the position of the sibling.
  842. $siblingposition = null;
  843. foreach ($parent->children as $childposition => $child) {
  844. if ($child->name === $beforesibling) {
  845. $siblingposition = $childposition;
  846. break;
  847. }
  848. }
  849. if (is_null($siblingposition)) {
  850. debugging('Sibling '.$beforesibling.' not found', DEBUG_DEVELOPER);
  851. $parent->children[] = $something;
  852. } else {
  853. $parent->children = array_merge(
  854. array_slice($parent->children, 0, $siblingposition),
  855. array($something),
  856. array_slice($parent->children, $siblingposition)
  857. );
  858. }
  859. }
  860. if ($something instanceof admin_category) {
  861. if (isset($this->category_cache[$something->name])) {
  862. debugging('Duplicate admin category name: '.$something->name);
  863. } else {
  864. $this->category_cache[$something->name] = $something;
  865. $something->category_cache =& $this->category_cache;
  866. foreach ($something->children as $child) {
  867. // just in case somebody already added subcategories
  868. if ($child instanceof admin_category) {
  869. if (isset($this->category_cache[$child->name])) {
  870. debugging('Duplicate admin category name: '.$child->name);
  871. } else {
  872. $this->category_cache[$child->name] = $child;
  873. $child->category_cache =& $this->category_cache;
  874. }
  875. }
  876. }
  877. }
  878. }
  879. return true;
  880. } else {
  881. debugging('error - can not add this element');
  882. return false;
  883. }
  884. }
  885. /**
  886. * Checks if the user has access to anything in this category.
  887. *
  888. * @return bool True if the user has access to at least one child in this category, false otherwise.
  889. */
  890. public function check_access() {
  891. foreach ($this->children as $child) {
  892. if ($child->check_access()) {
  893. return true;
  894. }
  895. }
  896. return false;
  897. }
  898. /**
  899. * Is this category hidden in admin tree block?
  900. *
  901. * @return bool True if hidden
  902. */
  903. public function is_hidden() {
  904. return $this->hidden;
  905. }
  906. /**
  907. * Show we display Save button at the page bottom?
  908. * @return bool
  909. */
  910. public function show_save() {
  911. foreach ($this->children as $child) {
  912. if ($child->show_save()) {
  913. return true;
  914. }
  915. }
  916. return false;
  917. }
  918. /**
  919. * Sets sorting on this category.
  920. *
  921. * Please note this function doesn't actually do the sorting.
  922. * It can be called anytime.
  923. * Sorting occurs when the user calls get_children.
  924. * Code using the children array directly won't see the sorted results.
  925. *
  926. * @param bool $sort If set to true children will be sorted, if false they won't be.
  927. * @param bool $asc If true sorting will be ascending, otherwise descending.
  928. * @param bool $split If true we sort pages and sub categories separately.
  929. */
  930. public function set_sorting($sort, $asc = true, $split = true) {
  931. $this->sort = (bool)$sort;
  932. $this->sortasc = (bool)$asc;
  933. $this->sortsplit = (bool)$split;
  934. }
  935. /**
  936. * Returns the children associated with this category.
  937. *
  938. * @return part_of_admin_tree[]
  939. */
  940. public function get_children() {
  941. // If we should sort and it hasn't already been sorted.
  942. if ($this->sort && !$this->sorted) {
  943. if ($this->sortsplit) {
  944. $categories = array();
  945. $pages = array();
  946. foreach ($this->children as $child) {
  947. if ($child instanceof admin_category) {
  948. $categories[] = $child;
  949. } else {
  950. $pages[] = $child;
  951. }
  952. }
  953. core_collator::asort_objects_by_property($categories, 'visiblename');
  954. core_collator::asort_objects_by_property($pages, 'visiblename');
  955. if (!$this->sortasc) {
  956. $categories = array_reverse($categories);
  957. $pages = array_reverse($pages);
  958. }
  959. $this->children = array_merge($pages, $categories);
  960. } else {
  961. core_collator::asort_objects_by_property($this->children, 'visiblename');
  962. if (!$this->sortasc) {
  963. $this->children = array_reverse($this->children);
  964. }
  965. }
  966. $this->sorted = true;
  967. }
  968. return $this->children;
  969. }
  970. /**
  971. * Magically gets a property from this object.
  972. *
  973. * @param $property
  974. * @return part_of_admin_tree[]
  975. * @throws coding_exception
  976. */
  977. public function __get($property) {
  978. if ($property === 'children') {
  979. return $this->get_children();
  980. }
  981. throw new coding_exception('Invalid property requested.');
  982. }
  983. /**
  984. * Magically sets a property against this object.
  985. *
  986. * @param string $property
  987. * @param mixed $value
  988. * @throws coding_exception
  989. */
  990. public function __set($property, $value) {
  991. if ($property === 'children') {
  992. $this->sorted = false;
  993. $this->children = $value;
  994. } else {
  995. throw new coding_exception('Invalid property requested.');
  996. }
  997. }
  998. /**
  999. * Checks if an inaccessible property is set.
  1000. *
  1001. * @param string $property
  1002. * @return bool
  1003. * @throws coding_exception
  1004. */
  1005. public function __isset($property) {
  1006. if ($property === 'children') {
  1007. return isset($this->children);
  1008. }
  1009. throw new coding_exception('Invalid property requested.');
  1010. }
  1011. }
  1012. /**
  1013. * Root of admin settings tree, does not have any parent.
  1014. *
  1015. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  1016. */
  1017. class admin_root extends admin_category {
  1018. /** @var array List of errors */
  1019. public $errors;
  1020. /** @var string search query */
  1021. public $search;
  1022. /** @var bool full tree flag - true means all settings required, false only pages required */
  1023. public $fulltree;
  1024. /** @var bool flag indicating loaded tree */
  1025. public $loaded;
  1026. /** @var mixed site custom defaults overriding defaults in settings files*/
  1027. public $custom_defaults;
  1028. /**
  1029. * @param bool $fulltree true means all settings required,
  1030. * false only pages required
  1031. */
  1032. public function __construct($fulltree) {
  1033. global $CFG;
  1034. parent::__construct('root', get_string('administration'), false);
  1035. $this->errors = array();
  1036. $this->search = '';
  1037. $this->fulltree = $fulltree;
  1038. $this->loaded = false;
  1039. $this->category_cache = array();
  1040. // load custom defaults if found
  1041. $this->custom_defaults = null;
  1042. $defaultsfile = "$CFG->dirroot/local/defaults.php";
  1043. if (is_readable($defaultsfile)) {
  1044. $defaults = array();
  1045. include($defaultsfile);
  1046. if (is_array($defaults) and count($defaults)) {
  1047. $this->custom_defaults = $defaults;
  1048. }
  1049. }
  1050. }
  1051. /**
  1052. * Empties children array, and sets loaded to false
  1053. *
  1054. * @param bool $requirefulltree
  1055. */
  1056. public function purge_children($requirefulltree) {
  1057. $this->children = array();
  1058. $this->fulltree = ($requirefulltree || $this->fulltree);
  1059. $this->loaded = false;
  1060. //break circular dependencies - this helps PHP 5.2
  1061. while($this->category_cache) {
  1062. array_pop($this->category_cache);
  1063. }
  1064. $this->category_cache = array();
  1065. }
  1066. }
  1067. /**
  1068. * Links external PHP pages into the admin tree.
  1069. *
  1070. * See detailed usage example at the top of this document (adminlib.php)
  1071. *
  1072. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  1073. */
  1074. class admin_externalpage implements part_of_admin_tree, linkable_settings_page {
  1075. /** @var string An internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects */
  1076. public $name;
  1077. /** @var string The displayed name for this external page. Usually obtained through get_string(). */
  1078. public $visiblename;
  1079. /** @var string The external URL that we should link to when someone requests this external page. */
  1080. public $url;
  1081. /** @var array The role capability/permission a user must have to access this external page. */
  1082. public $req_capability;
  1083. /** @var object The context in which capability/permission should be checked, default is site context. */
  1084. public $context;
  1085. /** @var bool hidden in admin tree block. */
  1086. public $hidden;
  1087. /** @var mixed either string or array of string */
  1088. public $path;
  1089. /** @var array list of visible names of page parents */
  1090. public $visiblepath;
  1091. /**
  1092. * Constructor for adding an external page into the admin tree.
  1093. *
  1094. * @param string $name The internal name for this external page. Must be unique amongst ALL part_of_admin_tree objects.
  1095. * @param string $visiblename The displayed name for this external page. Usually obtained through get_string().
  1096. * @param string $url The external URL that we should link to when someone requests this external page.
  1097. * @param mixed $req_capability The role capability/permission a user must have to access this external page. Defaults to 'moodle/site:config'.
  1098. * @param boolean $hidden Is this external page hidden in admin tree block? Default false.
  1099. * @param stdClass $context The context the page relates to. Not sure what happens
  1100. * if you specify something other than system or front page. Defaults to system.
  1101. */
  1102. public function __construct($name, $visiblename, $url, $req_capability='moodle/site:config', $hidden=false, $context=NULL) {
  1103. $this->name = $name;
  1104. $this->visiblename = $visiblename;
  1105. $this->url = $url;
  1106. if (is_array($req_capability)) {
  1107. $this->req_capability = $req_capability;
  1108. } else {
  1109. $this->req_capability = array($req_capability);
  1110. }
  1111. $this->hidden = $hidden;
  1112. $this->context = $context;
  1113. }
  1114. /**
  1115. * Get the URL to view this settings page.
  1116. *
  1117. * @return moodle_url
  1118. */
  1119. public function get_settings_page_url(): moodle_url {
  1120. return new moodle_url($this->url);
  1121. }
  1122. /**
  1123. * Returns a reference to the part_of_admin_tree object with internal name $name.
  1124. *
  1125. * @param string $name The internal name of the object we want.
  1126. * @param bool $findpath defaults to false
  1127. * @return mixed A reference to the object with internal name $name if found, otherwise a reference to NULL.
  1128. */
  1129. public function locate($name, $findpath=false) {
  1130. if ($this->name == $name) {
  1131. if ($findpath) {
  1132. $this->visiblepath = array($this->visiblename);
  1133. $this->path = array($this->name);
  1134. }
  1135. return $this;
  1136. } else {
  1137. $return = NULL;
  1138. return $return;
  1139. }
  1140. }
  1141. /**
  1142. * This function always returns false, required function by interface
  1143. *
  1144. * @param string $name
  1145. * @return false
  1146. */
  1147. public function prune($name) {
  1148. return false;
  1149. }
  1150. /**
  1151. * Search using query
  1152. *
  1153. * @param string $query
  1154. * @return mixed array-object structure of found settings and pages
  1155. */
  1156. public function search($query) {
  1157. $found = false;
  1158. if (strpos(strtolower($this->name), $query) !== false) {
  1159. $found = true;
  1160. } else if (strpos(core_text::strtolower($this->visiblename), $query) !== false) {
  1161. $found = true;
  1162. }
  1163. if ($found) {
  1164. $result = new stdClass();
  1165. $result->page = $this;
  1166. $result->settings = array();
  1167. return array($this->name => $result);
  1168. } else {
  1169. return array();
  1170. }
  1171. }
  1172. /**
  1173. * Determines if the current user has access to this external page based on $this->req_capability.
  1174. *
  1175. * @return bool True if user has access, false otherwise.
  1176. */
  1177. public function check_access() {
  1178. global $CFG;
  1179. $context = empty($this->context) ? context_system::instance() : $this->context;
  1180. foreach($this->req_capability as $cap) {
  1181. if (has_capability($cap, $context)) {
  1182. return true;
  1183. }
  1184. }
  1185. return false;
  1186. }
  1187. /**
  1188. * Is this external page hidden in admin tree block?
  1189. *
  1190. * @return bool True if hidden
  1191. */
  1192. public function is_hidden() {
  1193. return $this->hidden;
  1194. }
  1195. /**
  1196. * Show we display Save button at the page bottom?
  1197. * @return bool
  1198. */
  1199. public function show_save() {
  1200. return false;
  1201. }
  1202. }
  1203. /**
  1204. * Used to store details of the dependency between two settings elements.
  1205. *
  1206. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  1207. * @copyright 2017 Davo Smith, Synergy Learning
  1208. */
  1209. class admin_settingdependency {
  1210. /** @var string the name of the setting to be shown/hidden */
  1211. public $settingname;
  1212. /** @var string the setting this is dependent on */
  1213. public $dependenton;
  1214. /** @var string the condition to show/hide the element */
  1215. public $condition;
  1216. /** @var string the value to compare against */
  1217. public $value;
  1218. /** @var string[] list of valid conditions */
  1219. private static $validconditions = ['checked', 'notchecked', 'noitemselected', 'eq', 'neq', 'in'];
  1220. /**
  1221. * admin_settingdependency constructor.
  1222. * @param string $settingname
  1223. * @param string $dependenton
  1224. * @param string $condition
  1225. * @param string $value
  1226. * @throws \coding_exception
  1227. */
  1228. public function __construct($settingname, $dependenton, $condition, $value) {
  1229. $this->settingname = $this->parse_name($settingname);
  1230. $this->dependenton = $this->parse_name($dependenton);
  1231. $this->condition = $condition;
  1232. $this->value = $value;
  1233. if (!in_array($this->condition, self::$validconditions)) {
  1234. throw new coding_exception("Invalid condition '$condition'");
  1235. }
  1236. }
  1237. /**
  1238. * Convert the setting name into the form field name.
  1239. * @param string $name
  1240. * @return string
  1241. */
  1242. private function parse_name($name) {
  1243. $bits = explode('/', $name);
  1244. $name = array_pop($bits);
  1245. $plugin = '';
  1246. if ($bits) {
  1247. $plugin = array_pop($bits);
  1248. if ($plugin === 'moodle') {
  1249. $plugin = '';
  1250. }
  1251. }
  1252. return 's_'.$plugin.'_'.$name;
  1253. }
  1254. /**
  1255. * Gather toget…

Large files files are truncated, but you can click here to view the full file