weekly release 4.0dev
[moodle.git] / admin / tool / health / index.php
CommitLineData
f578af59 1<?php
7f1d34f0
PS
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
17/**
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 */
527d9cdd 25
9bbb40d6 26 ob_start(); //for whitespace test
7f1d34f0 27 require('../../../config.php');
14074ef7 28 $extraws = ob_get_clean();
9bbb40d6 29
c9a49cea 30 require_once($CFG->libdir.'/adminlib.php');
a1ceaf89 31 require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/health/locallib.php');
1ae083e4 32
7f1d34f0 33 admin_externalpage_setup('toolhealth');
c9a49cea 34
527d9cdd 35 define('SEVERITY_NOTICE', 'notice');
36 define('SEVERITY_ANNOYANCE', 'annoyance');
37 define('SEVERITY_SIGNIFICANT', 'significant');
38 define('SEVERITY_CRITICAL', 'critical');
39
aff24313 40 $solution = optional_param('solution', 0, PARAM_PLUGIN);
993ef10d 41
527d9cdd 42 $site = get_site();
527d9cdd 43
61ef8f9f 44 echo $OUTPUT->header();
527d9cdd 45
045e9e24 46 if(strpos($solution, 'problem_') === 0 && class_exists($solution)) {
527d9cdd 47 health_print_solution($solution);
48 }
49 else {
50 health_find_problems();
51 }
52
53
73d6f52f 54 echo $OUTPUT->footer();
527d9cdd 55
56
57function health_find_problems() {
395a095e 58 global $OUTPUT;
527d9cdd 59
7f1d34f0 60 echo $OUTPUT->heading(get_string('pluginname', 'tool_health'));
527d9cdd 61
62 $issues = array(
63 SEVERITY_CRITICAL => array(),
64 SEVERITY_SIGNIFICANT => array(),
65 SEVERITY_ANNOYANCE => array(),
66 SEVERITY_NOTICE => array(),
67 );
68 $problems = 0;
69
70 for($i = 1; $i < 1000000; ++$i) {
71 $classname = sprintf('problem_%06d', $i);
72 if(!class_exists($classname)) {
7f1d34f0 73 continue;
527d9cdd 74 }
75 $problem = new $classname;
7f1d34f0 76
527d9cdd 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 }
88
89 if($problems == 0) {
90 echo '<div id="healthnoproblemsfound">';
7f1d34f0 91 echo get_string('healthnoproblemsfound', 'tool_health');
527d9cdd 92 echo '</div>';
93 }
94 else {
7f1d34f0 95 echo $OUTPUT->heading(get_string('healthproblemsdetected', 'tool_health'));
527d9cdd 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'];
7f1d34f0 103 echo '<form action="index.php#solution" method="get">';
527d9cdd 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 }
111}
112
113function health_print_solution($classname) {
2fff8846 114 global $OUTPUT;
527d9cdd 115 $problem = new $classname;
116 $data = array(
117 'title' => $problem->title(),
118 'severity' => $problem->severity(),
119 'description' => $problem->description(),
120 'solution' => $problem->solution()
121 );
122
7f1d34f0
PS
123 echo $OUTPUT->heading(get_string('pluginname', 'tool_health'));
124 echo $OUTPUT->heading(get_string('healthproblemsolution', 'tool_health'));
527d9cdd 125 echo '<dl class="healthissues '.$data['severity'].'">';
126 echo '<dt>'.$data['title'].'</dt>';
127 echo '<dd>'.$data['description'].'</dd>';
7f1d34f0 128 echo '<dt id="solution" class="solution">'.get_string('healthsolution', 'tool_health').'</dt>';
527d9cdd 129 echo '<dd class="solution">'.$data['solution'].'</dd></dl>';
7f1d34f0
PS
130 echo '<form id="healthformreturn" action="index.php#'.$classname.'" method="get">';
131 echo '<input type="submit" value="'.get_string('healthreturntomain', 'tool_health').'" />';
527d9cdd 132 echo '</form>';
133}
134
135class 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 }
151}
152
527d9cdd 153class problem_000002 extends problem_base {
154 function title() {
9bbb40d6 155 return 'Extra characters at the end of config.php or other library function';
527d9cdd 156 }
157 function exists() {
9bbb40d6 158 global $extraws;
159
160 if($extraws === '') {
161 return false;
527d9cdd 162 }
163 return true;
164 }
165 function severity() {
166 return SEVERITY_SIGNIFICANT;
167 }
168 function description() {
9bbb40d6 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.';
527d9cdd 170 }
171 function solution() {
172 global $CFG;
9bbb40d6 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.';
527d9cdd 174 }
175}
176
177class 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 }
199}
200
201class problem_000004 extends problem_base {
202 function title() {
203 return 'cron.php is not set up to run automatically';
204 }
205 function exists() {
1d8bf5f0 206 global $DB;
207 $lastcron = $DB->get_field_sql('SELECT max(lastcron) FROM {modules}');
527d9cdd 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 }
220}
221
222class 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 }
239}
240
527d9cdd 241class 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 }
258}
259
260class problem_000008 extends problem_base {
261 function title() {
262 return 'PHP: memory_limit cannot be controlled by Moodle';
263 }
264 function exists() {
395a095e
PS
265 global $CFG;
266
8a91d7fc 267 $oldmemlimit = @ini_get('memory_limit');
346c5887 268 if (empty($oldmemlimit)) {
527d9cdd 269 // PHP not compiled with memory limits, this means that it's
8a91d7fc 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);
346c5887
PS
276 //now lets change the memory limit to something higher
277 $newmemlimit = ($oldmemlimit + 1024*1024*5);
278 raise_memory_limit($newmemlimit);
8a91d7fc 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 }
527d9cdd 288 }
7022dd39 289 reduce_memory_limit($oldmemlimit);
527d9cdd 290 return false;
291 }
292 function severity() {
8a91d7fc 293 return SEVERITY_NOTICE;
527d9cdd 294 }
295 function description() {
8a91d7fc 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.';
527d9cdd 300 }
301 function solution() {
8a91d7fc 302 return 'It is recommended that you contact your web server administrator to address this issue.';
527d9cdd 303 }
304}
305
b70b8fe1 306class 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 }
325}
c5b26d78 326/* // not implemented in 2.0 yet
39ba6d58 327class 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()) {
eef868d1 333 return true;
39ba6d58 334 }
48283ff6 335 if ($this->status() < 1) {
39ba6d58 336 return true;
337 }
338 return false;
339 }
340 function severity() {
341 if ($this->is_enabled() and $this->status() == 0) {
342 return SEVERITY_SIGNIFICANT;
eef868d1 343 } else {
39ba6d58 344 return SEVERITY_ANNOYANCE;
345 }
346 }
347 function description() {
48283ff6 348 global $CFG;
39ba6d58 349 $desc = 'Slasharguments are needed for relative linking in uploaded resources:<ul>';
350 if (!$this->is_enabled()) {
eef868d1 351 $desc .= '<li>slasharguments are <strong>disabled</strong> in Moodle configuration</li>';
39ba6d58 352 } else {
eef868d1 353 $desc .= '<li>slasharguments are enabled in Moodle configuration</li>';
39ba6d58 354 }
48283ff6 355 if ($this->status() == -1) {
6544f8d8 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>';
48283ff6 357 } else if ($this->status() == 0) {
39ba6d58 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() {
48283ff6 366 global $CFG;
39ba6d58 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 />';
48283ff6 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 />';
39ba6d58 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 }
48283ff6 383 if ($status < 1) {
eef868d1 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>';
39ba6d58 387 }
388 return $solution;
389 }
390 function is_enabled() {
391 global $CFG;
392 return !empty($CFG->slasharguments);
393 }
394 function status() {
395 global $CFG;
48283ff6 396 $handle = @fopen($CFG->wwwroot.'/file.php?file=/testslasharguments', "r");
9bbb40d6 397 $contents = @trim(fread($handle, 10));
48283ff6 398 @fclose($handle);
399 if ($contents != 'test -1') {
400 return -1;
401 }
402 $handle = @fopen($CFG->wwwroot.'/file.php/testslasharguments', "r");
9bbb40d6 403 $contents = trim(@fread($handle, 10));
39ba6d58 404 @fclose($handle);
405 switch ($contents) {
48283ff6 406 case 'test 1': return 1;
407 case 'test 2': return 2;
39ba6d58 408 default: return 0;
48283ff6 409 }
39ba6d58 410 }
c5b26d78 411}*/
39ba6d58 412
ff5328a2 413class 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 }
435}
436
437class problem_000013 extends problem_base {
438 function title() {
439 return 'Multi-answer questions data consistency';
440 }
441 function exists() {
442 global $DB;
20207b82 443 $positionexpr = $DB->sql_position($DB->sql_concat("','", "q.id", "','"),
ff5328a2 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 }
472}
473
474class 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 }
497}
498
499class 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";
ff5328a2 524 foreach ($problemcategories as $cat) {
af2f92d0 525 $table .= "<tr><td>$cat->id</td><td>" . s($cat->name) . "</td><td>" .
ff5328a2 526 $cat->contextid ."</td><td>$cat->numquestions</td></tr>\n";
ff5328a2 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>
9f64eef7 538DELETE FROM ' . $CFG->prefix . 'question_categories
ff5328a2 539WHERE
9f64eef7 540 NOT EXISTS (SELECT * FROM ' . $CFG->prefix . 'question q WHERE q.category = ' . $CFG->prefix . 'question_categories.id)
541AND NOT EXISTS (SELECT * FROM ' . $CFG->prefix . 'context con WHERE contextid = con.id)
ff5328a2 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 }
545}
546
547class 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) {
af2f92d0 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) .
ff5328a2 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 }
590}
527d9cdd 591
68f0aac8 592class 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;
599
600 if (is_null($answer)) {
601 $categories = $DB->get_records('question_categories', array(), 'id');
602
603 // Look for missing parents.
a1ceaf89 604 $missingparent = tool_health_category_find_missing_parents($categories);
68f0aac8 605
606 // Look for loops.
a1ceaf89 607 $loops = tool_health_category_find_loops($categories);
68f0aac8 608
609 $answer = array($missingparent, $loops);
610 }
611
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();
623
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>';
627
a1ceaf89
DM
628 $description .= tool_health_category_list_missing_parents($missingparent);
629 $description .= tool_health_category_list_loops($loops);
8db188c8
LR
630
631 return $description;
632 }
633
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 */
a1ceaf89 640 public function solution() {
8db188c8
LR
641 global $CFG;
642 list($missingparent, $loops) = $this->find_problems();
643
644 $solution = '<p>Consider executing the following SQL queries. These fix ' .
645 'the problem by moving some categories to the top level.</p>';
646
68f0aac8 647 if (!empty($missingparent)) {
8db188c8
LR
648 $solution .= "<pre>UPDATE " . $CFG->prefix . "question_categories\n" .
649 " SET parent = 0\n" .
650 " WHERE id IN (" . implode(',', array_keys($missingparent)) . ");</pre>\n";
68f0aac8 651 }
652
653 if (!empty($loops)) {
8db188c8
LR
654 $solution .= "<pre>UPDATE " . $CFG->prefix . "question_categories\n" .
655 " SET parent = 0\n" .
656 " WHERE id IN (" . implode(',', array_keys($loops)) . ");</pre>\n";
68f0aac8 657 }
658
8db188c8
LR
659 return $solution;
660 }
661}
662
663/**
664 * Check course categories tree structure for problems.
a1ceaf89
DM
665 *
666 * @copyright 2013 Marko Vidberg
667 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
8db188c8
LR
668 */
669class problem_000018 extends problem_base {
670 /**
671 * Generate title for this problem.
672 *
673 * @return string Title of problem.
674 */
a1ceaf89 675 public function title() {
8db188c8
LR
676 return 'Course categories tree structure';
677 }
678
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 */
a1ceaf89 685 public function find_problems() {
8db188c8
LR
686 global $DB;
687 static $answer = null;
688
689 if (is_null($answer)) {
690 $categories = $DB->get_records('course_categories', array(), 'id');
691
692 // Look for missing parents.
a1ceaf89 693 $missingparent = tool_health_category_find_missing_parents($categories);
8db188c8
LR
694
695 // Look for loops.
a1ceaf89 696 $loops = tool_health_category_find_loops($categories);
8db188c8
LR
697
698 $answer = array($missingparent, $loops);
699 }
700
701 return $answer;
702 }
703
704 /**
705 * Check if the problem exists.
706 *
707 * @return boolean True if either missing parents or loops found
708 */
a1ceaf89 709 public function exists() {
8db188c8
LR
710 list($missingparent, $loops) = $this->find_problems();
711 return !empty($missingparent) || !empty($loops);
712 }
713
714 /**
715 * Set problem severity.
716 *
717 * @return constant Problem severity.
718 */
a1ceaf89
DM
719 public function severity() {
720 return SEVERITY_SIGNIFICANT;
8db188c8
LR
721 }
722
723 /**
724 * Generate problem description.
725 *
726 * @return string HTML containing details of the problem.
727 */
a1ceaf89 728 public function description() {
8db188c8
LR
729 list($missingparent, $loops) = $this->find_problems();
730
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>';
734
a1ceaf89
DM
735 $description .= tool_health_category_list_missing_parents($missingparent);
736 $description .= tool_health_category_list_loops($loops);
8db188c8 737
68f0aac8 738 return $description;
739 }
8db188c8
LR
740
741 /**
742 * Generate solution text.
743 *
744 * @uses $CFG
745 * @return string HTML containing the suggested solution.
746 */
559d6c00 747 public function solution() {
68f0aac8 748 global $CFG;
749 list($missingparent, $loops) = $this->find_problems();
750
751 $solution = '<p>Consider executing the following SQL queries. These fix ' .
752 'the problem by moving some categories to the top level.</p>';
753
754 if (!empty($missingparent)) {
8db188c8
LR
755 $solution .= "<pre>UPDATE " . $CFG->prefix . "course_categories\n" .
756 " SET parent = 0, depth = 1, path = CONCAT('/', id)\n" .
68f0aac8 757 " WHERE id IN (" . implode(',', array_keys($missingparent)) . ");</pre>\n";
758 }
759
760 if (!empty($loops)) {
8db188c8
LR
761 $solution .= "<pre>UPDATE " . $CFG->prefix . "course_categories\n" .
762 " SET parent = 0, depth = 1, path = CONCAT('/', id)\n" .
68f0aac8 763 " WHERE id IN (" . implode(',', array_keys($loops)) . ");</pre>\n";
764 }
765
766 return $solution;
767 }
768}
769
527d9cdd 770class problem_00000x extends problem_base {
b70b8fe1 771 function title() {
772 return '';
773 }
527d9cdd 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 }
787}
788
789/*
790
791TODO:
792
793 session.save_path -- it doesn't really matter because we are already IN a session, right?
e25f766d 794 detect unsupported characters in $CFG->wwwroot - see bug Bug #6091 - relative vs absolute path during backup/restore process
48283ff6 795
527d9cdd 796*/