diff --git a/admin/cli/checks.php b/admin/cli/checks.php new file mode 100644 index 0000000000000..40deb4d553533 --- /dev/null +++ b/admin/cli/checks.php @@ -0,0 +1,171 @@ +. + +/** + * CLI tool for system checks + * + * @package core + * @category check + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('CLI_SCRIPT', true); + +require(__DIR__ . '/../../config.php'); +require_once($CFG->libdir.'/clilib.php'); + +use core\check\result; + +list($options, $unrecognized) = cli_get_params([ + 'help' => false, + 'filter' => '', + 'type' => 'status', + 'verbose' => false, +], [ + 'h' => 'help', + 'f' => 'filter', + 'v' => 'verbose', + 't' => 'type', +]); + +if ($unrecognized) { + $unrecognized = implode("\n ", $unrecognized); + cli_error(get_string('cliunknowoption', 'admin', $unrecognized)); +} + +$checks = \core\check\manager::get_checks($options['type']); +$types = join(', ', \core\check\manager::TYPES); + +$help = "Run Moodle system checks + +Options: + -h, --help Print out this help + -f, --filter Filter to a subset of checks + -t, --type Which set of checks? Defaults to 'status' + One of $types + -v, --verbose Show details of all checks, not just failed checks + +Example: + + sudo -u www-data php admin/cli/checks.php + sudo -u www-data php admin/cli/checks.php -v + sudo -u www-data php admin/cli/checks.php -v --filter=environment + +"; + +if ($options['help']) { + echo $help; + die(); +} + +$filter = $options['filter']; +if ($filter) { + $checks = array_filter($checks, function($check, $key) use ($filter) { + $ref = $check->get_ref(); + return (strpos($ref, $filter) !== false); + }, 1); +} + +// These shell exit codes and labels align with the NRPE standard. +$exitcodes = [ + result::NA => 0, + result::OK => 0, + result::INFO => 0, + result::UNKNOWN => 3, + result::WARNING => 1, + result::ERROR => 2, + result::CRITICAL => 2, +]; +$exitlabel = [ + result::NA => 'OK', + result::OK => 'OK', + result::INFO => 'OK', + result::UNKNOWN => 'UNKNOWN', + result::WARNING => 'WARNING', + result::ERROR => 'CRITICAL', + result::CRITICAL => 'CRITICAL', +]; + +$format = "% 10s| % -60s\n"; +$spacer = "----------+--------------------------------------------------------------------\n"; +$prefix = ' |'; + +$output = ''; +$header = $exitlabel[result::OK] . ': ' . get_string('checksok', '', $options['type']) . "\n"; +$exitcode = $exitcodes[result::OK]; + +foreach ($checks as $check) { + $ref = $check->get_ref(); + $result = $check->get_result(); + + $status = $result->get_status(); + $checkexitcode = $exitcodes[$status]; + + // Summary is treated as html. + $summary = $result->get_summary(); + $summary = html_to_text($summary, 60, false); + + if ($checkexitcode > $exitcode) { + $exitcode = $checkexitcode; + $header = $exitlabel[$status] . ': ' . $check->get_name() . " (" . $check->get_ref() . ")\n"; + } + + if (empty($messages[$status])) { + $messages[$status] = $result; + } + + $len = strlen(get_string('status' . $status)); + + if ($options['verbose'] || + $status == result::WARNING || + $status == result::CRITICAL || + $status == result::ERROR) { + + $output .= sprintf( + $format, + $OUTPUT->check_result($result), + sprintf('%s (%s)', $check->get_name(), $ref) + ); + + $summary = str_replace("\n", "\n" . $prefix . ' ', $summary); + $output .= sprintf( $format, '', ' ' . $summary); + + if ($options['verbose']) { + $actionlink = $check->get_action_link(); + if ($actionlink) { + $output .= sprintf( $format, '', ' ' . $actionlink->url); + } + $output .= sprintf( $format, '', ''); + } + } +} + +// Print NRPE header. +print $header; + +// Only show the table header if there is anything to show. +if ($output) { + print sprintf($format, + get_string('status'). ' ', + get_string('check') + ) . $spacer; + print $output; +} + +// NRPE shell exit code. +exit($exitcode); + diff --git a/lang/en/admin.php b/lang/en/admin.php index d4a8f676e7fdc..fefbb20fcd387 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -117,6 +117,7 @@ $string['categoryemail'] = 'Email'; $string['cfgwwwrootslashwarning'] = '$CFG->wwwroot is defined incorrectly in the config.php file. It includes a \'/\' character at the end which must be removed.'; $string['cfgwwwrootwarning'] = '$CFG->wwwroot is defined incorrectly in the config.php file. It should match the URL you are using to access this page.'; +$string['checkupgradepending'] = 'Upgrade'; $string['cleanup'] = 'Cleanup'; $string['clianswerno'] = 'n'; $string['cliansweryes'] = 'y'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 7aa080a4e4d67..50fb3224281fb 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1000,6 +1000,7 @@ $string['changessaved'] = 'Changes saved'; $string['check'] = 'Check'; $string['checks'] = 'Checks'; +$string['checksok'] = 'All \'{$a}\' checks ok'; $string['checkall'] = 'Check all'; $string['checkingbackup'] = 'Checking backup'; $string['checkingcourse'] = 'Checking course'; diff --git a/lib/classes/check/environment/environment.php b/lib/classes/check/environment/environment.php new file mode 100644 index 0000000000000..cf221b72a17bb --- /dev/null +++ b/lib/classes/check/environment/environment.php @@ -0,0 +1,83 @@ +. + +/** + * Environment check + * + * @package core + * @category check + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\check\environment; + +defined('MOODLE_INTERNAL') || die(); + +use core\check\check; +use core\check\result; + +/** + * Environment check + * + * @package core + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class environment extends check { + + /** + * Get the short check name + * + * @return string + */ + public function get_name(): string { + return get_string('environment', 'admin'); + } + + /** + * A link to a place to action this + * + * @return action_link|null + */ + public function get_action_link(): ?\action_link { + return new \action_link( + new \moodle_url('/admin/environment.php'), + get_string('environment', 'admin')); + } + + /** + * Return result + * @return result + */ + public function get_result(): result { + global $CFG; + + require_once($CFG->libdir.'/environmentlib.php'); + list($status, $details) = check_moodle_environment($CFG->release, ENV_SELECT_NEWER); + + if ($status) { + $summary = get_string('environmentok', 'admin'); + $status = result::OK; + } else { + $summary = get_string('environmenterrortodo', 'admin'); + $status = result::ERROR; + } + + return new result($status, $summary, ''); + } +} + diff --git a/lib/classes/check/environment/upgradecheck.php b/lib/classes/check/environment/upgradecheck.php new file mode 100644 index 0000000000000..706f98723bfff --- /dev/null +++ b/lib/classes/check/environment/upgradecheck.php @@ -0,0 +1,85 @@ +. + +/** + * Upgrade check + * + * @package core + * @category check + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\check\environment; + +defined('MOODLE_INTERNAL') || die(); + +use core\check\check; +use core\check\result; + +/** + * Upgrade check + * + * @package core + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class upgradecheck extends check { + + /** + * Get the short check name + * + * @return string + */ + public function get_name(): string { + return get_string('checkupgradepending', 'admin'); + } + + /** + * A link to a place to action this + * + * @return action_link|null + */ + public function get_action_link(): ?\action_link { + return new \action_link( + new \moodle_url('/admin/index.php?cache=1'), + get_string('notifications', 'admin')); + } + + /** + * Return result + * @return result + */ + public function get_result(): result { + global $CFG; + + require("$CFG->dirroot/version.php"); + $newversion = "$release ($version)"; + + if ($version < $CFG->version) { + $status = result::ERROR; + $summary = get_string('downgradedcore', 'error'); + } else if (moodle_needs_upgrading()) { + $status = result::ERROR; + $summary = get_string('cliupgradepending', 'admin'); + } else { + $status = result::OK; + $summary = get_string('cliupgradenoneed', 'core_admin', $newversion); + } + return new result($status, $summary); + } +} + diff --git a/lib/classes/check/manager.php b/lib/classes/check/manager.php index ee0254604e4f2..cb99ff49200a3 100644 --- a/lib/classes/check/manager.php +++ b/lib/classes/check/manager.php @@ -40,7 +40,7 @@ class manager { /** * The list of valid check types */ - public const TYPES = ['security']; + public const TYPES = ['status', 'security']; /** * Return all status checks @@ -57,6 +57,32 @@ public static function get_checks(string $type): array { return $checks; } + /** + * Return all status checks + * + * @return array of check objects + */ + public static function get_status_checks(): array { + $checks = [ + new environment\environment(), + new environment\upgradecheck(), + ]; + + // Any plugin can add status checks to this report by implementing a callback + // _status_checks() which returns a check object. + $morechecks = get_plugins_with_function('status_checks', 'lib.php'); + foreach ($morechecks as $plugintype => $plugins) { + foreach ($plugins as $plugin => $pluginfunction) { + $result = $pluginfunction(); + foreach ($result as $check) { + $check->set_component($plugintype . '_' . $plugin); + $checks[] = $check; + } + } + } + return $checks; + } + /** * Return all security checks * diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php index 65a99c4306ef9..e4d617c140136 100644 --- a/lib/outputrenderers.php +++ b/lib/outputrenderers.php @@ -4775,6 +4775,41 @@ public function header() { return $this->page->heading . "\n"; } + /** + * Renders a Check API result + * + * To aid in CLI consistency this status is NOT translated and the visual + * width is always exactly 10 chars. + * + * @param result $result + * @return string HTML fragment + */ + protected function render_check_result(core\check\result $result) { + $status = $result->get_status(); + + $labels = [ + core\check\result::NA => ' ' . cli_ansi_format('' ) . ' NA ', + core\check\result::OK => ' ' . cli_ansi_format('') . ' OK ', + core\check\result::INFO => ' ' . cli_ansi_format('' ) . ' INFO ', + core\check\result::UNKNOWN => ' ' . cli_ansi_format('' ) . ' UNKNOWN ', + core\check\result::WARNING => ' ' . cli_ansi_format('') . ' WARNING ', + core\check\result::ERROR => ' ' . cli_ansi_format('') . ' ERROR ', + core\check\result::CRITICAL => '' . cli_ansi_format('') . ' CRITICAL ', + ]; + $string = $labels[$status] . cli_ansi_format(''); + return $string; + } + + /** + * Renders a Check API result + * + * @param result $result + * @return string fragment + */ + public function check_result(core\check\result $result) { + return $this->render_check_result($result); + } + /** * Returns a template fragment representing a Heading. * diff --git a/report/status/classes/privacy/provider.php b/report/status/classes/privacy/provider.php new file mode 100644 index 0000000000000..a686b907c28e7 --- /dev/null +++ b/report/status/classes/privacy/provider.php @@ -0,0 +1,48 @@ +. + +/** + * Privacy provider. + * + * @package report_status + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace report_status\privacy; + +defined('MOODLE_INTERNAL') || die; + +/** + * Privacy provider. + * + * @package report_status + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} + diff --git a/report/status/db/access.php b/report/status/db/access.php new file mode 100644 index 0000000000000..5e96935847864 --- /dev/null +++ b/report/status/db/access.php @@ -0,0 +1,37 @@ +. + +/** + * System status capabilities + * + * @package report_status + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = [ + + 'report/status:view' => [ + 'riskbitmask' => RISK_CONFIG, + 'captype' => 'read', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => [ + 'manager' => CAP_ALLOW + ], + ] +]; diff --git a/report/status/index.php b/report/status/index.php new file mode 100644 index 0000000000000..660e01f9b4e1b --- /dev/null +++ b/report/status/index.php @@ -0,0 +1,108 @@ +. + +/** + * System Status report + * + * @package report_status + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('NO_OUTPUT_BUFFERING', true); + +require('../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); + +use core\check\check; +use core\check\result; + +// Print the header. +admin_externalpage_setup('reportstatus', '', null, '', ['pagelayout' => 'report']); + +// We may need a bit more memory and this may take a long time to process. +raise_memory_limit(MEMORY_EXTRA); +core_php_time_limit::raise(); + +$checks = \core\check\manager::get_checks('status'); + +$detail = optional_param('detail', '', PARAM_TEXT); // Show detailed info about one check only. +if ($detail) { + $checks = array_filter($checks, function($check) use ($detail) { + return $detail == $check->get_ref(); + }); + $checks = array_values($checks); + if (!empty($checks)) { + $PAGE->set_docs_path('report/status/index.php?detail=' . $detail); + $PAGE->navbar->add($checks[0]->get_name()); + } +} + +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('pluginname', 'report_status')); + +$url = "$CFG->wwwroot/report/status/index.php"; + +$table = new html_table(); +$table->data = []; +$table->head = [ + get_string('status'), + get_string('check'), + get_string('summary'), + get_string('action'), +]; +$table->colclasses = [ + 'rightalign status', + 'leftalign check', + 'leftalign summary', + 'leftalign action', +]; +$table->id = 'statusreporttable'; +$table->attributes = ['class' => 'admintable statusreport generaltable']; + +$manager = core_plugin_manager::instance(); + +foreach ($checks as $check) { + $ref = $check->get_ref(); + $result = $check->get_result(); + $component = $check->get_component(); + $actionlink = $check->get_action_link(); + + $link = new \moodle_url('/report/status/index.php', ['detail' => $ref]); + + $row = []; + $row[] = $OUTPUT->result($result); + $row[] = $OUTPUT->action_link($link, $check->get_name()); + + $row[] = $result->get_summary(); + if ($actionlink) { + $row[] = $OUTPUT->render($actionlink); + } else { + $row[] = ''; + } + + $table->data[] = $row; +} +echo html_writer::table($table); + +if ($detail && $result) { + echo $OUTPUT->heading(get_string('description'), 3); + echo $OUTPUT->box($result->get_details(), 'generalbox boxwidthnormal boxaligncenter'); + echo $OUTPUT->continue_button($url); +} + +echo $OUTPUT->footer(); + diff --git a/report/status/lang/en/report_status.php b/report/status/lang/en/report_status.php new file mode 100644 index 0000000000000..5251062c2249f --- /dev/null +++ b/report/status/lang/en/report_status.php @@ -0,0 +1,27 @@ +. + +/** + * Strings for component 'tool_task', language 'en' + * + * @package report_status + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'System status'; +$string['status:view'] = 'View system status'; +$string['privacy:metadata'] = 'This plugin does not store any personal data.'; diff --git a/report/status/settings.php b/report/status/settings.php new file mode 100644 index 0000000000000..0575fe5c967e4 --- /dev/null +++ b/report/status/settings.php @@ -0,0 +1,30 @@ +. + +/** + * Plugin version info + * + * @package report_status + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$ADMIN->add('reports', new admin_externalpage('reportstatus', get_string('pluginname', 'report_status'), + "$CFG->wwwroot/report/status/index.php", 'report/status:view')); + +$settings = null; diff --git a/report/status/version.php b/report/status/version.php new file mode 100644 index 0000000000000..df6ae86aa0258 --- /dev/null +++ b/report/status/version.php @@ -0,0 +1,30 @@ +. + +/** + * Plugin version info + * + * @package report_status + * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2019111800; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2019111200; // Requires this Moodle version. +$plugin->component = 'report_status'; // Full name of the plugin (used for diagnostics). +