";
66 | $dbport="3306";
67 |
68 | $cssfile="default.css";
69 |
70 | $default_hostlookup = 1; // Hostname resolution: 1=on 0=off (Turning off host lookup greatly speeds up the program in the case of mail servers that have ceased to exist)
71 | $default_sort = 1; // Report listing Start Date: 1=ASCdending 0=DESCending (ASCending is default behaviour )
72 | ```
73 | #### dmarcts-report-viewer-js
74 | Finally, edit these basic configuration options near the top of the `dmarcts-report-viewer.js` file with your preferences:
75 |
76 | ```
77 | var default_reportlist_height = 60; // Main Report List height as a percentage of
78 | // browser window height (without the % mark)
79 | ```
80 |
81 | ## Usage
82 |
83 | Navigate in your browser to the location of the `dmarcts-report-viewer.php` file.
84 |
85 | You should be presented with the basic Report List view, allowing you to navigate through the reports that have been parsed.
86 |
87 | ### Icon Color Legend
88 | * Green : *Both* DKIM and SPF = pass
89 | * Red : *Both* DKIM and SPF = fail
90 | * Orange : *Either* DKIM or SPF (but not both) = fail
91 | * Yellow : Some other condition, and should be investigated (e.g. DKIM or SPF result are missing, "softfail", "temperror", etc.)
92 |
93 | ### Option Bar
94 | At the top of the page you will find the option bar where you can set:
95 |
96 | 1. Hostname on/off: This determines whether or not the IP address of the mailserver is resolved into a hostname in the Report Detail.
97 | Hostname resolution is fine until an IP address no longer has a reverse DNS entry (as when a mail server is de-commissioned) and it takes an excessive amount of time before the DNS resolution times out. If this is the case, you can turn off hostname resolution.
98 |
99 | 2. Filter Controls:
100 |
101 | * DMARC Result: Filter by the combined result of DKIM/SPF: pass/pass, fail/fail, pass or fail, other condition
102 | * Month: Filter by any month of reports
103 | * Domain(s): Filter by any domain
104 | * Reporter(s): Filter by any reporting organization
105 |
106 | If the filter returns no reports, an error message will inform you that no reports meet the criteria you have set. In this case, you can change the filter settings or click on the *Reset* button to clear the filter.
107 |
108 | 3. Buttons
109 |
110 | * Refresh: This will refresh the data in the Report List while maintaining the currently set filter.
111 | * Reset: This will reset the filter to show all reports in the Report List and then refresh the data.
112 |
113 | ### Report List
114 | The Report List table displays all the parsed DMARC reports, initially sorted by Start Date (whether initially ascending or descending is determined by the `$default_hostlookup` option in `dmarcts-report-viewer-config.php`) and initially filtered to show only those reports from the latest month available.
115 |
116 | Clicking on a column heading will toggle the sort direction of the Report List table by that column. Clicking on any line of the Report List will display the detailed DMARC information of the selected report below the Report List table.
117 |
118 | ### Report Detail
119 | The Report Detail table displays the details of the selected DMARC report, initially sorted by IP Address ascending.
120 |
121 | Clicking on a column heading will toggle the sort direction of the Report Detail table by that column.
122 |
123 | ### Raw Report XML
124 | Clicking on the XML icon  will display the raw XML of the currently displayed report. Clicking on the HTML icon  will hide the raw XML report.
125 |
126 |
127 | More info can currently be found at : [TechSneeze.com](http://www.techsneeze.com/dmarc-report/)
128 |
--------------------------------------------------------------------------------
/dmarcts-report-viewer-options.php:
--------------------------------------------------------------------------------
1 | .
22 | //
23 | //####################################################################
24 | //### configuration ##################################################
25 | //####################################################################
26 |
27 | // Copy dmarcts-report-viewer-config.php.sample to
28 | // dmarcts-report-viewer-config.php and edit with the appropriate info
29 | // for your database authentication and location.
30 | //
31 | // Edit the configuration variables in dmarcts-report-viewer.js with your preferences.
32 | //
33 | //####################################################################
34 | //### functions ######################################################
35 | //####################################################################
36 |
37 | function html ($domains = array(), $orgs = array(), $dmarc_result_select = array(), $report_status_select = array(), $report_list_columns = array(), $cssfiles = array() ) {
38 |
39 | global $dmarc_result;
40 | global $options;
41 | global $cookie_options;
42 |
43 | global $html;
44 |
45 | $html[] = "";
46 | $html[] = "";
47 | $html[] = " ";
48 | $html[] = " DMARC Report Viewer ";
49 | $html[] = " ";
50 | $html[] = " ";
51 | $html[] = " ";
52 | $html[] = " ";
53 | $html[] = " ";
54 | $html[] = " ";
55 |
56 | $html[] = " ";
57 |
58 | // if ($_SERVER["REQUEST_METHOD"] == "POST") {
59 | // $html[] = "";
60 | // $html[] = "
";
61 | // $html[] = "Your settings have been saved to the database. ";
62 | // $html[] = "Return to Reports ";
63 | // $html[] = "Return to Options ";
64 | // $html[] = "
";
65 | // $html[] = "
";
66 | // $filter = "style='filter: blur(3px);opacity: 50%;'";
67 | // }
68 | // Note: If the above is implemented, and a blur effect behind the dialog is wanted, instead of using the $filter method, simply use backdrop-filter/-webkit-backdrop-filter on screen_overlay (see https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter)
69 |
70 | $html[] = " DMARCTS Options
";
71 |
72 | $html[] = " ";
110 | $html[] = " ";
111 |
112 | // Page Footer
113 | // --------------------------------------------------------------------------
114 | $html[] = " ";
115 | $html[] = " ";
116 | $html[] = "";
117 |
118 | return implode("\n",$html);
119 | }
120 |
121 | function create_heading($option_label) {
122 |
123 | global $html;
124 |
125 | $html[] = " ";
126 | $html[] = " ";
127 | $html[] = " " . $option_label . " ";
128 | $html[] = " ";
129 | $html[] = " ";
130 | }
131 |
132 | function create_input_text($option_name, $option = array()) {
133 |
134 | global $html;
135 | global $cookie_options;
136 |
137 | $extra_options = "";
138 | $after = "";
139 |
140 | $values = $option["option_values"];
141 |
142 | if (isset($cookie_options[$option_name]) ) {
143 | $value = "value='" . preg_replace("/'/", "'", $cookie_options[$option_name]). "'";
144 | } else {
145 | $value = "";
146 | }
147 | $html[] = " ";
148 | $html[] = " ";
149 | $html[] = " " . $option["option_label"] . " ";
150 | $html[] = " ";
151 | $html[] = " " . $option["option_description"] . " ";
152 | $html[] = " ";
153 | $html[] = " ";
154 |
155 | switch ($option["option_type"]) {
156 | case "number":
157 | if ( $values['min'] != "" && $values['max'] != "" ) {
158 | $extra_options = " min='" . $values['min'] . "' max='" . $values['max'] . "'";
159 | }
160 | if ( $values['units'] != "" ) {
161 | $after = " " . $values['units'];
162 | }
163 | break;
164 | default:
165 | break;
166 | }
167 |
168 | $html[] = " " . $after . " ";
169 | $html[] = " ";
170 | $html[] = " ";
171 | }
172 |
173 | function create_input_radio($option_name) {
174 |
175 | global $html;
176 | global $options;
177 | global $cookie_options;
178 |
179 | $values = $options[$option_name]["option_values"];
180 | $html[] = " ";
181 | $html[] = " ";
182 | $html[] = " " . $options[$option_name]["option_label"] . " ";
183 | $html[] = " ";
184 | $html[] = " " . $options[$option_name]["option_description"] . " ";
185 | $html[] = " ";
186 | $html[] = " ";
187 | $html[] = " ";
188 | for ($i = 0; $i < sizeof($values); $i+=2) {
189 | $html[] = " ";
190 | $html[] = " " . $values[$i+1] . " ";
191 | }
192 | $html[] = "
";
193 | $html[] = " ";
194 | $html[] = " ";
195 | }
196 |
197 | function checked($option_name, $values) {
198 |
199 | global $options;
200 | global $cookie_options;
201 | $option_values = $options[$option_name]["option_values"];
202 |
203 | if ( is_array($cookie_options[$option_name]) ) {
204 | foreach ( $cookie_options[$option_name] as $cookie_option_value ) {
205 | if ( $cookie_option_value == $values ) {
206 | return " checked=\"checked\"";
207 | }
208 | }
209 | } else {
210 | if ( $cookie_options[$option_name] == $values ) {
211 | return " checked=\"checked\"";
212 | }
213 | }
214 | }
215 |
216 | function create_select($option_name, $option = array(), $var) {
217 |
218 | global $html;
219 | global $cookie_options;
220 | // $ var is the array variable, e.g. $org
221 |
222 | $values = $option["option_values"];
223 | $selected = "";
224 | $js = "";
225 |
226 | $html[] = " ";
227 | $html[] = " ";
228 | $html[] = " " . $option["option_label"] . " ";
229 | $html[] = " ";
230 | $html[] = " " . $option["option_description"] . " ";
231 | $html[] = " ";
232 | $html[] = " ";
233 |
234 | if ( $option_name == "cssfile" ) {
235 | $js = " onchange='change_stylesheet();'";
236 | }
237 |
238 | $html[] = " ";
239 | foreach ($var as $key => $value) {
240 | if ( $cookie_options[$option_name] == $key ) {
241 | $selected = "selected";
242 | } else {
243 | $selected = "";
244 | }
245 | $html[] = " " . $value . " ";
246 | }
247 |
248 | $html[] = " ";
249 | $html[] = " ";
250 | }
251 |
252 |
253 | //####################################################################
254 | //### main ###########################################################
255 | //####################################################################
256 |
257 | // These files must exist, in the same folder as this script.
258 | include "dmarcts-report-viewer-config.php";
259 | include "dmarcts-report-viewer-common.php";
260 |
261 | // Get all configuration options
262 | // --------------------------------------------------------------------------
263 | configure();
264 |
265 |
266 | // Make a DB Connection
267 | // --------------------------------------------------------------------------
268 | $dbh = connect_db($dbtype, $dbhost, $dbport, $dbname, $dbuser, $dbpass);
269 |
270 |
271 | // Get all css files in dmartcts directory
272 | // --------------------------------------------------------------------------
273 | $cssfiles = array();
274 | $dir = dirname(__FILE__);
275 | $scan_arr = scandir($dir);
276 | $files_arr = array_diff($scan_arr, array('.','..') );
277 |
278 | foreach ($files_arr as $file) {
279 | $file_ext = pathinfo($file, PATHINFO_EXTENSION);
280 | if ( $file_ext=="css" ) {
281 | $cssfiles[$file] = $file;
282 | }
283 | }
284 |
285 |
286 | // Get all domains reported
287 | // --------------------------------------------------------------------------
288 | $sql="
289 | SELECT
290 | DISTINCT domain
291 | FROM
292 | report
293 | ORDER BY domain";
294 |
295 | $query = $dbh->query($sql);
296 |
297 | $domains['all'] = "[all]";
298 | foreach($query as $row) {
299 | $domains[$row['domain']] = $row['domain'];
300 | }
301 |
302 | // Get all reporters
303 | // --------------------------------------------------------------------------
304 | $sql="
305 | SELECT
306 | DISTINCT org
307 | FROM
308 | report
309 | ORDER BY org";
310 |
311 |
312 | $i = 1;
313 | $dmarc_result_select['all'] = "[all]";
314 | foreach($dmarc_result as $key => $value) {
315 | $dmarc_result_select[$key] = $value['text'];
316 | $i++;
317 | }
318 |
319 |
320 | $i = 1;
321 | $report_status_select['all'] = "[all]";
322 | foreach($dmarc_result as $key => $value) {
323 | $report_status_select[$key] = $value['status_text'];
324 | $i++;
325 | }
326 |
327 |
328 | $query = $dbh->query($sql);
329 | $orgs['all'] = "[all]";
330 | foreach($query as $row) {
331 | $orgs[$row['org']] = $row['org'];
332 | }
333 |
334 |
335 | // Generate Page with report list and report data (if a report is selected).
336 | // --------------------------------------------------------------------------
337 | echo html(
338 | $domains,
339 | $orgs,
340 | $dmarc_result_select,
341 | $report_status_select,
342 | $report_list_columns,
343 | $cssfiles
344 | );
345 | // }
346 | ?>
347 |
--------------------------------------------------------------------------------
/dmarcts-report-viewer.php:
--------------------------------------------------------------------------------
1 | .
22 | //
23 | //####################################################################
24 | //### configuration ##################################################
25 | //####################################################################
26 |
27 | // Copy dmarcts-report-viewer-config.php.sample to
28 | // dmarcts-report-viewer-config.php and edit with the appropriate info
29 | // for your database authentication and location.
30 | //
31 | // Edit the configuration variables in dmarcts-report-viewer.js with your preferences.
32 | //
33 | //####################################################################
34 | //### functions ######################################################
35 | //####################################################################
36 |
37 | function html ($domains = array(), $orgs = array(), $periods = array() ) {
38 |
39 | global $dmarc_result;
40 |
41 | global $options;
42 | global $cookie_options;
43 | $html = array();
44 | $html[] = "";
45 | $html[] = "";
46 | $html[] = " ";
47 | $html[] = " DMARC Report Viewer ";
48 | $html[] = " ";
49 | $html[] = " ";
50 |
51 | if ( $cookie_options['xml_data_hljs'] ) {
52 | $html[] = " ";
53 | $html[] = " ";
54 | }
55 |
56 | $html[] = " ";
63 | $html[] = " ";
64 | $html[] = " ";
65 | $html[] = " ";
66 | $html[] = " ";
67 |
68 | $html[] = " ";
69 | $html[] = "
70 | ";
71 | $html[] = "
";
72 |
73 |
74 | // Optionblock form
75 | // --------------------------------------------------------------------------
76 | $html[] = " ";
193 |
194 |
195 | // Dropdown menu
196 | // --------------------------------------------------------------------------
197 | $html[] = "";
198 |
199 |
200 | // Report divs
201 | // --------------------------------------------------------------------------
202 | $html[] = "";
203 | $html[] = "DMARC Reports
";
204 | $html[] = "";
205 | $html[] = "
";
206 | $html[] = "";
207 |
208 | $html[] = "";
209 | $html[] = "";
210 | $html[] = "
";
211 | $html[] = "";
212 |
213 | // Page Footer
214 | // --------------------------------------------------------------------------
215 | $html[] = " ";
216 | $html[] = " ";
217 | $html[] = "";
218 |
219 | return implode("\n",$html);
220 | }
221 |
222 | //####################################################################
223 | //### main ###########################################################
224 | //####################################################################
225 |
226 | // These files must exist, in the same folder as this script.
227 | include "dmarcts-report-viewer-config.php";
228 | include "dmarcts-report-viewer-common.php";
229 |
230 | // Get all configuration options
231 | // --------------------------------------------------------------------------
232 | configure();
233 |
234 | setcookie("dmarcts-options-tmp", "", time() - 3600, "/");
235 |
236 | // Make a DB Connection
237 | // --------------------------------------------------------------------------
238 | $dbh = connect_db($dbtype, $dbhost, $dbport, $dbname, $dbuser, $dbpass);
239 |
240 |
241 | // Get all domains reported
242 | // --------------------------------------------------------------------------
243 | $sql = "
244 | SELECT DISTINCT
245 | domain
246 | FROM
247 | report
248 | ORDER BY
249 | domain
250 | ";
251 |
252 | $query = $dbh->query($sql);
253 |
254 | foreach($query as $row) {
255 | $domains[] = $row['domain'];
256 | }
257 |
258 | // Get all organisations
259 | // --------------------------------------------------------------------------
260 | $sql = "
261 | SELECT DISTINCT
262 | org
263 | FROM
264 | report
265 | ORDER BY
266 | org
267 | ";
268 |
269 | $query = $dbh->query($sql);
270 |
271 | foreach($query as $row) {
272 | $orgs[] = $row['org'];
273 | }
274 |
275 | // Get all periods
276 | // --------------------------------------------------------------------------
277 | $sql = "
278 | (
279 | SELECT
280 | EXTRACT(YEAR FROM mindate) AS year,
281 | EXTRACT(MONTH FROM mindate) AS month
282 | FROM
283 | report
284 | )
285 | UNION
286 | (
287 | SELECT
288 | EXTRACT(YEAR FROM mindate) AS year,
289 | EXTRACT(MONTH FROM mindate) AS month
290 | FROM
291 | report
292 | )
293 | ORDER BY
294 | year DESC,
295 | month DESC
296 | ";
297 |
298 | $query = $dbh->query($sql);
299 |
300 | foreach($query as $row) {
301 | $periods[] = sprintf( "%'.04d-%'.02d", $row['year'], $row['month'] );
302 | }
303 |
304 | // When arrays of data do not exist, for example in a new & empty set-up, create empty arrays
305 | // --------------------------------------------------------------------------
306 | if(!isset($domains)){
307 | $domains = array();
308 | }
309 | if(!isset($orgs)){
310 | $orgs = array();
311 | }
312 | if(!isset($periods)){
313 | $periods = array();
314 | }
315 |
316 |
317 | // Generate Page with report list and report data (if a report is selected).
318 | // --------------------------------------------------------------------------
319 | echo html(
320 | $domains,
321 | $orgs,
322 | $periods
323 | );
324 | ?>
325 |
--------------------------------------------------------------------------------
/dmarcts-report-viewer-report-list.php:
--------------------------------------------------------------------------------
1 | .
22 | //
23 | //####################################################################
24 | //### configuration ##################################################
25 | //####################################################################
26 |
27 | // Copy dmarcts-report-viewer-config.php.sample to
28 | // dmarcts-report-viewer-config.php and edit with the appropriate info
29 | // for your database authentication and location.
30 | //
31 | // Edit the configuration variables in dmarcts-report-viewer.js with your preferences.
32 | //
33 | //
34 | //####################################################################
35 | //### functions ######################################################
36 | //####################################################################
37 |
38 | function tmpl_reportList($reports, $sort) {
39 |
40 | global $options;
41 | global $cookie_options;
42 |
43 | $reportlist[] = "";
44 |
45 | if (sizeof($reports) == 0) {
46 | $reportlist[] = "No Reports Match this filter Click the Reset button or choose a different value for DMARC Result , Month , Domain(s) or Reporter(s) .
";
47 | } else {
48 | $title_message_th = "Click to toggle sort direction by this column.";
49 | $title_message_tr = "Click to view detailed report data.";
50 |
51 | // Resizer handles
52 | // --------------------------------------------------------------------------
53 | $reportlist[] = "
";
54 | $reportlist[] = "
";
55 | $reportlist[] = "";
56 | $reportlist[] = " ";
57 | $reportlist[] = " ";
58 |
59 | $triangle = ($cookie_options['sort'] ? "asc":"desc") . "_triangle ";
60 | $reportlist[] = "
1 ";
61 | $reportlist[] = " ";
62 | $reportlist[] = "
1 ";
63 | $reportlist[] = " Start Date ";
64 | $reportlist[] = " End Date ";
65 | $reportlist[] = " Domain ";
66 | $reportlist[] = " Reporting Organization ";
67 | $reportlist[] = " Report ID ";
68 | $reportlist[] = " Messages ";
69 | $reportlist[] = " ";
70 | $reportlist[] = " ";
71 |
72 | $reportlist[] = " ";
73 | $reportsum = 0;
74 |
75 | foreach ($reports as $row) {
76 | $row = array_map('html_escape', $row);
77 | $reportlist[] = " ";
78 |
79 | $reportlist[] = " " . get_dmarc_result($row)['status_sort_key'] . " "; // Col 0
80 | $reportlist[] = " "; // Col 0
81 | $reportlist[] = " " . get_report_status($row)['status_sort_key'] . " "; // Col 0
82 | $reportlist[] = " ". format_date($row['mindate'], $cookie_options['date_format']). " "; // Col 1
83 | $reportlist[] = " ". format_date($row['maxdate'], $cookie_options['date_format']). " "; // Col 3
84 | $reportlist[] = " ". $row['domain']. " "; // Col 5
85 | $reportlist[] = " ". $row['org']. " "; // Col 6
86 | $reportlist[] = " ". $row['reportid'] . " ";
87 | $reportlist[] = " ". number_format($row['rcount']+0,0). " "; // Col 9
88 | $reportlist[] = " ";
89 | $reportsum += $row['rcount'];
90 | }
91 |
92 | $reportlist[] = " ";
93 | $reportlist[] = "Sum: ".number_format($reportsum,0)." ";
94 | $reportlist[] = "
";
95 |
96 | $reportlist[] = "";
97 | $reportlist[] = "";
98 | }
99 | #indent generated html by 2 extra spaces
100 | return implode("\n ",$reportlist);
101 | }
102 |
103 | //####################################################################
104 | //### main ###########################################################
105 | //####################################################################
106 |
107 | // These files are expected to be in the same folder as this script, and must exist.
108 | include "dmarcts-report-viewer-config.php";
109 | include "dmarcts-report-viewer-common.php";
110 |
111 | // Get all configuration options
112 | // --------------------------------------------------------------------------
113 | configure();
114 |
115 | $dom_select= '';
116 | $org_select= '';
117 | $per_select= '';
118 | $dmarc_select= '';
119 | $report_status = '';
120 | $where = '';
121 |
122 | // Parameters of GET
123 | // --------------------------------------------------------------------------
124 |
125 | if(isset($_GET['sortorder']) && is_numeric($_GET['sortorder'])){
126 | $sortorder=$_GET['sortorder']+0;
127 | }elseif(!isset($_GET['sortorder'])){
128 | $sortorder= isset( $default_sort ) ? $default_sort : 1;
129 | }else{
130 | die('Invalid sortorder flag');
131 | }
132 |
133 | if(isset($_GET['d'])){
134 | $dom_select=$_GET['d'];
135 | }else{
136 | $dom_select= '';
137 | }
138 |
139 | if( $dom_select == "all" ) {
140 | $dom_select= '';
141 | }
142 |
143 | if(isset($_GET['o'])){
144 | $org_select=$_GET['o'];
145 | }else{
146 | $org_select= '';
147 | }
148 |
149 | if( $org_select == "all" ) {
150 | $org_select= '';
151 | }
152 |
153 | if(isset($_GET['p'])){
154 | $per_select=$_GET['p'];
155 | }else{
156 | $per_select= date( 'Y-m' );
157 | }
158 |
159 | if( $per_select == "all" ) {
160 | $per_select= '';
161 | }
162 |
163 | if(isset($_GET['dmarc'])){
164 | $dmarc_select=$_GET['dmarc'];
165 | }else{
166 | $dmarc_select= '';
167 | }
168 |
169 | if(isset($_GET['rptstat'])){
170 | $report_status = $_GET['rptstat'];
171 | }else{
172 | $report_status = '';
173 | }
174 |
175 | // Debug
176 | // echo " D=$dom_select O=$org_select ";
177 | // echo " DMARC=$dmarc_select ";
178 |
179 | // Make a DB Connection
180 | // --------------------------------------------------------------------------
181 | $dbh = connect_db($dbtype, $dbhost, $dbport, $dbname, $dbuser, $dbpass);
182 |
183 | // Get allowed reports and cache them - using serial as key
184 | // --------------------------------------------------------------------------
185 | $reports = array();
186 |
187 | // set sort direction
188 | // --------------------------------------------------------------------------
189 | $sort = '';
190 | if( $sortorder ) {
191 | $sort = "ASC";
192 | } else {
193 | $sort = "DESC";
194 | }
195 |
196 | // Build SQL WHERE clause
197 |
198 | // DMARC Result
199 | // --------------------------------------------------------------------------
200 | switch ($dmarc_select) {
201 | case "all": // Everything
202 | break;
203 | case "DMARC_FAIL": // DMARC Fail
204 | $where .= ( $where <> '' ? " AND" : " WHERE" ) . " dmarc_result_min = 0 AND dmarc_result_max = 0";
205 | break;
206 | case "DMARC_PASS_AND_FAIL": // DMARC Pass and Fail
207 | $where .= ( $where <> '' ? " AND" : " WHERE" ) . " dmarc_result_min = 0 AND (dmarc_result_max = 1 OR dmarc_result_max = 2)";
208 | break;
209 | case "DMARC_OTHER_CONDITION": // Other condition: Yellow
210 | $where .= ( $where <> '' ? " AND" : " WHERE" ) . " dmarc_result_min >= 3 AND dmarc_result_max >= 3";
211 | break;
212 | case "DMARC_PASS": // DMARC Pass
213 | $where .= ( $where <> '' ? " AND" : " WHERE" ) . " (dmarc_result_min = 1 OR dmarc_result_min = 2) AND (dmarc_result_max <= 2)";
214 | break;
215 | default:
216 | break;
217 | }
218 |
219 | // Report Status
220 | // --------------------------------------------------------------------------
221 | if ( $report_status != "all" && $report_status != "" ) {
222 | $where .= ( $where <> '' ? " AND" : " WHERE" ) . " " . $dmarc_result[$report_status]['status_sql_where'];
223 | }
224 |
225 | // Domains
226 | // --------------------------------------------------------------------------
227 | if( $dom_select <> '' ) {
228 | $where .= ( $where <> '' ? " AND" : " WHERE" ) . " domain=" . $dbh->quote($dom_select);
229 | }
230 |
231 | // Organisations
232 | // --------------------------------------------------------------------------
233 | if( $org_select <> '' ) {
234 | $where .= ( $where <> '' ? " AND" : " WHERE" ) . " org=" . $dbh->quote($org_select);
235 | }
236 |
237 | // Periods
238 | // --------------------------------------------------------------------------
239 | if( $per_select <> '' ) {
240 | $ye = substr( $per_select, 0, 4) + 0;
241 | $mo = substr( $per_select, 5, 2) + 0;
242 | $where .= ( $where <> '' ? " AND" : " WHERE" )
243 | . " ((extract(year from mindate) = $ye AND extract(month from mindate) = $mo) "
244 | . " OR (extract(year from maxdate) = $ye AND extract(month from maxdate) = $mo)) ";
245 | }
246 |
247 | // Include the rcount via left join, so we do not have to make an sql query
248 | // for every single report.
249 | // --------------------------------------------------------------------------
250 |
251 | $sql = "
252 | SELECT
253 | report.*,
254 | rcount,
255 | dkim_align_min,
256 | spf_align_min,
257 | dkim_result_min,
258 | spf_result_min,
259 | dmarc_result_min,
260 | dmarc_result_max
261 | FROM
262 | report
263 | LEFT JOIN
264 | (
265 | SELECT
266 | SUM(rcount) AS rcount,
267 | serial,
268 | MIN(
269 | (CASE
270 | WHEN dkim_align = 'fail' THEN 0
271 | WHEN dkim_align = 'pass' THEN 2
272 | ELSE 1
273 | END)
274 | )
275 | AS dkim_align_min,
276 | MIN(
277 | (CASE
278 | WHEN spf_align = 'fail' THEN 0
279 | WHEN spf_align = 'pass' THEN 2
280 | ELSE 1
281 | END)
282 | )
283 | AS spf_align_min,
284 | MIN(
285 | (CASE
286 | WHEN dkimresult = 'fail' THEN 0
287 | WHEN dkimresult = 'pass' THEN 2
288 | ELSE 1
289 | END)
290 | )
291 | AS dkim_result_min,
292 | MIN(
293 | (CASE
294 | WHEN spfresult = 'fail' THEN 0
295 | WHEN spfresult = 'pass' THEN 2
296 | ELSE 1
297 | END)
298 | )
299 | AS spf_result_min,
300 | MIN(
301 | (CASE
302 | WHEN dkim_align = 'fail' THEN 0
303 | WHEN dkim_align = 'pass' THEN 1
304 | ELSE 3
305 | END)
306 | +
307 | (CASE
308 | WHEN spf_align = 'fail' THEN 0
309 | WHEN spf_align = 'pass' THEN 1
310 | ELSE 3
311 | END)
312 | )
313 | AS dmarc_result_min,
314 | MAX(
315 | (CASE
316 | WHEN dkim_align = 'fail' THEN 0
317 | WHEN dkim_align = 'pass' THEN 1
318 | ELSE 3
319 | END)
320 | +
321 | (CASE
322 | WHEN spf_align = 'fail' THEN 0
323 | WHEN spf_align = 'pass' THEN 1
324 | ELSE 3
325 | END)
326 | )
327 | AS dmarc_result_max
328 | FROM
329 | rptrecord
330 | GROUP BY
331 | serial
332 | )
333 | AS rptrecord
334 | ON
335 | report.serial = rptrecord.serial
336 | $where
337 | ORDER BY
338 | " . $cookie_options['sort_column'] . ( $cookie_options['sort'] ? " ASC" : " DESC" )
339 | ;
340 |
341 | // Debug
342 | // echo " sql where = $where ";
343 | // echo "Data List sql: $sql ";
344 | // echo " per_select = " . urlencode($per_select) . " ";
345 |
346 | $query = $dbh->query($sql);
347 | foreach($query as $row) {
348 | if (true) {
349 | //add data by serial
350 | $reports[$row['serial']] = $row;
351 | }
352 | }
353 |
354 | // Generate Report List
355 | // --------------------------------------------------------------------------
356 | echo tmpl_reportList($reports, $sort);
357 |
358 | ?>
359 |
--------------------------------------------------------------------------------
/dmarcts-report-viewer-report-data.php:
--------------------------------------------------------------------------------
1 | .
22 | //
23 | //####################################################################
24 | //### configuration ##################################################
25 | //####################################################################
26 |
27 | // Copy dmarcts-report-viewer-config.php.sample to
28 | // dmarcts-report-viewer-config.php and edit with the appropriate info
29 | // for your database authentication and location.
30 | //
31 | // Edit the configuration variables in dmarcts-report-viewer.js with your preferences.
32 | //
33 | //
34 | //####################################################################
35 | //### functions ######################################################
36 | //####################################################################
37 |
38 | function tmpl_reportData($reportnumber, $reports, $host_lookup = 1) {
39 |
40 | global $dmarc_where;
41 | global $cookie_options;
42 |
43 | $title_message = "Click to toggle sort direction by this column";
44 |
45 | if (!$reportnumber) {
46 | return "";
47 | }
48 |
49 | $reportdata[] = "";
50 | $reportdata[] = "";
51 |
52 | $reportdata[] = "";
53 |
54 | $reportsum = 0;
55 |
56 | if (isset($reports[$reportnumber])) {
57 | $row = $reports[$reportnumber];
58 |
59 | $row['raw_xml'] = formatXML($row['raw_xml'], $reportnumber);
60 | foreach ($row as $k => $v) {
61 | if ($k !== 'raw_xml') {
62 | $row[$k] = html_escape($v);
63 | }
64 | }
65 |
66 | $reportdata[] = "";
67 | $reportdata[] = "
Report from ".$row['org']." for ".$row['domain']." ". format_date($row['mindate'], $cookie_options['date_format']). " to ".format_date($row['maxdate'], $cookie_options['date_format'])." Policies: adkim=" . $row['policy_adkim'] . ", aspf=" . $row['policy_aspf'] . ", p=" . $row['policy_p'] . ", sp=" . $row['policy_sp'] . ", pct=" . $row['policy_pct'] . "
";
68 |
69 | $reportdata[] = "
";
70 |
71 | $reportdata[] = "
";
72 |
73 | } else {
74 | return "Unknown report number!";
75 | }
76 |
77 | $reportdata[] = "";
78 | $reportdata[] = $row['raw_xml'];
79 | $reportdata[] = "
";
80 |
81 | $reportdata[] = "";
82 | if ( $cookie_options['dmarc_results_matching_only'] ) {
83 | $reportdata[] = "\"Show Only Matching Report Data records\" option is \"On\".
Some report records may not be displayed. ";
84 | }
85 | $reportdata[] = "
";
86 | $reportdata[] = " ";
87 | $reportdata[] = " ";
88 | $reportdata[] = " IP ";
89 | $reportdata[] = " Host Name ";
90 | $reportdata[] = " Message Count ";
91 | $reportdata[] = " Disposition ";
92 | $reportdata[] = " Reason ";
93 | $reportdata[] = " DKIM Domain ";
94 | $reportdata[] = " DKIM Auth ";
95 | $reportdata[] = " SPF Domain ";
96 | $reportdata[] = " SPF Auth ";
97 | $reportdata[] = " DKIM Align ";
98 | $reportdata[] = " SPF Align ";
99 | $reportdata[] = " DMARC ";
100 | $reportdata[] = " ";
101 | $reportdata[] = " ";
102 | $reportdata[] = " ";
103 |
104 | global $dbtype;
105 | global $dbh;
106 |
107 | $sql = "
108 | SELECT
109 | *,
110 | (CASE
111 | WHEN dkim_align = 'fail' THEN 0
112 | WHEN dkim_align = 'pass' THEN 1
113 | ELSE 3
114 | END)
115 | +
116 | (CASE
117 | WHEN spf_align = 'fail' THEN 0
118 | WHEN spf_align = 'pass' THEN 1
119 | ELSE 3
120 | END)
121 | AS dmarc_result_min,
122 | (CASE
123 | WHEN dkim_align = 'fail' THEN 0
124 | WHEN dkim_align = 'pass' THEN 1
125 | ELSE 3
126 | END)
127 | +
128 | (CASE
129 | WHEN spf_align = 'fail' THEN 0
130 | WHEN spf_align = 'pass' THEN 1
131 | ELSE 3
132 | END)
133 | AS dmarc_result_max
134 | FROM
135 | rptrecord
136 | WHERE
137 | serial = " . $reportnumber . ( $dmarc_where ? " AND $dmarc_where" : "" ) . "
138 | ORDER BY
139 | ip ASC
140 | ";
141 |
142 | $query = $dbh->query($sql);
143 | foreach($query as $row) {
144 | if ( $row['ip'] ) {
145 | $ip = long2ip(intval($row['ip']));
146 | } elseif ( $row['ip6'] ) {
147 | if ( $dbtype == 'pgsql') {
148 | $row['ip6'] = stream_get_contents($row['ip6']);
149 | }
150 | $ip = inet_ntop($row['ip6']);
151 | } else {
152 | $ip = "-";
153 | }
154 |
155 | /* escape html characters after exploring binary values, which will be messed up */
156 | $row = array_map('html_escape', $row);
157 |
158 | $reportdata[] = " ";
159 | $reportdata[] = " ". htmlspecialchars($ip) . " ";
160 | if ( $host_lookup ) {
161 | $reportdata[] = " ". gethostbyaddr($ip). " ";
162 | } else {
163 | $reportdata[] = " #off# ";
164 | }
165 | $reportdata[] = " ". $row['rcount']. " ";
166 | $reportdata[] = " ". $row['disposition']. " ";
167 | $reportdata[] = " ". $row['reason']. " ";
168 | $reportdata[] = " ". $row['dkimdomain']. " ";
169 | $reportdata[] = " ". $row['dkimresult']. " ";
170 | $reportdata[] = " ". $row['spfdomain']. " ";
171 | $reportdata[] = " ". $row['spfresult']. " ";
172 | $reportdata[] = " ". $row['dkim_align']. " ";
173 | $reportdata[] = " ". $row['spf_align']. " ";
174 | $reportdata[] = " " . get_dmarc_result($row)['result'] . " ";
175 | $reportdata[] = " ";
176 |
177 | $reportsum += $row['rcount'];
178 | }
179 | $reportdata[] = " ";
180 | $reportdata[] = "Sum: $reportsum ";
181 | $reportdata[] = "
";
182 |
183 | $reportdata[] = "
";
184 |
185 | $reportdata[] = "";
186 |
187 | #indent generated html by 2 extra spaces
188 | return implode("\n ",$reportdata);
189 | }
190 |
191 | function formatXML($raw_xml, $reportnumber) {
192 |
193 | global $dbh;
194 |
195 | $out = "";
196 | $html = "";
197 |
198 | $sql = "
199 | SELECT
200 | MIN(id) AS id_min,
201 | MAX(id) AS id_max
202 | FROM
203 | rptrecord
204 | WHERE
205 | serial = $reportnumber;
206 | ";
207 |
208 | $query = $dbh->query($sql);
209 |
210 | foreach($query as $row) {
211 | $id_min = $row['id_min'];
212 | $id_max = $row['id_max'];
213 | }
214 |
215 | $dom = new DOMDocument();
216 | $dom->preserveWhiteSpace = false;
217 | $dom->formatOutput = true;
218 | $dom->loadXML($raw_xml);
219 |
220 | // Extract and print from raw_xml, if it matches the regex pattern.
221 | if (preg_match("/<\?xml([^?>]*)\?>/", $raw_xml, $matches)) {
222 | $html .= "" . htmlspecialchars($matches[0]) . " ";
223 | }
224 |
225 | // Extract and print root from raw_xml.
226 | $root = $dom->firstChild;
227 | if (preg_match("/<". $root->localName ."([^>]*)>/", $raw_xml, $matches)) {
228 | $html .= "" . htmlspecialchars($matches[0]) . " ";
229 | }
230 |
231 | // Print all child nodes
232 | foreach ($root->childNodes as $element) {
233 | $out = $dom->saveXML($element);
234 | $out = htmlspecialchars($out);
235 |
236 | $elementName = $element->localName;
237 |
238 | // If element is a 'record', append database id to unique HTML id
239 | if ($elementName === "record") {
240 | $elementName .= $id_min;
241 | $id_min++;
242 | }
243 |
244 | $html .= "";
245 | }
246 |
247 | // Extract closing from raw_xml.
248 | if (preg_match("/<\/". $root->localName .">/", $raw_xml, $matches)) {
249 | $html .= "" . htmlspecialchars($matches[0]) . " ";
250 | }
251 |
252 | return $html;
253 | }
254 |
255 | //####################################################################
256 | //### main ###########################################################
257 | //####################################################################
258 |
259 | // These files are expected to be in the same folder as this script, and must exist.
260 | include "dmarcts-report-viewer-config.php";
261 | include "dmarcts-report-viewer-common.php";
262 |
263 | // Get all configuration options
264 | // --------------------------------------------------------------------------
265 | configure();
266 |
267 |
268 | // Parameters of GET
269 | // --------------------------------------------------------------------------
270 |
271 | if(isset($_GET['report']) && is_numeric($_GET['report'])){
272 | $reportid=$_GET['report']+0;
273 | }elseif(!isset($_GET['report'])){
274 | $reportid=false;
275 | }else{
276 | die('Invalid Report ID');
277 | }
278 |
279 | if(isset($_GET['hostlookup']) && is_numeric($_GET['hostlookup'])){
280 | $hostlookup=$_GET['hostlookup']+0;
281 | }elseif(!isset($_GET['hostlookup'])){
282 | $hostlookup= isset( $default_lookup ) ? $default_lookup : 1;
283 | }else{
284 | die('Invalid hostlookup flag');
285 | }
286 |
287 | if(isset($_GET['sortorder']) && is_numeric($_GET['sortorder'])){
288 | $sortorder=$_GET['sortorder']+0;
289 | }elseif(!isset($_GET['sortorder'])){
290 | $sortorder= isset( $default_sort ) ? $default_sort : 1;
291 | }else{
292 | die('Invalid sortorder flag');
293 | }
294 |
295 | if( $cookie_options['dmarc_results_matching_only'] && isset($_GET['dmarc']) ) {
296 | $dmarc_select=$_GET['dmarc'];
297 | }else{
298 | $dmarc_select= '';
299 | }
300 |
301 | if( $dmarc_select == "all" ) {
302 | $dmarc_select= '';
303 | }
304 |
305 | // Debug
306 | //echo " D=$dom_select O=$org_select ";
307 |
308 | // Make a DB Connection
309 | // --------------------------------------------------------------------------
310 | $dbh = connect_db($dbtype, $dbhost, $dbport, $dbname, $dbuser, $dbpass);
311 |
312 | // // Get allowed reports and cache them - using serial as key
313 | // --------------------------------------------------------------------------
314 | $reports = array();
315 |
316 | // set sort direction
317 | // --------------------------------------------------------------------------
318 | $sort = '';
319 | if( $sortorder ) {
320 | $sort = "ASC";
321 | } else {
322 | $sort = "DESC";
323 | }
324 |
325 | // DMARC
326 | // dkimresult spfresult
327 | // --------------------------------------------------------------------------
328 | switch ($dmarc_select) {
329 | case "DMARC_PASS": // DKIM and SPF Pass: Green
330 | $dmarc_where = "(rptrecord.dkimresult='pass' AND rptrecord.spfresult='pass')";
331 | break;
332 | case "DMARC_PASS_AND_FAIL": // DKIM or SPF Fail: Orange
333 | $dmarc_where = "(rptrecord.dkimresult='fail' OR rptrecord.spfresult='fail')";
334 | break;
335 | case "DMARC_FAIL": // DKIM and SPF Fail: Red
336 | $dmarc_where = "(rptrecord.dkimresult='fail' AND rptrecord.spfresult='fail')";
337 | break;
338 | case "DMARC_OTHER_CONDITION": // Other condition: Yellow
339 | $dmarc_where = "NOT ((rptrecord.dkimresult='pass' AND rptrecord.spfresult='pass') OR (rptrecord.dkimresult='fail' OR rptrecord.spfresult='fail') OR (rptrecord.dkimresult='fail' AND rptrecord.spfresult='fail'))"; // In other words, "NOT" all three other conditions
340 | break;
341 | default:
342 | break;
343 | }
344 |
345 | // Include the rcount via left join, so we do not have to make an sql query
346 | // for every single report.
347 | // --------------------------------------------------------------------------
348 |
349 | $sql = "
350 | SELECT
351 | *
352 | FROM
353 | report
354 | WHERE
355 | serial = " . $dbh->quote($reportid)
356 | ;
357 |
358 | // Debug
359 | // echo "Data Report sql: $sql ";
360 |
361 | $query = $dbh->query($sql);
362 | foreach($query as $row) {
363 | if (true) {
364 | //add data by serial
365 | $reports[$row['serial']] = $row;
366 | }
367 | }
368 |
369 | // Generate Page with report list and report data (if a report is selected).
370 | // --------------------------------------------------------------------------
371 | echo tmpl_reportData($reportid, $reports, $hostlookup );
372 |
373 | ?>
374 |
--------------------------------------------------------------------------------
/default.css:
--------------------------------------------------------------------------------
1 | /*
2 | default.css - The default theme css file for dmarcts-report-viewer, a PHP based viewer of parsed DMARC reports.
3 | Copyright (C) 2016 TechSneeze.com, John Bieling and John P. New
4 | with additional extensions (sort order) of Klaus Tachtler.
5 |
6 | Available at:
7 | https://github.com/techsneeze/dmarcts-report-viewer
8 |
9 | This file is free software: you can redistribute it and/or modify it
10 | under the terms of the GNU General Public License as published by the Free
11 | Software Foundation, either version 3 of the License, or (at your option)
12 | any later version.
13 |
14 | This file is distributed in the hope that it will be useful, but WITHOUT
15 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17 | more details.
18 |
19 | You should have received a copy of the GNU General Public License along with
20 | this program. If not, see .
21 | */
22 |
23 | /* All colors are controlled by the following section */
24 | :root {
25 | /* Ordered from darkest the lightest */
26 | --text: black;
27 | --text_over_colorbg: #000000;
28 | --shadow: grey;
29 | --header: silver;
30 | --selected: gainsboro;
31 | --hover: whitesmoke;
32 | --background: white;
33 |
34 | --link: #0000EE;
35 | --link_visited: #551A8B;
36 | --green: #00FF00;
37 | --yellow: #FFFF00;
38 | --orange: #FFA500;
39 | --red: #FF0000;
40 | --xml_highlighted: lightcyan;
41 | --xml_pinned: #99ffff;
42 | }
43 |
44 | body {
45 | color: var(--text);
46 | background-color: var(--background);
47 | }
48 |
49 | h1 {
50 | }
51 |
52 | pre {
53 | margin: 0px;
54 | cursor: pointer;
55 | }
56 |
57 | a {
58 | color: var(--link);
59 | }
60 |
61 | option.green {
62 | background-color: var(--green);
63 | }
64 |
65 | option.yellow {
66 | background-color: var(--yellow);
67 | }
68 |
69 | option.orange {
70 | background-color: var(--orange);
71 | }
72 |
73 | option.red {
74 | background-color: var(--red);
75 | }
76 |
77 | a:visted {
78 | color: var(--link_visited);
79 | }
80 |
81 | .title {
82 | font-size: 140%;
83 | font-weight: bold;
84 | text-align: center;
85 | padding: 5px 0px;
86 | }
87 |
88 | #screen_overlay {
89 | top: 0px;
90 | left: 0px;
91 | height: 100%;
92 | width: 100%;
93 | position: absolute;
94 | display: block;
95 | z-index: 1;
96 | }
97 |
98 | table.optionlist tr.option_title {
99 | font-size: 120%;
100 | font-weight: bold;
101 | text-align: left;
102 | background-color: var(--header);
103 | }
104 |
105 | table.optionlist {
106 | margin: auto;
107 | border-spacing: 0 15px;
108 | clear: both;
109 | cursor: inherit;
110 | }
111 |
112 | table.optionlist td {
113 | padding-right: 10px;
114 | vertical-align: baseline;
115 | padding-left: 10px;
116 | }
117 |
118 | table.optionlist td.left_column {
119 | padding-right: 10px;
120 | padding-left: 10px;
121 | vertical-align: baseline;
122 | border-right: 1px solid var(--text);
123 | width: 50%
124 | }
125 |
126 | table.optionlist td.right_column {
127 | vertical-align: middle;
128 | }
129 | table.optionlist td {
130 | vertical-align: baseline;
131 | }
132 |
133 | table.optionlist span.bold {
134 | vertical-align: baseline;
135 | font-weight: bold;
136 | }
137 |
138 | .option_description {
139 | font-family: sans-serif;
140 | font-size: 95%;
141 | font-style: italic;
142 | }
143 |
144 | table.reportlist {
145 | margin: auto;
146 | border-collapse: collapse;
147 | clear: both;
148 | cursor: pointer;
149 | }
150 |
151 | table.reportlist td {
152 | padding:0px 10px;
153 | }
154 |
155 | table.reportlist td.circle_container {
156 | padding-left: 0px;
157 | padding-right: 0px;
158 | }
159 |
160 | table.reportlist td span.status_sort_key {
161 | display: none;
162 | }
163 |
164 | table.reportlist th {
165 | padding:3px;
166 | position: -webkit-sticky; /* Safari */
167 | position: sticky;
168 | top: 0;
169 | background-color: var(--header);
170 | white-space: nowrap;
171 | }
172 |
173 | table.reportlist th.circle_container {
174 | padding-left: 0px;
175 | padding-right: 0px;
176 | }
177 |
178 | table.reportlist tr:hover {
179 | background-color: var(--hover);
180 | }
181 |
182 | table.reportlist tbody tr:first-child td {
183 | padding-top: 10px;
184 | }
185 |
186 | table.reportlist tr.sum {
187 | border-top: 1px solid var(--shadow);
188 | }
189 |
190 | table.reportlist tr.selected {
191 | background-color: var(--selected);
192 | }
193 |
194 | table.reportlist td.hidden, table.reportlist th.hidden {
195 | display: none;
196 | }
197 |
198 | .reportdesc {
199 | display:inline-block;
200 | font-weight: bold;
201 | padding: 1em 0;
202 | margin: 0 auto;
203 | }
204 |
205 | .reportdesc_container {
206 | border-top: 2px solid var(--shadow);
207 | margin: 0 auto;
208 | }
209 |
210 | table.reportdata {
211 | margin: 0 auto;
212 | }
213 |
214 | table.reportdata thead {
215 | cursor:pointer;
216 | }
217 |
218 | table.reportdata tr {
219 | color: var(--text_over_colorbg);
220 | text-align: center;
221 | padding: 3px;
222 | }
223 |
224 | table.reportdata th {
225 | color: var(--text);
226 | }
227 |
228 | table.reportdata tr th {
229 | text-align: center;
230 | padding: 3px;
231 | position: -webkit-sticky; /* Safari */
232 | position: sticky;
233 | top: 0px;
234 | background-color: var(--header);
235 | }
236 |
237 | table.reportdata tr.sum {
238 | cursor: default;
239 | color: var(--text);
240 | }
241 |
242 | table.reportdata td.right {
243 | text-align: right;
244 | }
245 |
246 | table.reportdata tr.red {
247 | background-color: var(--red);
248 | }
249 |
250 | table.reportdata td.red {
251 | background-color: var(--red);
252 | }
253 |
254 | table.reportdata tr.orange {
255 | background-color: var(--orange);
256 | }
257 |
258 | table.reportdata td.orange {
259 | background-color: var(--orange);
260 | }
261 |
262 | table.reportdata tr.green {
263 | background-color: var(--green);
264 | }
265 |
266 | table.reportdata td.green {
267 | background-color: var(--green);
268 | }
269 |
270 | table.reportdata tr.yellow {
271 | background-color: var(--yellow);
272 | }
273 |
274 | table.reportdata td.yellow {
275 | background-color: var(--yellow);
276 | }
277 |
278 | table.reportdata tr.highlight {
279 | background-color: var(--xml_highlighted);
280 | color: var(--text)
281 | }
282 |
283 | table.reportdata tr.pinned {
284 | background-color: var(--xml_pinned);
285 | color: var(--text)
286 | }
287 |
288 | .highlight {
289 | background-color: var(--xml_highlighted);
290 | color: var(--text)
291 | }
292 |
293 | .pinned {
294 | background-color: var(--xml_pinned);
295 | color: var(--text)
296 | }
297 |
298 | .footer {
299 | font-size: 70%;
300 | text-align: center;
301 | border-top: 2px solid var(--shadow);
302 | width: 100%;
303 | margin: 0px auto;
304 | padding: 10px 0px;
305 | position: fixed;
306 | bottom: 0;
307 | background-color: var(--background);
308 | }
309 |
310 | form {
311 | vertical-align: bottom;
312 | }
313 |
314 | .optionblock {
315 | white-space: nowrap;
316 | overflow: auto;
317 | font-size: 80%;
318 | padding: .5em;
319 | background-color: var(--header);
320 | margin: auto;
321 | text-align: center;
322 | }
323 |
324 | .optionlabel {
325 | font-weight: bold;
326 | }
327 |
328 | .options {
329 | margin-right: .5em;
330 | display: inline-block;
331 | border-right: 1px solid var(--text);
332 | padding-right: 1em;
333 | cursor: default;
334 | text-align: center;
335 | vertical-align: bottom;
336 | }
337 |
338 | .menu_icon {
339 | width: 1.5em;
340 | cursor: default;
341 | border: .2em solid var(--header);
342 | border-radius: .3em;
343 | padding: .3em;
344 | vertical-align: bottom;
345 | transition: 0.15s all linear;
346 | }
347 |
348 | .menu_icon:hover{
349 | border-color: var(--shadow);
350 | }
351 |
352 | .menu {
353 | font-family: arial, sans-serif;
354 | color: var(--text);
355 | position: absolute;
356 | display: none;
357 | z-index: 2;
358 | background: var(--hover);
359 | margin-top: 5px; /* Controls how close the main bubble is the the calling div */
360 | border-radius: 2px;
361 | box-shadow: 7px 7px 3px var(--shadow);
362 | }
363 |
364 | /* menu callout tail */
365 | .menu::after {
366 | position: absolute;
367 | content: '';
368 | border: 15px solid transparent; /* The 'border-width' property controls the size of the tail and should be the same as .top.menu::after {top: } */
369 | }
370 |
371 | /* Tail position on top */
372 | .top.menu::after {
373 | /* up triangle */
374 | border-bottom-color: var(--hover);
375 | border-top: 0;
376 | top: -15px; /* Controls how close the tail is to the main bubble and should be the same as .menu::after {border-width:} */
377 | left: 95%; /* Controls how close the tail is to the right corner of the main bubble */
378 | margin-left: -20px;
379 | }
380 |
381 | .menu_option {
382 | width: 100%;
383 | padding: 7px 0;
384 | margin-right: 20px;
385 | cursor: default;
386 | }
387 |
388 | .menu_option:hover {
389 | background-color: var(--selected);
390 | }
391 |
392 | .center {
393 | text-align: center;
394 | }
395 |
396 | .right {
397 | text-align: right;
398 | }
399 |
400 | .circle {
401 | width: 7px;
402 | height: 14px;
403 | margin-top: 4px;
404 | margin-bottom: 2px;
405 | }
406 |
407 | .circle_right {
408 | border-bottom-right-radius: 500px;
409 | border-top-right-radius: 500px;
410 | border-left: 0;
411 | display: inline-block;
412 | }
413 |
414 | .circle_left {
415 | border-bottom-left-radius: 500px;
416 | border-top-left-radius: 500px;
417 | border-right: 0;
418 | display: inline-block;
419 | }
420 |
421 | .circle_yellow {
422 | width: 6px;
423 | height: 12px;
424 | background-color: yellow;
425 | border-top: 1px solid var(--selected);
426 | border-right: 1px solid var(--selected);
427 | border-bottom: 1px solid var(--selected);
428 |
429 | }
430 |
431 | .circle_green {
432 | background-color: lime;
433 | }
434 |
435 | .circle_orange {
436 | background-color: orange;
437 | }
438 |
439 | .circle_red {
440 | background-color: red;
441 | }
442 |
443 | .circle_black {
444 | background-color: var(--text);
445 | }
446 |
447 | .asc_triangle:after {
448 | content: ' \25B2';
449 | font-size: 15px;
450 | vertical-align: top;
451 | }
452 |
453 | .desc_triangle:after {
454 | content: ' \25BC';
455 | font-size: 15px;
456 | }
457 |
458 | .resizer {
459 | display: none;
460 | position: absolute;
461 | border-radius: 7px;
462 | background: var(--background);
463 | border: 2px solid var(--shadow);
464 | top: 8px;
465 | left: 50%;
466 | }
467 |
468 | .resizer_horizontal {
469 | width: 30px;
470 | height: 10px;
471 | cursor: ns-resize;
472 | }
473 |
474 | .resizer_vertical {
475 | width: 10px;
476 | height: 30px;
477 | cursor: ew-resize;
478 | }
479 |
480 | /* Cross-browser (hopefully) styling of buttons and inputs
481 | Based on https://github.com/filamentgroup/select-css */
482 |
483 | /* class applies to select element itself, not a wrapper element */
484 | .x-css {
485 | font-size: inherit;
486 | font-family: inherit;
487 | display: block;
488 | margin: auto;
489 | color: var(--text);
490 | padding: .2em 1.4em .2em .3em;
491 | box-sizing: border-box;
492 | border: .2em solid var(--header);
493 | border-radius: .3em;
494 | transition: 0.15s all linear;
495 | -moz-appearance: none;
496 | -webkit-appearance: none;
497 | appearance: none;
498 | background-image:
499 | /* Note: background-image uses 2 urls.
500 | The first is an svg data uri for the arrow icon.
501 | The second is the gradient for the icon.
502 | If you want to change the color, be sure to use `%23` instead of `#`, since it's a url.
503 | You can also swap in a different svg icon or an external image reference */
504 | url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%233DAEE9%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'),
505 | linear-gradient(to bottom, var(--background) 0%,var(--selected) 100%);
506 | background-repeat: no-repeat, repeat;
507 | /* arrow icon position (1em from the right, 50% vertical) , then gradient position*/
508 | background-position: right .5em top 50%, 0 0;
509 | /* icon size, then gradient */
510 | background-size: .65em auto, 100%;
511 | }
512 |
513 | .x-css-left-align {
514 | margin: 0;
515 | }
516 | .x-css option {
517 | color: var(--text_over_colorbg);
518 | }
519 |
520 | .x-css label {
521 | padding: 0 .5em 0 0;
522 | }
523 |
524 | div.x-css,
525 | button.x-css,
526 | input[type=submit].x-css,
527 | input[type=number].x-css,
528 | input[type=radio].x-css {
529 | display: inline-block;
530 | padding: .2em .5em;
531 | background-image: linear-gradient(to bottom, var(--background) 0%,var(--selected) 100%);
532 | background-repeat: repeat;
533 | background-position: 0 0;
534 | background-size: 100%;
535 | }
536 |
537 | input[type=radio].x-css {
538 | margin: 0 0 0 .15em;
539 | }
540 |
541 | /* button.x-css,
542 | input[type=submit].x-css {
543 | border: 2px outset var(--shadow);
544 | } */
545 |
546 | div.x-css {
547 | padding: .25em .3em .1em .3em;
548 | }
549 |
550 | input[type=number].x-css {
551 | padding: .1em .3em;
552 | }
553 |
554 | input[type=radio].x-css,
555 | label.x-css {
556 | background-image: unset;
557 | padding: 0;
558 | border-radius: 49%;
559 | width: .8em;
560 | height: .8em;
561 | border: .15em solid var(--text);
562 | }
563 |
564 | button:active,
565 | input[type=submit]:active {
566 | transform: translate(0.08em, 0.1em);
567 | }
568 |
569 | input[type=radio].x-css:checked {
570 | background-color: #3daee9;
571 | }
572 |
573 | /* Hide arrow icon in IE browsers */
574 | .x-css::-ms-expand {
575 | display: none;
576 | }
577 |
578 | /* Hover style */
579 | .x-css:hover {
580 | border-color: var(--shadow);
581 | }
582 |
583 | .x-css:focus,
584 | .menu_icon:focus,
585 | button:focus,
586 | input[type=number]:focus,
587 | input[type=submit]:focus,
588 | input[type=radio]:focus {
589 | /* border-color: #aaa; */
590 | /* It'd be nice to use -webkit-focus-ring-color here but it doesn't work on box-shadow */
591 | box-shadow: 0 0 1px 2px rgba(59, 153, 252, .7);
592 | box-shadow: 0 0 0 2px -moz-mac-focusring;
593 | color: var(--text);
594 | outline: none;
595 | }
596 |
597 | /* Support for rtl text, explicit support for Arabic and Hebrew */
598 | *[dir="rtl"] .x-css,
599 | :root:lang(ar) .x-css,
600 | :root:lang(iw) .x-css {
601 | background-position: left .7em top 50%, 0 0;
602 | padding: .6em .8em .5em 1.4em;
603 | }
604 |
605 | /* Disabled styles */
606 | select.x-css:disabled,
607 | select.x-css[aria-disabled=true] {
608 | color: var(--shadow);
609 | background-image:
610 | /* Note: background-image uses 2 urls.
611 | The first is an svg data uri for the arrow icon.
612 | The second is the gradient for the icon.
613 | If you want to change the color, be sure to use `%23` instead of `#`, since it's a url.
614 | You can also swap in a different svg icon or an external image reference */
615 | url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22graytext%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'),
616 | linear-gradient(to bottom, #ffffff 0%,#e5e5e5 100%);
617 | }
618 |
619 | .x-css:disabled,
620 | .x-css[aria-disabled=true] {
621 | background-image: linear-gradient(to bottom, var(--selected) 0%,var(--hover) 100%);
622 | color: var(--shadow);
623 | }
624 |
625 | .x-css:disabled:hover,
626 | .x-css[aria-disabled=true] {
627 | border: .1em solid var(--hover);
628 | }
629 |
630 | /* Will have to work on this if ever need to disable radio buttons */
631 | /* input[type=radio].x-css {} */
632 |
--------------------------------------------------------------------------------
/dmarcts-report-viewer-common.php:
--------------------------------------------------------------------------------
1 | .
22 | //
23 | //####################################################################
24 | //### configuration ##################################################
25 | //####################################################################
26 |
27 | // Copy dmarcts-report-viewer-config.php.sample to
28 | // dmarcts-report-viewer-config.php and edit with the appropriate info
29 | // for your database authentication and location.
30 | //
31 | // Edit the configuration variables in dmarcts-report-viewer.js with your preferences.
32 |
33 | //####################################################################
34 | //### variables ######################################################
35 | //####################################################################
36 |
37 | $cookie_name = "dmarcts-options";
38 |
39 | // The order in which the options appear here is the order they appear in the DMARC Results dropdown box
40 | $dmarc_result = array(
41 |
42 | 'DMARC_PASS' => array(
43 | 'text' => 'Pass',
44 | 'status_text' => 'All Passed',
45 | 'color' => 'green',
46 | 'status_sort_key' => 3,
47 | 'status_sql_where' => "dkim_align_min = 2 AND spf_align_min = 2 AND dkim_result_min = 2 AND spf_result_min = 2 AND dmarc_result_min = 2 AND dmarc_result_max = 2",
48 | ),
49 | 'DMARC_FAIL' => array(
50 | 'text' => 'Fail',
51 | 'status_text' => 'All Failed',
52 | 'color' => 'red',
53 | 'status_sort_key' => 0,
54 | 'status_sql_where' => "dkim_align_min = 0 AND spf_align_min = 0 AND dkim_result_min = 0 AND spf_result_min = 0 AND dmarc_result_min = 0 AND dmarc_result_max = 0",
55 | ),
56 | 'DMARC_PASS_AND_FAIL' => array(
57 | 'text' => 'Mixed',
58 | 'status_text' => 'At least one failed result',
59 | 'color' => 'orange',
60 | 'status_sort_key' => 1,
61 | 'status_sql_where' => "( dkim_align_min = 0 OR spf_align_min = 0 OR dkim_result_min = 0 OR spf_result_min = 0 OR dmarc_result_min = 0 OR dmarc_result_max = 0 )",
62 | ),
63 | 'DMARC_OTHER_CONDITION' => array(
64 | 'text' => 'Other',
65 | 'status_text' => 'Other condition',
66 | 'color' => 'yellow',
67 | 'status_sort_key' => 2,
68 | 'status_sql_where' => "( dkim_align_min = 1 OR spf_align_min = 1 OR dkim_result_min = 1 OR spf_result_min = 1 OR dmarc_result_min >= 3 OR dmarc_result_max >= 3 )",
69 | ),
70 | );
71 |
72 | // Sortable Report List column headers
73 | // --------------------------------------------------------------------------
74 | // Array to be used in 'Default sort column' option in dmarcts-report-viewer-options.php
75 |
76 | $report_list_columns = array(
77 | "mindate" => "Start Date",
78 | "maxdate" => "End Date",
79 | "domain" => "Domain",
80 | "org" => "Reporter",
81 | "reportid" => "Report ID",
82 | "rcount" => "# Messages"
83 | );
84 |
85 | // Program Options
86 | // --------------------------------------------------------------------------
87 |
88 | // When a new option is added, check the size of the cookie stored. The cookie size should be less than half of the maximum cookie size allowed per domain.
89 | // Less than half because sometimes the cookie is stored twice (once as dmarcts-options and once as dmarcts-options-tmp). Most browsers have a cookie limit of 4KB.
90 | // Currently, the following options generate a cookie of about 0.5KB
91 |
92 | // Option Names must be unique.
93 | // The order in which the options appear below is the order they are rendered in the browser.
94 | // If sections are re-arranged, you don't have to re-name the corresponding heading (i.e. the heading option name (e.g. option_group_3_heading)) because it has no bearing on the order rendered in the browser.
95 | $options = array(
96 | "option_group_1_heading" => array(
97 | "option_type" => "heading",
98 | "option_label" => "Appearance",
99 | "option_values" => "",
100 | "option_value" => "",
101 | "option_description" => "",
102 | ),
103 | "cssfile" => array(
104 | "option_type" => "select",
105 | "option_label" => "Default css file",
106 | "option_values" => "\$cssfiles",
107 | "option_value" => "default.css",
108 | "option_description" => "Name of the css file to be used. The dropdown list is automatically generated from any css files in the main dmarcts-report-viewer directory. The css is immediately applied to this page when selected.",
109 | ),
110 | "date_format" => array(
111 | "option_type" => "text",
112 | "option_label" => "Date Format",
113 | "option_values" => "",
114 | "option_value" => "Y-m-d G:i:s T",
115 | "option_description" => "String to format the dates in the Report List and the Report Description. The default format string is Y-m-d G:i:s T which shows, for example, 2020-01-01 00:00:00 UTC. For the allowable options in the format string, see the documentation .",
116 | ),
117 | "option_group_2_heading" => array(
118 | "option_type" => "heading",
119 | "option_label" => "Filters",
120 | "option_values" => "",
121 | "option_value" => "",
122 | "option_description" => "Default filters",
123 | ),
124 | "DMARC" => array(
125 | "option_type" => "select",
126 | "option_label" => "Default DMARC Result",
127 | "option_values" => "\$dmarc_result_select",
128 | "option_value" => "all",
129 | "option_description" => "Default for DMARC Result drop-down list.",
130 | ),
131 | "dmarc_results_matching_only" => array(
132 | "option_type" => "radio",
133 | "option_label" => "Show Only Matching Report Data records.",
134 | "option_values" => array(1,"On",0,"Off"),
135 | "option_value" => 0,
136 | "option_description" => "When enabled, only those records matching the DMARC Results dropdown box are shown in the Report Data table.",
137 | ),
138 | "ReportStatus" => array(
139 | "option_type" => "select",
140 | "option_label" => "Default Report Data Status",
141 | "option_values" => "\$report_status_select",
142 | "option_value" => "all",
143 | "option_description" => "Default for Report Data Status drop-down list.",
144 | ),
145 | "Period" => array(
146 | "option_type" => "radio",
147 | "option_label" => "Default period",
148 | "option_values" => array(0,"All",1,"Current Month"),
149 | "option_value" => 1,
150 | "option_description" => "Default for the Month drop-down.",
151 | ),
152 | "Domain" => array(
153 | "option_type" => "select",
154 | "option_label" => "Default domain",
155 | "option_values" => "\$domains",
156 | "option_value" => "all",
157 | "option_description" => "Default for the Domain(s) drop-down list.",
158 | ),
159 | "Organisation" => array(
160 | "option_type" => "select",
161 | "option_label" => "Default reporter",
162 | "option_values" => "\$orgs",
163 | "option_value" => "all",
164 | "option_description" => "Default for the Reporter(s) drop-down list.",
165 | ),
166 | "option_group_3_heading" => array(
167 | "option_type" => "heading",
168 | "option_label" => "Initial Settings",
169 | "option_values" => "",
170 | "option_value" => "",
171 | "option_description" => "Startup Defaults",
172 | ),
173 | "HostLookup" => array(
174 | "option_type" => "radio",
175 | "option_label" => "Host lookup",
176 | "option_values" => array(1,"On",0,"Off"),
177 | "option_value" => 1,
178 | "option_description" => "Turning off host lookup speeds up the display of the results, especially in the case of mail servers that have ceased to exist.",
179 | ),
180 | "report_list_height_percent" => array(
181 | "option_type" => "number",
182 | "option_label" => "Report List - Initial Height",
183 | "option_values" => array("units"=>"percent","min"=>"0","max"=>100),
184 | "option_value" => 60,
185 | "option_description" => "Initial height of the Report List window, a percentage of the height of the main browser window.",
186 | ),
187 | "sort_column" => array(
188 | "option_type" => "select",
189 | "option_label" => "Default sort column",
190 | "option_values" => "\$report_list_columns",
191 | "option_value" => "maxdate",
192 | "option_description" => "Report List column to sort initially.",
193 | ),
194 | "sort" => array(
195 | "option_type" => "radio",
196 | "option_label" => "Default sort order",
197 | "option_values" => array(1,"Ascending",0,"Descending"),
198 | "option_value" => 0,
199 | "option_description" => "Default sort order of Report List column chosen above.",
200 | ),
201 | "option_group_4_heading" => array(
202 | "option_type" => "heading",
203 | "option_label" => "XML Data",
204 | "option_values" => "",
205 | "option_value" => "",
206 | "option_description" => "Startup Defaults",
207 | ),
208 | "xml_data_open" => array(
209 | "option_type" => "radio",
210 | "option_label" => "Show Report Data XML",
211 | "option_values" => array(1,"On",0,"Off"),
212 | "option_value" => 0,
213 | "option_description" => "When a report is selected in the Report List, automatically open the XML view along with the Report Table.",
214 | ),
215 | "report_data_xml_width_percent" => array(
216 | "option_type" => "number",
217 | "option_label" => "Report Data XML - Initial Width",
218 | "option_values" => array("units"=>"percent","min"=>"0","max"=>"100"),
219 | "option_value" => 25,
220 | "option_description" => "Initial width of the Report Data XML window when it is opened, a percentage of the width of the main browser window.",
221 | ),
222 | "xml_data_highlight" => array(
223 | "option_type" => "radio",
224 | "option_label" => "Use Report Data to Raw XML Highlighting",
225 | "option_values" => array(1,"On",0,"Off"),
226 | "option_value" => "1",
227 | "option_description" => "When the raw XML view is open, and when the mouse hovers over, or clicks on, a line of the Report Data table or the Report Data description, highlight the section in the raw XML that corresponds to that row or description. Also works in the opposite direction (i.e. hover/click on a XML record to highlight the corresponding Report Data table line or description). Facilitates determining which XML record corresponds to which line of the table.",
228 | ),
229 | "xml_data_hljs" => array(
230 | "option_type" => "radio",
231 | "option_label" => "Use XML Syntax Highlighting",
232 | "option_values" => array(1,"On",0,"Off"),
233 | "option_value" => "1",
234 | "option_description" => "Use syntax highlighting on the Raw XML. This uses a small external javascript file which may or may not slow down the program.",
235 | )
236 | // This option will be implemented in a future version of dmarcts-reports-viewer.
237 | // ),
238 | // "alignment_unknown" => array(
239 | // "option_type" => "radio",
240 | // "option_label" => "Unknown SPF/DKIM Alignments",
241 | // "option_values" => array(1,"Consider \"Failed\"",0,"Keep as \"Unknown\""),
242 | // "option_value" => "0",
243 | // "option_description" => "The DMARC specification dictates that reporting SPF/DKIM alignments is mandatory. However, there could be a situation where this information is not included. This option specifies whether or not those unknown results are included as an \"alignment failure\" or remain as \"unknown\".",
244 | // )
245 | );
246 |
247 |
248 | //####################################################################
249 | //### functions ######################################################
250 | //####################################################################
251 |
252 | function main() {
253 |
254 | include "dmarcts-report-viewer-config.php";
255 | }
256 |
257 | // This function sets variables for the DMARC Result portion (left half-circle) in the Report List
258 | function get_dmarc_result($row) {
259 |
260 | global $dmarc_result;
261 | $color = "";
262 | $color_sort_key = "";
263 | $result_text = "";
264 |
265 | if (($row['dmarc_result_min'] == 0) && ($row['dmarc_result_max'] == 0)) {
266 | $color = $dmarc_result['DMARC_FAIL']['color'];
267 | $color_sort_key = $dmarc_result['DMARC_FAIL']['status_sort_key'];
268 | $result_text = $dmarc_result['DMARC_FAIL']['text'];
269 | } elseif (($row['dmarc_result_min'] == 0) && ($row['dmarc_result_max'] == 1 || $row['dmarc_result_max'] == 2)) {
270 | $color = $dmarc_result['DMARC_PASS_AND_FAIL']['color'];
271 | $color_sort_key = $dmarc_result['DMARC_PASS_AND_FAIL']['status_sort_key'];
272 | $result_text = $dmarc_result['DMARC_PASS_AND_FAIL']['text'];
273 | } elseif (($row['dmarc_result_min'] == 1 || $row['dmarc_result_min'] == 2) && ($row['dmarc_result_max'] == 1 || $row['dmarc_result_max'] == 2)) {
274 | $color = $dmarc_result['DMARC_PASS']['color'];
275 | $color_sort_key = $dmarc_result['DMARC_PASS']['status_sort_key'];
276 | $result_text = $dmarc_result['DMARC_PASS']['text'];
277 | } else {
278 | $color = $dmarc_result['DMARC_OTHER_CONDITION']['color'];
279 | $color_sort_key = $dmarc_result['DMARC_OTHER_CONDITION']['status_sort_key'];
280 | $result_text = $dmarc_result['DMARC_OTHER_CONDITION']['text'];
281 | }
282 | return array('color' => $color, 'status_sort_key' => $color_sort_key, 'result' => $result_text);
283 | }
284 |
285 | // This function sets variables for the All Results portion (right half-circle) in the Report List table
286 | function get_report_status($row) {
287 |
288 | global $dmarc_result;
289 | $color = "";
290 | $color_sort_key = "";
291 | $status_text = "";
292 | $status_sql_where = "";
293 |
294 | $report_status_min = min($row['dkim_align_min'],$row['spf_align_min'],$row['dkim_result_min'],$row['spf_result_min'],$row['dmarc_result_min']);
295 |
296 | if ($row['dkim_align_min'] == 0 && $row['spf_align_min'] == 0 && $row['dkim_result_min'] == 0 && $row['spf_result_min'] == 0 && $row['dmarc_result_min'] == 0) {
297 | $color = $dmarc_result['DMARC_FAIL']['color'];
298 | $color_sort_key = $dmarc_result['DMARC_FAIL']['status_sort_key'];
299 | $status_text = $dmarc_result['DMARC_FAIL']['status_text'];
300 | } else {
301 | switch ($report_status_min) {
302 | case 0:
303 | $color = $dmarc_result['DMARC_PASS_AND_FAIL']['color'];
304 | $color_sort_key = $dmarc_result['DMARC_PASS_AND_FAIL']['status_sort_key'];
305 | $status_text = $dmarc_result['DMARC_PASS_AND_FAIL']['status_text'];
306 | break;
307 | case 1:
308 | $color = $dmarc_result['DMARC_OTHER_CONDITION']['color'];
309 | $color_sort_key = $dmarc_result['DMARC_OTHER_CONDITION']['status_sort_key'];
310 | $status_text = $dmarc_result['DMARC_OTHER_CONDITION']['status_text'];
311 | break;
312 | case 2:
313 | $color = $dmarc_result['DMARC_PASS']['color'];
314 | $color_sort_key = $dmarc_result['DMARC_PASS']['status_sort_key'];
315 | $status_text = $dmarc_result['DMARC_PASS']['status_text'];
316 | break;
317 | default:
318 | break;
319 | }
320 | }
321 |
322 | return array('color' => $color, 'status_sort_key' => $color_sort_key, 'status_text' => $status_text);
323 | }
324 |
325 | // This function sets variables for individual cells in the Report Data table
326 | function get_status_color($result) {
327 |
328 | global $dmarc_result;
329 | $color = "";
330 | $color_sort_key = "";
331 |
332 | if ($result == "fail") {
333 | $color = $dmarc_result['DMARC_FAIL']['color'];
334 | # $color_sort_key = $dmarc_result['STATUS_FAIL']['status_sort_key'];
335 | } elseif ($result == "pass") {
336 | $color = $dmarc_result['DMARC_PASS']['color'];
337 | # $color_sort_key = $dmarc_result['STATUS_PASS']['status_sort_key'];
338 | } else {
339 | $color = $dmarc_result['DMARC_OTHER_CONDITION']['color'];
340 | # $color_sort_key = $dmarc_result['STATUS_OTHER_CONDITION']['status_sort_key'];
341 | }
342 |
343 | return array('color' => $color, 'status_sort_key' => $color_sort_key);
344 | }
345 |
346 | function format_date($date, $format) {
347 |
348 | $answer = date($format, strtotime($date));
349 | return $answer;
350 | };
351 |
352 | // null-safe version of htmlspecialchars for PHP 8+ compatibility
353 | // --------------------------------------------------------------------------
354 | function html_escape($str) {
355 | if ($str == null) {
356 | return null;
357 | }
358 | return htmlspecialchars($str);
359 | }
360 |
361 | // Get all configuration options
362 | // --------------------------------------------------------------------------
363 | function configure() {
364 |
365 | global $cookie_name;
366 | global $cookie_options;
367 | global $options;
368 |
369 | $option = array_keys($options);
370 | $cookie_options = array();
371 | $cookie_timeout = 60*60*24*365;
372 |
373 | if(!isset($_COOKIE[$cookie_name]) || $_COOKIE[$cookie_name] == "" ) {
374 | // No Cookie
375 | foreach ($option as $option_name) {
376 | if ( $options[$option_name]['option_type'] != "heading" ) {
377 | $cookie_options += array($option_name => $options[$option_name]['option_value']);
378 | // foreach($options[$option_name] as $key=>$value) {
379 | }
380 | }
381 | setcookie($cookie_name, json_encode($cookie_options), time() + $cookie_timeout, "/");
382 | } else {
383 | // Cookie exists
384 | if ($_SERVER["REQUEST_METHOD"] == "POST") {
385 | // POST
386 | foreach ($option as $option_name) {
387 | if ( $options[$option_name]['option_type'] != "heading" ) {
388 | if ( is_null($_POST[$option_name]) ) {
389 | $cookie_options += array($option_name => "");
390 | } else {
391 | $cookie_options += array($option_name => test_input($_POST[$option_name]));
392 | }
393 | }
394 | }
395 | setcookie($cookie_name, json_encode($cookie_options), time() + $cookie_timeout, "/");
396 | header("Location: dmarcts-report-viewer.php");
397 | exit;
398 | } else { // Not POST
399 | $cookie_options = json_decode($_COOKIE[$cookie_name], true);
400 |
401 | // Check if any options have been removed or added to $options[]
402 | // Update $cookie_options with any new options from $options (excluding headings)
403 | foreach ($option as $option_name) {
404 | if ( $options[$option_name]['option_type'] != "heading" && is_null($cookie_options[$option_name]) ) {
405 | $cookie_options[$option_name] = $options[$option_name]['option_value'];
406 | }
407 | }
408 | // Remove any options from $cookie_options which are not in $options
409 | foreach ($cookie_options as $key => $value) {
410 | if ( $options[$key] == null ) {
411 | unset($cookie_options[$key]);
412 | }
413 | }
414 | setcookie($cookie_name, json_encode($cookie_options), time() + $cookie_timeout, "/");
415 | }
416 | }
417 | }
418 |
419 | function test_input($data) {
420 |
421 | $data = trim($data);
422 | $data = stripslashes($data);
423 | $data = htmlspecialchars($data);
424 |
425 | return $data;
426 | }
427 |
428 | // This functions opens a connection to the database using PDO
429 | function connect_db($dbtype, $dbhost, $dbport, $dbname, $dbuser, $dbpass) {
430 | $dbtype = $dbtype ?: 'mysql';
431 | try {
432 | $dbh = new PDO("$dbtype:host=$dbhost;port=$dbport;dbname=$dbname", $dbuser, $dbpass);
433 | $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
434 | $dbh->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
435 | return $dbh;
436 | } catch (PDOException $e) {
437 | echo "Error: Failed to make a database connection ";
438 | echo "Error: " . $e->getMessage() . " ";
439 | // Debug ONLY. This will expose database credentials when database connection fails
440 | // echo "Database connection information: dbhost: " . $dbhost . " dbuser: " . $dbuser . " dbpass: " . $dbpass . " dbname: " . $dbname . " dbport: " . $dbport . " ";
441 | exit;
442 | }
443 | }
444 |
--------------------------------------------------------------------------------
/highlight.js/highlight.pack.js:
--------------------------------------------------------------------------------
1 | /*
2 | Highlight.js 10.7.2 (00233d63)
3 | License: BSD-3-Clause
4 | Copyright (c) 2006-2021, Ivan Sagalaev
5 | See https://github.com/highlightjs/highlight.js
6 | */
7 | var hljs=function(){"use strict";function e(t){
8 | return t instanceof Map?t.clear=t.delete=t.set=()=>{
9 | throw Error("map is read-only")}:t instanceof Set&&(t.add=t.clear=t.delete=()=>{
10 | throw Error("set is read-only")
11 | }),Object.freeze(t),Object.getOwnPropertyNames(t).forEach((n=>{var i=t[n]
12 | ;"object"!=typeof i||Object.isFrozen(i)||e(i)})),t}var t=e,n=e;t.default=n
13 | ;class i{constructor(e){
14 | void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1}
15 | ignoreMatch(){this.isMatchIgnored=!0}}function s(e){
16 | return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")
17 | }function a(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t]
18 | ;return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n}const r=e=>!!e.kind
19 | ;class l{constructor(e,t){
20 | this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){
21 | this.buffer+=s(e)}openNode(e){if(!r(e))return;let t=e.kind
22 | ;e.sublanguage||(t=`${this.classPrefix}${t}`),this.span(t)}closeNode(e){
23 | r(e)&&(this.buffer+="")}value(){return this.buffer}span(e){
24 | this.buffer+=``}}class o{constructor(){this.rootNode={
25 | children:[]},this.stack=[this.rootNode]}get top(){
26 | return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){
27 | this.top.children.push(e)}openNode(e){const t={kind:e,children:[]}
28 | ;this.add(t),this.stack.push(t)}closeNode(){
29 | if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){
30 | for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}
31 | walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){
32 | return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t),
33 | t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){
34 | "string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{
35 | o._collapse(e)})))}}class c extends o{constructor(e){super(),this.options=e}
36 | addKeyword(e,t){""!==e&&(this.openNode(t),this.addText(e),this.closeNode())}
37 | addText(e){""!==e&&this.add(e)}addSublanguage(e,t){const n=e.root
38 | ;n.kind=t,n.sublanguage=!0,this.add(n)}toHTML(){
39 | return new l(this,this.options).value()}finalize(){return!0}}function g(e){
40 | return e?"string"==typeof e?e:e.source:null}
41 | const u=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./,h="[a-zA-Z]\\w*",d="[a-zA-Z_]\\w*",f="\\b\\d+(\\.\\d+)?",p="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",m="\\b(0b[01]+)",b={
42 | begin:"\\\\[\\s\\S]",relevance:0},E={className:"string",begin:"'",end:"'",
43 | illegal:"\\n",contains:[b]},x={className:"string",begin:'"',end:'"',
44 | illegal:"\\n",contains:[b]},v={
45 | begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/
46 | },w=(e,t,n={})=>{const i=a({className:"comment",begin:e,end:t,contains:[]},n)
47 | ;return i.contains.push(v),i.contains.push({className:"doctag",
48 | begin:"(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):",relevance:0}),i
49 | },y=w("//","$"),N=w("/\\*","\\*/"),R=w("#","$");var _=Object.freeze({
50 | __proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:h,UNDERSCORE_IDENT_RE:d,
51 | NUMBER_RE:f,C_NUMBER_RE:p,BINARY_NUMBER_RE:m,
52 | RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",
53 | SHEBANG:(e={})=>{const t=/^#![ ]*\//
54 | ;return e.binary&&(e.begin=((...e)=>e.map((e=>g(e))).join(""))(t,/.*\b/,e.binary,/\b.*/)),
55 | a({className:"meta",begin:t,end:/$/,relevance:0,"on:begin":(e,t)=>{
56 | 0!==e.index&&t.ignoreMatch()}},e)},BACKSLASH_ESCAPE:b,APOS_STRING_MODE:E,
57 | QUOTE_STRING_MODE:x,PHRASAL_WORDS_MODE:v,COMMENT:w,C_LINE_COMMENT_MODE:y,
58 | C_BLOCK_COMMENT_MODE:N,HASH_COMMENT_MODE:R,NUMBER_MODE:{className:"number",
59 | begin:f,relevance:0},C_NUMBER_MODE:{className:"number",begin:p,relevance:0},
60 | BINARY_NUMBER_MODE:{className:"number",begin:m,relevance:0},CSS_NUMBER_MODE:{
61 | className:"number",
62 | begin:f+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",
63 | relevance:0},REGEXP_MODE:{begin:/(?=\/[^/\n]*\/)/,contains:[{className:"regexp",
64 | begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[b,{begin:/\[/,end:/\]/,
65 | relevance:0,contains:[b]}]}]},TITLE_MODE:{className:"title",begin:h,relevance:0
66 | },UNDERSCORE_TITLE_MODE:{className:"title",begin:d,relevance:0},METHOD_GUARD:{
67 | begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0},END_SAME_AS_BEGIN:e=>Object.assign(e,{
68 | "on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{
69 | t.data._beginMatch!==e[1]&&t.ignoreMatch()}})});function k(e,t){
70 | "."===e.input[e.index-1]&&t.ignoreMatch()}function M(e,t){
71 | t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",
72 | e.__beforeBegin=k,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords,
73 | void 0===e.relevance&&(e.relevance=0))}function O(e,t){
74 | Array.isArray(e.illegal)&&(e.illegal=((...e)=>"("+e.map((e=>g(e))).join("|")+")")(...e.illegal))
75 | }function A(e,t){if(e.match){
76 | if(e.begin||e.end)throw Error("begin & end are not supported with match")
77 | ;e.begin=e.match,delete e.match}}function L(e,t){
78 | void 0===e.relevance&&(e.relevance=1)}
79 | const I=["of","and","for","in","not","or","if","then","parent","list","value"]
80 | ;function j(e,t,n="keyword"){const i={}
81 | ;return"string"==typeof e?s(n,e.split(" ")):Array.isArray(e)?s(n,e):Object.keys(e).forEach((n=>{
82 | Object.assign(i,j(e[n],t,n))})),i;function s(e,n){
83 | t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|")
84 | ;i[n[0]]=[e,B(n[0],n[1])]}))}}function B(e,t){
85 | return t?Number(t):(e=>I.includes(e.toLowerCase()))(e)?0:1}
86 | function T(e,{plugins:t}){function n(t,n){
87 | return RegExp(g(t),"m"+(e.case_insensitive?"i":"")+(n?"g":""))}class i{
88 | constructor(){
89 | this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}
90 | addRule(e,t){
91 | t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]),
92 | this.matchAt+=(e=>RegExp(e.toString()+"|").exec("").length-1)(e)+1}compile(){
93 | 0===this.regexes.length&&(this.exec=()=>null)
94 | ;const e=this.regexes.map((e=>e[1]));this.matcherRe=n(((e,t="|")=>{let n=0
95 | ;return e.map((e=>{n+=1;const t=n;let i=g(e),s="";for(;i.length>0;){
96 | const e=u.exec(i);if(!e){s+=i;break}
97 | s+=i.substring(0,e.index),i=i.substring(e.index+e[0].length),
98 | "\\"===e[0][0]&&e[1]?s+="\\"+(Number(e[1])+t):(s+=e[0],"("===e[0]&&n++)}return s
99 | })).map((e=>`(${e})`)).join(t)})(e),!0),this.lastIndex=0}exec(e){
100 | this.matcherRe.lastIndex=this.lastIndex;const t=this.matcherRe.exec(e)
101 | ;if(!t)return null
102 | ;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n]
103 | ;return t.splice(0,n),Object.assign(t,i)}}class s{constructor(){
104 | this.rules=[],this.multiRegexes=[],
105 | this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){
106 | if(this.multiRegexes[e])return this.multiRegexes[e];const t=new i
107 | ;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))),
108 | t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){
109 | return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){
110 | this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){
111 | const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex
112 | ;let n=t.exec(e)
113 | ;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{
114 | const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)}
115 | return n&&(this.regexIndex+=n.position+1,
116 | this.regexIndex===this.count&&this.considerAll()),n}}
117 | if(e.compilerExtensions||(e.compilerExtensions=[]),
118 | e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.")
119 | ;return e.classNameAliases=a(e.classNameAliases||{}),function t(i,r){const l=i
120 | ;if(i.isCompiled)return l
121 | ;[A].forEach((e=>e(i,r))),e.compilerExtensions.forEach((e=>e(i,r))),
122 | i.__beforeBegin=null,[M,O,L].forEach((e=>e(i,r))),i.isCompiled=!0;let o=null
123 | ;if("object"==typeof i.keywords&&(o=i.keywords.$pattern,
124 | delete i.keywords.$pattern),
125 | i.keywords&&(i.keywords=j(i.keywords,e.case_insensitive)),
126 | i.lexemes&&o)throw Error("ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) ")
127 | ;return o=o||i.lexemes||/\w+/,
128 | l.keywordPatternRe=n(o,!0),r&&(i.begin||(i.begin=/\B|\b/),
129 | l.beginRe=n(i.begin),i.endSameAsBegin&&(i.end=i.begin),
130 | i.end||i.endsWithParent||(i.end=/\B|\b/),
131 | i.end&&(l.endRe=n(i.end)),l.terminatorEnd=g(i.end)||"",
132 | i.endsWithParent&&r.terminatorEnd&&(l.terminatorEnd+=(i.end?"|":"")+r.terminatorEnd)),
133 | i.illegal&&(l.illegalRe=n(i.illegal)),
134 | i.contains||(i.contains=[]),i.contains=[].concat(...i.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>a(e,{
135 | variants:null},t)))),e.cachedVariants?e.cachedVariants:S(e)?a(e,{
136 | starts:e.starts?a(e.starts):null
137 | }):Object.isFrozen(e)?a(e):e))("self"===e?i:e)))),i.contains.forEach((e=>{t(e,l)
138 | })),i.starts&&t(i.starts,r),l.matcher=(e=>{const t=new s
139 | ;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin"
140 | }))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end"
141 | }),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(l),l}(e)}function S(e){
142 | return!!e&&(e.endsWithParent||S(e.starts))}function P(e){const t={
143 | props:["language","code","autodetect"],data:()=>({detectedLanguage:"",
144 | unknownLanguage:!1}),computed:{className(){
145 | return this.unknownLanguage?"":"hljs "+this.detectedLanguage},highlighted(){
146 | if(!this.autoDetect&&!e.getLanguage(this.language))return console.warn(`The language "${this.language}" you specified could not be found.`),
147 | this.unknownLanguage=!0,s(this.code);let t={}
148 | ;return this.autoDetect?(t=e.highlightAuto(this.code),
149 | this.detectedLanguage=t.language):(t=e.highlight(this.language,this.code,this.ignoreIllegals),
150 | this.detectedLanguage=this.language),t.value},autoDetect(){
151 | return!(this.language&&(e=this.autodetect,!e&&""!==e));var e},
152 | ignoreIllegals:()=>!0},render(e){return e("pre",{},[e("code",{
153 | class:this.className,domProps:{innerHTML:this.highlighted}})])}};return{
154 | Component:t,VuePlugin:{install(e){e.component("highlightjs",t)}}}}const D={
155 | "after:highlightElement":({el:e,result:t,text:n})=>{const i=H(e)
156 | ;if(!i.length)return;const a=document.createElement("div")
157 | ;a.innerHTML=t.value,t.value=((e,t,n)=>{let i=0,a="";const r=[];function l(){
158 | return e.length&&t.length?e[0].offset!==t[0].offset?e[0].offset"}function c(e){
161 | a+=""+C(e)+">"}function g(e){("start"===e.event?o:c)(e.node)}
162 | for(;e.length||t.length;){let t=l()
163 | ;if(a+=s(n.substring(i,t[0].offset)),i=t[0].offset,t===e){r.reverse().forEach(c)
164 | ;do{g(t.splice(0,1)[0]),t=l()}while(t===e&&t.length&&t[0].offset===i)
165 | ;r.reverse().forEach(o)
166 | }else"start"===t[0].event?r.push(t[0].node):r.pop(),g(t.splice(0,1)[0])}
167 | return a+s(n.substr(i))})(i,H(a),n)}};function C(e){
168 | return e.nodeName.toLowerCase()}function H(e){const t=[];return function e(n,i){
169 | for(let s=n.firstChild;s;s=s.nextSibling)3===s.nodeType?i+=s.nodeValue.length:1===s.nodeType&&(t.push({
170 | event:"start",offset:i,node:s}),i=e(s,i),C(s).match(/br|hr|img|input/)||t.push({
171 | event:"stop",offset:i,node:s}));return i}(e,0),t}const $={},U=e=>{
172 | console.error(e)},z=(e,...t)=>{console.log("WARN: "+e,...t)},K=(e,t)=>{
173 | $[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),$[`${e}/${t}`]=!0)
174 | },G=s,V=a,W=Symbol("nomatch");return(e=>{
175 | const n=Object.create(null),s=Object.create(null),a=[];let r=!0
176 | ;const l=/(^(<[^>]+>|\t|)+|\n)/gm,o="Could not find the language '{}', did you forget to load/include a language module?",g={
177 | disableAutodetect:!0,name:"Plain text",contains:[]};let u={
178 | noHighlightRe:/^(no-?highlight)$/i,
179 | languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",
180 | tabReplace:null,useBR:!1,languages:null,__emitter:c};function h(e){
181 | return u.noHighlightRe.test(e)}function d(e,t,n,i){let s="",a=""
182 | ;"object"==typeof t?(s=e,
183 | n=t.ignoreIllegals,a=t.language,i=void 0):(K("10.7.0","highlight(lang, code, ...args) has been deprecated."),
184 | K("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),
185 | a=e,s=t);const r={code:s,language:a};M("before:highlight",r)
186 | ;const l=r.result?r.result:f(r.language,r.code,n,i)
187 | ;return l.code=r.code,M("after:highlight",l),l}function f(e,t,s,l){
188 | function c(e,t){const n=v.case_insensitive?t[0].toLowerCase():t[0]
189 | ;return Object.prototype.hasOwnProperty.call(e.keywords,n)&&e.keywords[n]}
190 | function g(){null!=R.subLanguage?(()=>{if(""===M)return;let e=null
191 | ;if("string"==typeof R.subLanguage){
192 | if(!n[R.subLanguage])return void k.addText(M)
193 | ;e=f(R.subLanguage,M,!0,_[R.subLanguage]),_[R.subLanguage]=e.top
194 | }else e=p(M,R.subLanguage.length?R.subLanguage:null)
195 | ;R.relevance>0&&(O+=e.relevance),k.addSublanguage(e.emitter,e.language)
196 | })():(()=>{if(!R.keywords)return void k.addText(M);let e=0
197 | ;R.keywordPatternRe.lastIndex=0;let t=R.keywordPatternRe.exec(M),n="";for(;t;){
198 | n+=M.substring(e,t.index);const i=c(R,t);if(i){const[e,s]=i
199 | ;if(k.addText(n),n="",O+=s,e.startsWith("_"))n+=t[0];else{
200 | const n=v.classNameAliases[e]||e;k.addKeyword(t[0],n)}}else n+=t[0]
201 | ;e=R.keywordPatternRe.lastIndex,t=R.keywordPatternRe.exec(M)}
202 | n+=M.substr(e),k.addText(n)})(),M=""}function h(e){
203 | return e.className&&k.openNode(v.classNameAliases[e.className]||e.className),
204 | R=Object.create(e,{parent:{value:R}}),R}function d(e,t,n){let s=((e,t)=>{
205 | const n=e&&e.exec(t);return n&&0===n.index})(e.endRe,n);if(s){if(e["on:end"]){
206 | const n=new i(e);e["on:end"](t,n),n.isMatchIgnored&&(s=!1)}if(s){
207 | for(;e.endsParent&&e.parent;)e=e.parent;return e}}
208 | if(e.endsWithParent)return d(e.parent,t,n)}function m(e){
209 | return 0===R.matcher.regexIndex?(M+=e[0],1):(I=!0,0)}function b(e){
210 | const n=e[0],i=t.substr(e.index),s=d(R,e,i);if(!s)return W;const a=R
211 | ;a.skip?M+=n:(a.returnEnd||a.excludeEnd||(M+=n),g(),a.excludeEnd&&(M=n));do{
212 | R.className&&k.closeNode(),R.skip||R.subLanguage||(O+=R.relevance),R=R.parent
213 | }while(R!==s.parent)
214 | ;return s.starts&&(s.endSameAsBegin&&(s.starts.endRe=s.endRe),
215 | h(s.starts)),a.returnEnd?0:n.length}let E={};function x(n,a){const l=a&&a[0]
216 | ;if(M+=n,null==l)return g(),0
217 | ;if("begin"===E.type&&"end"===a.type&&E.index===a.index&&""===l){
218 | if(M+=t.slice(a.index,a.index+1),!r){const t=Error("0 width match regex")
219 | ;throw t.languageName=e,t.badRule=E.rule,t}return 1}
220 | if(E=a,"begin"===a.type)return function(e){
221 | const t=e[0],n=e.rule,s=new i(n),a=[n.__beforeBegin,n["on:begin"]]
222 | ;for(const n of a)if(n&&(n(e,s),s.isMatchIgnored))return m(t)
223 | ;return n&&n.endSameAsBegin&&(n.endRe=RegExp(t.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),"m")),
224 | n.skip?M+=t:(n.excludeBegin&&(M+=t),
225 | g(),n.returnBegin||n.excludeBegin||(M=t)),h(n),n.returnBegin?0:t.length}(a)
226 | ;if("illegal"===a.type&&!s){
227 | const e=Error('Illegal lexeme "'+l+'" for mode "'+(R.className||"")+'"')
228 | ;throw e.mode=R,e}if("end"===a.type){const e=b(a);if(e!==W)return e}
229 | if("illegal"===a.type&&""===l)return 1
230 | ;if(L>1e5&&L>3*a.index)throw Error("potential infinite loop, way more iterations than matches")
231 | ;return M+=l,l.length}const v=N(e)
232 | ;if(!v)throw U(o.replace("{}",e)),Error('Unknown language: "'+e+'"')
233 | ;const w=T(v,{plugins:a});let y="",R=l||w;const _={},k=new u.__emitter(u);(()=>{
234 | const e=[];for(let t=R;t!==v;t=t.parent)t.className&&e.unshift(t.className)
235 | ;e.forEach((e=>k.openNode(e)))})();let M="",O=0,A=0,L=0,I=!1;try{
236 | for(R.matcher.considerAll();;){
237 | L++,I?I=!1:R.matcher.considerAll(),R.matcher.lastIndex=A
238 | ;const e=R.matcher.exec(t);if(!e)break;const n=x(t.substring(A,e.index),e)
239 | ;A=e.index+n}return x(t.substr(A)),k.closeAllNodes(),k.finalize(),y=k.toHTML(),{
240 | relevance:Math.floor(O),value:y,language:e,illegal:!1,emitter:k,top:R}}catch(n){
241 | if(n.message&&n.message.includes("Illegal"))return{illegal:!0,illegalBy:{
242 | msg:n.message,context:t.slice(A-100,A+100),mode:n.mode},sofar:y,relevance:0,
243 | value:G(t),emitter:k};if(r)return{illegal:!1,relevance:0,value:G(t),emitter:k,
244 | language:e,top:R,errorRaised:n};throw n}}function p(e,t){
245 | t=t||u.languages||Object.keys(n);const i=(e=>{const t={relevance:0,
246 | emitter:new u.__emitter(u),value:G(e),illegal:!1,top:g}
247 | ;return t.emitter.addText(e),t})(e),s=t.filter(N).filter(k).map((t=>f(t,e,!1)))
248 | ;s.unshift(i);const a=s.sort(((e,t)=>{
249 | if(e.relevance!==t.relevance)return t.relevance-e.relevance
250 | ;if(e.language&&t.language){if(N(e.language).supersetOf===t.language)return 1
251 | ;if(N(t.language).supersetOf===e.language)return-1}return 0})),[r,l]=a,o=r
252 | ;return o.second_best=l,o}const m={"before:highlightElement":({el:e})=>{
253 | u.useBR&&(e.innerHTML=e.innerHTML.replace(/\n/g,"").replace(/ /g,"\n"))
254 | },"after:highlightElement":({result:e})=>{
255 | u.useBR&&(e.value=e.value.replace(/\n/g," "))}},b=/^(<[^>]+>|\t)+/gm,E={
256 | "after:highlightElement":({result:e})=>{
257 | u.tabReplace&&(e.value=e.value.replace(b,(e=>e.replace(/\t/g,u.tabReplace))))}}
258 | ;function x(e){let t=null;const n=(e=>{let t=e.className+" "
259 | ;t+=e.parentNode?e.parentNode.className:"";const n=u.languageDetectRe.exec(t)
260 | ;if(n){const t=N(n[1])
261 | ;return t||(z(o.replace("{}",n[1])),z("Falling back to no-highlight mode for this block.",e)),
262 | t?n[1]:"no-highlight"}return t.split(/\s+/).find((e=>h(e)||N(e)))})(e)
263 | ;if(h(n))return;M("before:highlightElement",{el:e,language:n}),t=e
264 | ;const i=t.textContent,a=n?d(i,{language:n,ignoreIllegals:!0}):p(i)
265 | ;M("after:highlightElement",{el:e,result:a,text:i
266 | }),e.innerHTML=a.value,((e,t,n)=>{const i=t?s[t]:n
267 | ;e.classList.add("hljs"),i&&e.classList.add(i)})(e,n,a.language),e.result={
268 | language:a.language,re:a.relevance,relavance:a.relevance
269 | },a.second_best&&(e.second_best={language:a.second_best.language,
270 | re:a.second_best.relevance,relavance:a.second_best.relevance})}const v=()=>{
271 | v.called||(v.called=!0,
272 | K("10.6.0","initHighlighting() is deprecated. Use highlightAll() instead."),
273 | document.querySelectorAll("pre code").forEach(x))};let w=!1;function y(){
274 | "loading"!==document.readyState?document.querySelectorAll("pre code").forEach(x):w=!0
275 | }function N(e){return e=(e||"").toLowerCase(),n[e]||n[s[e]]}
276 | function R(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{
277 | s[e.toLowerCase()]=t}))}function k(e){const t=N(e)
278 | ;return t&&!t.disableAutodetect}function M(e,t){const n=e;a.forEach((e=>{
279 | e[n]&&e[n](t)}))}
280 | "undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{
281 | w&&y()}),!1),Object.assign(e,{highlight:d,highlightAuto:p,highlightAll:y,
282 | fixMarkup:e=>{
283 | return K("10.2.0","fixMarkup will be removed entirely in v11.0"),K("10.2.0","Please see https://github.com/highlightjs/highlight.js/issues/2534"),
284 | t=e,
285 | u.tabReplace||u.useBR?t.replace(l,(e=>"\n"===e?u.useBR?" ":e:u.tabReplace?e.replace(/\t/g,u.tabReplace):e)):t
286 | ;var t},highlightElement:x,
287 | highlightBlock:e=>(K("10.7.0","highlightBlock will be removed entirely in v12.0"),
288 | K("10.7.0","Please use highlightElement now."),x(e)),configure:e=>{
289 | e.useBR&&(K("10.3.0","'useBR' will be removed entirely in v11.0"),
290 | K("10.3.0","Please see https://github.com/highlightjs/highlight.js/issues/2559")),
291 | u=V(u,e)},initHighlighting:v,initHighlightingOnLoad:()=>{
292 | K("10.6.0","initHighlightingOnLoad() is deprecated. Use highlightAll() instead."),
293 | w=!0},registerLanguage:(t,i)=>{let s=null;try{s=i(e)}catch(e){
294 | if(U("Language definition for '{}' could not be registered.".replace("{}",t)),
295 | !r)throw e;U(e),s=g}
296 | s.name||(s.name=t),n[t]=s,s.rawDefinition=i.bind(null,e),s.aliases&&R(s.aliases,{
297 | languageName:t})},unregisterLanguage:e=>{delete n[e]
298 | ;for(const t of Object.keys(s))s[t]===e&&delete s[t]},
299 | listLanguages:()=>Object.keys(n),getLanguage:N,registerAliases:R,
300 | requireLanguage:e=>{
301 | K("10.4.0","requireLanguage will be removed entirely in v11."),
302 | K("10.4.0","Please see https://github.com/highlightjs/highlight.js/pull/2844")
303 | ;const t=N(e);if(t)return t
304 | ;throw Error("The '{}' language is required, but not loaded.".replace("{}",e))},
305 | autoDetection:k,inherit:V,addPlugin:e=>{(e=>{
306 | e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{
307 | e["before:highlightBlock"](Object.assign({block:t.el},t))
308 | }),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{
309 | e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),a.push(e)},
310 | vuePlugin:P(e).VuePlugin}),e.debugMode=()=>{r=!1},e.safeMode=()=>{r=!0
311 | },e.versionString="10.7.2";for(const e in _)"object"==typeof _[e]&&t(_[e])
312 | ;return Object.assign(e,_),e.addPlugin(m),e.addPlugin(D),e.addPlugin(E),e})({})
313 | }();"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);hljs.registerLanguage("xml",(()=>{"use strict";function e(e){
314 | return e?"string"==typeof e?e:e.source:null}function n(e){return a("(?=",e,")")}
315 | function a(...n){return n.map((n=>e(n))).join("")}function s(...n){
316 | return"("+n.map((n=>e(n))).join("|")+")"}return e=>{
317 | const t=a(/[A-Z_]/,a("(",/[A-Z0-9_.-]*:/,")?"),/[A-Z0-9_.-]*/),i={
318 | className:"symbol",begin:/&[a-z]+;|[0-9]+;|[a-f0-9]+;/},r={begin:/\s/,
319 | contains:[{className:"meta-keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]
320 | },c=e.inherit(r,{begin:/\(/,end:/\)/}),l=e.inherit(e.APOS_STRING_MODE,{
321 | className:"meta-string"}),g=e.inherit(e.QUOTE_STRING_MODE,{
322 | className:"meta-string"}),m={endsWithParent:!0,illegal:/,relevance:0,
323 | contains:[{className:"attr",begin:/[A-Za-z0-9._:-]+/,relevance:0},{begin:/=\s*/,
324 | relevance:0,contains:[{className:"string",endsParent:!0,variants:[{begin:/"/,
325 | end:/"/,contains:[i]},{begin:/'/,end:/'/,contains:[i]},{begin:/[^\s"'=<>`]+/}]}]
326 | }]};return{name:"HTML, XML",
327 | aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],
328 | case_insensitive:!0,contains:[{className:"meta",begin://,
329 | relevance:10,contains:[r,g,l,c,{begin:/\[/,end:/\]/,contains:[{className:"meta",
330 | begin://,contains:[r,c,g,l]}]}]},e.COMMENT(//,{
331 | relevance:10}),{begin://,relevance:10},i,{
332 | className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",
333 | begin:/