diff --git a/classes/helper.php b/classes/helper.php index e042c72..222302d 100644 --- a/classes/helper.php +++ b/classes/helper.php @@ -171,4 +171,30 @@ public static function merge_duplicate_records(array $records): \stdClass { return $baserecord; } + + /** + * Estimate row count for local_csp table + * + * @return int + */ + public static function get_row_estimate(): int { + global $CFG, $DB; + + if ($DB->get_dbfamily() === 'mysql') { + // Use explain to keep accurate count after truncate. + $explainsql = 'EXPLAIN SELECT COUNT(1) FROM {local_csp}'; + $record = $DB->get_record_sql($explainsql); + return $record->rows ?? 0; + } else if ($DB->get_dbfamily() === 'postgres') { + $sql = "SELECT relname AS table_name, reltuples::BIGINT AS row_count + FROM pg_class + WHERE relname = :tablename"; + $params = ['tablename' => $CFG->prefix . 'local_csp']; + $record = $DB->get_record_sql($sql, $params); + return $record->row_count ?? 0; + } else { + // Estimates not currently supported for other databases, use a full count. + return $DB->count_records('local_csp'); + } + } } diff --git a/csp_report.php b/csp_report.php index 8e16335..f6a20db 100644 --- a/csp_report.php +++ b/csp_report.php @@ -29,6 +29,7 @@ $viewviolation = optional_param('violation', false, PARAM_TEXT); $removeviolation = optional_param('removeviolation', false, PARAM_TEXT); $removerecordwithid = optional_param('removerecordwithid', false, PARAM_TEXT); +$limitedreport = optional_param('limited', null, PARAM_INT); $download = optional_param('download', '', PARAM_ALPHA); admin_externalpage_setup('local_csp_report', '', null, '', array('pagelayout' => 'report')); @@ -113,6 +114,12 @@ $action, )); +// The local_csp table can grow very large if a policy is not set up correctly. +// This report can be slow even with indexes, so this can be used to load a limited version. +if (!isset($limitedreport)) { + $limitedreport = \local_csp\helper::get_row_estimate() > 5000000; +} + // If user has clicked on a violation to view all violation entries. if ($viewviolation) { $fields = 'id, sha1hash, blockeduri, blockeddomain, violateddirective, failcounter, timeupdated, documenturi, violationhash'; @@ -138,6 +145,38 @@ $action, )); + // Disable sorting on large tables as sorting by failcounter requires the full table. + if ($limitedreport) { + $table->sortable(false); + } + +} else if ($limitedreport) { + // If a table is too large only load the fields that can be loaded using hashes and no aggregation. + $table->define_columns([ + 'failcounter', + 'violateddirective', + 'blockeddomain', + 'timecreated', + 'action', + ]); + $table->define_headers([ + $failcounter, + $violateddirective, + $blockeddomain, + $timeupdated, + $action, + ]); + + // Summing failcounter may be too slow on huge tables, so use violationcount as a rough indicator instead. + $fields = 'id, blockeddomain, violateddirective, violationhash, violationcount as failcounter, timecreated'; + $from = "(SELECT MAX(id) AS maxid, count(1) AS violationcount + FROM {local_csp} + WHERE violationhash IS NOT NULL + GROUP BY violationhash) AS A + JOIN {local_csp} ON id = maxid"; + $where = '1 = 1'; + $params = []; + } else { $fields = 'id, blockeddomain, violateddirective, violationhash, A.failcounter, A.timecreated'; // Select the first blockedURI of a type, and collapse the rest while summing failcounter. @@ -152,10 +191,22 @@ $params = []; } +if (!$viewviolation) { + // Simplify count SQL to also speed up processing. + $table->set_count_sql("SELECT COUNT(DISTINCT violationhash) + FROM {local_csp} + WHERE violationhash IS NOT NULL"); +} + if (!$table->is_downloading()) { echo $OUTPUT->header(); echo $OUTPUT->heading($title); + if ($limitedreport) { + $fullreport = new moodle_url($PAGE->url, ['limited' => 0]); + echo $OUTPUT->notification(get_string('limitedreport', 'local_csp', $fullreport->out()), 'warning'); + } + $action = new \confirm_action(get_string('areyousuretodeleteallrecords', 'local_csp')); $urlresetallcspstatistics = new moodle_url($PAGE->url, array( 'resetallcspstatistics' => 1, diff --git a/lang/en/local_csp.php b/lang/en/local_csp.php index ef2834e..a2f9444 100644 --- a/lang/en/local_csp.php +++ b/lang/en/local_csp.php @@ -57,6 +57,7 @@ $string['failcounter'] = '#'; $string['highestviolaters'] = 'Top Violation Sources'; $string['invalidblockeduri'] = 'Invalid Blocked URI: {$a}'; +$string['limitedreport'] = 'The report columns and sorting options have been limited due to a large amount of data. This is usually an indication that the policy is not set up correctly, consider updating the policy and resetting the statistics. Load full report.'; $string['loaddata'] = 'Load data'; $string['loadexternaljavascript'] = 'Load external javascript from {$a}'; $string['loadingmixedcontentdescription'] = 'When accessing moodle website via HTTPS browser prohibits displaying of the below resources because they origin from HTTP.
You should be able to see it in your browser\'s Javascript console.';