Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  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   */
  25  
  26      ob_start(); //for whitespace test
  27      require('../../../config.php');
  28      $extraws = ob_get_clean();
  29  
  30      require_once($CFG->libdir.'/adminlib.php');
  31      require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/health/locallib.php');
  32  
  33      admin_externalpage_setup('toolhealth');
  34  
  35      define('SEVERITY_NOTICE',      'notice');
  36      define('SEVERITY_ANNOYANCE',   'annoyance');
  37      define('SEVERITY_SIGNIFICANT', 'significant');
  38      define('SEVERITY_CRITICAL',    'critical');
  39  
  40      $solution = optional_param('solution', 0, PARAM_PLUGIN);
  41  
  42      $site = get_site();
  43  
  44      echo $OUTPUT->header();
  45  
  46      if(strpos($solution, 'problem_') === 0 && class_exists($solution)) {
  47          health_print_solution($solution);
  48      }
  49      else {
  50          health_find_problems();
  51      }
  52  
  53  
  54      echo $OUTPUT->footer();
  55  
  56  
  57  function health_find_problems() {
  58      global $OUTPUT;
  59  
  60      echo $OUTPUT->heading(get_string('pluginname', 'tool_health'));
  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)) {
  73              continue;
  74          }
  75          $problem = new $classname;
  76  
  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">';
  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      }
 111  }
 112  
 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      );
 122  
 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>';
 133  }
 134  
 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      }
 151  }
 152  
 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;
 159  
 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      }
 175  }
 176  
 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      }
 199  }
 200  
 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      }
 220  }
 221  
 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      }
 239  }
 240  
 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      }
 258  }
 259  
 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;
 266  
 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      }
 304  }
 305  
 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      }
 325  }
 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  }*/
 412  
 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      }
 435  }
 436  
 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      }
 472  }
 473  
 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      }
 497  }
 498  
 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      }
 545  }
 546  
 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      }
 590  }
 591  
 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;
 599  
 600          if (is_null($answer)) {
 601              $categories = $DB->get_records('question_categories', array(), 'id');
 602  
 603              // Look for missing parents.
 604              $missingparent = tool_health_category_find_missing_parents($categories);
 605  
 606              // Look for loops.
 607              $loops = tool_health_category_find_loops($categories);
 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  
 628          $description .= tool_health_category_list_missing_parents($missingparent);
 629          $description .= tool_health_category_list_loops($loops);
 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       */
 640      public function solution() {
 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  
 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          }
 652  
 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          }
 658  
 659          return $solution;
 660      }
 661  }
 662  
 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      }
 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       */
 685      public function find_problems() {
 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.
 693              $missingparent = tool_health_category_find_missing_parents($categories);
 694  
 695              // Look for loops.
 696              $loops = tool_health_category_find_loops($categories);
 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       */
 709      public function exists() {
 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       */
 719      public function severity() {
 720          return SEVERITY_SIGNIFICANT;
 721      }
 722  
 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();
 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  
 735          $description .= tool_health_category_list_missing_parents($missingparent);
 736          $description .= tool_health_category_list_loops($loops);
 737  
 738          return $description;
 739      }
 740  
 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();
 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)) {
 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          }
 759  
 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          }
 765  
 766          return $solution;
 767      }
 768  }
 769  
 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      }
 787  }
 788  
 789  /*
 790  
 791  TODO:
 792  
 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
 795  
 796  */