PageRenderTime 74ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 2ms

/lib/adminlib.php

http://github.com/moodle/moodle
PHP | 11409 lines | 6370 code | 1335 blank | 3704 comment | 974 complexity | 1ecf6e7c44876bcc34cd9175bd18b7f9 MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.0, LGPL-2.1, BSD-3-Clause

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

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