Merge branch 'w01_MDL-43528_m27_magic' of https://github.com/skodak/moodle
[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');
32     admin_externalpage_setup('toolhealth');
34     define('SEVERITY_NOTICE',      'notice');
35     define('SEVERITY_ANNOYANCE',   'annoyance');
36     define('SEVERITY_SIGNIFICANT', 'significant');
37     define('SEVERITY_CRITICAL',    'critical');
39     $solution = optional_param('solution', 0, PARAM_PLUGIN);
41     require_login();
42     require_capability('moodle/site:config', context_system::instance());
44     $site = get_site();
46     echo $OUTPUT->header();
48     if(strpos($solution, 'problem_') === 0 && class_exists($solution)) {
49         health_print_solution($solution);
50     }
51     else {
52         health_find_problems();
53     }
56     echo $OUTPUT->footer();
59 function health_find_problems() {
60     global $OUTPUT;
62     echo $OUTPUT->heading(get_string('pluginname', 'tool_health'));
64     $issues   = array(
65         SEVERITY_CRITICAL    => array(),
66         SEVERITY_SIGNIFICANT => array(),
67         SEVERITY_ANNOYANCE   => array(),
68         SEVERITY_NOTICE      => array(),
69     );
70     $problems = 0;
72     for($i = 1; $i < 1000000; ++$i) {
73         $classname = sprintf('problem_%06d', $i);
74         if(!class_exists($classname)) {
75             continue;
76         }
77         $problem = new $classname;
79         if($problem->exists()) {
80             $severity = $problem->severity();
81             $issues[$severity][$classname] = array(
82                 'severity'    => $severity,
83                 'description' => $problem->description(),
84                 'title'       => $problem->title()
85             );
86             ++$problems;
87         }
88         unset($problem);
89     }
91     if($problems == 0) {
92         echo '<div id="healthnoproblemsfound">';
93         echo get_string('healthnoproblemsfound', 'tool_health');
94         echo '</div>';
95     }
96     else {
97         echo $OUTPUT->heading(get_string('healthproblemsdetected', 'tool_health'));
98         $severities = array(SEVERITY_CRITICAL, SEVERITY_SIGNIFICANT, SEVERITY_ANNOYANCE, SEVERITY_NOTICE);
99         foreach($severities as $severity) {
100             if(!empty($issues[$severity])) {
101                 echo '<dl class="healthissues '.$severity.'">';
102                 foreach($issues[$severity] as $classname => $data) {
103                     echo '<dt id="'.$classname.'">'.$data['title'].'</dt>';
104                     echo '<dd>'.$data['description'];
105                     echo '<form action="index.php#solution" method="get">';
106                     echo '<input type="hidden" name="solution" value="'.$classname.'" /><input type="submit" value="'.get_string('viewsolution').'" />';
107                     echo '</form></dd>';
108                 }
109                 echo '</dl>';
110             }
111         }
112     }
115 function health_print_solution($classname) {
116     global $OUTPUT;
117     $problem = new $classname;
118     $data = array(
119         'title'       => $problem->title(),
120         'severity'    => $problem->severity(),
121         'description' => $problem->description(),
122         'solution'    => $problem->solution()
123     );
125     echo $OUTPUT->heading(get_string('pluginname', 'tool_health'));
126     echo $OUTPUT->heading(get_string('healthproblemsolution', 'tool_health'));
127     echo '<dl class="healthissues '.$data['severity'].'">';
128     echo '<dt>'.$data['title'].'</dt>';
129     echo '<dd>'.$data['description'].'</dd>';
130     echo '<dt id="solution" class="solution">'.get_string('healthsolution', 'tool_health').'</dt>';
131     echo '<dd class="solution">'.$data['solution'].'</dd></dl>';
132     echo '<form id="healthformreturn" action="index.php#'.$classname.'" method="get">';
133     echo '<input type="submit" value="'.get_string('healthreturntomain', 'tool_health').'" />';
134     echo '</form>';
137 class problem_base {
138     function exists() {
139         return false;
140     }
141     function title() {
142         return '???';
143     }
144     function severity() {
145         return SEVERITY_NOTICE;
146     }
147     function description() {
148         return '';
149     }
150     function solution() {
151         return '';
152     }
155 class problem_000002 extends problem_base {
156     function title() {
157         return 'Extra characters at the end of config.php or other library function';
158     }
159     function exists() {
160         global $extraws;
162         if($extraws === '') {
163             return false;
164         }
165         return true;
166     }
167     function severity() {
168         return SEVERITY_SIGNIFICANT;
169     }
170     function description() {
171         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.';
172     }
173     function solution() {
174         global $CFG;
175         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.';
176     }
179 class problem_000003 extends problem_base {
180     function title() {
181         return '$CFG->dataroot does not exist or does not have write permissions';
182     }
183     function exists() {
184         global $CFG;
185         if(!is_dir($CFG->dataroot) || !is_writable($CFG->dataroot)) {
186             return true;
187         }
188         return false;
189     }
190     function severity() {
191         return SEVERITY_SIGNIFICANT;
192     }
193     function description() {
194         global $CFG;
195         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.';
196     }
197     function solution() {
198         global $CFG;
199         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.';
200     }
203 class problem_000004 extends problem_base {
204     function title() {
205         return 'cron.php is not set up to run automatically';
206     }
207     function exists() {
208         global $DB;
209         $lastcron = $DB->get_field_sql('SELECT max(lastcron) FROM {modules}');
210         return (time() - $lastcron > 3600 * 24);
211     }
212     function severity() {
213         return SEVERITY_SIGNIFICANT;
214     }
215     function description() {
216         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.';
217     }
218     function solution() {
219         global $CFG;
220         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.';
221     }
224 class problem_000005 extends problem_base {
225     function title() {
226         return 'PHP: session.auto_start is enabled';
227     }
228     function exists() {
229         return ini_get_bool('session.auto_start');
230     }
231     function severity() {
232         return SEVERITY_CRITICAL;
233     }
234     function description() {
235         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.';
236     }
237     function solution() {
238         global $CFG;
239         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>';
240     }
243 class problem_000007 extends problem_base {
244     function title() {
245         return 'PHP: file_uploads is disabled';
246     }
247     function exists() {
248         return !ini_get_bool('file_uploads');
249     }
250     function severity() {
251         return SEVERITY_SIGNIFICANT;
252     }
253     function description() {
254         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.';
255     }
256     function solution() {
257         global $CFG;
258         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>';
259     }
262 class problem_000008 extends problem_base {
263     function title() {
264         return 'PHP: memory_limit cannot be controlled by Moodle';
265     }
266     function exists() {
267         global $CFG;
269         $oldmemlimit = @ini_get('memory_limit');
270         if (empty($oldmemlimit)) {
271             // PHP not compiled with memory limits, this means that it's
272             // probably limited to 8M or in case of Windows not at all.
273             // We can ignore it for now - there is not much to test anyway
274             // TODO: add manual test that fills memory??
275             return false;
276         }
277         $oldmemlimit = get_real_size($oldmemlimit);
278         //now lets change the memory limit to something higher
279         $newmemlimit = ($oldmemlimit + 1024*1024*5);
280         raise_memory_limit($newmemlimit);
281         $testmemlimit = get_real_size(@ini_get('memory_limit'));
282         //verify the change had any effect at all
283         if ($oldmemlimit == $testmemlimit) {
284             //memory limit can not be changed - is it big enough then?
285             if ($oldmemlimit < get_real_size('128M')) {
286                 return true;
287             } else {
288                 return false;
289             }
290         }
291         reduce_memory_limit($oldmemlimit);
292         return false;
293     }
294     function severity() {
295         return SEVERITY_NOTICE;
296     }
297     function description() {
298         return 'The settings for PHP on your server do not allow a script to request more memory during its execution. '.
299                'This means that there is a hard limit of '.@ini_get('memory_limit').' for each script. '.
300                'It is possible that certain operations within Moodle will require more than this amount in order '.
301                'to complete successfully, especially if there are lots of data to be processed.';
302     }
303     function solution() {
304         return 'It is recommended that you contact your web server administrator to address this issue.';
305     }
308 class problem_000009 extends problem_base {
309     function title() {
310         return 'SQL: using account without password';
311     }
312     function exists() {
313         global $CFG;
314         return empty($CFG->dbpass);
315     }
316     function severity() {
317         return SEVERITY_CRITICAL;
318     }
319     function description() {
320         global $CFG;
321         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>');
322     }
323     function solution() {
324         global $CFG;
325         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.');
326     }
328 /* // not implemented in 2.0 yet
329 class problem_000010 extends problem_base {
330     function title() {
331         return 'Uploaded files: slasharguments disabled or not working';
332     }
333     function exists() {
334         if (!$this->is_enabled()) {
335             return true;
336         }
337         if ($this->status() < 1) {
338             return true;
339         }
340         return false;
341     }
342     function severity() {
343         if ($this->is_enabled() and $this->status() == 0) {
344             return SEVERITY_SIGNIFICANT;
345         } else {
346             return SEVERITY_ANNOYANCE;
347         }
348     }
349     function description() {
350         global $CFG;
351         $desc = 'Slasharguments are needed for relative linking in uploaded resources:<ul>';
352         if (!$this->is_enabled()) {
353             $desc .= '<li>slasharguments are <strong>disabled</strong> in Moodle configuration</li>';
354         } else {
355             $desc .= '<li>slasharguments are enabled in Moodle configuration</li>';
356         }
357         if ($this->status() == -1) {
358             $desc .= '<li>can not run automatic test, you can verify it <a href="'.$CFG->wwwroot.'/file.php/testslasharguments" target="_blank">here</a> manually</li>';
359         } else if ($this->status() == 0) {
360             $desc .= '<li>slashargument test <strong>failed</strong>, please check server configuration</li>';
361         } else {
362             $desc .= '<li>slashargument test passed</li>';
363         }
364         $desc .= '</ul>';
365         return $desc;
366     }
367     function solution() {
368         global $CFG;
369         $enabled = $this->is_enabled();
370         $status = $this->status();
371         $solution = '';
372         if ($enabled and ($status == 0)) {
373             $solution .= 'Slasharguments are enabled, but the test failed. Please disable slasharguments in Moodle configuration or fix the server configuration.<hr />';
374         } else if ((!$enabled) and ($status == 0)) {
375             $solution .= 'Slasharguments are disabled and the test failed. You may try to fix the server configuration.<hr />';
376         } else if ($enabled and ($status == -1)) {
377             $solution .= 'Slasharguments are enabled, <a href="'.$CFG->wwwroot.'/file.php/testslasharguments">automatic testing</a> not possible.<hr />';
378         } else if ((!$enabled) and ($status == -1)) {
379             $solution .= 'Slasharguments are disabled, <a href="'.$CFG->wwwroot.'/file.php/testslasharguments">automatic testing</a> not possible.<hr />';
380         } else if ((!$enabled) and ($status > 0)) {
381             $solution .= 'Slasharguments are disabled though the iternal test is OK. You should enable slasharguments in Moodle configuration.';
382         } else if ($enabled and ($status > 0)) {
383             $solution .= 'Congratulations - everything seems OK now :-D';
384         }
385         if ($status < 1) {
386             $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>';
387             $solution .= '<p>Apache 1:<ul><li>try to add <code>cgi.fix_pathinfo=1</code> to php.ini</li></ul></p>';
388             $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>';
389         }
390         return $solution;
391     }
392     function is_enabled() {
393         global $CFG;
394         return !empty($CFG->slasharguments);
395     }
396     function status() {
397         global $CFG;
398         $handle = @fopen($CFG->wwwroot.'/file.php?file=/testslasharguments', "r");
399         $contents = @trim(fread($handle, 10));
400         @fclose($handle);
401         if ($contents != 'test -1') {
402             return -1;
403         }
404         $handle = @fopen($CFG->wwwroot.'/file.php/testslasharguments', "r");
405         $contents = trim(@fread($handle, 10));
406         @fclose($handle);
407         switch ($contents) {
408             case 'test 1': return 1;
409             case 'test 2': return 2;
410             default:  return 0;
411         }
412     }
413 }*/
415 class problem_000012 extends problem_base {
416     function title() {
417         return 'Random questions data consistency';
418     }
419     function exists() {
420         global $DB;
421         return $DB->record_exists_select('question', "qtype = 'random' AND parent <> id", array());
422     }
423     function severity() {
424         return SEVERITY_ANNOYANCE;
425     }
426     function description() {
427         return '<p>For random questions, question.parent should equal question.id. ' .
428         'There are some questions in your database for which this is not true. ' .
429         'One way that this could have happened is for random questions restored from backup before ' .
430         '<a href="http://tracker.moodle.org/browse/MDL-5482">MDL-5482</a> was fixed.</p>';
431     }
432     function solution() {
433         global $CFG;
434         return '<p>Upgrade to Moodle 1.9.1 or later, or manually execute the SQL</p>' .
435         '<pre>UPDATE ' . $CFG->prefix . 'question SET parent = id WHERE qtype = \'random\' and parent &lt;> id;</pre>';
436     }
439 class problem_000013 extends problem_base {
440     function title() {
441         return 'Multi-answer questions data consistency';
442     }
443     function exists() {
444         global $DB;
445         $positionexpr = $DB->sql_position($DB->sql_concat("','", "q.id", "','"),
446                 $DB->sql_concat("','", "qma.sequence", "','"));
447         return $DB->record_exists_sql("
448                 SELECT * FROM {question} q
449                     JOIN {question_multianswer} qma ON $positionexpr > 0
450                 WHERE qma.question <> q.parent") ||
451             $DB->record_exists_sql("
452                 SELECT * FROM {question} q
453                     JOIN {question} parent_q ON parent_q.id = q.parent
454                 WHERE q.category <> parent_q.category");
455     }
456     function severity() {
457         return SEVERITY_ANNOYANCE;
458     }
459     function description() {
460         return '<p>For each sub-question whose id is listed in ' .
461         'question_multianswer.sequence, its question.parent field should equal ' .
462         'question_multianswer.question; and each sub-question should be in the same ' .
463         'category as its parent. There are questions in your database for ' .
464         'which this is not the case. One way that this could have happened is ' .
465         'for multi-answer questions restored from backup before ' .
466         '<a href="http://tracker.moodle.org/browse/MDL-14750">MDL-14750</a> was fixed.</p>';
467     }
468     function solution() {
469         return '<p>Upgrade to Moodle 1.9.1 or later, or manually execute the ' .
470         'code in question_multianswer_fix_subquestion_parents_and_categories in ' .
471         '<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' .
472         'from the 1.9 stable branch</a>.</p>';
473     }
476 class problem_000014 extends problem_base {
477     function title() {
478         return 'Only multianswer and random questions should be the parent of another question';
479     }
480     function exists() {
481         global $DB;
482         return $DB->record_exists_sql("
483                 SELECT * FROM {question} q
484                     JOIN {question} parent_q ON parent_q.id = q.parent
485                 WHERE parent_q.qtype NOT IN ('random', 'multianswer')");
486     }
487     function severity() {
488         return SEVERITY_ANNOYANCE;
489     }
490     function description() {
491         return '<p>You have questions that violate this in your databse. ' .
492         'You will need to investigate to determine how this happened.</p>';
493     }
494     function solution() {
495         return '<p>It is impossible to give a solution without knowing more about ' .
496         ' how the problem was caused. You may be able to get help from the ' .
497         '<a href="http://moodle.org/mod/forum/view.php?f=121">Quiz forum</a>.</p>';
498     }
501 class problem_000015 extends problem_base {
502     function title() {
503         return 'Question categories should belong to a valid context';
504     }
505     function exists() {
506         global $DB;
507         return $DB->record_exists_sql("
508             SELECT qc.*, (SELECT COUNT(1) FROM {question} q WHERE q.category = qc.id) AS numquestions
509             FROM {question_categories} qc
510                 LEFT JOIN {context} con ON qc.contextid = con.id
511             WHERE con.id IS NULL");
512     }
513     function severity() {
514         return SEVERITY_ANNOYANCE;
515     }
516     function description() {
517         global $DB;
518         $problemcategories = $DB->get_records_sql("
519             SELECT qc.id, qc.name, qc.contextid, (SELECT COUNT(1) FROM {question} q WHERE q.category = qc.id) AS numquestions
520             FROM {question_categories} qc
521                 LEFT JOIN {context} con ON qc.contextid = con.id
522             WHERE con.id IS NULL
523             ORDER BY numquestions DESC, qc.name");
524         $table = '<table><thead><tr><th>Cat id</th><th>Category name</th>' .
525         "<th>Context id</th><th>Num Questions</th></tr></thead><tbody>\n";
526         foreach ($problemcategories as $cat) {
527             $table .= "<tr><td>$cat->id</td><td>" . s($cat->name) . "</td><td>" .
528             $cat->contextid ."</td><td>$cat->numquestions</td></tr>\n";
529         }
530         $table .= '</tbody></table>';
531         return '<p>All question categories are linked to a context id, and, ' .
532         'the context they are linked to must exist. The following categories ' .
533         'belong to a non-existant category:</p>' . $table . '<p>Any of these ' .
534         'categories that contain no questions can just be deleted form the database. ' .
535         'Other categories will require more thought.</p>';
536     }
537     function solution() {
538         global $CFG;
539         return '<p>You can delete the empty categories by executing the following SQL:</p><pre>
540 DELETE FROM ' . $CFG->prefix . 'question_categories
541 WHERE
542     NOT EXISTS (SELECT * FROM ' . $CFG->prefix . 'question q WHERE q.category = ' . $CFG->prefix . 'question_categories.id)
543 AND NOT EXISTS (SELECT * FROM ' . $CFG->prefix . 'context con WHERE contextid = con.id)
544         </pre><p>Any remaining categories that contain questions will require more thought. ' .
545         'People in the <a href="http://moodle.org/mod/forum/view.php?f=121">Quiz forum</a> may be able to help.</p>';
546     }
549 class problem_000016 extends problem_base {
550     function title() {
551         return 'Question categories should belong to the same context as their parent';
552     }
553     function exists() {
554         global $DB;
555         return $DB->record_exists_sql("
556             SELECT parent_qc.id AS parent, child_qc.id AS child, child_qc.contextid
557             FROM {question_categories} child_qc
558                 JOIN {question_categories} parent_qc ON child_qc.parent = parent_qc.id
559             WHERE child_qc.contextid <> parent_qc.contextid");
560     }
561     function severity() {
562         return SEVERITY_ANNOYANCE;
563     }
564     function description() {
565         global $DB;
566         $problemcategories = $DB->get_records_sql("
567             SELECT
568                 parent_qc.id AS parentid, parent_qc.name AS parentname, parent_qc.contextid AS parentcon,
569                 child_qc.id AS childid, child_qc.name AS childname, child_qc.contextid AS childcon
570             FROM {question_categories} child_qc
571                 JOIN {question_categories} parent_qc ON child_qc.parent = parent_qc.id
572             WHERE child_qc.contextid <> parent_qc.contextid");
573         $table = '<table><thead><tr><th colspan="3">Child category</th><th colspan="3">Parent category</th></tr><tr>' .
574         '<th>Id</th><th>Name</th><th>Context id</th>' .
575         '<th>Id</th><th>Name</th><th>Context id</th>' .
576         "</tr></thead><tbody>\n";
577         foreach ($problemcategories as $cat) {
578             $table .= "<tr><td>$cat->childid</td><td>" . s($cat->childname) .
579             "</td><td>$cat->childcon</td><td>$cat->parentid</td><td>" . s($cat->parentname) .
580             "</td><td>$cat->parentcon</td></tr>\n";
581         }
582         $table .= '</tbody></table>';
583         return '<p>When one question category is the parent of another, then they ' .
584         'should both belong to the same context. This is not true for the following categories:</p>' .
585         $table;
586     }
587     function solution() {
588         return '<p>An automated solution is difficult. It depends whether the ' .
589         'parent or child category is in the wrong pace.' .
590         'People in the <a href="http://moodle.org/mod/forum/view.php?f=121">Quiz forum</a> may be able to help.</p>';
591     }
594 class problem_000017 extends problem_base {
595     function title() {
596         return 'Question categories tree structure';
597     }
598     function find_problems() {
599         global $DB;
600         static $answer = null;
602         if (is_null($answer)) {
603             $categories = $DB->get_records('question_categories', array(), 'id');
605             // Look for missing parents.
606             $missingparent = array();
607             foreach ($categories as $category) {
608                 if ($category->parent != 0 && !array_key_exists($category->parent, $categories)) {
609                     $missingparent[$category->id] = $category;
610                 }
611             }
613             // Look for loops.
614             $loops = array();
615             while (!empty($categories)) {
616                 $current = array_pop($categories);
617                 $thisloop = array($current->id => $current);
618                 while (true) {
619                     if (isset($thisloop[$current->parent])) {
620                         // Loop detected
621                         $loops[$current->id] = $thisloop;
622                         break;
623                     } else if (!isset($categories[$current->parent])) {
624                         // Got to the top level, or a category we already know is OK.
625                         break;
626                     } else {
627                         // Continue following the path.
628                         $current = $categories[$current->parent];
629                         $thisloop[$current->id] = $current;
630                         unset($categories[$current->id]);
631                     }
632                 }
633             }
635             $answer = array($missingparent, $loops);
636         }
638         return $answer;
639     }
640     function exists() {
641         list($missingparent, $loops) = $this->find_problems();
642         return !empty($missingparent) || !empty($loops);
643     }
644     function severity() {
645         return SEVERITY_ANNOYANCE;
646     }
647     function description() {
648         list($missingparent, $loops) = $this->find_problems();
650         $description = '<p>The question categories should be arranged into tree ' .
651                 ' structures by the question_categories.parent field. Sometimes ' .
652                 ' this tree structure gets messed up.</p>';
654         if (!empty($missingparent)) {
655             $description .= '<p>The following categories are missing their parents:</p><ul>';
656             foreach ($missingparent as $cat) {
657                 $description .= "<li>Category $cat->id: " . s($cat->name) . "</li>\n";
658             }
659             $description .= "</ul>\n";
660         }
662         if (!empty($loops)) {
663             $description .= '<p>The following categories form a loop of parents:</p><ul>';
664             foreach ($loops as $loop) {
665                 $description .= "<li><ul>\n";
666                 foreach ($loop as $cat) {
667                     $description .= "<li>Category $cat->id: " . s($cat->name) . " has parent $cat->parent</li>\n";
668                 }
669                 $description .= "</ul></li>\n";
670             }
671             $description .= "</ul>\n";
672         }
674         return $description;
675     }
676     function solution() {
677         global $CFG;
678         list($missingparent, $loops) = $this->find_problems();
680         $solution = '<p>Consider executing the following SQL queries. These fix ' .
681                 'the problem by moving some categories to the top level.</p>';
683         if (!empty($missingparent)) {
684             $solution .= "<pre>UPDATE " . $CFG->prefix . "question_categories\n" .
685                     "        SET parent = 0\n" .
686                     "        WHERE id IN (" . implode(',', array_keys($missingparent)) . ");</pre>\n";
687         }
689         if (!empty($loops)) {
690             $solution .= "<pre>UPDATE " . $CFG->prefix . "question_categories\n" .
691                     "        SET parent = 0\n" .
692                     "        WHERE id IN (" . implode(',', array_keys($loops)) . ");</pre>\n";
693         }
695         return $solution;
696     }
699 class problem_00000x extends problem_base {
700     function title() {
701         return '';
702     }
703     function exists() {
704         return false;
705     }
706     function severity() {
707         return SEVERITY_SIGNIFICANT;
708     }
709     function description() {
710         return '';
711     }
712     function solution() {
713         global $CFG;
714         return '';
715     }
718 /*
720 TODO:
722     session.save_path -- it doesn't really matter because we are already IN a session, right?
723     detect unsupported characters in $CFG->wwwroot - see bug Bug #6091 - relative vs absolute path during backup/restore process
725 */