%PDF- %PDF-
Direktori : /www/varak.net/dmarc.varak.net/classes/Report/ |
Current File : //www/varak.net/dmarc.varak.net/classes/Report/ReportData.php |
<?php /** * dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports. * Copyright (C) 2020-2025 Aleksey Andreev (liuch) * * Available at: * https://github.com/liuch/dmarc-srg * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, either version 3 of the License. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see <http://www.gnu.org/licenses/>. */ namespace Liuch\DmarcSrg\Report; use Liuch\DmarcSrg\DateTime; use Liuch\DmarcSrg\Exception\RuntimeException; class ReportData { private $rep_data = null; private $tag_id = null; private $skip_depth = 0; private $strict_mode = false; private function __construct(bool $strict = false) { $this->strict_mode = $strict; } public static function fromArray(array $data, bool $strict = false) { $new_data = []; self::copyFields(self::$fields, $data, $new_data); self::copySubdata(self::$dfields, $data, $new_data, 'date'); self::copySubdata(self::$pfields, $data, $new_data, 'policy'); if (isset($data['records']) && gettype($data['records']) === 'array') { $new_data['records'] = []; foreach ($data['records'] as &$rec) { $new_rec = []; self::copyFields(self::$rfields, $rec, $new_rec); $new_data['records'][] = $new_rec; } unset($rec); } $rdata = new self($strict); $rdata->rep_data = $new_data; return $rdata; } public static function fromXmlFile($fd, $strict = false) { $rdata = new self($strict); $rdata->tag_id = '<root>'; $rdata->rep_data = [ 'date' => [], 'policy' => [], 'records' => [] ]; $parser = xml_parser_create(); xml_set_element_handler($parser, [ $rdata, 'xmlStartTag' ], [ $rdata, 'xmlEndTag' ]); xml_set_character_data_handler($parser, [ $rdata, 'xmlTagData' ]); xml_set_external_entity_ref_handler($parser, function () { throw new RuntimeException('The XML document has an external entity!'); }); try { while ($file_data = fread($fd, 4096)) { if (!xml_parse($parser, $file_data, feof($fd))) { $pc = xml_get_error_code($parser); $error_str = 'XML error!' . PHP_EOL; $error_str .= 'Parser code: ' . $pc . PHP_EOL; $error_str .= 'Parser message: ' . xml_error_string($pc) . PHP_EOL; $error_str .= 'Line: ' . xml_get_current_line_number($parser); $error_str .= '; Column: ' . xml_get_current_column_number($parser) . PHP_EOL; throw new RuntimeException('Incorrect XML report file', -1, new RuntimeException($error_str)); } } } finally { xml_parser_free($parser); unset($parser); } return $rdata; } public function toArray(): array { return $this->rep_data; } public function &__get(string $name) { if (!array_key_exists($name, self::$fields)) { throw new RuntimeException('Getting an unknown report property: ' . $name); } if (!array_key_exists($name, $this->rep_data)) { throw new RuntimeException('Getting an undefined report property: ' . $name); } return $this->rep_data[$name]; } public function __set(string $name, $value): void { if (!array_key_exists($name, self::$fields)) { throw new RuntimeException('Accessing an unknown report property: ' . $name); } $this->rep_data[$name] = $value; } public function __isset(string $name): bool { return isset($this->rep_data); } /** * Checks report data for correctness and completeness * * @return bool */ public function isValid(): bool { if (!self::checkRow($this->rep_data, self::$fields) || count($this->rep_data['records']) === 0) { return false; } if (!self::checkRow($this->rep_data['date'], self::$dfields)) { return false; } if (!self::checkRow($this->rep_data['policy'], self::$pfields)) { return false; } foreach ($this->rep_data['records'] as &$rec) { if (gettype($rec) !== 'array' || !self::checkRow($rec, self::$rfields)) { return false; } } return true; } /** * Checks one row of report data * * @param array $row Data row * @param array $def Row definition * * @return bool */ private static function checkRow(array &$row, array &$def): bool { foreach ($def as $key => &$dd) { if (isset($row[$key])) { if (gettype($row[$key]) !== $dd['type']) { return false; } } elseif ($dd['required']) { return false; } } return true; } private function xmlStartTag($parser, string $name, array $attributes): void { if ($this->skip_depth || !$this->xmlEnterTag($name)) { ++$this->skip_depth; return; } switch ($this->tag_id) { case 'rec': $this->rep_data['records'][] = []; break; case 'error_string': if (!isset($this->rep_data['error_string'])) { $this->rep_data['error_string'] = []; } break; case 'reason': case 'dkim_auth': case 'spf_auth': $idx = array_key_last($this->rep_data['records']); if (!isset($this->rep_data['records'][$idx][$this->tag_id])) { $this->rep_data['records'][$idx][$this->tag_id] = []; } $this->report_tags[$this->tag_id]['tmp_data'] = []; break; } } private function xmlEndTag($parser, string $name): void { if ($this->skip_depth) { --$this->skip_depth; return; } switch ($this->tag_id) { case 'reason': case 'dkim_auth': case 'spf_auth': $idx = array_key_last($this->rep_data['records']); $this->rep_data['records'][$idx][$this->tag_id][] = $this->report_tags[$this->tag_id]['tmp_data']; unset($this->report_tags[$this->tag_id]['tmp_data']); break; case 'feedback': // Set the default value if it's necessary and there is no data foreach ($this->report_tags as $tag_id => &$tag_data) { if (array_key_exists('default', $tag_data)) { // not isset() because of null values $def = $tag_data['default']; $key = $tag_data['key'] ?? $tag_id; $ptr = null; switch ($tag_data['type'] ?? '') { case 'M': // metadata $ptr = &$this->rep_data; break; case 'D': // data range $ptr = &$this->rep_data['date']; break; case 'P': // policy $ptr = &$this->rep_data['policy']; break; default: // record foreach ($this->rep_data['records'] as &$rec_val) { if (!isset($rec_val[$key])) { $rec_val[$key] = $def; } } unset($rec_val); } if ($ptr) { if (!isset($ptr[$key])) { $ptr[$key] = $def; } unset($ptr); } } } unset($tag_data); $b_ts = intval($this->rep_data['date']['begin'] ?? 0); $e_ts = intval($this->rep_data['date']['end'] ?? 0); $this->rep_data['date']['begin'] = new DateTime('@' . ($b_ts < 0 ? 0 : $b_ts)); $this->rep_data['date']['end'] = new DateTime('@' . ($e_ts < 0 ? 0 : $e_ts)); foreach ($this->rep_data['records'] as &$rec_data) { $rec_data['count'] = intval($rec_data['rcount']); } unset($rec_data); break; } $this->xmlLeaveTag(); } private function xmlTagData($parser, $data): void { if ($this->skip_depth) { return; } switch ($this->tag_id) { case 'error_string': if ($this->tag_id === 'error_string') { $this->rep_data['error_string'][] = $data; } break; case 'reason_type': $this->report_tags['reason']['tmp_data']['type'] = $data; break; case 'reason_comment': $this->report_tags['reason']['tmp_data']['comment'] = $data; break; case 'dkim_domain': $this->report_tags['dkim_auth']['tmp_data']['domain'] = $data; break; case 'dkim_selector': $this->report_tags['dkim_auth']['tmp_data']['selector'] = $data; break; case 'dkim_result': $this->report_tags['dkim_auth']['tmp_data']['result'] = $data; break; case 'dkim_human_result': $this->report_tags['dkim_auth']['tmp_data']['human_result'] = $data; break; case 'spf_domain': $this->report_tags['spf_auth']['tmp_data']['domain'] = $data; break; case 'spf_scope': $this->report_tags['spf_auth']['tmp_data']['scope'] = $data; break; case 'spf_result': $this->report_tags['spf_auth']['tmp_data']['result'] = $data; break; default: $t_id = $this->tag_id; if (!isset($this->report_tags[$t_id]['children'])) { $key = $this->report_tags[$t_id]['key'] ?? $t_id; switch ($this->report_tags[$t_id]['type'] ?? '') { case 'M': // metadata $ptr = &$this->rep_data; break; case 'D': // date range $ptr = &$this->rep_data['date']; break; case 'P': // policy $ptr = &$this->rep_data['policy']; break; default: // record $ptr = &$this->rep_data['records'][array_key_last($this->rep_data['records'])]; break; } if (isset($ptr[$key])) { $ptr[$key] .= $data; } else { $ptr[$key] = $data; } unset($ptr); } } } private function xmlEnterTag(string $name): bool { if (!isset($this->report_tags[$this->tag_id]['children'][$name])) { if ($this->strict_mode) { throw new RuntimeException("Unknown tag: {$name}"); } return false; } $this->tag_id = $this->report_tags[$this->tag_id]['children'][$name]; return true; } private function xmlLeaveTag(): void { $this->tag_id = $this->report_tags[$this->tag_id]['parent']; } private static function copyFields(array &$flist, array &$sou_data, array &$des_data): void { foreach ($flist as $fn => &$fp) { $ft = $fp['type']; if ($ft !== 'array' && array_key_exists($fn, $sou_data) && gettype($sou_data[$fn]) === $ft) { $des_data[$fn] = $sou_data[$fn]; } } unset($fp); } private static function copySubdata(array &$flist, array &$sou_data, array &$des_data, string $key): void { if (isset($sou_data[$key]) && gettype($sou_data[$key]) === 'array') { $des_data[$key] = []; self::copyFields($flist, $sou_data[$key], $des_data[$key]); } } private static $fields = [ 'domain' => [ 'required' => true, 'type' => 'string' ], 'date' => [ 'required' => true, 'type' => 'array' ], 'org_name' => [ 'required' => true, 'type' => 'string' ], 'report_id' => [ 'required' => true, 'type' => 'string' ], 'email' => [ 'required' => false, 'type' => 'string' ], 'extra_contact_info' => [ 'required' => false, 'type' => 'string' ], 'error_string' => [ 'required' => false, 'type' => 'array' ], 'policy' => [ 'required' => true, 'type' => 'array' ], 'loaded_time' => [ 'required' => false, 'type' => 'object' ], 'records' => [ 'required' => true, 'type' => 'array' ] ]; private static $dfields = [ 'begin' => [ 'required' => true, 'type' => 'object' ], 'end' => [ 'required' => true, 'type' => 'object' ], ]; private static $pfields = [ 'adkim' => [ 'required' => false, 'type' => 'string' ], 'aspf' => [ 'required' => false, 'type' => 'string' ], 'p' => [ 'required' => false, 'type' => 'string' ], 'sp' => [ 'required' => false, 'type' => 'string' ], 'np' => [ 'required' => false, 'type' => 'string' ], 'pct' => [ 'required' => false, 'type' => 'string' ], 'fo' => [ 'required' => false, 'type' => 'string' ], ]; private static $rfields = [ 'ip' => [ 'required' => true, 'type' => 'string' ], 'count' => [ 'required' => true, 'type' => 'integer' ], 'disposition' => [ 'required' => true, 'type' => 'string' ], 'reason' => [ 'required' => false, 'type' => 'array' ], 'dkim_auth' => [ 'required' => false, 'type' => 'array' ], 'spf_auth' => [ 'required' => false, 'type' => 'array' ], 'dkim_align' => [ 'required' => true, 'type' => 'string' ], 'spf_align' => [ 'required' => true, 'type' => 'string' ], 'envelope_to' => [ 'required' => false, 'type' => 'string' ], 'envelope_from' => [ 'required' => false, 'type' => 'string' ], 'header_from' => [ 'required' => false, 'type' => 'string' ] ]; private $report_tags = [ '<root>' => [ 'children' => [ 'FEEDBACK' => 'feedback' ] ], 'feedback' => [ 'parent' => '<root>', 'children' => [ 'VERSION' => 'ver', 'REPORT_METADATA' => 'rmd', 'POLICY_PUBLISHED' => 'p_p', 'RECORD' => 'rec' ] ], 'ver' => [ 'parent' => 'feedback', 'type' => 'M', 'default' => null ], 'rmd' => [ 'parent' => 'feedback', 'children' => [ 'ORG_NAME' => 'org_name', 'EMAIL' => 'email', 'EXTRA_CONTACT_INFO' => 'extra_contact_info', 'REPORT_ID' => 'report_id', 'DATE_RANGE' => 'd_range', 'ERROR' => 'error_string' ] ], 'p_p' => [ 'parent' => 'feedback', 'children' => [ 'DOMAIN' => 'domain', 'ADKIM' => 'policy_adkim', 'ASPF' => 'policy_aspf', 'P' => 'policy_p', 'SP' => 'policy_sp', 'NP' => 'policy_np', 'PCT' => 'policy_pct', 'FO' => 'policy_fo' ] ], 'rec' => [ 'parent' => 'feedback', 'children' => [ 'ROW' => 'row', 'IDENTIFIERS' => 'ident', 'AUTH_RESULTS' => 'au_res' ] ], 'org_name' => [ 'parent' => 'rmd', 'type' => 'M' ], 'email' => [ 'parent' => 'rmd', 'type' => 'M', 'default' => null ], 'extra_contact_info' => [ 'parent' => 'rmd', 'type' => 'M', 'default' => null ], 'report_id' => [ 'parent' => 'rmd', 'type' => 'M' ], 'd_range' => [ 'parent' => 'rmd', 'children' => [ 'BEGIN' => 'begin_time', 'END' => 'end_time' ] ], 'domain' => [ 'parent' => 'p_p', 'type' => 'M' ], 'error_string' => [ 'parent' => 'rmd', 'type' => 'M', 'default' => null ], 'begin_time' => [ 'parent' => 'd_range', 'type' => 'D', 'key' => 'begin' ], 'end_time' => [ 'parent' => 'd_range', 'type' => 'D', 'key' => 'end' ], 'policy_adkim' => [ 'parent' => 'p_p', 'type' => 'P', 'key' => 'adkim', 'default' => null ], 'policy_aspf' => [ 'parent' => 'p_p', 'type' => 'P', 'key' => 'aspf', 'default' => null ], 'policy_p' => [ 'parent' => 'p_p', 'type' => 'P', 'key' => 'p', 'default' => null ], 'policy_sp' => [ 'parent' => 'p_p', 'type' => 'P', 'key' => 'sp', 'default' => null ], 'policy_np' => [ 'parent' => 'p_p', 'type' => 'P', 'key' => 'np', 'default' => null ], 'policy_pct' => [ 'parent' => 'p_p', 'type' => 'P', 'key' => 'pct', 'default' => null ], 'policy_fo' => [ 'parent' => 'p_p', 'type' => 'P', 'key' => 'fo', 'default' => null ], 'row' => [ 'parent' => 'rec', 'children' => [ 'SOURCE_IP' => 'ip', 'COUNT' => 'rcount', 'POLICY_EVALUATED' => 'p_e' ] ], 'ident' => [ 'parent' => 'rec', 'children' => [ 'ENVELOPE_TO' => 'envelope_to', 'ENVELOPE_FROM' => 'envelope_from', 'HEADER_FROM' => 'header_from' ] ], 'au_res' => [ 'parent' => 'rec', 'children' => [ 'DKIM' => 'dkim_auth', 'SPF' => 'spf_auth' ] ], 'ip' => [ 'parent' => 'row' ], 'rcount' => [ 'parent' => 'row' ], 'p_e' => [ 'parent' => 'row', 'children' => [ 'DISPOSITION' => 'disposition', 'DKIM' => 'dkim_align', 'SPF' => 'spf_align', 'REASON' => 'reason' ] ], 'disposition' => [ 'parent' => 'p_e' ], 'dkim_align' => [ 'parent' => 'p_e' ], 'spf_align' => [ 'parent' => 'p_e' ], 'reason' => [ 'parent' => 'p_e', 'default' => null, 'children' => [ 'TYPE' => 'reason_type', 'COMMENT' => 'reason_comment' ] ], 'envelope_to' => [ 'parent' => 'ident', 'default' => null ], 'envelope_from' => [ 'parent' => 'ident', 'default' => null ], 'header_from' => [ 'parent' => 'ident', 'default' => null ], 'dkim_auth' => [ 'parent' => 'au_res', 'default' => null, 'children' => [ 'DOMAIN' => 'dkim_domain', 'SELECTOR' => 'dkim_selector', 'RESULT' => 'dkim_result', 'HUMAN_RESULT' => 'dkim_human_result' ] ], 'spf_auth' => [ 'parent' => 'au_res', 'default' => null, 'children' => [ 'DOMAIN' => 'spf_domain', 'SCOPE' => 'spf_scope', 'RESULT' => 'spf_result' ] ], 'reason_type' => [ 'parent' => 'reason' ], 'reason_comment' => [ 'parent' => 'reason' ], 'dkim_domain' => [ 'parent' => 'dkim_auth' ], 'dkim_selector' => [ 'parent' => 'dkim_auth' ], 'dkim_result' => [ 'parent' => 'dkim_auth' ], 'dkim_human_result' => [ 'parent' => 'dkim_auth' ], 'spf_domain' => [ 'parent' => 'spf_auth' ], 'spf_scope' => [ 'parent' => 'spf_auth' ], 'spf_result' => [ 'parent' => 'spf_auth' ] ]; }