xref: /webtrees/app/Report/ReportParserGenerate.php (revision 6bdf767435631ad1dc27ec1ffd855d43dbdce907)
1<?php
2/**
3 * webtrees: online genealogy
4 * Copyright (C) 2017 webtrees development team
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
16namespace Fisharebest\Webtrees\Report;
17
18use Fisharebest\Webtrees\Auth;
19use Fisharebest\Webtrees\Database;
20use Fisharebest\Webtrees\Date;
21use Fisharebest\Webtrees\Family;
22use Fisharebest\Webtrees\Filter;
23use Fisharebest\Webtrees\Functions\Functions;
24use Fisharebest\Webtrees\Functions\FunctionsDate;
25use Fisharebest\Webtrees\GedcomRecord;
26use Fisharebest\Webtrees\GedcomTag;
27use Fisharebest\Webtrees\I18N;
28use Fisharebest\Webtrees\Individual;
29use Fisharebest\Webtrees\Log;
30use Fisharebest\Webtrees\Media;
31use Fisharebest\Webtrees\Note;
32use Fisharebest\Webtrees\Place;
33
34/**
35 * Class ReportParserGenerate - parse a report.xml file and generate the report.
36 */
37class ReportParserGenerate extends ReportParserBase {
38	/** @var bool Are we collecting data from <Footnote> elements  */
39	private $process_footnote = true;
40
41	/** @var bool Are we currently outputing data? */
42	private $print_data = false;
43
44	/** @var bool[] Push-down stack of $print_data */
45	private $print_data_stack = [];
46
47	/** @var int Are we processing GEDCOM data */
48	private $process_gedcoms = 0;
49
50	/** @var int Are we processing conditionals */
51	private $process_ifs = 0;
52
53	/** @var int Are we processing repeats*/
54	private $process_repeats = 0;
55
56	/** @var int Quantity of data to repeat during loops */
57	private $repeat_bytes = 0;
58
59	/** @var array[] Repeated data when iterating over loops */
60	private $repeats = [];
61
62	/** @var array[] Nested repeating data */
63	private $repeats_stack = [];
64
65	/** @var ReportBase[] Nested repeating data */
66	private $wt_report_stack = [];
67
68	/** @var resource Nested repeating data */
69	private $parser;
70
71	/** @var resource[] Nested repeating data */
72	private $parser_stack = [];
73
74	/** @var string The current GEDCOM record */
75	private $gedrec = '';
76
77	/** @var string[] Nested GEDCOM records */
78	private $gedrec_stack = [];
79
80	/** @var ReportBaseElement The currently processed element */
81	private $current_element;
82
83	/** @var ReportBaseElement The currently processed element */
84	private $footnote_element;
85
86	/** @var string The GEDCOM fact currently being processed */
87	private $fact = '';
88
89	/** @var string The GEDCOM value currently being processed */
90	private $desc = '';
91
92	/** @var string The GEDCOM type currently being processed */
93	private $type = '';
94
95	/** @var int The current generational level */
96	private $generation = 1;
97
98	/** @var array Source data for processing lists */
99	private $list = [];
100
101	/** @var int Number of items in lists */
102	private $list_total = 0;
103
104	/** @var int Number of items filtered from lists */
105	private $list_private = 0;
106
107	/** @var ReportBase A factory for creating report elements */
108	private $report_root;
109
110	/** @var ReportBase Nested report elements */
111	private $wt_report;
112
113	/** @var string[][] Variables defined in the report at run-time */
114	private $vars;
115
116	/**
117	 * Create a parser for a report
118	 *
119	 * @param string     $report     The XML filename
120	 * @param ReportBase $report_root
121	 * @param string[][] $vars
122	 */
123	public function __construct($report, ReportBase $report_root = null, array $vars = []) {
124		$this->report_root     = $report_root;
125		$this->wt_report       = $report_root;
126		$this->current_element = new ReportBaseElement;
127		$this->vars            = $vars;
128		parent::__construct($report);
129	}
130
131	/**
132	 * XML start element handler
133	 *
134	 * This function is called whenever a starting element is reached
135	 * The element handler will be called if found, otherwise it must be HTML
136	 *
137	 * @param resource $parser the resource handler for the XML parser
138	 * @param string   $name   the name of the XML element parsed
139	 * @param array    $attrs  an array of key value pairs for the attributes
140	 */
141	protected function startElement($parser, $name, $attrs) {
142		$newattrs = [];
143
144		foreach ($attrs as $key => $value) {
145			if (preg_match("/^\\$(\w+)$/", $value, $match)) {
146				if ((isset($this->vars[$match[1]]['id'])) && (!isset($this->vars[$match[1]]['gedcom']))) {
147					$value = $this->vars[$match[1]]['id'];
148				}
149			}
150			$newattrs[$key] = $value;
151		}
152		$attrs = $newattrs;
153		if ($this->process_footnote && ($this->process_ifs === 0 || $name === 'if') && ($this->process_gedcoms === 0 || $name === 'Gedcom') && ($this->process_repeats === 0 || $name === 'Facts' || $name === 'RepeatTag')) {
154			$start_method = $name . 'StartHandler';
155			$end_method   = $name . 'EndHandler';
156			if (method_exists($this, $start_method)) {
157				$this->$start_method($attrs);
158			} elseif (!method_exists($this, $end_method)) {
159				$this->htmlStartHandler($name, $attrs);
160			}
161		}
162	}
163
164	/**
165	 * XML end element handler
166	 *
167	 * This function is called whenever an ending element is reached
168	 * The element handler will be called if found, otherwise it must be HTML
169	 *
170	 * @param resource $parser the resource handler for the XML parser
171	 * @param string   $name   the name of the XML element parsed
172	 */
173	protected function endElement($parser, $name) {
174		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')) {
175			$start_method = $name . 'StartHandler';
176			$end_method   = $name . 'EndHandler';
177			if (method_exists($this, $end_method)) {
178				$this->$end_method();
179			} elseif (!method_exists($this, $start_method)) {
180				$this->htmlEndHandler($name);
181			}
182		}
183	}
184
185	/**
186	 * XML character data handler
187	 *
188	 * @param resource $parser the resource handler for the XML parser
189	 * @param string   $data   the name of the XML element parsed
190	 */
191	protected function characterData($parser, $data) {
192		if ($this->print_data && $this->process_gedcoms === 0 && $this->process_ifs === 0 && $this->process_repeats === 0) {
193			$this->current_element->addText($data);
194		}
195	}
196
197	/**
198	 * XML <style>
199	 *
200	 * @param array $attrs an array of key value pairs for the attributes
201	 */
202	private function styleStartHandler($attrs) {
203		if (empty($attrs['name'])) {
204			throw new \DomainException('REPORT ERROR Style: The "name" of the style is missing or not set in the XML file.');
205		}
206
207		// array Style that will be passed on
208		$s = [];
209
210		// string Name af the style
211		$s['name'] = $attrs['name'];
212
213		// string Name of the DEFAULT font
214		$s['font'] = $this->wt_report->defaultFont;
215		if (!empty($attrs['font'])) {
216			$s['font'] = $attrs['font'];
217		}
218
219		// int The size of the font in points
220		$s['size'] = $this->wt_report->defaultFontSize;
221		if (!empty($attrs['size'])) {
222			$s['size'] = (int) $attrs['size'];
223		} // Get it as int to ignore all decimal points or text (if any text then int(0))
224
225		// string B: bold, I: italic, U: underline, D: line trough, The default value is regular.
226		$s['style'] = '';
227		if (!empty($attrs['style'])) {
228			$s['style'] = $attrs['style'];
229		}
230
231		$this->wt_report->addStyle($s);
232	}
233
234	/**
235	 * XML <Doc>
236	 *
237	 * Sets up the basics of the document proparties
238	 *
239	 * @param array $attrs an array of key value pairs for the attributes
240	 */
241	private function docStartHandler($attrs) {
242		$this->parser = $this->xml_parser;
243
244		// Custom page width
245		if (!empty($attrs['customwidth'])) {
246			$this->wt_report->pagew = (int) $attrs['customwidth'];
247		} // Get it as int to ignore all decimal points or text (if any text then int(0))
248		// Custom Page height
249		if (!empty($attrs['customheight'])) {
250			$this->wt_report->pageh = (int) $attrs['customheight'];
251		} // Get it as int to ignore all decimal points or text (if any text then int(0))
252
253		// Left Margin
254		if (isset($attrs['leftmargin'])) {
255			if ($attrs['leftmargin'] === '0') {
256				$this->wt_report->leftmargin = 0;
257			} elseif (!empty($attrs['leftmargin'])) {
258				$this->wt_report->leftmargin = (int) $attrs['leftmargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
259			}
260		}
261		// Right Margin
262		if (isset($attrs['rightmargin'])) {
263			if ($attrs['rightmargin'] === '0') {
264				$this->wt_report->rightmargin = 0;
265			} elseif (!empty($attrs['rightmargin'])) {
266				$this->wt_report->rightmargin = (int) $attrs['rightmargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
267			}
268		}
269		// Top Margin
270		if (isset($attrs['topmargin'])) {
271			if ($attrs['topmargin'] === '0') {
272				$this->wt_report->topmargin = 0;
273			} elseif (!empty($attrs['topmargin'])) {
274				$this->wt_report->topmargin = (int) $attrs['topmargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
275			}
276		}
277		// Bottom Margin
278		if (isset($attrs['bottommargin'])) {
279			if ($attrs['bottommargin'] === '0') {
280				$this->wt_report->bottommargin = 0;
281			} elseif (!empty($attrs['bottommargin'])) {
282				$this->wt_report->bottommargin = (int) $attrs['bottommargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
283			}
284		}
285		// Header Margin
286		if (isset($attrs['headermargin'])) {
287			if ($attrs['headermargin'] === '0') {
288				$this->wt_report->headermargin = 0;
289			} elseif (!empty($attrs['headermargin'])) {
290				$this->wt_report->headermargin = (int) $attrs['headermargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
291			}
292		}
293		// Footer Margin
294		if (isset($attrs['footermargin'])) {
295			if ($attrs['footermargin'] === '0') {
296				$this->wt_report->footermargin = 0;
297			} elseif (!empty($attrs['footermargin'])) {
298				$this->wt_report->footermargin = (int) $attrs['footermargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
299			}
300		}
301
302		// Page Orientation
303		if (!empty($attrs['orientation'])) {
304			if ($attrs['orientation'] == 'landscape') {
305				$this->wt_report->orientation = 'landscape';
306			} elseif ($attrs['orientation'] == 'portrait') {
307				$this->wt_report->orientation = 'portrait';
308			}
309		}
310		// Page Size
311		if (!empty($attrs['pageSize'])) {
312			$this->wt_report->pageFormat = strtoupper($attrs['pageSize']);
313		}
314
315		// Show Generated By...
316		if (isset($attrs['showGeneratedBy'])) {
317			if ($attrs['showGeneratedBy'] === '0') {
318				$this->wt_report->showGenText = false;
319			} elseif ($attrs['showGeneratedBy'] === '1') {
320				$this->wt_report->showGenText = true;
321			}
322		}
323
324		$this->wt_report->setup();
325	}
326
327	/**
328	 * XML </Doc>
329	 */
330	private function docEndHandler() {
331		$this->wt_report->run();
332	}
333
334	/**
335	 * XML <Header>
336	 */
337	private function headerStartHandler() {
338		// Clear the Header before any new elements are added
339		$this->wt_report->clearHeader();
340		$this->wt_report->setProcessing('H');
341	}
342
343	/**
344	 * XML <PageHeader>
345	 */
346	private function pageHeaderStartHandler() {
347		array_push($this->print_data_stack, $this->print_data);
348		$this->print_data = false;
349		array_push($this->wt_report_stack, $this->wt_report);
350		$this->wt_report = $this->report_root->createPageHeader();
351	}
352
353	/**
354	 * XML <pageHeaderEndHandler>
355	 */
356	private function pageHeaderEndHandler() {
357		$this->print_data        = array_pop($this->print_data_stack);
358		$this->current_element   = $this->wt_report;
359		$this->wt_report         = array_pop($this->wt_report_stack);
360		$this->wt_report->addElement($this->current_element);
361	}
362
363	/**
364	 * XML <bodyStartHandler>
365	 */
366	private function bodyStartHandler() {
367		$this->wt_report->setProcessing('B');
368	}
369
370	/**
371	 * XML <footerStartHandler>
372	 */
373	private function footerStartHandler() {
374		$this->wt_report->setProcessing('F');
375	}
376
377	/**
378	 * XML <Cell>
379	 *
380	 * @param array $attrs an array of key value pairs for the attributes
381	 */
382	private function cellStartHandler($attrs) {
383		// string The text alignment of the text in this box.
384		$align = '';
385		if (!empty($attrs['align'])) {
386			$align = $attrs['align'];
387			// RTL supported left/right alignment
388			if ($align == 'rightrtl') {
389				if ($this->wt_report->rtl) {
390					$align = 'left';
391				} else {
392					$align = 'right';
393				}
394			} elseif ($align == 'leftrtl') {
395				if ($this->wt_report->rtl) {
396					$align = 'right';
397				} else {
398					$align = 'left';
399				}
400			}
401		}
402
403		// string The color to fill the background of this cell
404		$bgcolor = '';
405		if (!empty($attrs['bgcolor'])) {
406			$bgcolor = $attrs['bgcolor'];
407		}
408
409		// int Whether or not the background should be painted
410		$fill = 1;
411		if (isset($attrs['fill'])) {
412			if ($attrs['fill'] === '0') {
413				$fill = 0;
414			} elseif ($attrs['fill'] === '1') {
415				$fill = 1;
416			}
417		}
418
419		$reseth = true;
420		// boolean   if true reset the last cell height (default true)
421		if (isset($attrs['reseth'])) {
422			if ($attrs['reseth'] === '0') {
423				$reseth = false;
424			} elseif ($attrs['reseth'] === '1') {
425				$reseth = true;
426			}
427		}
428
429		// mixed Whether or not a border should be printed around this box
430		$border = 0;
431		if (!empty($attrs['border'])) {
432			$border = $attrs['border'];
433		}
434		// string Border color in HTML code
435		$bocolor = '';
436		if (!empty($attrs['bocolor'])) {
437			$bocolor = $attrs['bocolor'];
438		}
439
440		// int Cell height (expressed in points) The starting height of this cell. If the text wraps the height will automatically be adjusted.
441		$height = 0;
442		if (!empty($attrs['height'])) {
443			$height = (int) $attrs['height'];
444		}
445		// int Cell width (expressed in points) Setting the width to 0 will make it the width from the current location to the right margin.
446		$width = 0;
447		if (!empty($attrs['width'])) {
448			$width = (int) $attrs['width'];
449		}
450
451		// int Stretch carachter mode
452		$stretch = 0;
453		if (!empty($attrs['stretch'])) {
454			$stretch = (int) $attrs['stretch'];
455		}
456
457		// mixed Position the left corner of this box on the page. The default is the current position.
458		$left = '.';
459		if (isset($attrs['left'])) {
460			if ($attrs['left'] === '.') {
461				$left = '.';
462			} elseif (!empty($attrs['left'])) {
463				$left = (int) $attrs['left'];
464			} elseif ($attrs['left'] === '0') {
465				$left = 0;
466			}
467		}
468		// mixed Position the top corner of this box on the page. the default is the current position
469		$top = '.';
470		if (isset($attrs['top'])) {
471			if ($attrs['top'] === '.') {
472				$top = '.';
473			} elseif (!empty($attrs['top'])) {
474				$top = (int) $attrs['top'];
475			} elseif ($attrs['top'] === '0') {
476				$top = 0;
477			}
478		}
479
480		// string The name of the Style that should be used to render the text.
481		$style = '';
482		if (!empty($attrs['style'])) {
483			$style = $attrs['style'];
484		}
485
486		// string Text color in html code
487		$tcolor = '';
488		if (!empty($attrs['tcolor'])) {
489			$tcolor = $attrs['tcolor'];
490		}
491
492		// int Indicates where the current position should go after the call.
493		$ln = 0;
494		if (isset($attrs['newline'])) {
495			if (!empty($attrs['newline'])) {
496				$ln = (int) $attrs['newline'];
497			} elseif ($attrs['newline'] === '0') {
498				$ln = 0;
499			}
500		}
501
502		if ($align == 'left') {
503			$align = 'L';
504		} elseif ($align == 'right') {
505			$align = 'R';
506		} elseif ($align == 'center') {
507			$align = 'C';
508		} elseif ($align == 'justify') {
509			$align = 'J';
510		}
511
512		array_push($this->print_data_stack, $this->print_data);
513		$this->print_data = true;
514
515		$this->current_element = $this->report_root->createCell(
516			$width,
517			$height,
518			$border,
519			$align,
520			$bgcolor,
521			$style,
522			$ln,
523			$top,
524			$left,
525			$fill,
526			$stretch,
527			$bocolor,
528			$tcolor,
529			$reseth
530		);
531	}
532
533	/**
534	 * XML </Cell>
535	 */
536	private function cellEndHandler() {
537		$this->print_data = array_pop($this->print_data_stack);
538		$this->wt_report->addElement($this->current_element);
539	}
540
541	/**
542	 * XML <Now /> element handler
543	 */
544	private function nowStartHandler() {
545		$g = FunctionsDate::timestampToGedcomDate(WT_TIMESTAMP + WT_TIMESTAMP_OFFSET);
546		$this->current_element->addText($g->display());
547	}
548
549	/**
550	 * XML <PageNum /> element handler
551	 */
552	private function pageNumStartHandler() {
553		$this->current_element->addText('#PAGENUM#');
554	}
555
556	/**
557	 * XML <TotalPages /> element handler
558	 */
559	private function totalPagesStartHandler() {
560		$this->current_element->addText('{{:ptp:}}');
561	}
562
563	/**
564	 * Called at the start of an element.
565	 *
566	 * @param array $attrs an array of key value pairs for the attributes
567	 */
568	private function gedcomStartHandler($attrs) {
569		global $WT_TREE;
570
571		if ($this->process_gedcoms > 0) {
572			$this->process_gedcoms++;
573
574			return;
575		}
576
577		$tag       = $attrs['id'];
578		$tag       = str_replace('@fact', $this->fact, $tag);
579		$tags      = explode(':', $tag);
580		$newgedrec = '';
581		if (count($tags) < 2) {
582			$tmp       = GedcomRecord::getInstance($attrs['id'], $WT_TREE);
583			$newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($WT_TREE)) : '';
584		}
585		if (empty($newgedrec)) {
586			$tgedrec   = $this->gedrec;
587			$newgedrec = '';
588			foreach ($tags as $tag) {
589				if (preg_match('/\$(.+)/', $tag, $match)) {
590					if (isset($this->vars[$match[1]]['gedcom'])) {
591						$newgedrec = $this->vars[$match[1]]['gedcom'];
592					} else {
593						$tmp       = GedcomRecord::getInstance($match[1], $WT_TREE);
594						$newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($WT_TREE)) : '';
595					}
596				} else {
597					if (preg_match('/@(.+)/', $tag, $match)) {
598						$gmatch = [];
599						if (preg_match("/\d $match[1] @([^@]+)@/", $tgedrec, $gmatch)) {
600							$tmp       = GedcomRecord::getInstance($gmatch[1], $WT_TREE);
601							$newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($WT_TREE)) : '';
602							$tgedrec   = $newgedrec;
603						} else {
604							$newgedrec = '';
605							break;
606						}
607					} else {
608						$temp      = explode(' ', trim($tgedrec));
609						$level     = $temp[0] + 1;
610						$newgedrec = Functions::getSubRecord($level, "$level $tag", $tgedrec);
611						$tgedrec   = $newgedrec;
612					}
613				}
614			}
615		}
616		if (!empty($newgedrec)) {
617			array_push($this->gedrec_stack, [$this->gedrec, $this->fact, $this->desc]);
618			$this->gedrec = $newgedrec;
619			if (preg_match("/(\d+) (_?[A-Z0-9]+) (.*)/", $this->gedrec, $match)) {
620				$this->fact = $match[2];
621				$this->desc = trim($match[3]);
622			}
623		} else {
624			$this->process_gedcoms++;
625		}
626	}
627
628	/**
629	 * Called at the end of an element.
630	 */
631	private function gedcomEndHandler() {
632		if ($this->process_gedcoms > 0) {
633			$this->process_gedcoms--;
634		} else {
635			list($this->gedrec, $this->fact, $this->desc) = array_pop($this->gedrec_stack);
636		}
637	}
638
639	/**
640	 * XML <textBoxStartHandler>
641	 *
642	 * @param array $attrs an array of key value pairs for the attributes
643	 */
644	private function textBoxStartHandler($attrs) {
645		// string Background color code
646		$bgcolor = '';
647		if (!empty($attrs['bgcolor'])) {
648			$bgcolor = $attrs['bgcolor'];
649		}
650
651		// boolean Wether or not fill the background color
652		$fill = true;
653		if (isset($attrs['fill'])) {
654			if ($attrs['fill'] === '0') {
655				$fill = false;
656			} elseif ($attrs['fill'] === '1') {
657				$fill = true;
658			}
659		}
660
661		// var boolean Whether or not a border should be printed around this box. 0 = no border, 1 = border. Default is 0
662		$border = false;
663		if (isset($attrs['border'])) {
664			if ($attrs['border'] === '1') {
665				$border = true;
666			} elseif ($attrs['border'] === '0') {
667				$border = false;
668			}
669		}
670
671		// int The starting height of this cell. If the text wraps the height will automatically be adjusted
672		$height = 0;
673		if (!empty($attrs['height'])) {
674			$height = (int) $attrs['height'];
675		}
676		// int Setting the width to 0 will make it the width from the current location to the margin
677		$width = 0;
678		if (!empty($attrs['width'])) {
679			$width = (int) $attrs['width'];
680		}
681
682		// mixed Position the left corner of this box on the page. The default is the current position.
683		$left = '.';
684		if (isset($attrs['left'])) {
685			if ($attrs['left'] === '.') {
686				$left = '.';
687			} elseif (!empty($attrs['left'])) {
688				$left = (int) $attrs['left'];
689			} elseif ($attrs['left'] === '0') {
690				$left = 0;
691			}
692		}
693		// mixed Position the top corner of this box on the page. the default is the current position
694		$top = '.';
695		if (isset($attrs['top'])) {
696			if ($attrs['top'] === '.') {
697				$top = '.';
698			} elseif (!empty($attrs['top'])) {
699				$top = (int) $attrs['top'];
700			} elseif ($attrs['top'] === '0') {
701				$top = 0;
702			}
703		}
704		// boolean After this box is finished rendering, should the next section of text start immediately after the this box or should it start on a new line under this box. 0 = no new line, 1 = force new line. Default is 0
705		$newline = false;
706		if (isset($attrs['newline'])) {
707			if ($attrs['newline'] === '1') {
708				$newline = true;
709			} elseif ($attrs['newline'] === '0') {
710				$newline = false;
711			}
712		}
713		// boolean
714		$pagecheck = true;
715		if (isset($attrs['pagecheck'])) {
716			if ($attrs['pagecheck'] === '0') {
717				$pagecheck = false;
718			} elseif ($attrs['pagecheck'] === '1') {
719				$pagecheck = true;
720			}
721		}
722		// boolean Cell padding
723		$padding = true;
724		if (isset($attrs['padding'])) {
725			if ($attrs['padding'] === '0') {
726				$padding = false;
727			} elseif ($attrs['padding'] === '1') {
728				$padding = true;
729			}
730		}
731		// boolean Reset this box Height
732		$reseth = false;
733		if (isset($attrs['reseth'])) {
734			if ($attrs['reseth'] === '1') {
735				$reseth = true;
736			} elseif ($attrs['reseth'] === '0') {
737				$reseth = false;
738			}
739		}
740
741		// string Style of rendering
742		$style = '';
743
744		array_push($this->print_data_stack, $this->print_data);
745		$this->print_data = false;
746
747		array_push($this->wt_report_stack, $this->wt_report);
748		$this->wt_report = $this->report_root->createTextBox(
749			$width,
750			$height,
751			$border,
752			$bgcolor,
753			$newline,
754			$left,
755			$top,
756			$pagecheck,
757			$style,
758			$fill,
759			$padding,
760			$reseth
761		);
762	}
763
764	/**
765	 * XML <textBoxEndHandler>
766	 */
767	private function textBoxEndHandler() {
768		$this->print_data      = array_pop($this->print_data_stack);
769		$this->current_element = $this->wt_report;
770		$this->wt_report       = array_pop($this->wt_report_stack);
771		$this->wt_report->addElement($this->current_element);
772	}
773
774	/**
775	 * XLM <Text>.
776	 *
777	 * @param array $attrs an array of key value pairs for the attributes
778	 */
779	private function textStartHandler($attrs) {
780		array_push($this->print_data_stack, $this->print_data);
781		$this->print_data = true;
782
783		// string The name of the Style that should be used to render the text.
784		$style = '';
785		if (!empty($attrs['style'])) {
786			$style = $attrs['style'];
787		}
788
789		// string  The color of the text - Keep the black color as default
790		$color = '';
791		if (!empty($attrs['color'])) {
792			$color = $attrs['color'];
793		}
794
795		$this->current_element = $this->report_root->createText($style, $color);
796	}
797
798	/**
799	 * XML </Text>
800	 */
801	private function textEndHandler() {
802		$this->print_data = array_pop($this->print_data_stack);
803		$this->wt_report->addElement($this->current_element);
804	}
805
806	/**
807	 * XML <GetPersonName/>
808	 *
809	 * Get the name
810	 * 1. id is empty - current GEDCOM record
811	 * 2. id is set with a record id
812	 *
813	 * @param array $attrs an array of key value pairs for the attributes
814	 */
815	private function getPersonNameStartHandler($attrs) {
816		global $WT_TREE;
817
818		$id    = '';
819		$match = [];
820		if (empty($attrs['id'])) {
821			if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
822				$id = $match[1];
823			}
824		} else {
825			if (preg_match('/\$(.+)/', $attrs['id'], $match)) {
826				if (isset($this->vars[$match[1]]['id'])) {
827					$id = $this->vars[$match[1]]['id'];
828				}
829			} else {
830				if (preg_match('/@(.+)/', $attrs['id'], $match)) {
831					$gmatch = [];
832					if (preg_match("/\d $match[1] @([^@]+)@/", $this->gedrec, $gmatch)) {
833						$id = $gmatch[1];
834					}
835				} else {
836					$id = $attrs['id'];
837				}
838			}
839		}
840		if (!empty($id)) {
841			$record = GedcomRecord::getInstance($id, $WT_TREE);
842			if (is_null($record)) {
843				return;
844			}
845			if (!$record->canShowName()) {
846				$this->current_element->addText(I18N::translate('Private'));
847			} else {
848				$name = $record->getFullName();
849				$name = preg_replace(
850					['/<span class="starredname">/', '/<\/span><\/span>/', '/<\/span>/'],
851					['«', '', '»'],
852					$name
853				);
854				$name = strip_tags($name);
855				if (!empty($attrs['truncate'])) {
856					if (mb_strlen($name) > $attrs['truncate']) {
857						$name  = preg_replace("/\(.*\) ?/", '', $name); //removes () and text inbetween - what about ", [ and { etc?
858						$words = preg_split('/[, -]+/', $name); // names separated with space, comma or hyphen - any others?
859						$name  = $words[count($words) - 1];
860						for ($i = count($words) - 2; $i >= 0; $i--) {
861							$len = mb_strlen($name);
862							for ($j = count($words) - 3; $j >= 0; $j--) {
863								$len += mb_strlen($words[$j]);
864							}
865							if ($len > $attrs['truncate']) {
866								$first_letter = mb_substr($words[$i], 0, 1);
867								// Do not show " of nick-names
868								if ($first_letter != '"') {
869									$name = mb_substr($words[$i], 0, 1) . '. ' . $name;
870								}
871							} else {
872								$name = $words[$i] . ' ' . $name;
873							}
874						}
875					}
876				} else {
877					$addname = $record->getAddName();
878					$addname = preg_replace(
879						['/<span class="starredname">/', '/<\/span><\/span>/', '/<\/span>/'],
880						['«', '', '»'],
881						$addname
882					);
883					$addname = strip_tags($addname);
884					if (!empty($addname)) {
885						$name .= ' ' . $addname;
886					}
887				}
888				$this->current_element->addText(trim($name));
889			}
890		}
891	}
892
893	/**
894	 * XML <GedcomValue/>
895	 *
896	 * @param array $attrs an array of key value pairs for the attributes
897	 */
898	private function gedcomValueStartHandler($attrs) {
899		global $WT_TREE;
900
901		$id    = '';
902		$match = [];
903		if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
904			$id = $match[1];
905		}
906
907		if (isset($attrs['newline']) && $attrs['newline'] == '1') {
908			$useBreak = '1';
909		} else {
910			$useBreak = '0';
911		}
912
913		$tag = $attrs['tag'];
914		if (!empty($tag)) {
915			if ($tag == '@desc') {
916				$value = $this->desc;
917				$value = trim($value);
918				$this->current_element->addText($value);
919			}
920			if ($tag == '@id') {
921				$this->current_element->addText($id);
922			} else {
923				$tag = str_replace('@fact', $this->fact, $tag);
924				if (empty($attrs['level'])) {
925					$temp  = explode(' ', trim($this->gedrec));
926					$level = $temp[0];
927					if ($level == 0) {
928						$level++;
929					}
930				} else {
931					$level = $attrs['level'];
932				}
933				$tags  = preg_split('/[: ]/', $tag);
934				$value = $this->getGedcomValue($tag, $level, $this->gedrec);
935				switch (end($tags)) {
936				case 'DATE':
937					$tmp   = new Date($value);
938					$value = $tmp->display();
939					break;
940				case 'PLAC':
941					$tmp   = new Place($value, $WT_TREE);
942					$value = $tmp->getShortName();
943					break;
944				}
945				if ($useBreak == '1') {
946					// Insert <br> when multiple dates exist.
947					// This works around a TCPDF bug that incorrectly wraps RTL dates on LTR pages
948					$value = str_replace('(', '<br>(', $value);
949					$value = str_replace('<span dir="ltr"><br>', '<br><span dir="ltr">', $value);
950					$value = str_replace('<span dir="rtl"><br>', '<br><span dir="rtl">', $value);
951					if (substr($value, 0, 6) == '<br>') {
952						$value = substr($value, 6);
953					}
954				}
955				$tmp = explode(':', $tag);
956				if (in_array(end($tmp), ['NOTE', 'TEXT'])) {
957					$value = Filter::formatText($value, $WT_TREE); // We'll strip HTML in addText()
958				}
959				$this->current_element->addText($value);
960			}
961		}
962	}
963
964	/**
965	 * XML <RepeatTag>
966	 *
967	 * @param array $attrs an array of key value pairs for the attributes
968	 */
969	private function repeatTagStartHandler($attrs) {
970		global $WT_TREE;
971
972		$this->process_repeats++;
973		if ($this->process_repeats > 1) {
974			return;
975		}
976
977		array_push($this->repeats_stack, [$this->repeats, $this->repeat_bytes]);
978		$this->repeats      = [];
979		$this->repeat_bytes = xml_get_current_line_number($this->parser);
980
981		$tag = '';
982		if (isset($attrs['tag'])) {
983			$tag = $attrs['tag'];
984		}
985		if (!empty($tag)) {
986			if ($tag == '@desc') {
987				$value = $this->desc;
988				$value = trim($value);
989				$this->current_element->addText($value);
990			} else {
991				$tag   = str_replace('@fact', $this->fact, $tag);
992				$tags  = explode(':', $tag);
993				$temp  = explode(' ', trim($this->gedrec));
994				$level = $temp[0];
995				if ($level == 0) {
996					$level++;
997				}
998				$subrec = $this->gedrec;
999				$t      = $tag;
1000				$count  = count($tags);
1001				$i      = 0;
1002				while ($i < $count) {
1003					$t = $tags[$i];
1004					if (!empty($t)) {
1005						if ($i < ($count - 1)) {
1006							$subrec = Functions::getSubRecord($level, "$level $t", $subrec);
1007							if (empty($subrec)) {
1008								$level--;
1009								$subrec = Functions::getSubRecord($level, "@ $t", $this->gedrec);
1010								if (empty($subrec)) {
1011									return;
1012								}
1013							}
1014						}
1015						$level++;
1016					}
1017					$i++;
1018				}
1019				$level--;
1020				$count = preg_match_all("/$level $t(.*)/", $subrec, $match, PREG_SET_ORDER);
1021				$i     = 0;
1022				while ($i < $count) {
1023					$i++;
1024					// Privacy check - is this a link, and are we allowed to view the linked object?
1025					$subrecord = Functions::getSubRecord($level, "$level $t", $subrec, $i);
1026					if (preg_match('/^\d ' . WT_REGEX_TAG . ' @(' . WT_REGEX_XREF . ')@/', $subrecord, $xref_match)) {
1027						$linked_object = GedcomRecord::getInstance($xref_match[1], $WT_TREE);
1028						if ($linked_object && !$linked_object->canShow()) {
1029							continue;
1030						}
1031					}
1032					$this->repeats[] = $subrecord;
1033				}
1034			}
1035		}
1036	}
1037
1038	/**
1039	 * XML </ RepeatTag>
1040	 */
1041	private function repeatTagEndHandler() {
1042		global $report;
1043
1044		$this->process_repeats--;
1045		if ($this->process_repeats > 0) {
1046			return;
1047		}
1048
1049		// Check if there is anything to repeat
1050		if (count($this->repeats) > 0) {
1051			// No need to load them if not used...
1052
1053			$lineoffset = 0;
1054			foreach ($this->repeats_stack as $rep) {
1055				$lineoffset += $rep[1];
1056			}
1057			//-- read the xml from the file
1058			$lines = file($report);
1059			while (strpos($lines[$lineoffset + $this->repeat_bytes], '<RepeatTag') === false) {
1060				$lineoffset--;
1061			}
1062			$lineoffset++;
1063			$reportxml = "<tempdoc>\n";
1064			$line_nr   = $lineoffset + $this->repeat_bytes;
1065			// RepeatTag Level counter
1066			$count = 1;
1067			while (0 < $count) {
1068				if (strstr($lines[$line_nr], '<RepeatTag') !== false) {
1069					$count++;
1070				} elseif (strstr($lines[$line_nr], '</RepeatTag') !== false) {
1071					$count--;
1072				}
1073				if (0 < $count) {
1074					$reportxml .= $lines[$line_nr];
1075				}
1076				$line_nr++;
1077			}
1078			// No need to drag this
1079			unset($lines);
1080			$reportxml .= "</tempdoc>\n";
1081			// Save original values
1082			array_push($this->parser_stack, $this->parser);
1083			$oldgedrec = $this->gedrec;
1084			foreach ($this->repeats as $gedrec) {
1085				$this->gedrec  = $gedrec;
1086				$repeat_parser = xml_parser_create();
1087				$this->parser  = $repeat_parser;
1088				xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false);
1089				xml_set_element_handler($repeat_parser, [$this, 'startElement'], [$this, 'endElement']);
1090				xml_set_character_data_handler($repeat_parser, [$this, 'characterData']);
1091				if (!xml_parse($repeat_parser, $reportxml, true)) {
1092					throw new \DomainException(sprintf(
1093						'RepeatTagEHandler XML error: %s at line %d',
1094						xml_error_string(xml_get_error_code($repeat_parser)),
1095						xml_get_current_line_number($repeat_parser)
1096					));
1097				}
1098				xml_parser_free($repeat_parser);
1099			}
1100			// Restore original values
1101			$this->gedrec = $oldgedrec;
1102			$this->parser = array_pop($this->parser_stack);
1103		}
1104		list($this->repeats, $this->repeat_bytes) = array_pop($this->repeats_stack);
1105	}
1106
1107	/**
1108	 * Variable lookup
1109	 *
1110	 * Retrieve predefined variables :
1111	 *
1112	 * @ desc GEDCOM fact description, example:
1113	 *        1 EVEN This is a description
1114	 * @ fact GEDCOM fact tag, such as BIRT, DEAT etc.
1115	 * $ I18N::translate('....')
1116	 * $ language_settings[]
1117	 *
1118	 * @param array $attrs an array of key value pairs for the attributes
1119	 */
1120	private function varStartHandler($attrs) {
1121		if (empty($attrs['var'])) {
1122			throw new \DomainException('REPORT ERROR var: The attribute "var=" is missing or not set in the XML file on line: ' . xml_get_current_line_number($this->parser));
1123		}
1124
1125		$var = $attrs['var'];
1126		// SetVar element preset variables
1127		if (!empty($this->vars[$var]['id'])) {
1128			$var = $this->vars[$var]['id'];
1129		} else {
1130			$tfact = $this->fact;
1131			if (($this->fact === 'EVEN' || $this->fact === 'FACT') && $this->type !== ' ') {
1132				// Use :
1133				// n TYPE This text if string
1134				$tfact = $this->type;
1135			}
1136			$var = str_replace(['@fact', '@desc'], [GedcomTag::getLabel($tfact), $this->desc], $var);
1137			if (preg_match('/^I18N::number\((.+)\)$/', $var, $match)) {
1138				$var = I18N::number($match[1]);
1139			} elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $var, $match)) {
1140				$var = I18N::translate($match[1]);
1141			} elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $var, $match)) {
1142				$var = I18N::translateContext($match[1], $match[2]);
1143			}
1144		}
1145		// Check if variable is set as a date and reformat the date
1146		if (isset($attrs['date'])) {
1147			if ($attrs['date'] === '1') {
1148				$g   = new Date($var);
1149				$var = $g->display();
1150			}
1151		}
1152		$this->current_element->addText($var);
1153		$this->text = $var; // Used for title/descriptio
1154	}
1155
1156	/**
1157	 * XML <Facts>
1158	 *
1159	 * @param array $attrs an array of key value pairs for the attributes
1160	 */
1161	private function factsStartHandler($attrs) {
1162		global $WT_TREE;
1163
1164		$this->process_repeats++;
1165		if ($this->process_repeats > 1) {
1166			return;
1167		}
1168
1169		array_push($this->repeats_stack, [$this->repeats, $this->repeat_bytes]);
1170		$this->repeats      = [];
1171		$this->repeat_bytes = xml_get_current_line_number($this->parser);
1172
1173		$id    = '';
1174		$match = [];
1175		if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1176			$id = $match[1];
1177		}
1178		$tag = '';
1179		if (isset($attrs['ignore'])) {
1180			$tag .= $attrs['ignore'];
1181		}
1182		if (preg_match('/\$(.+)/', $tag, $match)) {
1183			$tag = $this->vars[$match[1]]['id'];
1184		}
1185
1186		$record = GedcomRecord::getInstance($id, $WT_TREE);
1187		if (empty($attrs['diff']) && !empty($id)) {
1188			$facts = $record->getFacts();
1189			Functions::sortFacts($facts);
1190			$this->repeats  = [];
1191			$nonfacts       = explode(',', $tag);
1192			foreach ($facts as $event) {
1193				if (!in_array($event->getTag(), $nonfacts)) {
1194					$this->repeats[] = $event->getGedcom();
1195				}
1196			}
1197		} else {
1198			foreach ($record->getFacts() as $fact) {
1199				if ($fact->isPendingAddition() && $fact->getTag() !== 'CHAN') {
1200					$this->repeats[] = $fact->getGedcom();
1201				}
1202			}
1203		}
1204	}
1205
1206	/**
1207	 * XML </Facts>
1208	 */
1209	private function factsEndHandler() {
1210		global $report;
1211
1212		$this->process_repeats--;
1213		if ($this->process_repeats > 0) {
1214			return;
1215		}
1216
1217		// Check if there is anything to repeat
1218		if (count($this->repeats) > 0) {
1219
1220			$line       = xml_get_current_line_number($this->parser) - 1;
1221			$lineoffset = 0;
1222			foreach ($this->repeats_stack as $rep) {
1223				$lineoffset += $rep[1];
1224			}
1225
1226			//-- read the xml from the file
1227			$lines = file($report);
1228			while ($lineoffset + $this->repeat_bytes > 0 && strpos($lines[$lineoffset + $this->repeat_bytes], '<Facts ') === false) {
1229				$lineoffset--;
1230			}
1231			$lineoffset++;
1232			$reportxml = "<tempdoc>\n";
1233			$i         = $line + $lineoffset;
1234			$line_nr   = $this->repeat_bytes + $lineoffset;
1235			while ($line_nr < $i) {
1236				$reportxml .= $lines[$line_nr];
1237				$line_nr++;
1238			}
1239			// No need to drag this
1240			unset($lines);
1241			$reportxml .= "</tempdoc>\n";
1242			// Save original values
1243			array_push($this->parser_stack, $this->parser);
1244			$oldgedrec = $this->gedrec;
1245			$count     = count($this->repeats);
1246			$i         = 0;
1247			while ($i < $count) {
1248				$this->gedrec = $this->repeats[$i];
1249				$this->fact   = '';
1250				$this->desc   = '';
1251				if (preg_match('/1 (\w+)(.*)/', $this->gedrec, $match)) {
1252					$this->fact = $match[1];
1253					if ($this->fact === 'EVEN' || $this->fact === 'FACT') {
1254						$tmatch = [];
1255						if (preg_match('/2 TYPE (.+)/', $this->gedrec, $tmatch)) {
1256							$this->type = trim($tmatch[1]);
1257						} else {
1258							$this->type = ' ';
1259						}
1260					}
1261					$this->desc = trim($match[2]);
1262					$this->desc .= Functions::getCont(2, $this->gedrec);
1263				}
1264				$repeat_parser = xml_parser_create();
1265				$this->parser  = $repeat_parser;
1266				xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false);
1267				xml_set_element_handler($repeat_parser, [$this, 'startElement'], [$this, 'endElement']);
1268				xml_set_character_data_handler($repeat_parser, [$this, 'characterData']);
1269				if (!xml_parse($repeat_parser, $reportxml, true)) {
1270					throw new \DomainException(sprintf(
1271						'FactsEHandler XML error: %s at line %d',
1272						xml_error_string(xml_get_error_code($repeat_parser)),
1273						xml_get_current_line_number($repeat_parser)
1274					));
1275				}
1276				xml_parser_free($repeat_parser);
1277				$i++;
1278			}
1279			// Restore original values
1280			$this->parser = array_pop($this->parser_stack);
1281			$this->gedrec = $oldgedrec;
1282		}
1283		list($this->repeats, $this->repeat_bytes) = array_pop($this->repeats_stack);
1284	}
1285
1286	/**
1287	 * Setting upp or changing variables in the XML
1288	 * The XML variable name and value is stored in $this->vars
1289	 *
1290	 * @param array $attrs an array of key value pairs for the attributes
1291	 */
1292	private function setVarStartHandler($attrs) {
1293		if (empty($attrs['name'])) {
1294			throw new \DomainException('REPORT ERROR var: The attribute "name" is missing or not set in the XML file');
1295		}
1296
1297		$name  = $attrs['name'];
1298		$value = $attrs['value'];
1299		$match = [];
1300		// Current GEDCOM record strings
1301		if ($value == '@ID') {
1302			if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1303				$value = $match[1];
1304			}
1305		} elseif ($value == '@fact') {
1306			$value = $this->fact;
1307		} elseif ($value == '@desc') {
1308			$value = $this->desc;
1309		} elseif ($value == '@generation') {
1310			$value = $this->generation;
1311		} elseif (preg_match("/@(\w+)/", $value, $match)) {
1312			$gmatch = [];
1313			if (preg_match("/\d $match[1] (.+)/", $this->gedrec, $gmatch)) {
1314				$value = str_replace('@', '', trim($gmatch[1]));
1315			}
1316		}
1317		if (preg_match("/\\$(\w+)/", $name, $match)) {
1318			$name = $this->vars["'" . $match[1] . "'"]['id'];
1319		}
1320		$count = preg_match_all("/\\$(\w+)/", $value, $match, PREG_SET_ORDER);
1321		$i     = 0;
1322		while ($i < $count) {
1323			$t     = $this->vars[$match[$i][1]]['id'];
1324			$value = preg_replace('/\$' . $match[$i][1] . '/', $t, $value, 1);
1325			$i++;
1326		}
1327		if (preg_match('/^I18N::number\((.+)\)$/', $value, $match)) {
1328			$value = I18N::number($match[1]);
1329		} elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $value, $match)) {
1330			$value = I18N::translate($match[1]);
1331		} elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $value, $match)) {
1332			$value = I18N::translateContext($match[1], $match[2]);
1333		}
1334		// Arithmetic functions
1335		if (preg_match("/(\d+)\s*([\-\+\*\/])\s*(\d+)/", $value, $match)) {
1336			switch ($match[2]) {
1337			case '+':
1338				$t     = $match[1] + $match[3];
1339				$value = preg_replace('/' . $match[1] . "\s*([\-\+\*\/])\s*" . $match[3] . '/', $t, $value);
1340				break;
1341			case '-':
1342				$t     = $match[1] - $match[3];
1343				$value = preg_replace('/' . $match[1] . "\s*([\-\+\*\/])\s*" . $match[3] . '/', $t, $value);
1344				break;
1345			case '*':
1346				$t     = $match[1] * $match[3];
1347				$value = preg_replace('/' . $match[1] . "\s*([\-\+\*\/])\s*" . $match[3] . '/', $t, $value);
1348				break;
1349			case '/':
1350				$t     = $match[1] / $match[3];
1351				$value = preg_replace('/' . $match[1] . "\s*([\-\+\*\/])\s*" . $match[3] . '/', $t, $value);
1352				break;
1353			}
1354		}
1355		if (strpos($value, '@') !== false) {
1356			$value = '';
1357		}
1358		$this->vars[$name]['id'] = $value;
1359	}
1360
1361	/**
1362	 * XML <if > start element
1363	 *
1364	 * @param array $attrs an array of key value pairs for the attributes
1365	 */
1366	private function ifStartHandler($attrs) {
1367		if ($this->process_ifs > 0) {
1368			$this->process_ifs++;
1369
1370			return;
1371		}
1372
1373		$condition = $attrs['condition'];
1374		$condition = $this->substituteVars($condition, true);
1375		$condition = str_replace([' LT ', ' GT '], ['<', '>'], $condition);
1376		// Replace the first accurance only once of @fact:DATE or in any other combinations to the current fact, such as BIRT
1377		$condition = str_replace('@fact:', $this->fact . ':', $condition);
1378		$match     = [];
1379		$count     = preg_match_all("/@([\w:\.]+)/", $condition, $match, PREG_SET_ORDER);
1380		$i         = 0;
1381		while ($i < $count) {
1382			$id    = $match[$i][1];
1383			$value = '""';
1384			if ($id == 'ID') {
1385				if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1386					$value = "'" . $match[1] . "'";
1387				}
1388			} elseif ($id === 'fact') {
1389				$value = '"' . $this->fact . '"';
1390			} elseif ($id === 'desc') {
1391				$value = '"' . addslashes($this->desc) . '"';
1392			} elseif ($id === 'generation') {
1393				$value = '"' . $this->generation . '"';
1394			} else {
1395
1396				$temp  = explode(' ', trim($this->gedrec));
1397				$level = $temp[0];
1398				if ($level == 0) {
1399					$level++;
1400				}
1401				$value = $this->getGedcomValue($id, $level, $this->gedrec);
1402				if (empty($value)) {
1403					$level++;
1404					$value = $this->getGedcomValue($id, $level, $this->gedrec);
1405				}
1406				$value = preg_replace('/^@(' . WT_REGEX_XREF . ')@$/', '$1', $value);
1407				$value = '"' . addslashes($value) . '"';
1408			}
1409			$condition = str_replace("@$id", $value, $condition);
1410			$i++;
1411		}
1412		$ret = eval("return (bool) ($condition);");
1413		if (!$ret) {
1414			$this->process_ifs++;
1415		}
1416	}
1417
1418	/**
1419	 * XML <if /> end element
1420	 */
1421	private function ifEndHandler() {
1422		if ($this->process_ifs > 0) {
1423			$this->process_ifs--;
1424		}
1425	}
1426
1427	/**
1428	 * XML <Footnote > start element
1429	 * Collect the Footnote links
1430	 * GEDCOM Records that are protected by Privacy setting will be ignore
1431	 *
1432	 * @param array $attrs an array of key value pairs for the attributes
1433	 */
1434	private function footnoteStartHandler($attrs) {
1435		global $WT_TREE;
1436
1437		$id = '';
1438		if (preg_match('/[0-9] (.+) @(.+)@/', $this->gedrec, $match)) {
1439			$id = $match[2];
1440		}
1441		$record = GedcomRecord::getInstance($id, $WT_TREE);
1442		if ($record && $record->canShow()) {
1443			array_push($this->print_data_stack, $this->print_data);
1444			$this->print_data = true;
1445			$style            = '';
1446			if (!empty($attrs['style'])) {
1447				$style = $attrs['style'];
1448			}
1449			$this->footnote_element = $this->current_element;
1450			$this->current_element  = $this->report_root->createFootnote($style);
1451		} else {
1452			$this->print_data       = false;
1453			$this->process_footnote = false;
1454		}
1455	}
1456
1457	/**
1458	 * XML <Footnote /> end element
1459	 * Print the collected Footnote data
1460	 */
1461	private function footnoteEndHandler() {
1462		if ($this->process_footnote) {
1463			$this->print_data = array_pop($this->print_data_stack);
1464			$temp             = trim($this->current_element->getValue());
1465			if (strlen($temp) > 3) {
1466				$this->wt_report->addElement($this->current_element);
1467			}
1468			$this->current_element = $this->footnote_element;
1469		} else {
1470			$this->process_footnote = true;
1471		}
1472	}
1473
1474	/**
1475	 * XML <FootnoteTexts /> element
1476	 */
1477	private function footnoteTextsStartHandler() {
1478		$temp = 'footnotetexts';
1479		$this->wt_report->addElement($temp);
1480	}
1481
1482	/**
1483	 * XML <AgeAtDeath /> element handler
1484	 */
1485	private function ageAtDeathStartHandler() {
1486		// This duplicates functionality in FunctionsPrint::format_fact_date()
1487		global $factrec, $WT_TREE;
1488
1489		$match = [];
1490		if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1491			$person = Individual::getInstance($match[1], $WT_TREE);
1492			// Recorded age
1493			if (preg_match('/\n2 AGE (.+)/', $factrec, $match)) {
1494				$fact_age = $match[1];
1495			} else {
1496				$fact_age = '';
1497			}
1498			if (preg_match('/\n2 HUSB\n3 AGE (.+)/', $factrec, $match)) {
1499				$husb_age = $match[1];
1500			} else {
1501				$husb_age = '';
1502			}
1503			if (preg_match('/\n2 WIFE\n3 AGE (.+)/', $factrec, $match)) {
1504				$wife_age = $match[1];
1505			} else {
1506				$wife_age = '';
1507			}
1508
1509			// Calculated age
1510			$birth_date = $person->getBirthDate();
1511			// Can't use getDeathDate(), as this also gives BURI/CREM events, which
1512			// wouldn't give the correct "days after death" result for people with
1513			// no DEAT.
1514			$death_event = $person->getFirstFact('DEAT');
1515			if ($death_event) {
1516				$death_date = $death_event->getDate();
1517			} else {
1518				$death_date = new Date('');
1519			}
1520			$value = '';
1521			if (Date::compare($birth_date, $death_date) <= 0 || !$person->isDead()) {
1522				$age = Date::getAgeGedcom($birth_date, $death_date);
1523				// Only show calculated age if it differs from recorded age
1524				if ($age != '' && $age != '0d') {
1525					if ($fact_age != '' && $fact_age != $age || $fact_age == '' && $husb_age == '' && $wife_age == '' || $husb_age != '' && $person->getSex() == 'M' && $husb_age != $age || $wife_age != '' && $person->getSex() == 'F' && $wife_age != $age
1526					) {
1527						$value  = FunctionsDate::getAgeAtEvent($age);
1528						$abbrev = substr($value, 0, strpos($value, ' ') + 5);
1529						if ($value !== $abbrev) {
1530							$value = $abbrev . '.';
1531						}
1532					}
1533				}
1534			}
1535			$this->current_element->addText($value);
1536		}
1537	}
1538
1539	/**
1540	 * XML element Forced line break handler - HTML code
1541	 */
1542	private function brStartHandler() {
1543		if ($this->print_data && $this->process_gedcoms === 0) {
1544			$this->current_element->addText('<br>');
1545		}
1546	}
1547
1548	/**
1549	 * XML <sp />element Forced space handler
1550	 */
1551	private function spStartHandler() {
1552		if ($this->print_data && $this->process_gedcoms === 0) {
1553			$this->current_element->addText(' ');
1554		}
1555	}
1556
1557	/**
1558	 * XML <HighlightedImage/>
1559	 *
1560	 * @param array $attrs an array of key value pairs for the attributes
1561	 */
1562	private function highlightedImageStartHandler($attrs) {
1563		global $WT_TREE;
1564
1565		$id    = '';
1566		$match = [];
1567		if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1568			$id = $match[1];
1569		}
1570
1571		// mixed Position the top corner of this box on the page. the default is the current position
1572		$top = '.';
1573		if (isset($attrs['top'])) {
1574			if ($attrs['top'] === '0') {
1575				$top = 0;
1576			} elseif ($attrs['top'] === '.') {
1577				$top = '.';
1578			} elseif (!empty($attrs['top'])) {
1579				$top = (int) $attrs['top'];
1580			}
1581		}
1582
1583		// mixed Position the left corner of this box on the page. the default is the current position
1584		$left = '.';
1585		if (isset($attrs['left'])) {
1586			if ($attrs['left'] === '0') {
1587				$left = 0;
1588			} elseif ($attrs['left'] === '.') {
1589				$left = '.';
1590			} elseif (!empty($attrs['left'])) {
1591				$left = (int) $attrs['left'];
1592			}
1593		}
1594
1595		// string Align the image in left, center, right
1596		$align = '';
1597		if (!empty($attrs['align'])) {
1598			$align = $attrs['align'];
1599		}
1600
1601		// string Next Line should be T:next to the image, N:next line
1602		$ln = '';
1603		if (!empty($attrs['ln'])) {
1604			$ln = $attrs['ln'];
1605		}
1606
1607		$width  = 0;
1608		$height = 0;
1609		if (!empty($attrs['width'])) {
1610			$width = (int) $attrs['width'];
1611		}
1612		if (!empty($attrs['height'])) {
1613			$height = (int) $attrs['height'];
1614		}
1615
1616		$person      = Individual::getInstance($id, $WT_TREE);
1617		$mediaobject = $person->findHighlightedMedia();
1618		if ($mediaobject) {
1619			$attributes = $mediaobject->getImageAttributes('thumb');
1620			if (in_array(
1621					$attributes['ext'],
1622					[
1623						'GIF',
1624						'JPG',
1625						'PNG',
1626						'SWF',
1627						'PSD',
1628						'BMP',
1629						'TIFF',
1630						'TIFF',
1631						'JPC',
1632						'JP2',
1633						'JPX',
1634						'JB2',
1635						'SWC',
1636						'IFF',
1637						'WBMP',
1638						'XBM',
1639					]
1640				) && $mediaobject->canShow() && $mediaobject->fileExists('thumb')
1641			) {
1642				if ($width > 0 && $height == 0) {
1643					$perc   = $width / $attributes['adjW'];
1644					$height = round($attributes['adjH'] * $perc);
1645				} elseif ($height > 0 && $width == 0) {
1646					$perc  = $height / $attributes['adjH'];
1647					$width = round($attributes['adjW'] * $perc);
1648				} else {
1649					$width  = $attributes['adjW'];
1650					$height = $attributes['adjH'];
1651				}
1652				$image = $this->report_root->createImageFromObject($mediaobject, $left, $top, $width, $height, $align, $ln);
1653				$this->wt_report->addElement($image);
1654			}
1655		}
1656	}
1657
1658	/**
1659	 * XML <Image/>
1660	 *
1661	 * @param array $attrs an array of key value pairs for the attributes
1662	 */
1663	private function imageStartHandler($attrs) {
1664		global $WT_TREE;
1665
1666		// mixed Position the top corner of this box on the page. the default is the current position
1667		$top = '.';
1668		if (isset($attrs['top'])) {
1669			if ($attrs['top'] === '0') {
1670				$top = 0;
1671			} elseif ($attrs['top'] === '.') {
1672				$top = '.';
1673			} elseif (!empty($attrs['top'])) {
1674				$top = (int) $attrs['top'];
1675			}
1676		}
1677
1678		// mixed Position the left corner of this box on the page. the default is the current position
1679		$left = '.';
1680		if (isset($attrs['left'])) {
1681			if ($attrs['left'] === '0') {
1682				$left = 0;
1683			} elseif ($attrs['left'] === '.') {
1684				$left = '.';
1685			} elseif (!empty($attrs['left'])) {
1686				$left = (int) $attrs['left'];
1687			}
1688		}
1689
1690		// string Align the image in left, center, right
1691		$align = '';
1692		if (!empty($attrs['align'])) {
1693			$align = $attrs['align'];
1694		}
1695
1696		// string Next Line should be T:next to the image, N:next line
1697		$ln = 'T';
1698		if (!empty($attrs['ln'])) {
1699			$ln = $attrs['ln'];
1700		}
1701
1702		$width  = 0;
1703		$height = 0;
1704		if (!empty($attrs['width'])) {
1705			$width = (int) $attrs['width'];
1706		}
1707		if (!empty($attrs['height'])) {
1708			$height = (int) $attrs['height'];
1709		}
1710
1711		$file = '';
1712		if (!empty($attrs['file'])) {
1713			$file = $attrs['file'];
1714		}
1715		if ($file == '@FILE') {
1716			$match = [];
1717			if (preg_match("/\d OBJE @(.+)@/", $this->gedrec, $match)) {
1718				$mediaobject = Media::getInstance($match[1], $WT_TREE);
1719				$attributes  = $mediaobject->getImageAttributes('thumb');
1720				if (in_array(
1721						$attributes['ext'],
1722						[
1723							'GIF',
1724							'JPG',
1725							'PNG',
1726							'SWF',
1727							'PSD',
1728							'BMP',
1729							'TIFF',
1730							'TIFF',
1731							'JPC',
1732							'JP2',
1733							'JPX',
1734							'JB2',
1735							'SWC',
1736							'IFF',
1737							'WBMP',
1738							'XBM',
1739						]
1740					) && $mediaobject->canShow() && $mediaobject->fileExists('thumb')
1741				) {
1742					if ($width > 0 && $height == 0) {
1743						$perc   = $width / $attributes['adjW'];
1744						$height = round($attributes['adjH'] * $perc);
1745					} elseif ($height > 0 && $width == 0) {
1746						$perc  = $height / $attributes['adjH'];
1747						$width = round($attributes['adjW'] * $perc);
1748					} else {
1749						$width  = $attributes['adjW'];
1750						$height = $attributes['adjH'];
1751					}
1752					$image = $this->report_root->createImageFromObject($mediaobject, $left, $top, $width, $height, $align, $ln);
1753					$this->wt_report->addElement($image);
1754				}
1755			}
1756		} else {
1757			if (file_exists($file) && preg_match('/(jpg|jpeg|png|gif)$/i', $file)) {
1758				$size = getimagesize($file);
1759				if ($width > 0 && $height == 0) {
1760					$perc   = $width / $size[0];
1761					$height = round($size[1] * $perc);
1762				} elseif ($height > 0 && $width == 0) {
1763					$perc  = $height / $size[1];
1764					$width = round($size[0] * $perc);
1765				} else {
1766					$width  = $size[0];
1767					$height = $size[1];
1768				}
1769				$image = $this->report_root->createImage($file, $left, $top, $width, $height, $align, $ln);
1770				$this->wt_report->addElement($image);
1771			}
1772		}
1773	}
1774
1775	/**
1776	 * XML <Line> element handler
1777	 *
1778	 * @param array $attrs an array of key value pairs for the attributes
1779	 */
1780	private function lineStartHandler($attrs) {
1781		// Start horizontal position, current position (default)
1782		$x1 = '.';
1783		if (isset($attrs['x1'])) {
1784			if ($attrs['x1'] === '0') {
1785				$x1 = 0;
1786			} elseif ($attrs['x1'] === '.') {
1787				$x1 = '.';
1788			} elseif (!empty($attrs['x1'])) {
1789				$x1 = (int) $attrs['x1'];
1790			}
1791		}
1792		// Start vertical position, current position (default)
1793		$y1 = '.';
1794		if (isset($attrs['y1'])) {
1795			if ($attrs['y1'] === '0') {
1796				$y1 = 0;
1797			} elseif ($attrs['y1'] === '.') {
1798				$y1 = '.';
1799			} elseif (!empty($attrs['y1'])) {
1800				$y1 = (int) $attrs['y1'];
1801			}
1802		}
1803		// End horizontal position, maximum width (default)
1804		$x2 = '.';
1805		if (isset($attrs['x2'])) {
1806			if ($attrs['x2'] === '0') {
1807				$x2 = 0;
1808			} elseif ($attrs['x2'] === '.') {
1809				$x2 = '.';
1810			} elseif (!empty($attrs['x2'])) {
1811				$x2 = (int) $attrs['x2'];
1812			}
1813		}
1814		// End vertical position
1815		$y2 = '.';
1816		if (isset($attrs['y2'])) {
1817			if ($attrs['y2'] === '0') {
1818				$y2 = 0;
1819			} elseif ($attrs['y2'] === '.') {
1820				$y2 = '.';
1821			} elseif (!empty($attrs['y2'])) {
1822				$y2 = (int) $attrs['y2'];
1823			}
1824		}
1825
1826		$line = $this->report_root->createLine($x1, $y1, $x2, $y2);
1827		$this->wt_report->addElement($line);
1828	}
1829
1830	/**
1831	 * XML <List>
1832	 *
1833	 * @param array $attrs an array of key value pairs for the attributes
1834	 */
1835	private function listStartHandler($attrs) {
1836		global $WT_TREE;
1837
1838		$this->process_repeats++;
1839		if ($this->process_repeats > 1) {
1840			return;
1841		}
1842
1843		$match = [];
1844		if (isset($attrs['sortby'])) {
1845			$sortby = $attrs['sortby'];
1846			if (preg_match("/\\$(\w+)/", $sortby, $match)) {
1847				$sortby = $this->vars[$match[1]]['id'];
1848				$sortby = trim($sortby);
1849			}
1850		} else {
1851			$sortby = 'NAME';
1852		}
1853
1854		if (isset($attrs['list'])) {
1855			$listname = $attrs['list'];
1856		} else {
1857			$listname = 'individual';
1858		}
1859		// Some filters/sorts can be applied using SQL, while others require PHP
1860		switch ($listname) {
1861		case 'pending':
1862			$rows = Database::prepare(
1863				"SELECT xref, CASE new_gedcom WHEN '' THEN old_gedcom ELSE new_gedcom END AS gedcom" .
1864				" FROM `##change`" . " WHERE (xref, change_id) IN (" .
1865				"  SELECT xref, MAX(change_id)" .
1866				"  FROM `##change`" .
1867				"  WHERE status = 'pending' AND gedcom_id = :tree_id" .
1868				"  GROUP BY xref" .
1869				" )"
1870			)->execute([
1871				'tree_id' => $WT_TREE->getTreeId(),
1872			])->fetchAll();
1873			$this->list = [];
1874			foreach ($rows as $row) {
1875				$this->list[] = GedcomRecord::getInstance($row->xref, $WT_TREE, $row->gedcom);
1876			}
1877			break;
1878		case 'individual':
1879			$sql_select   = "SELECT i_id AS xref, i_gedcom AS gedcom FROM `##individuals` ";
1880			$sql_join     = "";
1881			$sql_where    = " WHERE i_file = :tree_id";
1882			$sql_order_by = "";
1883			$sql_params   = ['tree_id' => $WT_TREE->getTreeId()];
1884			foreach ($attrs as $attr => $value) {
1885				if (strpos($attr, 'filter') === 0 && $value) {
1886					$value = $this->substituteVars($value, false);
1887					// Convert the various filters into SQL
1888					if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) {
1889						$sql_join .= " JOIN `##dates` AS {$attr} ON ({$attr}.d_file=i_file AND {$attr}.d_gid=i_id)";
1890						$sql_where .= " AND {$attr}.d_fact = :{$attr}fact";
1891						$sql_params[$attr . 'fact'] = $match[1];
1892						$date                       = new Date($match[3]);
1893						if ($match[2] == 'LTE') {
1894							$sql_where .= " AND {$attr}.d_julianday2 <= :{$attr}date";
1895							$sql_params[$attr . 'date'] = $date->maximumJulianDay();
1896						} else {
1897							$sql_where .= " AND {$attr}.d_julianday1 >= :{$attr}date";
1898							$sql_params[$attr . 'date'] = $date->minimumJulianDay();
1899						}
1900						if ($sortby == $match[1]) {
1901							$sortby = "";
1902							$sql_order_by .= ($sql_order_by ? ", " : " ORDER BY ") . "{$attr}.d_julianday1";
1903						}
1904						unset($attrs[$attr]); // This filter has been fully processed
1905					} elseif (preg_match('/^NAME CONTAINS (.*)$/', $value, $match)) {
1906						// Do nothing, unless you have to
1907						if ($match[1] != '' || $sortby == 'NAME') {
1908							$sql_join .= " JOIN `##name` AS {$attr} ON (n_file=i_file AND n_id=i_id)";
1909							// Search the DB only if there is any name supplied
1910							if ($match[1] != '') {
1911								$names = explode(' ', $match[1]);
1912								foreach ($names as $n => $name) {
1913									$sql_where .= " AND {$attr}.n_full LIKE CONCAT('%', :{$attr}name{$n}, '%')";
1914									$sql_params[$attr . 'name' . $n] = $name;
1915								}
1916							}
1917							// Let the DB do the name sorting even when no name was entered
1918							if ($sortby == 'NAME') {
1919								$sortby = '';
1920								$sql_order_by .= ($sql_order_by ? ', ' : ' ORDER BY ') . "{$attr}.n_sort";
1921							}
1922						}
1923						unset($attrs[$attr]); // This filter has been fully processed
1924					} elseif (preg_match('/^REGEXP \/(.+)\//', $value, $match)) {
1925						$sql_where .= " AND i_gedcom REGEXP :{$attr}gedcom";
1926						// PDO helpfully escapes backslashes for us, preventing us from matching "\n1 FACT"
1927						$sql_params[$attr . 'gedcom'] = str_replace('\n', "\n", $match[1]);
1928						unset($attrs[$attr]); // This filter has been fully processed
1929					} elseif (preg_match('/^(?:\w+):PLAC CONTAINS (.+)$/', $value, $match)) {
1930						$sql_join .= " JOIN `##places` AS {$attr}a ON ({$attr}a.p_file = i_file)";
1931						$sql_join .= " JOIN `##placelinks` AS {$attr}b ON ({$attr}a.p_file = {$attr}b.pl_file AND {$attr}b.pl_p_id = {$attr}a.p_id AND {$attr}b.pl_gid = i_id)";
1932						$sql_where .= " AND {$attr}a.p_place LIKE CONCAT('%', :{$attr}place, '%')";
1933						$sql_params[$attr . 'place'] = $match[1];
1934						// Don't unset this filter. This is just initial filtering
1935					} elseif (preg_match('/^(\w*):*(\w*) CONTAINS (.+)$/', $value, $match)) {
1936						$sql_where .= " AND i_gedcom LIKE CONCAT('%', :{$attr}contains1, '%', :{$attr}contains2, '%', :{$attr}contains3, '%')";
1937						$sql_params[$attr . 'contains1'] = $match[1];
1938						$sql_params[$attr . 'contains2'] = $match[2];
1939						$sql_params[$attr . 'contains3'] = $match[3];
1940						// Don't unset this filter. This is just initial filtering
1941					}
1942				}
1943			}
1944
1945			$this->list = [];
1946			$rows       = Database::prepare(
1947				$sql_select . $sql_join . $sql_where . $sql_order_by
1948			)->execute($sql_params)->fetchAll();
1949
1950			foreach ($rows as $row) {
1951				$this->list[$row->xref] = Individual::getInstance($row->xref, $WT_TREE, $row->gedcom);
1952			}
1953			break;
1954
1955		case 'family':
1956			$sql_select   = "SELECT f_id AS xref, f_gedcom AS gedcom FROM `##families`";
1957			$sql_join     = "";
1958			$sql_where    = " WHERE f_file = :tree_id";
1959			$sql_order_by = "";
1960			$sql_params   = ['tree_id' => $WT_TREE->getTreeId()];
1961			foreach ($attrs as $attr => $value) {
1962				if (strpos($attr, 'filter') === 0 && $value) {
1963					$value = $this->substituteVars($value, false);
1964					// Convert the various filters into SQL
1965					if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) {
1966						$sql_join .= " JOIN `##dates` AS {$attr} ON ({$attr}.d_file=f_file AND {$attr}.d_gid=f_id)";
1967						$sql_where .= " AND {$attr}.d_fact = :{$attr}fact";
1968						$sql_params[$attr . 'fact'] = $match[1];
1969						$date                       = new Date($match[3]);
1970						if ($match[2] == 'LTE') {
1971							$sql_where .= " AND {$attr}.d_julianday2 <= :{$attr}date";
1972							$sql_params[$attr . 'date'] = $date->maximumJulianDay();
1973						} else {
1974							$sql_where .= " AND {$attr}.d_julianday1 >= :{$attr}date";
1975							$sql_params[$attr . 'date'] = $date->minimumJulianDay();
1976						}
1977						if ($sortby == $match[1]) {
1978							$sortby = '';
1979							$sql_order_by .= ($sql_order_by ? ', ' : ' ORDER BY ') . "{$attr}.d_julianday1";
1980						}
1981						unset($attrs[$attr]); // This filter has been fully processed
1982					} elseif (preg_match('/^REGEXP \/(.+)\//', $value, $match)) {
1983						$sql_where .= " AND f_gedcom REGEXP :{$attr}gedcom";
1984						// PDO helpfully escapes backslashes for us, preventing us from matching "\n1 FACT"
1985						$sql_params[$attr . 'gedcom'] = str_replace('\n', "\n", $match[1]);
1986						unset($attrs[$attr]); // This filter has been fully processed
1987					} elseif (preg_match('/^NAME CONTAINS (.+)$/', $value, $match)) {
1988						// Do nothing, unless you have to
1989						if ($match[1] != '' || $sortby == 'NAME') {
1990							$sql_join .= " JOIN `##name` AS {$attr} ON n_file = f_file AND n_id IN (f_husb, f_wife)";
1991							// Search the DB only if there is any name supplied
1992							if ($match[1] != '') {
1993								$names = explode(' ', $match[1]);
1994								foreach ($names as $n => $name) {
1995									$sql_where .= " AND {$attr}.n_full LIKE CONCAT('%', :{$attr}name{$n}, '%')";
1996									$sql_params[$attr . 'name' . $n] = $name;
1997								}
1998							}
1999							// Let the DB do the name sorting even when no name was entered
2000							if ($sortby == 'NAME') {
2001								$sortby = '';
2002								$sql_order_by .= ($sql_order_by ? ', ' : ' ORDER BY ') . "{$attr}.n_sort";
2003							}
2004						}
2005						unset($attrs[$attr]); // This filter has been fully processed
2006
2007					} elseif (preg_match('/^(?:\w+):PLAC CONTAINS (.+)$/', $value, $match)) {
2008						$sql_join .= " JOIN `##places` AS {$attr}a ON ({$attr}a.p_file=f_file)";
2009						$sql_join .= " JOIN `##placelinks` AS {$attr}b ON ({$attr}a.p_file={$attr}b.pl_file AND {$attr}b.pl_p_id={$attr}a.p_id AND {$attr}b.pl_gid=f_id)";
2010						$sql_where .= " AND {$attr}a.p_place LIKE CONCAT('%', :{$attr}place, '%')";
2011						$sql_params[$attr . 'place'] = $match[1];
2012						// Don't unset this filter. This is just initial filtering
2013					} elseif (preg_match('/^(\w*):*(\w*) CONTAINS (.+)$/', $value, $match)) {
2014						$sql_where .= " AND f_gedcom LIKE CONCAT('%', :{$attr}contains1, '%', :{$attr}contains2, '%', :{$attr}contains3, '%')";
2015						$sql_params[$attr . 'contains1'] = $match[1];
2016						$sql_params[$attr . 'contains2'] = $match[2];
2017						$sql_params[$attr . 'contains3'] = $match[3];
2018						// Don't unset this filter. This is just initial filtering
2019					}
2020				}
2021			}
2022
2023			$this->list = [];
2024			$rows       = Database::prepare(
2025				$sql_select . $sql_join . $sql_where . $sql_order_by
2026			)->execute($sql_params)->fetchAll();
2027
2028			foreach ($rows as $row) {
2029				$this->list[$row->xref] = Family::getInstance($row->xref, $WT_TREE, $row->gedcom);
2030			}
2031			break;
2032
2033		default:
2034			throw new \DomainException('Invalid list name: ' . $listname);
2035		}
2036
2037		$filters  = [];
2038		$filters2 = [];
2039		if (isset($attrs['filter1']) && count($this->list) > 0) {
2040			foreach ($attrs as $key => $value) {
2041				if (preg_match("/filter(\d)/", $key)) {
2042					$condition = $value;
2043					if (preg_match("/@(\w+)/", $condition, $match)) {
2044						$id    = $match[1];
2045						$value = "''";
2046						if ($id == 'ID') {
2047							if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
2048								$value = "'" . $match[1] . "'";
2049							}
2050						} elseif ($id == 'fact') {
2051							$value = "'" . $this->fact . "'";
2052						} elseif ($id == 'desc') {
2053							$value = "'" . $this->desc . "'";
2054						} else {
2055							if (preg_match("/\d $id (.+)/", $this->gedrec, $match)) {
2056								$value = "'" . str_replace('@', '', trim($match[1])) . "'";
2057							}
2058						}
2059						$condition = preg_replace("/@$id/", $value, $condition);
2060					}
2061					//-- handle regular expressions
2062					if (preg_match("/([A-Z:]+)\s*([^\s]+)\s*(.+)/", $condition, $match)) {
2063						$tag  = trim($match[1]);
2064						$expr = trim($match[2]);
2065						$val  = trim($match[3]);
2066						if (preg_match("/\\$(\w+)/", $val, $match)) {
2067							$val = $this->vars[$match[1]]['id'];
2068							$val = trim($val);
2069						}
2070						if ($val) {
2071							$searchstr = '';
2072							$tags      = explode(':', $tag);
2073							//-- only limit to a level number if we are specifically looking at a level
2074							if (count($tags) > 1) {
2075								$level = 1;
2076								foreach ($tags as $t) {
2077									if (!empty($searchstr)) {
2078										$searchstr .= "[^\n]*(\n[2-9][^\n]*)*\n";
2079									}
2080									//-- search for both EMAIL and _EMAIL... silly double gedcom standard
2081									if ($t == 'EMAIL' || $t == '_EMAIL') {
2082										$t = '_?EMAIL';
2083									}
2084									$searchstr .= $level . ' ' . $t;
2085									$level++;
2086								}
2087							} else {
2088								if ($tag == 'EMAIL' || $tag == '_EMAIL') {
2089									$tag = '_?EMAIL';
2090								}
2091								$t         = $tag;
2092								$searchstr = '1 ' . $tag;
2093							}
2094							switch ($expr) {
2095							case 'CONTAINS':
2096								if ($t == 'PLAC') {
2097									$searchstr .= "[^\n]*[, ]*" . $val;
2098								} else {
2099									$searchstr .= "[^\n]*" . $val;
2100								}
2101								$filters[] = $searchstr;
2102								break;
2103							default:
2104								$filters2[] = ['tag' => $tag, 'expr' => $expr, 'val' => $val];
2105								break;
2106							}
2107						}
2108					}
2109				}
2110			}
2111		}
2112		//-- apply other filters to the list that could not be added to the search string
2113		if ($filters) {
2114			foreach ($this->list as $key => $record) {
2115				foreach ($filters as $filter) {
2116					if (!preg_match('/' . $filter . '/i', $record->privatizeGedcom(Auth::accessLevel($WT_TREE)))) {
2117						unset($this->list[$key]);
2118						break;
2119					}
2120				}
2121			}
2122		}
2123		if ($filters2) {
2124			$mylist = [];
2125			foreach ($this->list as $indi) {
2126				$key  = $indi->getXref();
2127				$grec = $indi->privatizeGedcom(Auth::accessLevel($WT_TREE));
2128				$keep = true;
2129				foreach ($filters2 as $filter) {
2130					if ($keep) {
2131						$tag  = $filter['tag'];
2132						$expr = $filter['expr'];
2133						$val  = $filter['val'];
2134						if ($val == "''") {
2135							$val = '';
2136						}
2137						$tags = explode(':', $tag);
2138						$t    = end($tags);
2139						$v    = $this->getGedcomValue($tag, 1, $grec);
2140						//-- check for EMAIL and _EMAIL (silly double gedcom standard :P)
2141						if ($t == 'EMAIL' && empty($v)) {
2142							$tag  = str_replace('EMAIL', '_EMAIL', $tag);
2143							$tags = explode(':', $tag);
2144							$t    = end($tags);
2145							$v    = Functions::getSubRecord(1, $tag, $grec);
2146						}
2147
2148						switch ($expr) {
2149						case 'GTE':
2150							if ($t == 'DATE') {
2151								$date1 = new Date($v);
2152								$date2 = new Date($val);
2153								$keep  = (Date::compare($date1, $date2) >= 0);
2154							} elseif ($val >= $v) {
2155								$keep = true;
2156							}
2157							break;
2158						case 'LTE':
2159							if ($t == 'DATE') {
2160								$date1 = new Date($v);
2161								$date2 = new Date($val);
2162								$keep  = (Date::compare($date1, $date2) <= 0);
2163							} elseif ($val >= $v) {
2164								$keep = true;
2165							}
2166							break;
2167						default:
2168							if ($v == $val) {
2169								$keep = true;
2170							} else {
2171								$keep = false;
2172							}
2173							break;
2174						}
2175					}
2176				}
2177				if ($keep) {
2178					$mylist[$key] = $indi;
2179				}
2180			}
2181			$this->list = $mylist;
2182		}
2183
2184		switch ($sortby) {
2185		case 'NAME':
2186			uasort($this->list, '\Fisharebest\Webtrees\GedcomRecord::compare');
2187			break;
2188		case 'CHAN':
2189			uasort($this->list, function (GedcomRecord $x, GedcomRecord $y) {
2190				return $y->lastChangeTimestamp(true) - $x->lastChangeTimestamp(true);
2191			});
2192			break;
2193		case 'BIRT:DATE':
2194			uasort($this->list, '\Fisharebest\Webtrees\Individual::compareBirthDate');
2195			break;
2196		case 'DEAT:DATE':
2197			uasort($this->list, '\Fisharebest\Webtrees\Individual::compareDeathDate');
2198			break;
2199		case 'MARR:DATE':
2200			uasort($this->list, '\Fisharebest\Webtrees\Family::compareMarrDate');
2201			break;
2202		default:
2203			// unsorted or already sorted by SQL
2204			break;
2205		}
2206
2207		array_push($this->repeats_stack, [$this->repeats, $this->repeat_bytes]);
2208		$this->repeat_bytes = xml_get_current_line_number($this->parser) + 1;
2209	}
2210
2211	/**
2212	 * XML <List>
2213	 */
2214	private function listEndHandler() {
2215		global $report;
2216
2217		$this->process_repeats--;
2218		if ($this->process_repeats > 0) {
2219			return;
2220		}
2221
2222		// Check if there is any list
2223		if (count($this->list) > 0) {
2224			$lineoffset = 0;
2225			foreach ($this->repeats_stack as $rep) {
2226				$lineoffset += $rep[1];
2227			}
2228			//-- read the xml from the file
2229			$lines = file($report);
2230			while ((strpos($lines[$lineoffset + $this->repeat_bytes], '<List') === false) && (($lineoffset + $this->repeat_bytes) > 0)) {
2231				$lineoffset--;
2232			}
2233			$lineoffset++;
2234			$reportxml = "<tempdoc>\n";
2235			$line_nr   = $lineoffset + $this->repeat_bytes;
2236			// List Level counter
2237			$count = 1;
2238			while (0 < $count) {
2239				if (strpos($lines[$line_nr], '<List') !== false) {
2240					$count++;
2241				} elseif (strpos($lines[$line_nr], '</List') !== false) {
2242					$count--;
2243				}
2244				if (0 < $count) {
2245					$reportxml .= $lines[$line_nr];
2246				}
2247				$line_nr++;
2248			}
2249			// No need to drag this
2250			unset($lines);
2251			$reportxml .= '</tempdoc>';
2252			// Save original values
2253			array_push($this->parser_stack, $this->parser);
2254			$oldgedrec = $this->gedrec;
2255
2256			$this->list_total   = count($this->list);
2257			$this->list_private = 0;
2258			foreach ($this->list as $record) {
2259				if ($record->canShow()) {
2260					$this->gedrec = $record->privatizeGedcom(Auth::accessLevel($record->getTree()));
2261					//-- start the sax parser
2262					$repeat_parser = xml_parser_create();
2263					$this->parser  = $repeat_parser;
2264					xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false);
2265					xml_set_element_handler($repeat_parser, [$this, 'startElement'], [$this, 'endElement']);
2266					xml_set_character_data_handler($repeat_parser, [$this, 'characterData']);
2267					if (!xml_parse($repeat_parser, $reportxml, true)) {
2268						throw new \DomainException(sprintf(
2269							'ListEHandler XML error: %s at line %d',
2270							xml_error_string(xml_get_error_code($repeat_parser)),
2271							xml_get_current_line_number($repeat_parser)
2272						));
2273					}
2274					xml_parser_free($repeat_parser);
2275				} else {
2276					$this->list_private++;
2277				}
2278			}
2279			$this->list   = [];
2280			$this->parser = array_pop($this->parser_stack);
2281			$this->gedrec = $oldgedrec;
2282		}
2283		list($this->repeats, $this->repeat_bytes) = array_pop($this->repeats_stack);
2284	}
2285
2286	/**
2287	 * XML <ListTotal> element handler
2288	 *
2289	 * Prints the total number of records in a list
2290	 * The total number is collected from
2291	 * List and Relatives
2292	 */
2293	private function listTotalStartHandler() {
2294		if ($this->list_private == 0) {
2295			$this->current_element->addText($this->list_total);
2296		} else {
2297			$this->current_element->addText(($this->list_total - $this->list_private) . ' / ' . $this->list_total);
2298		}
2299	}
2300
2301	/**
2302	 * XML <Relatives>
2303	 *
2304	 * @param array $attrs an array of key value pairs for the attributes
2305	 */
2306	private function relativesStartHandler($attrs) {
2307		global $WT_TREE;
2308
2309		$this->process_repeats++;
2310		if ($this->process_repeats > 1) {
2311			return;
2312		}
2313
2314		$sortby = 'NAME';
2315		if (isset($attrs['sortby'])) {
2316			$sortby = $attrs['sortby'];
2317		}
2318		$match = [];
2319		if (preg_match("/\\$(\w+)/", $sortby, $match)) {
2320			$sortby = $this->vars[$match[1]]['id'];
2321			$sortby = trim($sortby);
2322		}
2323
2324		$maxgen = -1;
2325		if (isset($attrs['maxgen'])) {
2326			$maxgen = $attrs['maxgen'];
2327		}
2328		if ($maxgen == '*') {
2329			$maxgen = -1;
2330		}
2331
2332		$group = 'child-family';
2333		if (isset($attrs['group'])) {
2334			$group = $attrs['group'];
2335		}
2336		if (preg_match("/\\$(\w+)/", $group, $match)) {
2337			$group = $this->vars[$match[1]]['id'];
2338			$group = trim($group);
2339		}
2340
2341		$id = '';
2342		if (isset($attrs['id'])) {
2343			$id = $attrs['id'];
2344		}
2345		if (preg_match("/\\$(\w+)/", $id, $match)) {
2346			$id = $this->vars[$match[1]]['id'];
2347			$id = trim($id);
2348		}
2349
2350		$this->list = [];
2351		$person     = Individual::getInstance($id, $WT_TREE);
2352		if (!empty($person)) {
2353			$this->list[$id] = $person;
2354			switch ($group) {
2355			case 'child-family':
2356				foreach ($person->getChildFamilies() as $family) {
2357					$husband = $family->getHusband();
2358					$wife    = $family->getWife();
2359					if (!empty($husband)) {
2360						$this->list[$husband->getXref()] = $husband;
2361					}
2362					if (!empty($wife)) {
2363						$this->list[$wife->getXref()] = $wife;
2364					}
2365					$children = $family->getChildren();
2366					foreach ($children as $child) {
2367						if (!empty($child)) {
2368							$this->list[$child->getXref()] = $child;
2369						}
2370					}
2371				}
2372				break;
2373			case 'spouse-family':
2374				foreach ($person->getSpouseFamilies() as $family) {
2375					$husband = $family->getHusband();
2376					$wife    = $family->getWife();
2377					if (!empty($husband)) {
2378						$this->list[$husband->getXref()] = $husband;
2379					}
2380					if (!empty($wife)) {
2381						$this->list[$wife->getXref()] = $wife;
2382					}
2383					$children = $family->getChildren();
2384					foreach ($children as $child) {
2385						if (!empty($child)) {
2386							$this->list[$child->getXref()] = $child;
2387						}
2388					}
2389				}
2390				break;
2391			case 'direct-ancestors':
2392				$this->addAncestors($this->list, $id, false, $maxgen);
2393				break;
2394			case 'ancestors':
2395				$this->addAncestors($this->list, $id, true, $maxgen);
2396				break;
2397			case 'descendants':
2398				$this->list[$id]->generation = 1;
2399				$this->addDescendancy($this->list, $id, false, $maxgen);
2400				break;
2401			case 'all':
2402				$this->addAncestors($this->list, $id, true, $maxgen);
2403				$this->addDescendancy($this->list, $id, true, $maxgen);
2404				break;
2405			}
2406		}
2407
2408		switch ($sortby) {
2409		case 'NAME':
2410			uasort($this->list, '\Fisharebest\Webtrees\GedcomRecord::compare');
2411			break;
2412		case 'BIRT:DATE':
2413			uasort($this->list, '\Fisharebest\Webtrees\Individual::compareBirthDate');
2414			break;
2415		case 'DEAT:DATE':
2416			uasort($this->list, '\Fisharebest\Webtrees\Individual::compareDeathDate');
2417			break;
2418		case 'generation':
2419			$newarray = [];
2420			reset($this->list);
2421			$genCounter = 1;
2422			while (count($newarray) < count($this->list)) {
2423				foreach ($this->list as $key => $value) {
2424					$this->generation = $value->generation;
2425					if ($this->generation == $genCounter) {
2426						$newarray[$key]             = new \stdClass;
2427						$newarray[$key]->generation = $this->generation;
2428					}
2429				}
2430				$genCounter++;
2431			}
2432			$this->list = $newarray;
2433			break;
2434		default:
2435			// unsorted
2436			break;
2437		}
2438		array_push($this->repeats_stack, [$this->repeats, $this->repeat_bytes]);
2439		$this->repeat_bytes = xml_get_current_line_number($this->parser) + 1;
2440	}
2441
2442	/**
2443	 * XML </ Relatives>
2444	 */
2445	private function relativesEndHandler() {
2446		global $report, $WT_TREE;
2447
2448		$this->process_repeats--;
2449		if ($this->process_repeats > 0) {
2450			return;
2451		}
2452
2453		// Check if there is any relatives
2454		if (count($this->list) > 0) {
2455			$lineoffset = 0;
2456			foreach ($this->repeats_stack as $rep) {
2457				$lineoffset += $rep[1];
2458			}
2459			//-- read the xml from the file
2460			$lines = file($report);
2461			while ((strpos($lines[$lineoffset + $this->repeat_bytes], '<Relatives') === false) && (($lineoffset + $this->repeat_bytes) > 0)) {
2462				$lineoffset--;
2463			}
2464			$lineoffset++;
2465			$reportxml = "<tempdoc>\n";
2466			$line_nr   = $lineoffset + $this->repeat_bytes;
2467			// Relatives Level counter
2468			$count = 1;
2469			while (0 < $count) {
2470				if (strpos($lines[$line_nr], '<Relatives') !== false) {
2471					$count++;
2472				} elseif (strpos($lines[$line_nr], '</Relatives') !== false) {
2473					$count--;
2474				}
2475				if (0 < $count) {
2476					$reportxml .= $lines[$line_nr];
2477				}
2478				$line_nr++;
2479			}
2480			// No need to drag this
2481			unset($lines);
2482			$reportxml .= "</tempdoc>\n";
2483			// Save original values
2484			array_push($this->parser_stack, $this->parser);
2485			$oldgedrec = $this->gedrec;
2486
2487			$this->list_total   = count($this->list);
2488			$this->list_private = 0;
2489			foreach ($this->list as $key => $value) {
2490				if (isset($value->generation)) {
2491					$this->generation = $value->generation;
2492				}
2493				$tmp          = GedcomRecord::getInstance($key, $WT_TREE);
2494				$this->gedrec = $tmp->privatizeGedcom(Auth::accessLevel($WT_TREE));
2495
2496				$repeat_parser = xml_parser_create();
2497				$this->parser  = $repeat_parser;
2498				xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false);
2499				xml_set_element_handler($repeat_parser, [$this, 'startElement'], [$this, 'endElement']);
2500				xml_set_character_data_handler($repeat_parser, [$this, 'characterData']);
2501
2502				if (!xml_parse($repeat_parser, $reportxml, true)) {
2503					throw new \DomainException(sprintf('RelativesEHandler XML error: %s at line %d', xml_error_string(xml_get_error_code($repeat_parser)), xml_get_current_line_number($repeat_parser)));
2504				}
2505				xml_parser_free($repeat_parser);
2506			}
2507			// Clean up the list array
2508			$this->list   = [];
2509			$this->parser = array_pop($this->parser_stack);
2510			$this->gedrec = $oldgedrec;
2511		}
2512		list($this->repeats, $this->repeat_bytes) = array_pop($this->repeats_stack);
2513	}
2514
2515	/**
2516	 * XML <Generation /> element handler
2517	 *
2518	 * Prints the number of generations
2519	 */
2520	private function generationStartHandler() {
2521		$this->current_element->addText($this->generation);
2522	}
2523
2524	/**
2525	 * XML <NewPage /> element handler
2526	 *
2527	 * Has to be placed in an element (header, pageheader, body or footer)
2528	 */
2529	private function newPageStartHandler() {
2530		$temp = 'addpage';
2531		$this->wt_report->addElement($temp);
2532	}
2533
2534	/**
2535	 * XML <html>
2536	 *
2537	 * @param string  $tag   HTML tag name
2538	 * @param array[] $attrs an array of key value pairs for the attributes
2539	 */
2540	private function htmlStartHandler($tag, $attrs) {
2541		if ($tag === 'tempdoc') {
2542			return;
2543		}
2544		array_push($this->wt_report_stack, $this->wt_report);
2545		$this->wt_report       = $this->report_root->createHTML($tag, $attrs);
2546		$this->current_element = $this->wt_report;
2547
2548		array_push($this->print_data_stack, $this->print_data);
2549		$this->print_data = true;
2550	}
2551
2552	/**
2553	 * XML </html>
2554	 *
2555	 * @param string $tag
2556	 */
2557	private function htmlEndHandler($tag) {
2558		if ($tag === 'tempdoc') {
2559			return;
2560		}
2561
2562		$this->print_data      = array_pop($this->print_data_stack);
2563		$this->current_element = $this->wt_report;
2564		$this->wt_report       = array_pop($this->wt_report_stack);
2565		if (!is_null($this->wt_report)) {
2566			$this->wt_report->addElement($this->current_element);
2567		} else {
2568			$this->wt_report = $this->current_element;
2569		}
2570	}
2571
2572	/**
2573	 * Handle <Input>
2574	 */
2575	private function inputStartHandler() {
2576		// Dummy function, to prevent the default HtmlStartHandler() being called
2577	}
2578
2579	/**
2580	 * Handle </Input>
2581	 */
2582	private function inputEndHandler() {
2583		// Dummy function, to prevent the default HtmlEndHandler() being called
2584	}
2585
2586	/**
2587	 * Handle <Report>
2588	 */
2589	private function reportStartHandler() {
2590		// Dummy function, to prevent the default HtmlStartHandler() being called
2591	}
2592
2593	/**
2594	 * Handle </Report>
2595	 */
2596	private function reportEndHandler() {
2597		// Dummy function, to prevent the default HtmlEndHandler() being called
2598	}
2599
2600	/**
2601	 * XML </titleEndHandler>
2602	 */
2603	private function titleEndHandler() {
2604		$this->report_root->addTitle($this->text);
2605	}
2606
2607	/**
2608	 * XML </descriptionEndHandler>
2609	 */
2610	private function descriptionEndHandler() {
2611		$this->report_root->addDescription($this->text);
2612	}
2613
2614	/**
2615	 * Create a list of all descendants.
2616	 *
2617	 * @param string[] $list
2618	 * @param string   $pid
2619	 * @param bool  $parents
2620	 * @param int  $generations
2621	 */
2622	private function addDescendancy(&$list, $pid, $parents = false, $generations = -1) {
2623		global $WT_TREE;
2624
2625		$person = Individual::getInstance($pid, $WT_TREE);
2626		if ($person === null) {
2627			return;
2628		}
2629		if (!isset($list[$pid])) {
2630			$list[$pid] = $person;
2631		}
2632		if (!isset($list[$pid]->generation)) {
2633			$list[$pid]->generation = 0;
2634		}
2635		foreach ($person->getSpouseFamilies() as $family) {
2636			if ($parents) {
2637				$husband = $family->getHusband();
2638				$wife    = $family->getWife();
2639				if ($husband) {
2640					$list[$husband->getXref()] = $husband;
2641					if (isset($list[$pid]->generation)) {
2642						$list[$husband->getXref()]->generation = $list[$pid]->generation - 1;
2643					} else {
2644						$list[$husband->getXref()]->generation = 1;
2645					}
2646				}
2647				if ($wife) {
2648					$list[$wife->getXref()] = $wife;
2649					if (isset($list[$pid]->generation)) {
2650						$list[$wife->getXref()]->generation = $list[$pid]->generation - 1;
2651					} else {
2652						$list[$wife->getXref()]->generation = 1;
2653					}
2654				}
2655			}
2656			$children = $family->getChildren();
2657			foreach ($children as $child) {
2658				if ($child) {
2659					$list[$child->getXref()] = $child;
2660					if (isset($list[$pid]->generation)) {
2661						$list[$child->getXref()]->generation = $list[$pid]->generation + 1;
2662					} else {
2663						$list[$child->getXref()]->generation = 2;
2664					}
2665				}
2666			}
2667			if ($generations == -1 || $list[$pid]->generation + 1 < $generations) {
2668				foreach ($children as $child) {
2669					$this->addDescendancy($list, $child->getXref(), $parents, $generations); // recurse on the childs family
2670				}
2671			}
2672		}
2673	}
2674
2675	/**
2676	 * Create a list of all ancestors.
2677	 *
2678	 * @param string[] $list
2679	 * @param string   $pid
2680	 * @param bool  $children
2681	 * @param int  $generations
2682	 */
2683	private function addAncestors(&$list, $pid, $children = false, $generations = -1) {
2684		global $WT_TREE;
2685
2686		$genlist                = [$pid];
2687		$list[$pid]->generation = 1;
2688		while (count($genlist) > 0) {
2689			$id = array_shift($genlist);
2690			if (strpos($id, 'empty') === 0) {
2691				continue; // id can be something like “empty7”
2692			}
2693			$person = Individual::getInstance($id, $WT_TREE);
2694			foreach ($person->getChildFamilies() as $family) {
2695				$husband = $family->getHusband();
2696				$wife    = $family->getWife();
2697				if ($husband) {
2698					$list[$husband->getXref()]             = $husband;
2699					$list[$husband->getXref()]->generation = $list[$id]->generation + 1;
2700				}
2701				if ($wife) {
2702					$list[$wife->getXref()]             = $wife;
2703					$list[$wife->getXref()]->generation = $list[$id]->generation + 1;
2704				}
2705				if ($generations == -1 || $list[$id]->generation + 1 < $generations) {
2706					if ($husband) {
2707						array_push($genlist, $husband->getXref());
2708					}
2709					if ($wife) {
2710						array_push($genlist, $wife->getXref());
2711					}
2712				}
2713				if ($children) {
2714					foreach ($family->getChildren() as $child) {
2715						$list[$child->getXref()] = $child;
2716						if (isset($list[$id]->generation)) {
2717							$list[$child->getXref()]->generation = $list[$id]->generation;
2718						} else {
2719							$list[$child->getXref()]->generation = 1;
2720						}
2721					}
2722				}
2723			}
2724		}
2725	}
2726
2727	/**
2728	 * get gedcom tag value
2729	 *
2730	 * @param string  $tag    The tag to find, use : to delineate subtags
2731	 * @param int $level  The gedcom line level of the first tag to find, setting level to 0 will cause it to use 1+ the level of the incoming record
2732	 * @param string  $gedrec The gedcom record to get the value from
2733	 *
2734	 * @return string the value of a gedcom tag from the given gedcom record
2735	 */
2736	private function getGedcomValue($tag, $level, $gedrec) {
2737		global $WT_TREE;
2738
2739		if (empty($gedrec)) {
2740			return '';
2741		}
2742		$tags      = explode(':', $tag);
2743		$origlevel = $level;
2744		if ($level == 0) {
2745			$level = $gedrec[0] + 1;
2746		}
2747
2748		$subrec = $gedrec;
2749		foreach ($tags as $t) {
2750			$lastsubrec = $subrec;
2751			$subrec     = Functions::getSubRecord($level, "$level $t", $subrec);
2752			if (empty($subrec) && $origlevel == 0) {
2753				$level--;
2754				$subrec = Functions::getSubRecord($level, "$level $t", $lastsubrec);
2755			}
2756			if (empty($subrec)) {
2757				if ($t == 'TITL') {
2758					$subrec = Functions::getSubRecord($level, "$level ABBR", $lastsubrec);
2759					if (!empty($subrec)) {
2760						$t = 'ABBR';
2761					}
2762				}
2763				if (empty($subrec)) {
2764					if ($level > 0) {
2765						$level--;
2766					}
2767					$subrec = Functions::getSubRecord($level, "@ $t", $gedrec);
2768					if (empty($subrec)) {
2769						return '';
2770					}
2771				}
2772			}
2773			$level++;
2774		}
2775		$level--;
2776		$ct = preg_match("/$level $t(.*)/", $subrec, $match);
2777		if ($ct == 0) {
2778			$ct = preg_match("/$level @.+@ (.+)/", $subrec, $match);
2779		}
2780		if ($ct == 0) {
2781			$ct = preg_match("/@ $t (.+)/", $subrec, $match);
2782		}
2783		if ($ct > 0) {
2784			$value = trim($match[1]);
2785			if ($t == 'NOTE' && preg_match('/^@(.+)@$/', $value, $match)) {
2786				$note = Note::getInstance($match[1], $WT_TREE);
2787				if ($note) {
2788					$value = $note->getNote();
2789				} else {
2790					//-- set the value to the id without the @
2791					$value = $match[1];
2792				}
2793			}
2794			if ($level != 0 || $t != 'NOTE') {
2795				$value .= Functions::getCont($level + 1, $subrec);
2796			}
2797
2798			return $value;
2799		}
2800
2801		return '';
2802	}
2803
2804	/**
2805	 * Replace variable identifiers with their values.
2806	 *
2807	 * @param string $expression An expression such as "$foo == 123"
2808	 * @param bool   $quote      Whether to add quotation marks
2809	 *
2810	 * @return string
2811	 */
2812	private function substituteVars($expression, $quote) {
2813		return preg_replace_callback(
2814			'/\$(\w+)/',
2815			function ($matches) use ($quote) {
2816				if (isset($this->vars[$matches[1]]['id'])) {
2817					if ($quote) {
2818						return "'" . addcslashes($this->vars[$matches[1]]['id'], "'") . "'";
2819					} else {
2820						return $this->vars[$matches[1]]['id'];
2821					}
2822				} else {
2823					Log::addErrorLog(sprintf('Undefined variable $%s in report', $matches[1]));
2824
2825					return '$' . $matches[1];
2826				}
2827			},
2828			$expression
2829		);
2830	}
2831}
2832