. */ declare(strict_types=1); namespace Fisharebest\Webtrees\Report; use DomainException; use Fisharebest\Webtrees\Auth; use Fisharebest\Webtrees\Carbon; use Fisharebest\Webtrees\Date; use Fisharebest\Webtrees\Elements\UnknownElement; use Fisharebest\Webtrees\Family; use Fisharebest\Webtrees\Gedcom; use Fisharebest\Webtrees\GedcomRecord; use Fisharebest\Webtrees\I18N; use Fisharebest\Webtrees\Individual; use Fisharebest\Webtrees\Log; use Fisharebest\Webtrees\MediaFile; use Fisharebest\Webtrees\Note; use Fisharebest\Webtrees\Place; use Fisharebest\Webtrees\Registry; use Fisharebest\Webtrees\Tree; use Illuminate\Database\Capsule\Manager as DB; use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\Expression; use Illuminate\Database\Query\JoinClause; use Illuminate\Support\Str; use League\Flysystem\FilesystemOperator; use LogicException; use Symfony\Component\Cache\Adapter\NullAdapter; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use XMLParser; use function addcslashes; use function addslashes; use function array_pop; use function array_shift; use function assert; use function call_user_func; use function count; use function end; use function explode; use function file; use function file_exists; use function getimagesize; use function imagecreatefromstring; use function imagesx; use function imagesy; use function in_array; use function method_exists; use function preg_match; use function preg_match_all; use function preg_replace; use function preg_replace_callback; use function preg_split; use function reset; use function round; use function sprintf; use function str_contains; use function str_replace; use function str_starts_with; use function strip_tags; use function strlen; use function strtoupper; use function substr; use function trim; use function uasort; use function xml_error_string; use function xml_get_current_line_number; use function xml_get_error_code; use function xml_parse; use function xml_parser_create; use function xml_parser_free; use function xml_parser_set_option; use function xml_set_character_data_handler; use function xml_set_element_handler; use const PREG_SET_ORDER; use const XML_OPTION_CASE_FOLDING; /** * Class ReportParserGenerate - parse a report.xml file and generate the report. */ class ReportParserGenerate extends ReportParserBase { /** @var bool Are we collecting data from elements */ private $process_footnote = true; /** @var bool Are we currently outputing data? */ private $print_data = false; /** @var bool[] Push-down stack of $print_data */ private $print_data_stack = []; /** @var int Are we processing GEDCOM data */ private $process_gedcoms = 0; /** @var int Are we processing conditionals */ private $process_ifs = 0; /** @var int Are we processing repeats */ private $process_repeats = 0; /** @var int Quantity of data to repeat during loops */ private $repeat_bytes = 0; /** @var array Repeated data when iterating over loops */ private array $repeats = []; /** @var array|int>> Nested repeating data */ private array $repeats_stack = []; /** @var array Nested repeating data */ private array $wt_report_stack = []; /** @var XMLParser (resource before PHP 8.0) Nested repeating data */ private $parser; /** @var XMLParser[] (resource[] before PHP 8.0) Nested repeating data */ private array $parser_stack = []; /** @var string The current GEDCOM record */ private $gedrec = ''; /** @var array Nested GEDCOM records */ private array $gedrec_stack = []; /** @var ReportBaseElement The currently processed element */ private $current_element; /** @var ReportBaseElement The currently processed element */ private $footnote_element; /** @var string The GEDCOM fact currently being processed */ private $fact = ''; /** @var string The GEDCOM value currently being processed */ private $desc = ''; /** @var string The GEDCOM type currently being processed */ private $type = ''; /** @var int The current generational level */ private $generation = 1; /** @var array Source data for processing lists */ private array $list = []; /** @var int Number of items in lists */ private $list_total = 0; /** @var int Number of items filtered from lists */ private $list_private = 0; /** @var string The filename of the XML report */ protected $report; /** @var AbstractRenderer A factory for creating report elements */ private $report_root; /** @var AbstractRenderer Nested report elements */ private $wt_report; /** @var array> Variables defined in the report at run-time */ private array $vars; private Tree $tree; private FilesystemOperator $data_filesystem; /** * Create a parser for a report * * @param string $report The XML filename * @param AbstractRenderer $report_root * @param array> $vars * @param Tree $tree * @param FilesystemOperator $data_filesystem */ public function __construct( string $report, AbstractRenderer $report_root, array $vars, Tree $tree, FilesystemOperator $data_filesystem ) { $this->report = $report; $this->report_root = $report_root; $this->wt_report = $report_root; $this->current_element = new ReportBaseElement(); $this->vars = $vars; $this->tree = $tree; $this->data_filesystem = $data_filesystem; parent::__construct($report); } /** * get a gedcom subrecord * * searches a gedcom record and returns a subrecord of it. A subrecord is defined starting at a * line with level N and all subsequent lines greater than N until the next N level is reached. * For example, the following is a BIRT subrecord: * 1 BIRT * 2 DATE 1 JAN 1900 * 2 PLAC Phoenix, Maricopa, Arizona * The following example is the DATE subrecord of the above BIRT subrecord: * 2 DATE 1 JAN 1900 * * @param int $level the N level of the subrecord to get * @param string $tag a gedcom tag or string to search for in the record (ie 1 BIRT or 2 DATE) * @param string $gedrec the parent gedcom record to search in * @param int $num this allows you to specify which matching $tag to get. Oftentimes a * gedcom record will have more that 1 of the same type of subrecord. An individual may have * multiple events for example. Passing $num=1 would get the first 1. Passing $num=2 would get the * second one, etc. * * @return string the subrecord that was found or an empty string "" if not found. */ public static function getSubRecord(int $level, string $tag, string $gedrec, int $num = 1): string { if ($gedrec === '') { return ''; } // -- adding \n before and after gedrec $gedrec = "\n" . $gedrec . "\n"; $tag = trim($tag); $searchTarget = "~[\n]" . $tag . "[\s]~"; $ct = preg_match_all($searchTarget, $gedrec, $match, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); if ($ct === 0) { return ''; } if ($ct < $num) { return ''; } $pos1 = $match[$num - 1][0][1]; $pos2 = strpos($gedrec, "\n$level", $pos1 + 1); if (!$pos2) { $pos2 = strpos($gedrec, "\n1", $pos1 + 1); } if (!$pos2) { $pos2 = strpos($gedrec, "\nWT_", $pos1 + 1); // WT_SPOUSE, WT_FAMILY_ID ... } if (!$pos2) { return ltrim(substr($gedrec, $pos1)); } $subrec = substr($gedrec, $pos1, $pos2 - $pos1); return ltrim($subrec); } /** * get CONT lines * * get the N+1 CONT or CONC lines of a gedcom subrecord * * @param int $nlevel the level of the CONT lines to get * @param string $nrec the gedcom subrecord to search in * * @return string a string with all CONT lines merged */ public static function getCont(int $nlevel, string $nrec): string { $text = ''; $subrecords = explode("\n", $nrec); foreach ($subrecords as $thisSubrecord) { if (substr($thisSubrecord, 0, 2) !== $nlevel . ' ') { continue; } $subrecordType = substr($thisSubrecord, 2, 4); if ($subrecordType === 'CONT') { $text .= "\n" . substr($thisSubrecord, 7); } } return $text; } /** * XML start element handler * This function is called whenever a starting element is reached * The element handler will be called if found, otherwise it must be HTML * * @param resource $parser the resource handler for the XML parser * @param string $name the name of the XML element parsed * @param array $attrs an array of key value pairs for the attributes * * @return void */ protected function startElement($parser, string $name, array $attrs): void { $newattrs = []; foreach ($attrs as $key => $value) { if (preg_match("/^\\$(\w+)$/", $value, $match)) { if (isset($this->vars[$match[1]]['id']) && !isset($this->vars[$match[1]]['gedcom'])) { $value = $this->vars[$match[1]]['id']; } } $newattrs[$key] = $value; } $attrs = $newattrs; if ($this->process_footnote && ($this->process_ifs === 0 || $name === 'if') && ($this->process_gedcoms === 0 || $name === 'Gedcom') && ($this->process_repeats === 0 || $name === 'Facts' || $name === 'RepeatTag')) { $method = $name . 'StartHandler'; if (method_exists($this, $method)) { call_user_func([$this, $method], $attrs); } } } /** * XML end element handler * This function is called whenever an ending element is reached * The element handler will be called if found, otherwise it must be HTML * * @param resource $parser the resource handler for the XML parser * @param string $name the name of the XML element parsed * * @return void */ protected function endElement($parser, string $name): void { if (($this->process_footnote || $name === 'Footnote') && ($this->process_ifs === 0 || $name === 'if') && ($this->process_gedcoms === 0 || $name === 'Gedcom') && ($this->process_repeats === 0 || $name === 'Facts' || $name === 'RepeatTag' || $name === 'List' || $name === 'Relatives')) { $method = $name . 'EndHandler'; if (method_exists($this, $method)) { call_user_func([$this, $method]); } } } /** * XML character data handler * * @param resource $parser the resource handler for the XML parser * @param string $data the name of the XML element parsed * * @return void */ protected function characterData($parser, string $data): void { if ($this->print_data && $this->process_gedcoms === 0 && $this->process_ifs === 0 && $this->process_repeats === 0) { $this->current_element->addText($data); } } /** * Handle