adminlib.php 348 KB
Newer Older
1
<?php
2
3
// This file is part of Moodle - http://moodle.org/
//
4
5
6
7
8
9
10
11
12
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
13
//
14
15
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17

/**
18
 * Functions and classes used during installation, upgrades and for admin settings.
19
 *
20
 *  ADMIN SETTINGS TREE INTRODUCTION
21
22
23
24
 *
 *  This file performs the following tasks:
 *   -it defines the necessary objects and interfaces to build the Moodle
 *    admin hierarchy
25
 *   -it defines the admin_externalpage_setup()
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
 *
 *  ADMIN_SETTING OBJECTS
 *
 *  Moodle settings are represented by objects that inherit from the admin_setting
 *  class. These objects encapsulate how to read a setting, how to write a new value
 *  to a setting, and how to appropriately display the HTML to modify the setting.
 *
 *  ADMIN_SETTINGPAGE OBJECTS
 *
 *  The admin_setting objects are then grouped into admin_settingpages. The latter
 *  appear in the Moodle admin tree block. All interaction with admin_settingpage
 *  objects is handled by the admin/settings.php file.
 *
 *  ADMIN_EXTERNALPAGE OBJECTS
 *
 *  There are some settings in Moodle that are too complex to (efficiently) handle
 *  with admin_settingpages. (Consider, for example, user management and displaying
 *  lists of users.) In this case, we use the admin_externalpage object. This object
 *  places a link to an external PHP file in the admin tree block.
 *
 *  If you're using an admin_externalpage object for some settings, you can take
 *  advantage of the admin_externalpage_* functions. For example, suppose you wanted
 *  to add a foo.php file into admin. First off, you add the following line to
 *  admin/settings/first.php (at the end of the file) or to some other file in
 *  admin/settings:
 * <code>
 *     $ADMIN->add('userinterface', new admin_externalpage('foo', get_string('foo'),
 *         $CFG->wwwdir . '/' . '$CFG->admin . '/foo.php', 'some_role_permission'));
 * </code>
 *
 *  Next, in foo.php, your file structure would resemble the following:
 * <code>
58
 *         require(__DIR__.'/../../config.php');
59
60
61
 *         require_once($CFG->libdir.'/adminlib.php');
 *         admin_externalpage_setup('foo');
 *         // functionality like processing form submissions goes here
62
 *         echo $OUTPUT->header();
63
 *         // your HTML goes here
64
 *         echo $OUTPUT->footer();
65
66
67
68
 * </code>
 *
 *  The admin_externalpage_setup() function call ensures the user is logged in,
 *  and makes sure that they have the proper role permission to access the page.
69
 *  It also configures all $PAGE properties needed for navigation.
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
 *
 *  ADMIN_CATEGORY OBJECTS
 *
 *  Above and beyond all this, we have admin_category objects. These objects
 *  appear as folders in the admin tree block. They contain admin_settingpage's,
 *  admin_externalpage's, and other admin_category's.
 *
 *  OTHER NOTES
 *
 *  admin_settingpage's, admin_externalpage's, and admin_category's all inherit
 *  from part_of_admin_tree (a pseudointerface). This interface insists that
 *  a class has a check_access method for access permissions, a locate method
 *  used to find a specific node in the admin tree and find parent path.
 *
 *  admin_category's inherit from parentable_part_of_admin_tree. This pseudo-
 *  interface ensures that the class implements a recursive add function which
 *  accepts a part_of_admin_tree object and searches for the proper place to
 *  put it. parentable_part_of_admin_tree implies part_of_admin_tree.
 *
 *  Please note that the $this->name field of any part_of_admin_tree must be
 *  UNIQUE throughout the ENTIRE admin tree.
 *
 *  The $this->name field of an admin_setting object (which is *not* part_of_
 *  admin_tree) must be unique on the respective admin_settingpage where it is
 *  used.
 *
96
97
 * Original author: Vincenzo K. Marcovecchio
 * Maintainer:      Petr Skoda
98
 *
99
100
101
102
 * @package    core
 * @subpackage admin
 * @copyright  1999 onwards Martin Dougiamas  http://dougiamas.com
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
103
104
 */

105
106
defined('MOODLE_INTERNAL') || die();

107
108
/// Add libraries
require_once($CFG->libdir.'/ddllib.php');
109
require_once($CFG->libdir.'/xmlize.php');
110
require_once($CFG->libdir.'/messagelib.php');
111

112
113
define('INSECURE_DATAROOT_WARNING', 1);
define('INSECURE_DATAROOT_ERROR', 2);
114

115
116
117
/**
 * Automatically clean-up all plugin data and remove the plugin DB tables
 *
118
119
 * NOTE: do not call directly, use new /admin/plugins.php?uninstall=component instead!
 *
120
121
122
123
124
125
126
127
 * @param string $type The plugin type, eg. 'mod', 'qtype', 'workshopgrading' etc.
 * @param string $name The plugin name, eg. 'forum', 'multichoice', 'accumulative' etc.
 * @uses global $OUTPUT to produce notices and other messages
 * @return void
 */
function uninstall_plugin($type, $name) {
    global $CFG, $DB, $OUTPUT;

128
    // This may take a long time.
129
    core_php_time_limit::raise();
130

131
132
133
134
    // Recursively uninstall all subplugins first.
    $subplugintypes = core_component::get_plugin_types_with_subplugins();
    if (isset($subplugintypes[$type])) {
        $base = core_component::get_plugin_directory($type, $name);
135
        if (file_exists("$base/db/subplugins.php")) {
136
            $subplugins = array();
137
            include("$base/db/subplugins.php");
138
            foreach ($subplugins as $subplugintype=>$dir) {
139
                $instances = core_component::get_plugin_list($subplugintype);
140
141
142
                foreach ($instances as $subpluginname => $notusedpluginpath) {
                    uninstall_plugin($subplugintype, $subpluginname);
                }
143
144
            }
        }
145

146
147
    }

148
149
150
151
    $component = $type . '_' . $name;  // eg. 'qtype_multichoice' or 'workshopgrading_accumulative' or 'mod_forum'

    if ($type === 'mod') {
        $pluginname = $name;  // eg. 'forum'
152
153
154
155
156
        if (get_string_manager()->string_exists('modulename', $component)) {
            $strpluginname = get_string('modulename', $component);
        } else {
            $strpluginname = $component;
        }
157

158
    } else {
159
        $pluginname = $component;
160
161
162
163
164
        if (get_string_manager()->string_exists('pluginname', $component)) {
            $strpluginname = get_string('pluginname', $component);
        } else {
            $strpluginname = $component;
        }
165
    }
166

167
168
    echo $OUTPUT->heading($pluginname);

169
170
    // Delete all tag areas, collections and instances associated with this plugin.
    core_tag_area::uninstall($component);
171

172
    // Custom plugin uninstall.
173
    $plugindirectory = core_component::get_plugin_directory($type, $name);
174
175
176
    $uninstalllib = $plugindirectory . '/db/uninstall.php';
    if (file_exists($uninstalllib)) {
        require_once($uninstalllib);
177
        $uninstallfunction = 'xmldb_' . $pluginname . '_uninstall';    // eg. 'xmldb_workshop_uninstall()'
178
        if (function_exists($uninstallfunction)) {
179
180
            // Do not verify result, let plugin complain if necessary.
            $uninstallfunction();
181
182
183
        }
    }

184
    // Specific plugin type cleanup.
185
    $plugininfo = core_plugin_manager::instance()->get_plugin_info($component);
186
187
    if ($plugininfo) {
        $plugininfo->uninstall_cleanup();
188
        core_plugin_manager::reset_caches();
189
190
191
    }
    $plugininfo = null;

192
    // perform clean-up task common for all the plugin/subplugin types
193

194
195
196
197
    //delete the web service functions and pre-built services
    require_once($CFG->dirroot.'/lib/externallib.php');
    external_delete_descriptions($component);

198
    // delete calendar events
199
    $DB->delete_records('event', array('modulename' => $pluginname));
200

201
    // Delete scheduled tasks.
202
    $DB->delete_records('task_scheduled', array('component' => $component));
203

Andrew Nicols's avatar
Andrew Nicols committed
204
    // Delete Inbound Message datakeys.
205
    $DB->delete_records_select('messageinbound_datakeys',
206
            'handler IN (SELECT id FROM {messageinbound_handlers} WHERE component = ?)', array($component));
Andrew Nicols's avatar
Andrew Nicols committed
207
208

    // Delete Inbound Message handlers.
209
    $DB->delete_records('messageinbound_handlers', array('component' => $component));
Andrew Nicols's avatar
Andrew Nicols committed
210

211
    // delete all the logs
212
    $DB->delete_records('log', array('module' => $pluginname));
213
214

    // delete log_display information
215
    $DB->delete_records('log_display', array('component' => $component));
216
217

    // delete the module configuration records
218
219
220
221
    unset_all_config_for_plugin($component);
    if ($type === 'mod') {
        unset_all_config_for_plugin($pluginname);
    }
222

223
    // delete message provider
224
225
    message_provider_uninstall($component);

226
227
    // delete the plugin tables
    $xmldbfilepath = $plugindirectory . '/db/install.xml';
228
229
230
231
232
    drop_plugin_tables($component, $xmldbfilepath, false);
    if ($type === 'mod' or $type === 'block') {
        // non-frankenstyle table prefixes
        drop_plugin_tables($name, $xmldbfilepath, false);
    }
233
234
235
236

    // delete the capabilities that were defined by this module
    capabilities_cleanup($component);

Petr Skoda's avatar
Petr Skoda committed
237
    // remove event handlers and dequeue pending events
238
239
    events_uninstall($component);

240
241
242
243
    // Delete all remaining files in the filepool owned by the component.
    $fs = get_file_storage();
    $fs->delete_component_files($component);

244
245
246
    // Finally purge all caches.
    purge_all_caches();

247
248
249
    // Invalidate the hash used for upgrade detections.
    set_config('allversionshash', '');

250
251
252
    echo $OUTPUT->notification(get_string('success'), 'notifysuccess');
}

253
254
255
256
257
258
259
260
261
262
/**
 * Returns the version of installed component
 *
 * @param string $component component name
 * @param string $source either 'disk' or 'installed' - where to get the version information from
 * @return string|bool version number or false if the component is not found
 */
function get_component_version($component, $source='installed') {
    global $CFG, $DB;

263
    list($type, $name) = core_component::normalize_component($component);
264
265
266
267
268
269
270
271
272
273
274
275
276

    // moodle core or a core subsystem
    if ($type === 'core') {
        if ($source === 'installed') {
            if (empty($CFG->version)) {
                return false;
            } else {
                return $CFG->version;
            }
        } else {
            if (!is_readable($CFG->dirroot.'/version.php')) {
                return false;
            } else {
277
                $version = null; //initialize variable for IDEs
278
279
280
281
282
283
284
285
286
                include($CFG->dirroot.'/version.php');
                return $version;
            }
        }
    }

    // activity module
    if ($type === 'mod') {
        if ($source === 'installed') {
287
288
289
290
291
292
            if ($CFG->version < 2013092001.02) {
                return $DB->get_field('modules', 'version', array('name'=>$name));
            } else {
                return get_config('mod_'.$name, 'version');
            }

293
        } else {
294
            $mods = core_component::get_plugin_list('mod');
295
            if (empty($mods[$name]) or !is_readable($mods[$name].'/version.php')) {
296
297
                return false;
            } else {
298
299
300
                $plugin = new stdClass();
                $plugin->version = null;
                $module = $plugin;
301
                include($mods[$name].'/version.php');
302
                return $plugin->version;
303
304
305
306
307
308
309
            }
        }
    }

    // block
    if ($type === 'block') {
        if ($source === 'installed') {
310
311
312
313
314
            if ($CFG->version < 2013092001.02) {
                return $DB->get_field('block', 'version', array('name'=>$name));
            } else {
                return get_config('block_'.$name, 'version');
            }
315
        } else {
316
            $blocks = core_component::get_plugin_list('block');
317
318
319
320
321
322
323
324
325
326
327
328
329
330
            if (empty($blocks[$name]) or !is_readable($blocks[$name].'/version.php')) {
                return false;
            } else {
                $plugin = new stdclass();
                include($blocks[$name].'/version.php');
                return $plugin->version;
            }
        }
    }

    // all other plugin types
    if ($source === 'installed') {
        return get_config($type.'_'.$name, 'version');
    } else {
331
        $plugins = core_component::get_plugin_list($type);
332
333
334
335
336
337
338
339
340
341
        if (empty($plugins[$name])) {
            return false;
        } else {
            $plugin = new stdclass();
            include($plugins[$name].'/version.php');
            return $plugin->version;
        }
    }
}

342
343
/**
 * Delete all plugin tables
344
345
346
347
 *
 * @param string $name Name of plugin, used as table prefix
 * @param string $file Path to install.xml file
 * @param bool $feedback defaults to true
348
 * @return bool Always returns true
349
350
351
352
353
 */
function drop_plugin_tables($name, $file, $feedback=true) {
    global $CFG, $DB;

    // first try normal delete
354
    if (file_exists($file) and $DB->get_manager()->delete_tables_from_xmldb_file($file)) {
355
356
357
358
359
360
361
362
363
364
365
366
367
368
        return true;
    }

    // then try to find all tables that start with name and are not in any xml file
    $used_tables = get_used_table_names();

    $tables = $DB->get_tables();

    /// Iterate over, fixing id fields as necessary
    foreach ($tables as $table) {
        if (in_array($table, $used_tables)) {
            continue;
        }

369
370
371
372
        if (strpos($table, $name) !== 0) {
            continue;
        }

373
374
375
        // found orphan table --> delete it
        if ($DB->get_manager()->table_exists($table)) {
            $xmldb_table = new xmldb_table($table);
skodak's avatar
skodak committed
376
            $DB->get_manager()->drop_table($xmldb_table);
377
378
379
380
381
382
383
        }
    }

    return true;
}

/**
Petr Skoda's avatar
Petr Skoda committed
384
 * Returns names of all known tables == tables that moodle knows about.
385
386
 *
 * @return array Array of lowercase table names
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
 */
function get_used_table_names() {
    $table_names = array();
    $dbdirs = get_db_directories();

    foreach ($dbdirs as $dbdir) {
        $file = $dbdir.'/install.xml';

        $xmldb_file = new xmldb_file($file);

        if (!$xmldb_file->fileExists()) {
            continue;
        }

        $loaded    = $xmldb_file->loadXMLStructure();
402
        $structure = $xmldb_file->getStructure();
403
404
405

        if ($loaded and $tables = $structure->getTables()) {
            foreach($tables as $table) {
406
                $table_names[] = strtolower($table->getName());
407
408
409
410
411
412
413
414
415
            }
        }
    }

    return $table_names;
}

/**
 * Returns list of all directories where we expect install.xml files
416
 * @return array Array of paths
417
418
419
420
421
422
 */
function get_db_directories() {
    global $CFG;

    $dbdirs = array();

423
    /// First, the main one (lib/db)
424
425
    $dbdirs[] = $CFG->libdir.'/db';

426
427
    /// Then, all the ones defined by core_component::get_plugin_types()
    $plugintypes = core_component::get_plugin_types();
428
    foreach ($plugintypes as $plugintype => $pluginbasedir) {
429
        if ($plugins = core_component::get_plugin_list($plugintype)) {
430
431
            foreach ($plugins as $plugin => $plugindir) {
                $dbdirs[] = $plugindir.'/db';
432
            }
433
        }
434
    }
435
436
437
438

    return $dbdirs;
}

439
/**
440
441
 * Try to obtain or release the cron lock.
 * @param string  $name  name of lock
Petr Skoda's avatar
Petr Skoda committed
442
443
 * @param int  $until timestamp when this lock considered stale, null means remove lock unconditionally
 * @param bool $ignorecurrent ignore current lock state, usually extend previous lock, defaults to false
444
 * @return bool true if lock obtained
445
 */
446
function set_cron_lock($name, $until, $ignorecurrent=false) {
447
    global $DB;
448
    if (empty($name)) {
449
        debugging("Tried to get a cron lock for a null fieldname");
450
451
452
        return false;
    }

453
454
455
    // remove lock by force == remove from config table
    if (is_null($until)) {
        set_config($name, null);
456
457
458
        return true;
    }

459
    if (!$ignorecurrent) {
460
        // read value from db - other processes might have changed it
461
        $value = $DB->get_field('config', 'value', array('name'=>$name));
462
463

        if ($value and $value > time()) {
464
            //lock active
465
            return false;
466
467
        }
    }
468
469

    set_config($name, $until);
470
471
    return true;
}
472

473
474
475
476
477
478
479
/**
 * Test if and critical warnings are present
 * @return bool
 */
function admin_critical_warnings_present() {
    global $SESSION;

480
    if (!has_capability('moodle/site:config', context_system::instance())) {
481
482
483
484
485
        return 0;
    }

    if (!isset($SESSION->admin_critical_warning)) {
        $SESSION->admin_critical_warning = 0;
486
        if (is_dataroot_insecure(true) === INSECURE_DATAROOT_ERROR) {
487
488
489
490
491
492
493
            $SESSION->admin_critical_warning = 1;
        }
    }

    return $SESSION->admin_critical_warning;
}

494
/**
495
496
 * Detects if float supports at least 10 decimal digits
 *
Petr Skoda's avatar
Petr Skoda committed
497
 * Detects if float supports at least 10 decimal digits
498
 * and also if float-->string conversion works as expected.
499
 *
500
501
502
503
504
505
506
507
508
 * @return bool true if problem found
 */
function is_float_problem() {
    $num1 = 2009010200.01;
    $num2 = 2009010200.02;

    return ((string)$num1 === (string)$num2 or $num1 === $num2 or $num2 <= (string)$num1);
}

509
510
511
/**
 * Try to verify that dataroot is not accessible from web.
 *
512
513
 * Try to verify that dataroot is not accessible from web.
 * It is not 100% correct but might help to reduce number of vulnerable sites.
514
 * Protection from httpd.conf and .htaccess is not detected properly.
515
 *
516
517
518
 * @uses INSECURE_DATAROOT_WARNING
 * @uses INSECURE_DATAROOT_ERROR
 * @param bool $fetchtest try to test public access by fetching file, default false
Petr Skoda's avatar
Petr Skoda committed
519
 * @return mixed empty means secure, INSECURE_DATAROOT_ERROR found a critical problem, INSECURE_DATAROOT_WARNING might be problematic
520
 */
521
function is_dataroot_insecure($fetchtest=false) {
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
    global $CFG;

    $siteroot = str_replace('\\', '/', strrev($CFG->dirroot.'/')); // win32 backslash workaround

    $rp = preg_replace('|https?://[^/]+|i', '', $CFG->wwwroot, 1);
    $rp = strrev(trim($rp, '/'));
    $rp = explode('/', $rp);
    foreach($rp as $r) {
        if (strpos($siteroot, '/'.$r.'/') === 0) {
            $siteroot = substr($siteroot, strlen($r)+1); // moodle web in subdirectory
        } else {
            break; // probably alias root
        }
    }

    $siteroot = strrev($siteroot);
    $dataroot = str_replace('\\', '/', $CFG->dataroot.'/');

540
541
542
543
544
545
546
547
548
549
550
551
552
553
    if (strpos($dataroot, $siteroot) !== 0) {
        return false;
    }

    if (!$fetchtest) {
        return INSECURE_DATAROOT_WARNING;
    }

    // now try all methods to fetch a test file using http protocol

    $httpdocroot = str_replace('\\', '/', strrev($CFG->dirroot.'/'));
    preg_match('|(https?://[^/]+)|i', $CFG->wwwroot, $matches);
    $httpdocroot = $matches[1];
    $datarooturl = $httpdocroot.'/'. substr($dataroot, strlen($siteroot));
554
    make_upload_directory('diag');
555
556
557
    $testfile = $CFG->dataroot.'/diag/public.txt';
    if (!file_exists($testfile)) {
        file_put_contents($testfile, 'test file, do not delete');
558
        @chmod($testfile, $CFG->filepermissions);
559
560
561
    }
    $teststr = trim(file_get_contents($testfile));
    if (empty($teststr)) {
562
    // hmm, strange
563
564
565
566
        return INSECURE_DATAROOT_WARNING;
    }

    $testurl = $datarooturl.'/diag/public.txt';
567
568
569
570
    if (extension_loaded('curl') and
        !(stripos(ini_get('disable_functions'), 'curl_init') !== FALSE) and
        !(stripos(ini_get('disable_functions'), 'curl_setop') !== FALSE) and
        ($ch = @curl_init($testurl)) !== false) {
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, false);
        $data = curl_exec($ch);
        if (!curl_errno($ch)) {
            $data = trim($data);
            if ($data === $teststr) {
                curl_close($ch);
                return INSECURE_DATAROOT_ERROR;
            }
        }
        curl_close($ch);
    }

    if ($data = @file_get_contents($testurl)) {
        $data = trim($data);
        if ($data === $teststr) {
            return INSECURE_DATAROOT_ERROR;
        }
    }

    preg_match('|https?://([^/]+)|i', $testurl, $matches);
    $sitename = $matches[1];
    $error = 0;
    if ($fp = @fsockopen($sitename, 80, $error)) {
        preg_match('|https?://[^/]+(.*)|i', $testurl, $matches);
        $localurl = $matches[1];
        $out = "GET $localurl HTTP/1.1\r\n";
        $out .= "Host: $sitename\r\n";
        $out .= "Connection: Close\r\n\r\n";
        fwrite($fp, $out);
        $data = '';
        $incoming = false;
        while (!feof($fp)) {
            if ($incoming) {
                $data .= fgets($fp, 1024);
            } else if (@fgets($fp, 1024) === "\r\n") {
607
608
                    $incoming = true;
                }
609
610
611
612
613
614
        }
        fclose($fp);
        $data = trim($data);
        if ($data === $teststr) {
            return INSECURE_DATAROOT_ERROR;
        }
615
    }
616
617

    return INSECURE_DATAROOT_WARNING;
618
}
619

620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
/**
 * Enables CLI maintenance mode by creating new dataroot/climaintenance.html file.
 */
function enable_cli_maintenance_mode() {
    global $CFG;

    if (file_exists("$CFG->dataroot/climaintenance.html")) {
        unlink("$CFG->dataroot/climaintenance.html");
    }

    if (isset($CFG->maintenance_message) and !html_is_blank($CFG->maintenance_message)) {
        $data = $CFG->maintenance_message;
        $data = bootstrap_renderer::early_error_content($data, null, null, null);
        $data = bootstrap_renderer::plain_page(get_string('sitemaintenance', 'admin'), $data);

    } else if (file_exists("$CFG->dataroot/climaintenance.template.html")) {
        $data = file_get_contents("$CFG->dataroot/climaintenance.template.html");

    } else {
        $data = get_string('sitemaintenance', 'admin');
        $data = bootstrap_renderer::early_error_content($data, null, null, null);
        $data = bootstrap_renderer::plain_page(get_string('sitemaintenance', 'admin'), $data);
    }

    file_put_contents("$CFG->dataroot/climaintenance.html", $data);
    chmod("$CFG->dataroot/climaintenance.html", $CFG->filepermissions);
}

648
649
/// CLASS DEFINITIONS /////////////////////////////////////////////////////////

650

651
/**
Petr Skoda's avatar
Petr Skoda committed
652
 * Interface for anything appearing in the admin tree
653
 *
Petr Skoda's avatar
Petr Skoda committed
654
 * The interface that is implemented by anything that appears in the admin tree
655
656
657
 * block. It forces inheriting classes to define a method for checking user permissions
 * and methods for finding something in the admin tree.
 *
658
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
659
 */
660
interface part_of_admin_tree {
661

662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
/**
 * Finds a named part_of_admin_tree.
 *
 * Used to find a part_of_admin_tree. If a class only inherits part_of_admin_tree
 * and not parentable_part_of_admin_tree, then this function should only check if
 * $this->name matches $name. If it does, it should return a reference to $this,
 * otherwise, it should return a reference to NULL.
 *
 * If a class inherits parentable_part_of_admin_tree, this method should be called
 * recursively on all child objects (assuming, of course, the parent object's name
 * doesn't match the search criterion).
 *
 * @param string $name The internal name of the part_of_admin_tree we're searching for.
 * @return mixed An object reference or a NULL reference.
 */
677
    public function locate($name);
678
679
680
681
682

    /**
     * Removes named part_of_admin_tree.
     *
     * @param string $name The internal name of the part_of_admin_tree we want to remove.
683
     * @return bool success.
684
     */
685
    public function prune($name);
686

687
688
    /**
     * Search using query
689
     * @param string $query
690
691
     * @return mixed array-object structure of found settings and pages
     */
692
    public function search($query);
693

694
695
696
697
698
699
700
701
702
703
704
705
    /**
     * Verifies current user's access to this part_of_admin_tree.
     *
     * Used to check if the current user has access to this part of the admin tree or
     * not. If a class only inherits part_of_admin_tree and not parentable_part_of_admin_tree,
     * then this method is usually just a call to has_capability() in the site context.
     *
     * If a class inherits parentable_part_of_admin_tree, this method should return the
     * logical OR of the return of check_access() on all child objects.
     *
     * @return bool True if the user has access, false if she doesn't.
     */
706
    public function check_access();
707

708
    /**
Petr Skoda's avatar
Petr Skoda committed
709
     * Mostly useful for removing of some parts of the tree in admin tree block.
710
711
712
     *
     * @return True is hidden from normal list view
     */
713
    public function is_hidden();
714
715
716
717
718
719

    /**
     * Show we display Save button at the page bottom?
     * @return bool
     */
    public function show_save();
720
721
}

722

723
/**
Petr Skoda's avatar
Petr Skoda committed
724
 * Interface implemented by any part_of_admin_tree that has children.
725
 *
Petr Skoda's avatar
Petr Skoda committed
726
 * The interface implemented by any part_of_admin_tree that can be a parent
727
 * to other part_of_admin_tree's. (For now, this only includes admin_category.) Apart
728
 * from ensuring part_of_admin_tree compliancy, it also ensures inheriting methods
729
730
 * include an add method for adding other part_of_admin_tree objects as children.
 *
731
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
732
 */
733
interface parentable_part_of_admin_tree extends part_of_admin_tree {
734

735
736
737
738
739
740
741
742
/**
 * Adds a part_of_admin_tree object to the admin tree.
 *
 * Used to add a part_of_admin_tree object to this object or a child of this
 * object. $something should only be added if $destinationname matches
 * $this->name. If it doesn't, add should be called on child objects that are
 * also parentable_part_of_admin_tree's.
 *
743
744
745
746
747
 * $something should be appended as the last child in the $destinationname. If the
 * $beforesibling is specified, $something should be prepended to it. If the given
 * sibling is not found, $something should be appended to the end of $destinationname
 * and a developer debugging message should be displayed.
 *
748
749
750
751
 * @param string $destinationname The internal name of the new parent for $something.
 * @param part_of_admin_tree $something The object to be added.
 * @return bool True on success, false on failure.
 */
752
    public function add($destinationname, $something, $beforesibling = null);
753

754
755
}

756

757
758
/**
 * The object used to represent folders (a.k.a. categories) in the admin tree block.
759
 *
760
761
 * Each admin_category object contains a number of part_of_admin_tree objects.
 *
762
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
763
 */
764
class admin_category implements parentable_part_of_admin_tree {
765

766
767
    /** @var part_of_admin_tree[] An array of part_of_admin_tree objects that are this object's children */
    protected $children;
768
    /** @var string An internal name for this category. Must be unique amongst ALL part_of_admin_tree objects */
769
    public $name;
770
    /** @var string The displayed name for this category. Usually obtained through get_string() */
771
    public $visiblename;
772
    /** @var bool Should this category be hidden in admin tree block? */
773
    public $hidden;
774
    /** @var mixed Either a string or an array or strings */
775
    public $path;
776
    /** @var mixed Either a string or an array or strings */
777
    public $visiblepath;
778

779
780
781
    /** @var array fast lookup category cache, all categories of one tree point to one cache */
    protected $category_cache;

782
783
784
785
786
787
788
789
790
    /** @var bool If set to true children will be sorted when calling {@link admin_category::get_children()} */
    protected $sort = false;
    /** @var bool If set to true children will be sorted in ascending order. */
    protected $sortasc = true;
    /** @var bool If set to true sub categories and pages will be split and then sorted.. */
    protected $sortsplit = true;
    /** @var bool $sorted True if the children have been sorted and don't need resorting */
    protected $sorted = false;

791
792
793
794
795
    /**
     * Constructor for an empty admin category
     *
     * @param string $name The internal name for this category. Must be unique amongst ALL part_of_admin_tree objects
     * @param string $visiblename The displayed named for this category. Usually obtained through get_string()
796
     * @param bool $hidden hide category in admin tree block, defaults to false
797
     */
798
    public function __construct($name, $visiblename, $hidden=false) {
799
800
        $this->children    = array();
        $this->name        = $name;
801
        $this->visiblename = $visiblename;
802
        $this->hidden      = $hidden;
803
    }
804

805
    /**
806
     * Returns a reference to the part_of_admin_tree object with internal name $name.
807
     *
808
809
     * @param string $name The internal name of the object we want.
     * @param bool $findpath initialize path and visiblepath arrays
810
     * @return mixed A reference to the object with internal name $name if found, otherwise a reference to NULL.
811
     *                  defaults to false
812
     */
813
    public function locate($name, $findpath=false) {
814
        if (!isset($this->category_cache[$this->name])) {
815
816
817
818
            // somebody much have purged the cache
            $this->category_cache[$this->name] = $this;
        }

819
        if ($this->name == $name) {
820
821
822
823
824
            if ($findpath) {
                $this->visiblepath[] = $this->visiblename;
                $this->path[]        = $this->name;
            }
            return $this;
825
        }
826

827
        // quick category lookup
828
        if (!$findpath and isset($this->category_cache[$name])) {
829
830
831
            return $this->category_cache[$name];
        }

832
833
        $return = NULL;
        foreach($this->children as $childid=>$unused) {
834
            if ($return = $this->children[$childid]->locate($name, $findpath)) {
835
                break;
836
837
            }
        }
838

839
840
841
842
        if (!is_null($return) and $findpath) {
            $return->visiblepath[] = $this->visiblename;
            $return->path[]        = $this->name;
        }
843

844
        return $return;
845
846
847
    }

    /**
848
     * Search using query
849
850
     *
     * @param string query
851
     * @return mixed array-object structure of found settings and pages
852
     */
853
    public function search($query) {
854
        $result = array();
855
        foreach ($this->get_children() as $child) {
856
857
858
859
860
861
            $subsearch = $child->search($query);
            if (!is_array($subsearch)) {
                debugging('Incorrect search result from '.$child->name);
                continue;
            }
            $result = array_merge($result, $subsearch);
862
        }
863
        return $result;
864
865
    }

866
867
868
869
    /**
     * Removes part_of_admin_tree object with internal name $name.
     *
     * @param string $name The internal name of the object we want to remove.
870
     * @return bool success
871
     */
872
    public function prune($name) {
873
874
875
876
877
878
879

        if ($this->name == $name) {
            return false;  //can not remove itself
        }

        foreach($this->children as $precedence => $child) {
            if ($child->name == $name) {
880
                // clear cache and delete self
881
882
883
                while($this->category_cache) {
                    // delete the cache, but keep the original array address
                    array_pop($this->category_cache);
884
                }
885
                unset($this->children[$precedence]);
886
                return true;
887
            } else if ($this->children[$precedence]->prune($name)) {
888
889
890
891
892
893
                return true;
            }
        }
        return false;
    }

894
895
896
    /**
     * Adds a part_of_admin_tree to a child or grandchild (or great-grandchild, and so forth) of this object.
     *
897
898
899
900
901
902
     * By default the new part of the tree is appended as the last child of the parent. You
     * can specify a sibling node that the new part should be prepended to. If the given
     * sibling is not found, the part is appended to the end (as it would be by default) and
     * a developer debugging message is displayed.
     *
     * @throws coding_exception if the $beforesibling is empty string or is not string at all.
903
     * @param string $destinationame The internal name of the immediate parent that we want for $something.
Petr Skoda's avatar
Petr Skoda committed
904
     * @param mixed $something A part_of_admin_tree or setting instance to be added.
905
     * @param string $beforesibling The name of the parent's child the $something should be prepended to.
906
     * @return bool True if successfully added, false if $something can not be added.
907
     */
908
    public function add($parentname, $something, $beforesibling = null) {
909
910
        global $CFG;

911
        $parent = $this->locate($parentname);
912
913
        if (is_null($parent)) {
            debugging('parent does not exist!');
914
915
916
            return false;
        }

917
918
        if ($something instanceof part_of_admin_tree) {
            if (!($parent instanceof parentable_part_of_admin_tree)) {
919
920
                debugging('error - parts of tree can be inserted only into parentable parts');
                return false;
921
            }
922
            if ($CFG->debugdeveloper && !is_null($this->locate($something->name))) {
923
924
925
926
                // The name of the node is already used, simply warn the developer that this should not happen.
                // It is intentional to check for the debug level before performing the check.
                debugging('Duplicate admin page name: ' . $something->name, DEBUG_DEVELOPER);
            }
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
            if (is_null($beforesibling)) {
                // Append $something as the parent's last child.
                $parent->children[] = $something;
            } else {
                if (!is_string($beforesibling) or trim($beforesibling) === '') {
                    throw new coding_exception('Unexpected value of the beforesibling parameter');
                }
                // Try to find the position of the sibling.
                $siblingposition = null;
                foreach ($parent->children as $childposition => $child) {
                    if ($child->name === $beforesibling) {
                        $siblingposition = $childposition;
                        break;
                    }
                }
                if (is_null($siblingposition)) {
                    debugging('Sibling '.$beforesibling.' not found', DEBUG_DEVELOPER);
                    $parent->children[] = $something;
                } else {
                    $parent->children = array_merge(
                        array_slice($parent->children, 0, $siblingposition),
                        array($something),
                        array_slice($parent->children, $siblingposition)
                    );
                }
            }
953
            if ($something instanceof admin_category) {
954
                if (isset($this->category_cache[$something->name])) {
955
                    debugging('Duplicate admin category name: '.$something->name);
956
957
958
959
960
961
962
                } else {
                    $this->category_cache[$something->name] = $something;
                    $something->category_cache =& $this->category_cache;
                    foreach ($something->children as $child) {
                        // just in case somebody already added subcategories
                        if ($child instanceof admin_category) {
                            if (isset($this->category_cache[$child->name])) {
963
                                debugging('Duplicate admin category name: '.$child->name);
964
965
966
967
968
969
970
971
                            } else {
                                $this->category_cache[$child->name] = $child;
                                $child->category_cache =& $this->category_cache;
                            }
                        }
                    }
                }
            }
972
            return true;
973

974
975
976
        } else {
            debugging('error - can not add this element');
            return false;
977
        }
978

979
    }
980

981
982
983
    /**
     * Checks if the user has access to anything in this category.
     *
Petr Skoda's avatar
Petr Skoda committed
984
     * @return bool True if the user has access to at least one child in this category, false otherwise.
985
     */
986
    public function check_access() {
987
        foreach ($this->children as $child) {
988
989
990
            if ($child->check_access()) {
                return true;
            }
991
        }
992
        return false;
993
    }
994

995
996
997
998
999
    /**
     * Is this category hidden in admin tree block?
     *
     * @return bool True if hidden
     */
1000
    public function is_hidden() {
For faster browsing, not all history is shown. View entire blame