weekly release 4.0dev
[moodle.git] / admin / tool / health / index.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Strings for component 'tool_health', language 'en', branch 'MOODLE_22_STABLE'
19  *
20  * @package    tool
21  * @subpackage health
22  * @copyright  1999 onwards Martin Dougiamas (http://dougiamas.com)
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26     ob_start(); //for whitespace test
27     require('../../../config.php');
28     $extraws = ob_get_clean();
30     require_once($CFG->libdir.'/adminlib.php');
31     require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/health/locallib.php');
33     admin_externalpage_setup('toolhealth');
35     define('SEVERITY_NOTICE',      'notice');
36     define('SEVERITY_ANNOYANCE',   'annoyance');
37     define('SEVERITY_SIGNIFICANT', 'significant');
38     define('SEVERITY_CRITICAL',    'critical');
40     $solution = optional_param('solution', 0, PARAM_PLUGIN);
42     $site = get_site();
44     echo $OUTPUT->header();
46     if(strpos($solution, 'problem_') === 0 && class_exists($solution)) {
47         health_print_solution($solution);
48     }
49     else {
50         health_find_problems();
51     }
54     echo $OUTPUT->footer();
57 function health_find_problems() {
58     global $OUTPUT;
60     echo $OUTPUT->heading(get_string('pluginname', 'tool_health'));
62     $issues   = array(
63         SEVERITY_CRITICAL    => array(),
64         SEVERITY_SIGNIFICANT => array(),
65         SEVERITY_ANNOYANCE   => array(),
66         SEVERITY_NOTICE      => array(),
67     );
68     $problems = 0;
70     for($i = 1; $i < 1000000; ++$i) {
71         $classname = sprintf('problem_%06d', $i);
72         if(!class_exists($classname)) {
73             continue;
74         }
75         $problem = new $classname;
77         if($problem->exists()) {
78             $severity = $problem->severity();
79             $issues[$severity][$classname] = array(
80                 'severity'    => $severity,
81                 'description' => $problem->description(),
82                 'title'       => $problem->title()
83             );
84             ++$problems;
85         }
86         unset($problem);
87     }
89     if($problems == 0) {
90         echo '<div id="healthnoproblemsfound">';
91         echo get_string('healthnoproblemsfound', 'tool_health');
92         echo '</div>';
93     }
94     else {
95         echo $OUTPUT->heading(get_string('healthproblemsdetected', 'tool_health'));
96         $severities = array(SEVERITY_CRITICAL, SEVERITY_SIGNIFICANT, SEVERITY_ANNOYANCE, SEVERITY_NOTICE);
97         foreach($severities as $severity) {
98             if(!empty($issues[$severity])) {
99                 echo '<dl class="healthissues '.$severity.'">';
100                 foreach($issues[$severity] as $classname => $data) {
101                     echo '<dt id="'.$classname.'">'.$data['title'].'</dt>';
102                     echo '<dd>'.$data['description'];
103                     echo '<form action="index.php#solution" method="get">';
104                     echo '<input type="hidden" name="solution" value="'.$classname.'" /><input type="submit" value="'.get_string('viewsolution').'" />';
105                     echo '</form></dd>';
106                 }
107                 echo '</dl>';
108             }
109         }
110     }
113 function health_print_solution($classname) {
114     global $OUTPUT;
115     $problem = new $classname;
116     $data = array(
117         'title'       => $problem->title(),
118         'severity'    => $problem->severity(),
119         'description' => $problem->description(),
120         'solution'    => $problem->solution()
121     );
123     echo $OUTPUT->heading(get_string('pluginname', 'tool_health'));
124     echo $OUTPUT->heading(get_string('healthproblemsolution', 'tool_health'));
125     echo '<dl class="healthissues '.$data['severity'].'">';
126     echo '<dt>'.$data['title'].'</dt>';
127     echo '<dd>'.$data['description'].'</dd>';
128     echo '<dt id="solution" class="solution">'.get_string('healthsolution', 'tool_health').'</dt>';
129     echo '<dd class="solution">'.$data['solution'].'</dd></dl>';
130     echo '<form id="healthformreturn" action="index.php#'.$classname.'" method="get">';
131     echo '<input type="submit" value="'.get_string('healthreturntomain', 'tool_health').'" />';
132     echo '</form>';
135 class problem_base {
136     function exists() {
137         return false;
138     }
139     function title() {
140         return '???';
141     }
142     function severity() {
143         return SEVERITY_NOTICE;
144     }
145     function description() {
146         return '';
147     }
148     function solution() {
149         return '';
150     }
153 class problem_000002 extends problem_base {
154     function title() {
155         return 'Extra characters at the end of config.php or other library function';
156     }
157     function exists() {
158         global $extraws;
160         if($extraws === '') {
161             return false;
162         }
163         return true;
164     }
165     function severity() {
166         return SEVERITY_SIGNIFICANT;
167     }
168     function description() {
169         return 'Your Moodle configuration file config.php or another library file, contains some characters after the closing PHP tag (?>). This causes Moodle to exhibit several kinds of problems (such as broken downloaded files) and must be fixed.';
170     }
171     function solution() {
172         global $CFG;
173         return 'You need to edit <strong>'.$CFG->dirroot.'/config.php</strong> and remove all characters (including spaces and returns) after the ending ?> tag. These two characters should be the very last in that file. The extra trailing whitespace may be also present in other PHP files that are included from lib/setup.php.';
174     }
177 class problem_000003 extends problem_base {
178     function title() {
179         return '$CFG->dataroot does not exist or does not have write permissions';
180     }
181     function exists() {
182         global $CFG;
183         if(!is_dir($CFG->dataroot) || !is_writable($CFG->dataroot)) {
184             return true;
185         }
186         return false;
187     }
188     function severity() {
189         return SEVERITY_SIGNIFICANT;
190     }
191     function description() {
192         global $CFG;
193         return 'Your <strong>config.php</strong> says that your "data root" directory is <strong>'.$CFG->dataroot.'</strong>. However, this directory either does not exist or cannot be written to by Moodle. This means that a variety of problems will be present, such as users not being able to log in and not being able to upload any files. It is imperative that you address this problem for Moodle to work correctly.';
194     }
195     function solution() {
196         global $CFG;
197         return 'First of all, make sure that the directory <strong>'.$CFG->dataroot.'</strong> exists. If the directory does exist, then you must make sure that Moodle is able to write to it. Contact your web server administrator and request that he gives write permissions for that directory to the user that the web server process is running as.';
198     }
201 class problem_000004 extends problem_base {
202     function title() {
203         return 'cron.php is not set up to run automatically';
204     }
205     function exists() {
206         global $DB;
207         $lastcron = $DB->get_field_sql('SELECT max(lastcron) FROM {modules}');
208         return (time() - $lastcron > 3600 * 24);
209     }
210     function severity() {
211         return SEVERITY_SIGNIFICANT;
212     }
213     function description() {
214         return 'The cron.php mainenance script has not been run in the past 24 hours. This probably means that your server is not configured to automatically run this script in regular time intervals. If this is the case, then Moodle will mostly work as it should but some operations (notably sending email to users) will not be carried out at all.';
215     }
216     function solution() {
217         global $CFG;
218         return 'For detailed instructions on how to enable cron, see <a href="'.$CFG->wwwroot.'/doc/?file=install.html#cron">this section</a> of the installation manual.';
219     }
222 class problem_000005 extends problem_base {
223     function title() {
224         return 'PHP: session.auto_start is enabled';
225     }
226     function exists() {
227         return ini_get_bool('session.auto_start');
228     }
229     function severity() {
230         return SEVERITY_CRITICAL;
231     }
232     function description() {
233         return 'Your PHP configuration includes an enabled setting, session.auto_start, that <strong>must be disabled</strong> in order for Moodle to work correctly. Notable symptoms arising from this misconfiguration include fatal errors and/or blank pages when trying to log in.';
234     }
235     function solution() {
236         global $CFG;
237         return '<p>There are two ways you can solve this problem:</p><ol><li>If you have access to your main <strong>php.ini</strong> file, then find the line that looks like this: <pre>session.auto_start = 1</pre> and change it to <pre>session.auto_start = 0</pre> and then restart your web server. Be warned that this, as any other PHP setting change, might affect other web applications running on the server.</li><li>Finally, you may be able to change this setting just for your site by creating or editing the file <strong>'.$CFG->dirroot.'/.htaccess</strong> to contain this line: <pre>php_value session.auto_start "0"</pre></li></ol>';
238     }
241 class problem_000007 extends problem_base {
242     function title() {
243         return 'PHP: file_uploads is disabled';
244     }
245     function exists() {
246         return !ini_get_bool('file_uploads');
247     }
248     function severity() {
249         return SEVERITY_SIGNIFICANT;
250     }
251     function description() {
252         return 'Your PHP configuration includes a disabled setting, file_uploads, that <strong>must be enabled</strong> to let Moodle offer its full functionality. Until this setting is enabled, it will not be possible to upload any files into Moodle. This includes, for example, course content and user pictures.';
253     }
254     function solution() {
255         global $CFG;
256         return '<p>There are two ways you can solve this problem:</p><ol><li>If you have access to your main <strong>php.ini</strong> file, then find the line that looks like this: <pre>file_uploads = Off</pre> and change it to <pre>file_uploads = On</pre> and then restart your web server. Be warned that this, as any other PHP setting change, might affect other web applications running on the server.</li><li>Finally, you may be able to change this setting just for your site by creating or editing the file <strong>'.$CFG->dirroot.'/.htaccess</strong> to contain this line: <pre>php_value file_uploads "On"</pre></li></ol>';
257     }
260 class problem_000008 extends problem_base {
261     function title() {
262         return 'PHP: memory_limit cannot be controlled by Moodle';
263     }
264     function exists() {
265         global $CFG;
267         $oldmemlimit = @ini_get('memory_limit');
268         if (empty($oldmemlimit)) {
269             // PHP not compiled with memory limits, this means that it's
270             // probably limited to 8M or in case of Windows not at all.
271             // We can ignore it for now - there is not much to test anyway
272             // TODO: add manual test that fills memory??
273             return false;
274         }
275         $oldmemlimit = get_real_size($oldmemlimit);
276         //now lets change the memory limit to something higher
277         $newmemlimit = ($oldmemlimit + 1024*1024*5);
278         raise_memory_limit($newmemlimit);
279         $testmemlimit = get_real_size(@ini_get('memory_limit'));
280         //verify the change had any effect at all
281         if ($oldmemlimit == $testmemlimit) {
282             //memory limit can not be changed - is it big enough then?
283             if ($oldmemlimit < get_real_size('128M')) {
284                 return true;
285             } else {
286                 return false;
287             }
288         }
289         reduce_memory_limit($oldmemlimit);
290         return false;
291     }
292     function severity() {
293         return SEVERITY_NOTICE;
294     }
295     function description() {
296         return 'The settings for PHP on your server do not allow a script to request more memory during its execution. '.
297                'This means that there is a hard limit of '.@ini_get('memory_limit').' for each script. '.
298                'It is possible that certain operations within Moodle will require more than this amount in order '.
299                'to complete successfully, especially if there are lots of data to be processed.';
300     }
301     function solution() {
302         return 'It is recommended that you contact your web server administrator to address this issue.';
303     }
306 class problem_000009 extends problem_base {
307     function title() {
308         return 'SQL: using account without password';
309     }
310     function exists() {
311         global $CFG;
312         return empty($CFG->dbpass);
313     }
314     function severity() {
315         return SEVERITY_CRITICAL;
316     }
317     function description() {
318         global $CFG;
319         return 'The user account your are connecting to the database server with is set up without a password. This is a very big security risk and is only somewhat lessened if your database is configured to not accept connections from any hosts other than the server Moodle is running on. Unless you use a strong password to connect to the database, you risk unauthorized access to and manipulation of your data.'.($CFG->dbuser != 'root'?'':' <strong>This is especially alarming because such access to the database would be as the superuser (root)!</strong>');
320     }
321     function solution() {
322         global $CFG;
323         return 'You should change the password of the user <strong>'.$CFG->dbuser.'</strong> both in your database and in your Moodle <strong>config.php</strong> immediately!'.($CFG->dbuser != 'root'?'':' It would also be a good idea to change the user account from root to something else, because this would lessen the impact in the event that your database is compromised anyway.');
324     }
326 /* // not implemented in 2.0 yet
327 class problem_000010 extends problem_base {
328     function title() {
329         return 'Uploaded files: slasharguments disabled or not working';
330     }
331     function exists() {
332         if (!$this->is_enabled()) {
333             return true;
334         }
335         if ($this->status() < 1) {
336             return true;
337         }
338         return false;
339     }
340     function severity() {
341         if ($this->is_enabled() and $this->status() == 0) {
342             return SEVERITY_SIGNIFICANT;
343         } else {
344             return SEVERITY_ANNOYANCE;
345         }
346     }
347     function description() {
348         global $CFG;
349         $desc = 'Slasharguments are needed for relative linking in uploaded resources:<ul>';
350         if (!$this->is_enabled()) {
351             $desc .= '<li>slasharguments are <strong>disabled</strong> in Moodle configuration</li>';
352         } else {
353             $desc .= '<li>slasharguments are enabled in Moodle configuration</li>';
354         }
355         if ($this->status() == -1) {
356             $desc .= '<li>can not run automatic test, you can verify it <a href="'.$CFG->wwwroot.'/file.php/testslasharguments" target="_blank">here</a> manually</li>';
357         } else if ($this->status() == 0) {
358             $desc .= '<li>slashargument test <strong>failed</strong>, please check server configuration</li>';
359         } else {
360             $desc .= '<li>slashargument test passed</li>';
361         }
362         $desc .= '</ul>';
363         return $desc;
364     }
365     function solution() {
366         global $CFG;
367         $enabled = $this->is_enabled();
368         $status = $this->status();
369         $solution = '';
370         if ($enabled and ($status == 0)) {
371             $solution .= 'Slasharguments are enabled, but the test failed. Please disable slasharguments in Moodle configuration or fix the server configuration.<hr />';
372         } else if ((!$enabled) and ($status == 0)) {
373             $solution .= 'Slasharguments are disabled and the test failed. You may try to fix the server configuration.<hr />';
374         } else if ($enabled and ($status == -1)) {
375             $solution .= 'Slasharguments are enabled, <a href="'.$CFG->wwwroot.'/file.php/testslasharguments">automatic testing</a> not possible.<hr />';
376         } else if ((!$enabled) and ($status == -1)) {
377             $solution .= 'Slasharguments are disabled, <a href="'.$CFG->wwwroot.'/file.php/testslasharguments">automatic testing</a> not possible.<hr />';
378         } else if ((!$enabled) and ($status > 0)) {
379             $solution .= 'Slasharguments are disabled though the iternal test is OK. You should enable slasharguments in Moodle configuration.';
380         } else if ($enabled and ($status > 0)) {
381             $solution .= 'Congratulations - everything seems OK now :-D';
382         }
383         if ($status < 1) {
384             $solution .= '<p>IIS:<ul><li>try to add <code>cgi.fix_pathinfo=1</code> to php.ini</li><li>do NOT enable AllowPathInfoForScriptMappings !!!</li><li>slasharguments may not work when using ISAPI and PHP 4.3.10 and older</li></ul></p>';
385             $solution .= '<p>Apache 1:<ul><li>try to add <code>cgi.fix_pathinfo=1</code> to php.ini</li></ul></p>';
386             $solution .= '<p>Apache 2:<ul><li>you must add <code>AcceptPathInfo on</code> to php.ini or .htaccess</li><li>try to add <code>cgi.fix_pathinfo=1</code> to php.ini</li></ul></p>';
387         }
388         return $solution;
389     }
390     function is_enabled() {
391         global $CFG;
392         return !empty($CFG->slasharguments);
393     }
394     function status() {
395         global $CFG;
396         $handle = @fopen($CFG->wwwroot.'/file.php?file=/testslasharguments', "r");
397         $contents = @trim(fread($handle, 10));
398         @fclose($handle);
399         if ($contents != 'test -1') {
400             return -1;
401         }
402         $handle = @fopen($CFG->wwwroot.'/file.php/testslasharguments', "r");
403         $contents = trim(@fread($handle, 10));
404         @fclose($handle);
405         switch ($contents) {
406             case 'test 1': return 1;
407             case 'test 2': return 2;
408             default:  return 0;
409         }
410     }
411 }*/
413 class problem_000012 extends problem_base {
414     function title() {
415         return 'Random questions data consistency';
416     }
417     function exists() {
418         global $DB;
419         return $DB->record_exists_select('question', "qtype = 'random' AND parent <> id", array());
420     }
421     function severity() {
422         return SEVERITY_ANNOYANCE;
423     }
424     function description() {
425         return '<p>For random questions, question.parent should equal question.id. ' .
426         'There are some questions in your database for which this is not true. ' .
427         'One way that this could have happened is for random questions restored from backup before ' .
428         '<a href="http://tracker.moodle.org/browse/MDL-5482">MDL-5482</a> was fixed.</p>';
429     }
430     function solution() {
431         global $CFG;
432         return '<p>Upgrade to Moodle 1.9.1 or later, or manually execute the SQL</p>' .
433         '<pre>UPDATE ' . $CFG->prefix . 'question SET parent = id WHERE qtype = \'random\' and parent &lt;> id;</pre>';
434     }
437 class problem_000013 extends problem_base {
438     function title() {
439         return 'Multi-answer questions data consistency';
440     }
441     function exists() {
442         global $DB;
443         $positionexpr = $DB->sql_position($DB->sql_concat("','", "q.id", "','"),
444                 $DB->sql_concat("','", "qma.sequence", "','"));
445         return $DB->record_exists_sql("
446                 SELECT * FROM {question} q
447                     JOIN {question_multianswer} qma ON $positionexpr > 0
448                 WHERE qma.question <> q.parent") ||
449             $DB->record_exists_sql("
450                 SELECT * FROM {question} q
451                     JOIN {question} parent_q ON parent_q.id = q.parent
452                 WHERE q.category <> parent_q.category");
453     }
454     function severity() {
455         return SEVERITY_ANNOYANCE;
456     }
457     function description() {
458         return '<p>For each sub-question whose id is listed in ' .
459         'question_multianswer.sequence, its question.parent field should equal ' .
460         'question_multianswer.question; and each sub-question should be in the same ' .
461         'category as its parent. There are questions in your database for ' .
462         'which this is not the case. One way that this could have happened is ' .
463         'for multi-answer questions restored from backup before ' .
464         '<a href="http://tracker.moodle.org/browse/MDL-14750">MDL-14750</a> was fixed.</p>';
465     }
466     function solution() {
467         return '<p>Upgrade to Moodle 1.9.1 or later, or manually execute the ' .
468         'code in question_multianswer_fix_subquestion_parents_and_categories in ' .
469         '<a href="http://cvs.moodle.org/moodle/question/type/multianswer/db/upgrade.php?revision=1.1.10.2&amp;view=markup">/question/type/multianswer/db/upgrade.php' .
470         'from the 1.9 stable branch</a>.</p>';
471     }
474 class problem_000014 extends problem_base {
475     function title() {
476         return 'Only multianswer and random questions should be the parent of another question';
477     }
478     function exists() {
479         global $DB;
480         return $DB->record_exists_sql("
481                 SELECT * FROM {question} q
482                     JOIN {question} parent_q ON parent_q.id = q.parent
483                 WHERE parent_q.qtype NOT IN ('random', 'multianswer')");
484     }
485     function severity() {
486         return SEVERITY_ANNOYANCE;
487     }
488     function description() {
489         return '<p>You have questions that violate this in your databse. ' .
490         'You will need to investigate to determine how this happened.</p>';
491     }
492     function solution() {
493         return '<p>It is impossible to give a solution without knowing more about ' .
494         ' how the problem was caused. You may be able to get help from the ' .
495         '<a href="http://moodle.org/mod/forum/view.php?f=121">Quiz forum</a>.</p>';
496     }
499 class problem_000015 extends problem_base {
500     function title() {
501         return 'Question categories should belong to a valid context';
502     }
503     function exists() {
504         global $DB;
505         return $DB->record_exists_sql("
506             SELECT qc.*, (SELECT COUNT(1) FROM {question} q WHERE q.category = qc.id) AS numquestions
507             FROM {question_categories} qc
508                 LEFT JOIN {context} con ON qc.contextid = con.id
509             WHERE con.id IS NULL");
510     }
511     function severity() {
512         return SEVERITY_ANNOYANCE;
513     }
514     function description() {
515         global $DB;
516         $problemcategories = $DB->get_records_sql("
517             SELECT qc.id, qc.name, qc.contextid, (SELECT COUNT(1) FROM {question} q WHERE q.category = qc.id) AS numquestions
518             FROM {question_categories} qc
519                 LEFT JOIN {context} con ON qc.contextid = con.id
520             WHERE con.id IS NULL
521             ORDER BY numquestions DESC, qc.name");
522         $table = '<table><thead><tr><th>Cat id</th><th>Category name</th>' .
523         "<th>Context id</th><th>Num Questions</th></tr></thead><tbody>\n";
524         foreach ($problemcategories as $cat) {
525             $table .= "<tr><td>$cat->id</td><td>" . s($cat->name) . "</td><td>" .
526             $cat->contextid ."</td><td>$cat->numquestions</td></tr>\n";
527         }
528         $table .= '</tbody></table>';
529         return '<p>All question categories are linked to a context id, and, ' .
530         'the context they are linked to must exist. The following categories ' .
531         'belong to a non-existant category:</p>' . $table . '<p>Any of these ' .
532         'categories that contain no questions can just be deleted form the database. ' .
533         'Other categories will require more thought.</p>';
534     }
535     function solution() {
536         global $CFG;
537         return '<p>You can delete the empty categories by executing the following SQL:</p><pre>
538 DELETE FROM ' . $CFG->prefix . 'question_categories
539 WHERE
540     NOT EXISTS (SELECT * FROM ' . $CFG->prefix . 'question q WHERE q.category = ' . $CFG->prefix . 'question_categories.id)
541 AND NOT EXISTS (SELECT * FROM ' . $CFG->prefix . 'context con WHERE contextid = con.id)
542         </pre><p>Any remaining categories that contain questions will require more thought. ' .
543         'People in the <a href="http://moodle.org/mod/forum/view.php?f=121">Quiz forum</a> may be able to help.</p>';
544     }
547 class problem_000016 extends problem_base {
548     function title() {
549         return 'Question categories should belong to the same context as their parent';
550     }
551     function exists() {
552         global $DB;
553         return $DB->record_exists_sql("
554             SELECT parent_qc.id AS parent, child_qc.id AS child, child_qc.contextid
555             FROM {question_categories} child_qc
556                 JOIN {question_categories} parent_qc ON child_qc.parent = parent_qc.id
557             WHERE child_qc.contextid <> parent_qc.contextid");
558     }
559     function severity() {
560         return SEVERITY_ANNOYANCE;
561     }
562     function description() {
563         global $DB;
564         $problemcategories = $DB->get_records_sql("
565             SELECT
566                 parent_qc.id AS parentid, parent_qc.name AS parentname, parent_qc.contextid AS parentcon,
567                 child_qc.id AS childid, child_qc.name AS childname, child_qc.contextid AS childcon
568             FROM {question_categories} child_qc
569                 JOIN {question_categories} parent_qc ON child_qc.parent = parent_qc.id
570             WHERE child_qc.contextid <> parent_qc.contextid");
571         $table = '<table><thead><tr><th colspan="3">Child category</th><th colspan="3">Parent category</th></tr><tr>' .
572         '<th>Id</th><th>Name</th><th>Context id</th>' .
573         '<th>Id</th><th>Name</th><th>Context id</th>' .
574         "</tr></thead><tbody>\n";
575         foreach ($problemcategories as $cat) {
576             $table .= "<tr><td>$cat->childid</td><td>" . s($cat->childname) .
577             "</td><td>$cat->childcon</td><td>$cat->parentid</td><td>" . s($cat->parentname) .
578             "</td><td>$cat->parentcon</td></tr>\n";
579         }
580         $table .= '</tbody></table>';
581         return '<p>When one question category is the parent of another, then they ' .
582         'should both belong to the same context. This is not true for the following categories:</p>' .
583         $table;
584     }
585     function solution() {
586         return '<p>An automated solution is difficult. It depends whether the ' .
587         'parent or child category is in the wrong pace.' .
588         'People in the <a href="http://moodle.org/mod/forum/view.php?f=121">Quiz forum</a> may be able to help.</p>';
589     }
592 class problem_000017 extends problem_base {
593     function title() {
594         return 'Question categories tree structure';
595     }
596     function find_problems() {
597         global $DB;
598         static $answer = null;
600         if (is_null($answer)) {
601             $categories = $DB->get_records('question_categories', array(), 'id');
603             // Look for missing parents.
604             $missingparent = tool_health_category_find_missing_parents($categories);
606             // Look for loops.
607             $loops = tool_health_category_find_loops($categories);
609             $answer = array($missingparent, $loops);
610         }
612         return $answer;
613     }
614     function exists() {
615         list($missingparent, $loops) = $this->find_problems();
616         return !empty($missingparent) || !empty($loops);
617     }
618     function severity() {
619         return SEVERITY_ANNOYANCE;
620     }
621     function description() {
622         list($missingparent, $loops) = $this->find_problems();
624         $description = '<p>The question categories should be arranged into tree ' .
625                 ' structures by the question_categories.parent field. Sometimes ' .
626                 ' this tree structure gets messed up.</p>';
628         $description .= tool_health_category_list_missing_parents($missingparent);
629         $description .= tool_health_category_list_loops($loops);
631         return $description;
632     }
634     /**
635      * Outputs resolutions to problems outlined in MDL-34684 with items having themselves as parent
636      *
637      * @link https://tracker.moodle.org/browse/MDL-34684
638      * @return string Formatted html to be output to the browser with instructions and sql statements to run
639      */
640     public function solution() {
641         global $CFG;
642         list($missingparent, $loops) = $this->find_problems();
644         $solution = '<p>Consider executing the following SQL queries. These fix ' .
645                 'the problem by moving some categories to the top level.</p>';
647         if (!empty($missingparent)) {
648             $solution .= "<pre>UPDATE " . $CFG->prefix . "question_categories\n" .
649                     "        SET parent = 0\n" .
650                     "        WHERE id IN (" . implode(',', array_keys($missingparent)) . ");</pre>\n";
651         }
653         if (!empty($loops)) {
654             $solution .= "<pre>UPDATE " . $CFG->prefix . "question_categories\n" .
655                     "        SET parent = 0\n" .
656                     "        WHERE id IN (" . implode(',', array_keys($loops)) . ");</pre>\n";
657         }
659         return $solution;
660     }
663 /**
664  * Check course categories tree structure for problems.
665  *
666  * @copyright  2013 Marko Vidberg
667  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
668  */
669 class problem_000018 extends problem_base {
670     /**
671      * Generate title for this problem.
672      *
673      * @return string Title of problem.
674      */
675     public function title() {
676         return 'Course categories tree structure';
677     }
679     /**
680      * Search for problems in the course categories.
681      *
682      * @uses $DB
683      * @return array List of categories that contain missing parents or loops.
684      */
685     public function find_problems() {
686         global $DB;
687         static $answer = null;
689         if (is_null($answer)) {
690             $categories = $DB->get_records('course_categories', array(), 'id');
692             // Look for missing parents.
693             $missingparent = tool_health_category_find_missing_parents($categories);
695             // Look for loops.
696             $loops = tool_health_category_find_loops($categories);
698             $answer = array($missingparent, $loops);
699         }
701         return $answer;
702     }
704     /**
705      * Check if the problem exists.
706      *
707      * @return boolean True if either missing parents or loops found
708      */
709     public function exists() {
710         list($missingparent, $loops) = $this->find_problems();
711         return !empty($missingparent) || !empty($loops);
712     }
714     /**
715      * Set problem severity.
716      *
717      * @return constant Problem severity.
718      */
719     public function severity() {
720         return SEVERITY_SIGNIFICANT;
721     }
723     /**
724      * Generate problem description.
725      *
726      * @return string HTML containing details of the problem.
727      */
728     public function description() {
729         list($missingparent, $loops) = $this->find_problems();
731         $description = '<p>The course categories should be arranged into tree ' .
732                 ' structures by the course_categories.parent field. Sometimes ' .
733                 ' this tree structure gets messed up.</p>';
735         $description .= tool_health_category_list_missing_parents($missingparent);
736         $description .= tool_health_category_list_loops($loops);
738         return $description;
739     }
741     /**
742      * Generate solution text.
743      *
744      * @uses $CFG
745      * @return string HTML containing the suggested solution.
746      */
747     public function solution() {
748         global $CFG;
749         list($missingparent, $loops) = $this->find_problems();
751         $solution = '<p>Consider executing the following SQL queries. These fix ' .
752                 'the problem by moving some categories to the top level.</p>';
754         if (!empty($missingparent)) {
755             $solution .= "<pre>UPDATE " . $CFG->prefix . "course_categories\n" .
756                     "        SET parent = 0, depth = 1, path = CONCAT('/', id)\n" .
757                     "        WHERE id IN (" . implode(',', array_keys($missingparent)) . ");</pre>\n";
758         }
760         if (!empty($loops)) {
761             $solution .= "<pre>UPDATE " . $CFG->prefix . "course_categories\n" .
762                     "        SET parent = 0, depth = 1, path = CONCAT('/', id)\n" .
763                     "        WHERE id IN (" . implode(',', array_keys($loops)) . ");</pre>\n";
764         }
766         return $solution;
767     }
770 class problem_00000x extends problem_base {
771     function title() {
772         return '';
773     }
774     function exists() {
775         return false;
776     }
777     function severity() {
778         return SEVERITY_SIGNIFICANT;
779     }
780     function description() {
781         return '';
782     }
783     function solution() {
784         global $CFG;
785         return '';
786     }
789 /*
791 TODO:
793     session.save_path -- it doesn't really matter because we are already IN a session, right?
794     detect unsupported characters in $CFG->wwwroot - see bug Bug #6091 - relative vs absolute path during backup/restore process
796 */