xref: /webtrees/app/Report/ReportParserGenerate.php (revision 1f3fb95c905e623518962b025974a0e6c19d068b)
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			$line       = xml_get_current_line_number($this->parser) - 1;
1220			$lineoffset = 0;
1221			foreach ($this->repeats_stack as $rep) {
1222				$lineoffset += $rep[1];
1223			}
1224
1225			//-- read the xml from the file
1226			$lines = file($report);
1227			while ($lineoffset + $this->repeat_bytes > 0 && strpos($lines[$lineoffset + $this->repeat_bytes], '<Facts ') === false) {
1228				$lineoffset--;
1229			}
1230			$lineoffset++;
1231			$reportxml = "<tempdoc>\n";
1232			$i         = $line + $lineoffset;
1233			$line_nr   = $this->repeat_bytes + $lineoffset;
1234			while ($line_nr < $i) {
1235				$reportxml .= $lines[$line_nr];
1236				$line_nr++;
1237			}
1238			// No need to drag this
1239			unset($lines);
1240			$reportxml .= "</tempdoc>\n";
1241			// Save original values
1242			array_push($this->parser_stack, $this->parser);
1243			$oldgedrec = $this->gedrec;
1244			$count     = count($this->repeats);
1245			$i         = 0;
1246			while ($i < $count) {
1247				$this->gedrec = $this->repeats[$i];
1248				$this->fact   = '';
1249				$this->desc   = '';
1250				if (preg_match('/1 (\w+)(.*)/', $this->gedrec, $match)) {
1251					$this->fact = $match[1];
1252					if ($this->fact === 'EVEN' || $this->fact === 'FACT') {
1253						$tmatch = [];
1254						if (preg_match('/2 TYPE (.+)/', $this->gedrec, $tmatch)) {
1255							$this->type = trim($tmatch[1]);
1256						} else {
1257							$this->type = ' ';
1258						}
1259					}
1260					$this->desc = trim($match[2]);
1261					$this->desc .= Functions::getCont(2, $this->gedrec);
1262				}
1263				$repeat_parser = xml_parser_create();
1264				$this->parser  = $repeat_parser;
1265				xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false);
1266				xml_set_element_handler($repeat_parser, [$this, 'startElement'], [$this, 'endElement']);
1267				xml_set_character_data_handler($repeat_parser, [$this, 'characterData']);
1268				if (!xml_parse($repeat_parser, $reportxml, true)) {
1269					throw new \DomainException(sprintf(
1270						'FactsEHandler XML error: %s at line %d',
1271						xml_error_string(xml_get_error_code($repeat_parser)),
1272						xml_get_current_line_number($repeat_parser)
1273					));
1274				}
1275				xml_parser_free($repeat_parser);
1276				$i++;
1277			}
1278			// Restore original values
1279			$this->parser = array_pop($this->parser_stack);
1280			$this->gedrec = $oldgedrec;
1281		}
1282		list($this->repeats, $this->repeat_bytes) = array_pop($this->repeats_stack);
1283	}
1284
1285	/**
1286	 * Setting upp or changing variables in the XML
1287	 * The XML variable name and value is stored in $this->vars
1288	 *
1289	 * @param array $attrs an array of key value pairs for the attributes
1290	 */
1291	private function setVarStartHandler($attrs) {
1292		if (empty($attrs['name'])) {
1293			throw new \DomainException('REPORT ERROR var: The attribute "name" is missing or not set in the XML file');
1294		}
1295
1296		$name  = $attrs['name'];
1297		$value = $attrs['value'];
1298		$match = [];
1299		// Current GEDCOM record strings
1300		if ($value == '@ID') {
1301			if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1302				$value = $match[1];
1303			}
1304		} elseif ($value == '@fact') {
1305			$value = $this->fact;
1306		} elseif ($value == '@desc') {
1307			$value = $this->desc;
1308		} elseif ($value == '@generation') {
1309			$value = $this->generation;
1310		} elseif (preg_match("/@(\w+)/", $value, $match)) {
1311			$gmatch = [];
1312			if (preg_match("/\d $match[1] (.+)/", $this->gedrec, $gmatch)) {
1313				$value = str_replace('@', '', trim($gmatch[1]));
1314			}
1315		}
1316		if (preg_match("/\\$(\w+)/", $name, $match)) {
1317			$name = $this->vars["'" . $match[1] . "'"]['id'];
1318		}
1319		$count = preg_match_all("/\\$(\w+)/", $value, $match, PREG_SET_ORDER);
1320		$i     = 0;
1321		while ($i < $count) {
1322			$t     = $this->vars[$match[$i][1]]['id'];
1323			$value = preg_replace('/\$' . $match[$i][1] . '/', $t, $value, 1);
1324			$i++;
1325		}
1326		if (preg_match('/^I18N::number\((.+)\)$/', $value, $match)) {
1327			$value = I18N::number($match[1]);
1328		} elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $value, $match)) {
1329			$value = I18N::translate($match[1]);
1330		} elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $value, $match)) {
1331			$value = I18N::translateContext($match[1], $match[2]);
1332		}
1333		// Arithmetic functions
1334		if (preg_match("/(\d+)\s*([\-\+\*\/])\s*(\d+)/", $value, $match)) {
1335			switch ($match[2]) {
1336			case '+':
1337				$t     = $match[1] + $match[3];
1338				$value = preg_replace('/' . $match[1] . "\s*([\-\+\*\/])\s*" . $match[3] . '/', $t, $value);
1339				break;
1340			case '-':
1341				$t     = $match[1] - $match[3];
1342				$value = preg_replace('/' . $match[1] . "\s*([\-\+\*\/])\s*" . $match[3] . '/', $t, $value);
1343				break;
1344			case '*':
1345				$t     = $match[1] * $match[3];
1346				$value = preg_replace('/' . $match[1] . "\s*([\-\+\*\/])\s*" . $match[3] . '/', $t, $value);
1347				break;
1348			case '/':
1349				$t     = $match[1] / $match[3];
1350				$value = preg_replace('/' . $match[1] . "\s*([\-\+\*\/])\s*" . $match[3] . '/', $t, $value);
1351				break;
1352			}
1353		}
1354		if (strpos($value, '@') !== false) {
1355			$value = '';
1356		}
1357		$this->vars[$name]['id'] = $value;
1358	}
1359
1360	/**
1361	 * XML <if > start element
1362	 *
1363	 * @param array $attrs an array of key value pairs for the attributes
1364	 */
1365	private function ifStartHandler($attrs) {
1366		if ($this->process_ifs > 0) {
1367			$this->process_ifs++;
1368
1369			return;
1370		}
1371
1372		$condition = $attrs['condition'];
1373		$condition = $this->substituteVars($condition, true);
1374		$condition = str_replace([' LT ', ' GT '], ['<', '>'], $condition);
1375		// Replace the first accurance only once of @fact:DATE or in any other combinations to the current fact, such as BIRT
1376		$condition = str_replace('@fact:', $this->fact . ':', $condition);
1377		$match     = [];
1378		$count     = preg_match_all("/@([\w:\.]+)/", $condition, $match, PREG_SET_ORDER);
1379		$i         = 0;
1380		while ($i < $count) {
1381			$id    = $match[$i][1];
1382			$value = '""';
1383			if ($id == 'ID') {
1384				if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1385					$value = "'" . $match[1] . "'";
1386				}
1387			} elseif ($id === 'fact') {
1388				$value = '"' . $this->fact . '"';
1389			} elseif ($id === 'desc') {
1390				$value = '"' . addslashes($this->desc) . '"';
1391			} elseif ($id === 'generation') {
1392				$value = '"' . $this->generation . '"';
1393			} else {
1394				$temp  = explode(' ', trim($this->gedrec));
1395				$level = $temp[0];
1396				if ($level == 0) {
1397					$level++;
1398				}
1399				$value = $this->getGedcomValue($id, $level, $this->gedrec);
1400				if (empty($value)) {
1401					$level++;
1402					$value = $this->getGedcomValue($id, $level, $this->gedrec);
1403				}
1404				$value = preg_replace('/^@(' . WT_REGEX_XREF . ')@$/', '$1', $value);
1405				$value = '"' . addslashes($value) . '"';
1406			}
1407			$condition = str_replace("@$id", $value, $condition);
1408			$i++;
1409		}
1410		$ret = eval("return (bool) ($condition);");
1411		if (!$ret) {
1412			$this->process_ifs++;
1413		}
1414	}
1415
1416	/**
1417	 * XML <if /> end element
1418	 */
1419	private function ifEndHandler() {
1420		if ($this->process_ifs > 0) {
1421			$this->process_ifs--;
1422		}
1423	}
1424
1425	/**
1426	 * XML <Footnote > start element
1427	 * Collect the Footnote links
1428	 * GEDCOM Records that are protected by Privacy setting will be ignore
1429	 *
1430	 * @param array $attrs an array of key value pairs for the attributes
1431	 */
1432	private function footnoteStartHandler($attrs) {
1433		global $WT_TREE;
1434
1435		$id = '';
1436		if (preg_match('/[0-9] (.+) @(.+)@/', $this->gedrec, $match)) {
1437			$id = $match[2];
1438		}
1439		$record = GedcomRecord::getInstance($id, $WT_TREE);
1440		if ($record && $record->canShow()) {
1441			array_push($this->print_data_stack, $this->print_data);
1442			$this->print_data = true;
1443			$style            = '';
1444			if (!empty($attrs['style'])) {
1445				$style = $attrs['style'];
1446			}
1447			$this->footnote_element = $this->current_element;
1448			$this->current_element  = $this->report_root->createFootnote($style);
1449		} else {
1450			$this->print_data       = false;
1451			$this->process_footnote = false;
1452		}
1453	}
1454
1455	/**
1456	 * XML <Footnote /> end element
1457	 * Print the collected Footnote data
1458	 */
1459	private function footnoteEndHandler() {
1460		if ($this->process_footnote) {
1461			$this->print_data = array_pop($this->print_data_stack);
1462			$temp             = trim($this->current_element->getValue());
1463			if (strlen($temp) > 3) {
1464				$this->wt_report->addElement($this->current_element);
1465			}
1466			$this->current_element = $this->footnote_element;
1467		} else {
1468			$this->process_footnote = true;
1469		}
1470	}
1471
1472	/**
1473	 * XML <FootnoteTexts /> element
1474	 */
1475	private function footnoteTextsStartHandler() {
1476		$temp = 'footnotetexts';
1477		$this->wt_report->addElement($temp);
1478	}
1479
1480	/**
1481	 * XML <AgeAtDeath /> element handler
1482	 */
1483	private function ageAtDeathStartHandler() {
1484		// This duplicates functionality in FunctionsPrint::format_fact_date()
1485		global $factrec, $WT_TREE;
1486
1487		$match = [];
1488		if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1489			$person = Individual::getInstance($match[1], $WT_TREE);
1490			// Recorded age
1491			if (preg_match('/\n2 AGE (.+)/', $factrec, $match)) {
1492				$fact_age = $match[1];
1493			} else {
1494				$fact_age = '';
1495			}
1496			if (preg_match('/\n2 HUSB\n3 AGE (.+)/', $factrec, $match)) {
1497				$husb_age = $match[1];
1498			} else {
1499				$husb_age = '';
1500			}
1501			if (preg_match('/\n2 WIFE\n3 AGE (.+)/', $factrec, $match)) {
1502				$wife_age = $match[1];
1503			} else {
1504				$wife_age = '';
1505			}
1506
1507			// Calculated age
1508			$birth_date = $person->getBirthDate();
1509			// Can't use getDeathDate(), as this also gives BURI/CREM events, which
1510			// wouldn't give the correct "days after death" result for people with
1511			// no DEAT.
1512			$death_event = $person->getFirstFact('DEAT');
1513			if ($death_event) {
1514				$death_date = $death_event->getDate();
1515			} else {
1516				$death_date = new Date('');
1517			}
1518			$value = '';
1519			if (Date::compare($birth_date, $death_date) <= 0 || !$person->isDead()) {
1520				$age = Date::getAgeGedcom($birth_date, $death_date);
1521				// Only show calculated age if it differs from recorded age
1522				if ($age != '' && $age != '0d') {
1523					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
1524					) {
1525						$value  = FunctionsDate::getAgeAtEvent($age);
1526						$abbrev = substr($value, 0, strpos($value, ' ') + 5);
1527						if ($value !== $abbrev) {
1528							$value = $abbrev . '.';
1529						}
1530					}
1531				}
1532			}
1533			$this->current_element->addText($value);
1534		}
1535	}
1536
1537	/**
1538	 * XML element Forced line break handler - HTML code
1539	 */
1540	private function brStartHandler() {
1541		if ($this->print_data && $this->process_gedcoms === 0) {
1542			$this->current_element->addText('<br>');
1543		}
1544	}
1545
1546	/**
1547	 * XML <sp />element Forced space handler
1548	 */
1549	private function spStartHandler() {
1550		if ($this->print_data && $this->process_gedcoms === 0) {
1551			$this->current_element->addText(' ');
1552		}
1553	}
1554
1555	/**
1556	 * XML <HighlightedImage/>
1557	 *
1558	 * @param array $attrs an array of key value pairs for the attributes
1559	 */
1560	private function highlightedImageStartHandler($attrs) {
1561		global $WT_TREE;
1562
1563		$id    = '';
1564		$match = [];
1565		if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1566			$id = $match[1];
1567		}
1568
1569		// mixed Position the top corner of this box on the page. the default is the current position
1570		$top = '.';
1571		if (isset($attrs['top'])) {
1572			if ($attrs['top'] === '0') {
1573				$top = 0;
1574			} elseif ($attrs['top'] === '.') {
1575				$top = '.';
1576			} elseif (!empty($attrs['top'])) {
1577				$top = (int) $attrs['top'];
1578			}
1579		}
1580
1581		// mixed Position the left corner of this box on the page. the default is the current position
1582		$left = '.';
1583		if (isset($attrs['left'])) {
1584			if ($attrs['left'] === '0') {
1585				$left = 0;
1586			} elseif ($attrs['left'] === '.') {
1587				$left = '.';
1588			} elseif (!empty($attrs['left'])) {
1589				$left = (int) $attrs['left'];
1590			}
1591		}
1592
1593		// string Align the image in left, center, right
1594		$align = '';
1595		if (!empty($attrs['align'])) {
1596			$align = $attrs['align'];
1597		}
1598
1599		// string Next Line should be T:next to the image, N:next line
1600		$ln = '';
1601		if (!empty($attrs['ln'])) {
1602			$ln = $attrs['ln'];
1603		}
1604
1605		$width  = 0;
1606		$height = 0;
1607		if (!empty($attrs['width'])) {
1608			$width = (int) $attrs['width'];
1609		}
1610		if (!empty($attrs['height'])) {
1611			$height = (int) $attrs['height'];
1612		}
1613
1614		$person      = Individual::getInstance($id, $WT_TREE);
1615		$mediaobject = $person->findHighlightedMedia();
1616		if ($mediaobject) {
1617			$attributes = $mediaobject->getImageAttributes();
1618			if (in_array(
1619					$attributes['ext'],
1620					[
1621						'GIF',
1622						'JPG',
1623						'PNG',
1624						'SWF',
1625						'PSD',
1626						'BMP',
1627						'TIFF',
1628						'TIFF',
1629						'JPC',
1630						'JP2',
1631						'JPX',
1632						'JB2',
1633						'SWC',
1634						'IFF',
1635						'WBMP',
1636						'XBM',
1637					]
1638				) && $mediaobject->canShow() && $mediaobject->fileExists('main')
1639			) {
1640				if ($width > 0 && $height == 0) {
1641					$perc   = $width / $attributes[0];
1642					$height = round($attributes[1] * $perc);
1643				} elseif ($height > 0 && $width == 0) {
1644					$perc  = $height / $attributes[1];
1645					$width = round($attributes[0] * $perc);
1646				} else {
1647					$width  = $attributes[0];
1648					$height = $attributes[1];
1649				}
1650				$image = $this->report_root->createImageFromObject($mediaobject, $left, $top, $width, $height, $align, $ln);
1651				$this->wt_report->addElement($image);
1652			}
1653		}
1654	}
1655
1656	/**
1657	 * XML <Image/>
1658	 *
1659	 * @param array $attrs an array of key value pairs for the attributes
1660	 */
1661	private function imageStartHandler($attrs) {
1662		global $WT_TREE;
1663
1664		// mixed Position the top corner of this box on the page. the default is the current position
1665		$top = '.';
1666		if (isset($attrs['top'])) {
1667			if ($attrs['top'] === '0') {
1668				$top = 0;
1669			} elseif ($attrs['top'] === '.') {
1670				$top = '.';
1671			} elseif (!empty($attrs['top'])) {
1672				$top = (int) $attrs['top'];
1673			}
1674		}
1675
1676		// mixed Position the left corner of this box on the page. the default is the current position
1677		$left = '.';
1678		if (isset($attrs['left'])) {
1679			if ($attrs['left'] === '0') {
1680				$left = 0;
1681			} elseif ($attrs['left'] === '.') {
1682				$left = '.';
1683			} elseif (!empty($attrs['left'])) {
1684				$left = (int) $attrs['left'];
1685			}
1686		}
1687
1688		// string Align the image in left, center, right
1689		$align = '';
1690		if (!empty($attrs['align'])) {
1691			$align = $attrs['align'];
1692		}
1693
1694		// string Next Line should be T:next to the image, N:next line
1695		$ln = 'T';
1696		if (!empty($attrs['ln'])) {
1697			$ln = $attrs['ln'];
1698		}
1699
1700		$width  = 0;
1701		$height = 0;
1702		if (!empty($attrs['width'])) {
1703			$width = (int) $attrs['width'];
1704		}
1705		if (!empty($attrs['height'])) {
1706			$height = (int) $attrs['height'];
1707		}
1708
1709		$file = '';
1710		if (!empty($attrs['file'])) {
1711			$file = $attrs['file'];
1712		}
1713		if ($file == '@FILE') {
1714			$match = [];
1715			if (preg_match("/\d OBJE @(.+)@/", $this->gedrec, $match)) {
1716				$mediaobject = Media::getInstance($match[1], $WT_TREE);
1717				$attributes  = $mediaobject->getImageAttributes();
1718
1719				if (in_array(
1720						$attributes['ext'],
1721						[
1722							'GIF',
1723							'JPG',
1724							'PNG',
1725							'SWF',
1726							'PSD',
1727							'BMP',
1728							'TIFF',
1729							'TIFF',
1730							'JPC',
1731							'JP2',
1732							'JPX',
1733							'JB2',
1734							'SWC',
1735							'IFF',
1736							'WBMP',
1737							'XBM',
1738						]
1739					) && $mediaobject->canShow() && $mediaobject->fileExists('main')
1740				) {
1741					if ($width > 0 && $height == 0) {
1742						$perc   = $width / $attributes[0];
1743						$height = round($attributes[1] * $perc);
1744					} elseif ($height > 0 && $width == 0) {
1745						$perc  = $height / $attributes[1];
1746						$width = round($attributes[0] * $perc);
1747					} else {
1748						$width  = $attributes[0];
1749						$height = $attributes[1];
1750					}
1751					$image = $this->report_root->createImageFromObject($mediaobject, $left, $top, $width, $height, $align, $ln);
1752					$this->wt_report->addElement($image);
1753				}
1754			}
1755		} else {
1756			if (file_exists($file) && preg_match('/(jpg|jpeg|png|gif)$/i', $file)) {
1757				$size = getimagesize($file);
1758				if ($width > 0 && $height == 0) {
1759					$perc   = $width / $size[0];
1760					$height = round($size[1] * $perc);
1761				} elseif ($height > 0 && $width == 0) {
1762					$perc  = $height / $size[1];
1763					$width = round($size[0] * $perc);
1764				} else {
1765					$width  = $size[0];
1766					$height = $size[1];
1767				}
1768				$image = $this->report_root->createImage($file, $left, $top, $width, $height, $align, $ln);
1769				$this->wt_report->addElement($image);
1770			}
1771		}
1772	}
1773
1774	/**
1775	 * XML <Line> element handler
1776	 *
1777	 * @param array $attrs an array of key value pairs for the attributes
1778	 */
1779	private function lineStartHandler($attrs) {
1780		// Start horizontal position, current position (default)
1781		$x1 = '.';
1782		if (isset($attrs['x1'])) {
1783			if ($attrs['x1'] === '0') {
1784				$x1 = 0;
1785			} elseif ($attrs['x1'] === '.') {
1786				$x1 = '.';
1787			} elseif (!empty($attrs['x1'])) {
1788				$x1 = (int) $attrs['x1'];
1789			}
1790		}
1791		// Start vertical position, current position (default)
1792		$y1 = '.';
1793		if (isset($attrs['y1'])) {
1794			if ($attrs['y1'] === '0') {
1795				$y1 = 0;
1796			} elseif ($attrs['y1'] === '.') {
1797				$y1 = '.';
1798			} elseif (!empty($attrs['y1'])) {
1799				$y1 = (int) $attrs['y1'];
1800			}
1801		}
1802		// End horizontal position, maximum width (default)
1803		$x2 = '.';
1804		if (isset($attrs['x2'])) {
1805			if ($attrs['x2'] === '0') {
1806				$x2 = 0;
1807			} elseif ($attrs['x2'] === '.') {
1808				$x2 = '.';
1809			} elseif (!empty($attrs['x2'])) {
1810				$x2 = (int) $attrs['x2'];
1811			}
1812		}
1813		// End vertical position
1814		$y2 = '.';
1815		if (isset($attrs['y2'])) {
1816			if ($attrs['y2'] === '0') {
1817				$y2 = 0;
1818			} elseif ($attrs['y2'] === '.') {
1819				$y2 = '.';
1820			} elseif (!empty($attrs['y2'])) {
1821				$y2 = (int) $attrs['y2'];
1822			}
1823		}
1824
1825		$line = $this->report_root->createLine($x1, $y1, $x2, $y2);
1826		$this->wt_report->addElement($line);
1827	}
1828
1829	/**
1830	 * XML <List>
1831	 *
1832	 * @param array $attrs an array of key value pairs for the attributes
1833	 */
1834	private function listStartHandler($attrs) {
1835		global $WT_TREE;
1836
1837		$this->process_repeats++;
1838		if ($this->process_repeats > 1) {
1839			return;
1840		}
1841
1842		$match = [];
1843		if (isset($attrs['sortby'])) {
1844			$sortby = $attrs['sortby'];
1845			if (preg_match("/\\$(\w+)/", $sortby, $match)) {
1846				$sortby = $this->vars[$match[1]]['id'];
1847				$sortby = trim($sortby);
1848			}
1849		} else {
1850			$sortby = 'NAME';
1851		}
1852
1853		if (isset($attrs['list'])) {
1854			$listname = $attrs['list'];
1855		} else {
1856			$listname = 'individual';
1857		}
1858		// Some filters/sorts can be applied using SQL, while others require PHP
1859		switch ($listname) {
1860		case 'pending':
1861			$rows = Database::prepare(
1862				"SELECT xref, CASE new_gedcom WHEN '' THEN old_gedcom ELSE new_gedcom END AS gedcom" .
1863				" FROM `##change`" . " WHERE (xref, change_id) IN (" .
1864				"  SELECT xref, MAX(change_id)" .
1865				"  FROM `##change`" .
1866				"  WHERE status = 'pending' AND gedcom_id = :tree_id" .
1867				"  GROUP BY xref" .
1868				" )"
1869			)->execute([
1870				'tree_id' => $WT_TREE->getTreeId(),
1871			])->fetchAll();
1872			$this->list = [];
1873			foreach ($rows as $row) {
1874				$this->list[] = GedcomRecord::getInstance($row->xref, $WT_TREE, $row->gedcom);
1875			}
1876			break;
1877		case 'individual':
1878			$sql_select   = "SELECT i_id AS xref, i_gedcom AS gedcom FROM `##individuals` ";
1879			$sql_join     = "";
1880			$sql_where    = " WHERE i_file = :tree_id";
1881			$sql_order_by = "";
1882			$sql_params   = ['tree_id' => $WT_TREE->getTreeId()];
1883			foreach ($attrs as $attr => $value) {
1884				if (strpos($attr, 'filter') === 0 && $value) {
1885					$value = $this->substituteVars($value, false);
1886					// Convert the various filters into SQL
1887					if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) {
1888						$sql_join .= " JOIN `##dates` AS {$attr} ON ({$attr}.d_file=i_file AND {$attr}.d_gid=i_id)";
1889						$sql_where .= " AND {$attr}.d_fact = :{$attr}fact";
1890						$sql_params[$attr . 'fact'] = $match[1];
1891						$date                       = new Date($match[3]);
1892						if ($match[2] == 'LTE') {
1893							$sql_where .= " AND {$attr}.d_julianday2 <= :{$attr}date";
1894							$sql_params[$attr . 'date'] = $date->maximumJulianDay();
1895						} else {
1896							$sql_where .= " AND {$attr}.d_julianday1 >= :{$attr}date";
1897							$sql_params[$attr . 'date'] = $date->minimumJulianDay();
1898						}
1899						if ($sortby == $match[1]) {
1900							$sortby = "";
1901							$sql_order_by .= ($sql_order_by ? ", " : " ORDER BY ") . "{$attr}.d_julianday1";
1902						}
1903						unset($attrs[$attr]); // This filter has been fully processed
1904					} elseif (preg_match('/^NAME CONTAINS (.*)$/', $value, $match)) {
1905						// Do nothing, unless you have to
1906						if ($match[1] != '' || $sortby == 'NAME') {
1907							$sql_join .= " JOIN `##name` AS {$attr} ON (n_file=i_file AND n_id=i_id)";
1908							// Search the DB only if there is any name supplied
1909							if ($match[1] != '') {
1910								$names = explode(' ', $match[1]);
1911								foreach ($names as $n => $name) {
1912									$sql_where .= " AND {$attr}.n_full LIKE CONCAT('%', :{$attr}name{$n}, '%')";
1913									$sql_params[$attr . 'name' . $n] = $name;
1914								}
1915							}
1916							// Let the DB do the name sorting even when no name was entered
1917							if ($sortby == 'NAME') {
1918								$sortby = '';
1919								$sql_order_by .= ($sql_order_by ? ', ' : ' ORDER BY ') . "{$attr}.n_sort";
1920							}
1921						}
1922						unset($attrs[$attr]); // This filter has been fully processed
1923					} elseif (preg_match('/^REGEXP \/(.+)\//', $value, $match)) {
1924						$sql_where .= " AND i_gedcom REGEXP :{$attr}gedcom";
1925						// PDO helpfully escapes backslashes for us, preventing us from matching "\n1 FACT"
1926						$sql_params[$attr . 'gedcom'] = str_replace('\n', "\n", $match[1]);
1927						unset($attrs[$attr]); // This filter has been fully processed
1928					} elseif (preg_match('/^(?:\w+):PLAC CONTAINS (.+)$/', $value, $match)) {
1929						$sql_join .= " JOIN `##places` AS {$attr}a ON ({$attr}a.p_file = i_file)";
1930						$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)";
1931						$sql_where .= " AND {$attr}a.p_place LIKE CONCAT('%', :{$attr}place, '%')";
1932						$sql_params[$attr . 'place'] = $match[1];
1933						// Don't unset this filter. This is just initial filtering
1934					} elseif (preg_match('/^(\w*):*(\w*) CONTAINS (.+)$/', $value, $match)) {
1935						$sql_where .= " AND i_gedcom LIKE CONCAT('%', :{$attr}contains1, '%', :{$attr}contains2, '%', :{$attr}contains3, '%')";
1936						$sql_params[$attr . 'contains1'] = $match[1];
1937						$sql_params[$attr . 'contains2'] = $match[2];
1938						$sql_params[$attr . 'contains3'] = $match[3];
1939						// Don't unset this filter. This is just initial filtering
1940					}
1941				}
1942			}
1943
1944			$this->list = [];
1945			$rows       = Database::prepare(
1946				$sql_select . $sql_join . $sql_where . $sql_order_by
1947			)->execute($sql_params)->fetchAll();
1948
1949			foreach ($rows as $row) {
1950				$this->list[$row->xref] = Individual::getInstance($row->xref, $WT_TREE, $row->gedcom);
1951			}
1952			break;
1953
1954		case 'family':
1955			$sql_select   = "SELECT f_id AS xref, f_gedcom AS gedcom FROM `##families`";
1956			$sql_join     = "";
1957			$sql_where    = " WHERE f_file = :tree_id";
1958			$sql_order_by = "";
1959			$sql_params   = ['tree_id' => $WT_TREE->getTreeId()];
1960			foreach ($attrs as $attr => $value) {
1961				if (strpos($attr, 'filter') === 0 && $value) {
1962					$value = $this->substituteVars($value, false);
1963					// Convert the various filters into SQL
1964					if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) {
1965						$sql_join .= " JOIN `##dates` AS {$attr} ON ({$attr}.d_file=f_file AND {$attr}.d_gid=f_id)";
1966						$sql_where .= " AND {$attr}.d_fact = :{$attr}fact";
1967						$sql_params[$attr . 'fact'] = $match[1];
1968						$date                       = new Date($match[3]);
1969						if ($match[2] == 'LTE') {
1970							$sql_where .= " AND {$attr}.d_julianday2 <= :{$attr}date";
1971							$sql_params[$attr . 'date'] = $date->maximumJulianDay();
1972						} else {
1973							$sql_where .= " AND {$attr}.d_julianday1 >= :{$attr}date";
1974							$sql_params[$attr . 'date'] = $date->minimumJulianDay();
1975						}
1976						if ($sortby == $match[1]) {
1977							$sortby = '';
1978							$sql_order_by .= ($sql_order_by ? ', ' : ' ORDER BY ') . "{$attr}.d_julianday1";
1979						}
1980						unset($attrs[$attr]); // This filter has been fully processed
1981					} elseif (preg_match('/^REGEXP \/(.+)\//', $value, $match)) {
1982						$sql_where .= " AND f_gedcom REGEXP :{$attr}gedcom";
1983						// PDO helpfully escapes backslashes for us, preventing us from matching "\n1 FACT"
1984						$sql_params[$attr . 'gedcom'] = str_replace('\n', "\n", $match[1]);
1985						unset($attrs[$attr]); // This filter has been fully processed
1986					} elseif (preg_match('/^NAME CONTAINS (.+)$/', $value, $match)) {
1987						// Do nothing, unless you have to
1988						if ($match[1] != '' || $sortby == 'NAME') {
1989							$sql_join .= " JOIN `##name` AS {$attr} ON n_file = f_file AND n_id IN (f_husb, f_wife)";
1990							// Search the DB only if there is any name supplied
1991							if ($match[1] != '') {
1992								$names = explode(' ', $match[1]);
1993								foreach ($names as $n => $name) {
1994									$sql_where .= " AND {$attr}.n_full LIKE CONCAT('%', :{$attr}name{$n}, '%')";
1995									$sql_params[$attr . 'name' . $n] = $name;
1996								}
1997							}
1998							// Let the DB do the name sorting even when no name was entered
1999							if ($sortby == 'NAME') {
2000								$sortby = '';
2001								$sql_order_by .= ($sql_order_by ? ', ' : ' ORDER BY ') . "{$attr}.n_sort";
2002							}
2003						}
2004						unset($attrs[$attr]); // This filter has been fully processed
2005					} elseif (preg_match('/^(?:\w+):PLAC CONTAINS (.+)$/', $value, $match)) {
2006						$sql_join .= " JOIN `##places` AS {$attr}a ON ({$attr}a.p_file=f_file)";
2007						$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)";
2008						$sql_where .= " AND {$attr}a.p_place LIKE CONCAT('%', :{$attr}place, '%')";
2009						$sql_params[$attr . 'place'] = $match[1];
2010						// Don't unset this filter. This is just initial filtering
2011					} elseif (preg_match('/^(\w*):*(\w*) CONTAINS (.+)$/', $value, $match)) {
2012						$sql_where .= " AND f_gedcom LIKE CONCAT('%', :{$attr}contains1, '%', :{$attr}contains2, '%', :{$attr}contains3, '%')";
2013						$sql_params[$attr . 'contains1'] = $match[1];
2014						$sql_params[$attr . 'contains2'] = $match[2];
2015						$sql_params[$attr . 'contains3'] = $match[3];
2016						// Don't unset this filter. This is just initial filtering
2017					}
2018				}
2019			}
2020
2021			$this->list = [];
2022			$rows       = Database::prepare(
2023				$sql_select . $sql_join . $sql_where . $sql_order_by
2024			)->execute($sql_params)->fetchAll();
2025
2026			foreach ($rows as $row) {
2027				$this->list[$row->xref] = Family::getInstance($row->xref, $WT_TREE, $row->gedcom);
2028			}
2029			break;
2030
2031		default:
2032			throw new \DomainException('Invalid list name: ' . $listname);
2033		}
2034
2035		$filters  = [];
2036		$filters2 = [];
2037		if (isset($attrs['filter1']) && count($this->list) > 0) {
2038			foreach ($attrs as $key => $value) {
2039				if (preg_match("/filter(\d)/", $key)) {
2040					$condition = $value;
2041					if (preg_match("/@(\w+)/", $condition, $match)) {
2042						$id    = $match[1];
2043						$value = "''";
2044						if ($id == 'ID') {
2045							if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
2046								$value = "'" . $match[1] . "'";
2047							}
2048						} elseif ($id == 'fact') {
2049							$value = "'" . $this->fact . "'";
2050						} elseif ($id == 'desc') {
2051							$value = "'" . $this->desc . "'";
2052						} else {
2053							if (preg_match("/\d $id (.+)/", $this->gedrec, $match)) {
2054								$value = "'" . str_replace('@', '', trim($match[1])) . "'";
2055							}
2056						}
2057						$condition = preg_replace("/@$id/", $value, $condition);
2058					}
2059					//-- handle regular expressions
2060					if (preg_match("/([A-Z:]+)\s*([^\s]+)\s*(.+)/", $condition, $match)) {
2061						$tag  = trim($match[1]);
2062						$expr = trim($match[2]);
2063						$val  = trim($match[3]);
2064						if (preg_match("/\\$(\w+)/", $val, $match)) {
2065							$val = $this->vars[$match[1]]['id'];
2066							$val = trim($val);
2067						}
2068						if ($val) {
2069							$searchstr = '';
2070							$tags      = explode(':', $tag);
2071							//-- only limit to a level number if we are specifically looking at a level
2072							if (count($tags) > 1) {
2073								$level = 1;
2074								foreach ($tags as $t) {
2075									if (!empty($searchstr)) {
2076										$searchstr .= "[^\n]*(\n[2-9][^\n]*)*\n";
2077									}
2078									//-- search for both EMAIL and _EMAIL... silly double gedcom standard
2079									if ($t == 'EMAIL' || $t == '_EMAIL') {
2080										$t = '_?EMAIL';
2081									}
2082									$searchstr .= $level . ' ' . $t;
2083									$level++;
2084								}
2085							} else {
2086								if ($tag == 'EMAIL' || $tag == '_EMAIL') {
2087									$tag = '_?EMAIL';
2088								}
2089								$t         = $tag;
2090								$searchstr = '1 ' . $tag;
2091							}
2092							switch ($expr) {
2093							case 'CONTAINS':
2094								if ($t == 'PLAC') {
2095									$searchstr .= "[^\n]*[, ]*" . $val;
2096								} else {
2097									$searchstr .= "[^\n]*" . $val;
2098								}
2099								$filters[] = $searchstr;
2100								break;
2101							default:
2102								$filters2[] = ['tag' => $tag, 'expr' => $expr, 'val' => $val];
2103								break;
2104							}
2105						}
2106					}
2107				}
2108			}
2109		}
2110		//-- apply other filters to the list that could not be added to the search string
2111		if ($filters) {
2112			foreach ($this->list as $key => $record) {
2113				foreach ($filters as $filter) {
2114					if (!preg_match('/' . $filter . '/i', $record->privatizeGedcom(Auth::accessLevel($WT_TREE)))) {
2115						unset($this->list[$key]);
2116						break;
2117					}
2118				}
2119			}
2120		}
2121		if ($filters2) {
2122			$mylist = [];
2123			foreach ($this->list as $indi) {
2124				$key  = $indi->getXref();
2125				$grec = $indi->privatizeGedcom(Auth::accessLevel($WT_TREE));
2126				$keep = true;
2127				foreach ($filters2 as $filter) {
2128					if ($keep) {
2129						$tag  = $filter['tag'];
2130						$expr = $filter['expr'];
2131						$val  = $filter['val'];
2132						if ($val == "''") {
2133							$val = '';
2134						}
2135						$tags = explode(':', $tag);
2136						$t    = end($tags);
2137						$v    = $this->getGedcomValue($tag, 1, $grec);
2138						//-- check for EMAIL and _EMAIL (silly double gedcom standard :P)
2139						if ($t == 'EMAIL' && empty($v)) {
2140							$tag  = str_replace('EMAIL', '_EMAIL', $tag);
2141							$tags = explode(':', $tag);
2142							$t    = end($tags);
2143							$v    = Functions::getSubRecord(1, $tag, $grec);
2144						}
2145
2146						switch ($expr) {
2147						case 'GTE':
2148							if ($t == 'DATE') {
2149								$date1 = new Date($v);
2150								$date2 = new Date($val);
2151								$keep  = (Date::compare($date1, $date2) >= 0);
2152							} elseif ($val >= $v) {
2153								$keep = true;
2154							}
2155							break;
2156						case 'LTE':
2157							if ($t == 'DATE') {
2158								$date1 = new Date($v);
2159								$date2 = new Date($val);
2160								$keep  = (Date::compare($date1, $date2) <= 0);
2161							} elseif ($val >= $v) {
2162								$keep = true;
2163							}
2164							break;
2165						default:
2166							if ($v == $val) {
2167								$keep = true;
2168							} else {
2169								$keep = false;
2170							}
2171							break;
2172						}
2173					}
2174				}
2175				if ($keep) {
2176					$mylist[$key] = $indi;
2177				}
2178			}
2179			$this->list = $mylist;
2180		}
2181
2182		switch ($sortby) {
2183		case 'NAME':
2184			uasort($this->list, '\Fisharebest\Webtrees\GedcomRecord::compare');
2185			break;
2186		case 'CHAN':
2187			uasort($this->list, function (GedcomRecord $x, GedcomRecord $y) {
2188				return $y->lastChangeTimestamp(true) - $x->lastChangeTimestamp(true);
2189			});
2190			break;
2191		case 'BIRT:DATE':
2192			uasort($this->list, '\Fisharebest\Webtrees\Individual::compareBirthDate');
2193			break;
2194		case 'DEAT:DATE':
2195			uasort($this->list, '\Fisharebest\Webtrees\Individual::compareDeathDate');
2196			break;
2197		case 'MARR:DATE':
2198			uasort($this->list, '\Fisharebest\Webtrees\Family::compareMarrDate');
2199			break;
2200		default:
2201			// unsorted or already sorted by SQL
2202			break;
2203		}
2204
2205		array_push($this->repeats_stack, [$this->repeats, $this->repeat_bytes]);
2206		$this->repeat_bytes = xml_get_current_line_number($this->parser) + 1;
2207	}
2208
2209	/**
2210	 * XML <List>
2211	 */
2212	private function listEndHandler() {
2213		global $report;
2214
2215		$this->process_repeats--;
2216		if ($this->process_repeats > 0) {
2217			return;
2218		}
2219
2220		// Check if there is any list
2221		if (count($this->list) > 0) {
2222			$lineoffset = 0;
2223			foreach ($this->repeats_stack as $rep) {
2224				$lineoffset += $rep[1];
2225			}
2226			//-- read the xml from the file
2227			$lines = file($report);
2228			while ((strpos($lines[$lineoffset + $this->repeat_bytes], '<List') === false) && (($lineoffset + $this->repeat_bytes) > 0)) {
2229				$lineoffset--;
2230			}
2231			$lineoffset++;
2232			$reportxml = "<tempdoc>\n";
2233			$line_nr   = $lineoffset + $this->repeat_bytes;
2234			// List Level counter
2235			$count = 1;
2236			while (0 < $count) {
2237				if (strpos($lines[$line_nr], '<List') !== false) {
2238					$count++;
2239				} elseif (strpos($lines[$line_nr], '</List') !== false) {
2240					$count--;
2241				}
2242				if (0 < $count) {
2243					$reportxml .= $lines[$line_nr];
2244				}
2245				$line_nr++;
2246			}
2247			// No need to drag this
2248			unset($lines);
2249			$reportxml .= '</tempdoc>';
2250			// Save original values
2251			array_push($this->parser_stack, $this->parser);
2252			$oldgedrec = $this->gedrec;
2253
2254			$this->list_total   = count($this->list);
2255			$this->list_private = 0;
2256			foreach ($this->list as $record) {
2257				if ($record->canShow()) {
2258					$this->gedrec = $record->privatizeGedcom(Auth::accessLevel($record->getTree()));
2259					//-- start the sax parser
2260					$repeat_parser = xml_parser_create();
2261					$this->parser  = $repeat_parser;
2262					xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false);
2263					xml_set_element_handler($repeat_parser, [$this, 'startElement'], [$this, 'endElement']);
2264					xml_set_character_data_handler($repeat_parser, [$this, 'characterData']);
2265					if (!xml_parse($repeat_parser, $reportxml, true)) {
2266						throw new \DomainException(sprintf(
2267							'ListEHandler XML error: %s at line %d',
2268							xml_error_string(xml_get_error_code($repeat_parser)),
2269							xml_get_current_line_number($repeat_parser)
2270						));
2271					}
2272					xml_parser_free($repeat_parser);
2273				} else {
2274					$this->list_private++;
2275				}
2276			}
2277			$this->list   = [];
2278			$this->parser = array_pop($this->parser_stack);
2279			$this->gedrec = $oldgedrec;
2280		}
2281		list($this->repeats, $this->repeat_bytes) = array_pop($this->repeats_stack);
2282	}
2283
2284	/**
2285	 * XML <ListTotal> element handler
2286	 *
2287	 * Prints the total number of records in a list
2288	 * The total number is collected from
2289	 * List and Relatives
2290	 */
2291	private function listTotalStartHandler() {
2292		if ($this->list_private == 0) {
2293			$this->current_element->addText($this->list_total);
2294		} else {
2295			$this->current_element->addText(($this->list_total - $this->list_private) . ' / ' . $this->list_total);
2296		}
2297	}
2298
2299	/**
2300	 * XML <Relatives>
2301	 *
2302	 * @param array $attrs an array of key value pairs for the attributes
2303	 */
2304	private function relativesStartHandler($attrs) {
2305		global $WT_TREE;
2306
2307		$this->process_repeats++;
2308		if ($this->process_repeats > 1) {
2309			return;
2310		}
2311
2312		$sortby = 'NAME';
2313		if (isset($attrs['sortby'])) {
2314			$sortby = $attrs['sortby'];
2315		}
2316		$match = [];
2317		if (preg_match("/\\$(\w+)/", $sortby, $match)) {
2318			$sortby = $this->vars[$match[1]]['id'];
2319			$sortby = trim($sortby);
2320		}
2321
2322		$maxgen = -1;
2323		if (isset($attrs['maxgen'])) {
2324			$maxgen = $attrs['maxgen'];
2325		}
2326		if ($maxgen == '*') {
2327			$maxgen = -1;
2328		}
2329
2330		$group = 'child-family';
2331		if (isset($attrs['group'])) {
2332			$group = $attrs['group'];
2333		}
2334		if (preg_match("/\\$(\w+)/", $group, $match)) {
2335			$group = $this->vars[$match[1]]['id'];
2336			$group = trim($group);
2337		}
2338
2339		$id = '';
2340		if (isset($attrs['id'])) {
2341			$id = $attrs['id'];
2342		}
2343		if (preg_match("/\\$(\w+)/", $id, $match)) {
2344			$id = $this->vars[$match[1]]['id'];
2345			$id = trim($id);
2346		}
2347
2348		$this->list = [];
2349		$person     = Individual::getInstance($id, $WT_TREE);
2350		if (!empty($person)) {
2351			$this->list[$id] = $person;
2352			switch ($group) {
2353			case 'child-family':
2354				foreach ($person->getChildFamilies() as $family) {
2355					$husband = $family->getHusband();
2356					$wife    = $family->getWife();
2357					if (!empty($husband)) {
2358						$this->list[$husband->getXref()] = $husband;
2359					}
2360					if (!empty($wife)) {
2361						$this->list[$wife->getXref()] = $wife;
2362					}
2363					$children = $family->getChildren();
2364					foreach ($children as $child) {
2365						if (!empty($child)) {
2366							$this->list[$child->getXref()] = $child;
2367						}
2368					}
2369				}
2370				break;
2371			case 'spouse-family':
2372				foreach ($person->getSpouseFamilies() as $family) {
2373					$husband = $family->getHusband();
2374					$wife    = $family->getWife();
2375					if (!empty($husband)) {
2376						$this->list[$husband->getXref()] = $husband;
2377					}
2378					if (!empty($wife)) {
2379						$this->list[$wife->getXref()] = $wife;
2380					}
2381					$children = $family->getChildren();
2382					foreach ($children as $child) {
2383						if (!empty($child)) {
2384							$this->list[$child->getXref()] = $child;
2385						}
2386					}
2387				}
2388				break;
2389			case 'direct-ancestors':
2390				$this->addAncestors($this->list, $id, false, $maxgen);
2391				break;
2392			case 'ancestors':
2393				$this->addAncestors($this->list, $id, true, $maxgen);
2394				break;
2395			case 'descendants':
2396				$this->list[$id]->generation = 1;
2397				$this->addDescendancy($this->list, $id, false, $maxgen);
2398				break;
2399			case 'all':
2400				$this->addAncestors($this->list, $id, true, $maxgen);
2401				$this->addDescendancy($this->list, $id, true, $maxgen);
2402				break;
2403			}
2404		}
2405
2406		switch ($sortby) {
2407		case 'NAME':
2408			uasort($this->list, '\Fisharebest\Webtrees\GedcomRecord::compare');
2409			break;
2410		case 'BIRT:DATE':
2411			uasort($this->list, '\Fisharebest\Webtrees\Individual::compareBirthDate');
2412			break;
2413		case 'DEAT:DATE':
2414			uasort($this->list, '\Fisharebest\Webtrees\Individual::compareDeathDate');
2415			break;
2416		case 'generation':
2417			$newarray = [];
2418			reset($this->list);
2419			$genCounter = 1;
2420			while (count($newarray) < count($this->list)) {
2421				foreach ($this->list as $key => $value) {
2422					$this->generation = $value->generation;
2423					if ($this->generation == $genCounter) {
2424						$newarray[$key]             = new \stdClass;
2425						$newarray[$key]->generation = $this->generation;
2426					}
2427				}
2428				$genCounter++;
2429			}
2430			$this->list = $newarray;
2431			break;
2432		default:
2433			// unsorted
2434			break;
2435		}
2436		array_push($this->repeats_stack, [$this->repeats, $this->repeat_bytes]);
2437		$this->repeat_bytes = xml_get_current_line_number($this->parser) + 1;
2438	}
2439
2440	/**
2441	 * XML </ Relatives>
2442	 */
2443	private function relativesEndHandler() {
2444		global $report, $WT_TREE;
2445
2446		$this->process_repeats--;
2447		if ($this->process_repeats > 0) {
2448			return;
2449		}
2450
2451		// Check if there is any relatives
2452		if (count($this->list) > 0) {
2453			$lineoffset = 0;
2454			foreach ($this->repeats_stack as $rep) {
2455				$lineoffset += $rep[1];
2456			}
2457			//-- read the xml from the file
2458			$lines = file($report);
2459			while ((strpos($lines[$lineoffset + $this->repeat_bytes], '<Relatives') === false) && (($lineoffset + $this->repeat_bytes) > 0)) {
2460				$lineoffset--;
2461			}
2462			$lineoffset++;
2463			$reportxml = "<tempdoc>\n";
2464			$line_nr   = $lineoffset + $this->repeat_bytes;
2465			// Relatives Level counter
2466			$count = 1;
2467			while (0 < $count) {
2468				if (strpos($lines[$line_nr], '<Relatives') !== false) {
2469					$count++;
2470				} elseif (strpos($lines[$line_nr], '</Relatives') !== false) {
2471					$count--;
2472				}
2473				if (0 < $count) {
2474					$reportxml .= $lines[$line_nr];
2475				}
2476				$line_nr++;
2477			}
2478			// No need to drag this
2479			unset($lines);
2480			$reportxml .= "</tempdoc>\n";
2481			// Save original values
2482			array_push($this->parser_stack, $this->parser);
2483			$oldgedrec = $this->gedrec;
2484
2485			$this->list_total   = count($this->list);
2486			$this->list_private = 0;
2487			foreach ($this->list as $key => $value) {
2488				if (isset($value->generation)) {
2489					$this->generation = $value->generation;
2490				}
2491				$tmp          = GedcomRecord::getInstance($key, $WT_TREE);
2492				$this->gedrec = $tmp->privatizeGedcom(Auth::accessLevel($WT_TREE));
2493
2494				$repeat_parser = xml_parser_create();
2495				$this->parser  = $repeat_parser;
2496				xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false);
2497				xml_set_element_handler($repeat_parser, [$this, 'startElement'], [$this, 'endElement']);
2498				xml_set_character_data_handler($repeat_parser, [$this, 'characterData']);
2499
2500				if (!xml_parse($repeat_parser, $reportxml, true)) {
2501					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)));
2502				}
2503				xml_parser_free($repeat_parser);
2504			}
2505			// Clean up the list array
2506			$this->list   = [];
2507			$this->parser = array_pop($this->parser_stack);
2508			$this->gedrec = $oldgedrec;
2509		}
2510		list($this->repeats, $this->repeat_bytes) = array_pop($this->repeats_stack);
2511	}
2512
2513	/**
2514	 * XML <Generation /> element handler
2515	 *
2516	 * Prints the number of generations
2517	 */
2518	private function generationStartHandler() {
2519		$this->current_element->addText($this->generation);
2520	}
2521
2522	/**
2523	 * XML <NewPage /> element handler
2524	 *
2525	 * Has to be placed in an element (header, pageheader, body or footer)
2526	 */
2527	private function newPageStartHandler() {
2528		$temp = 'addpage';
2529		$this->wt_report->addElement($temp);
2530	}
2531
2532	/**
2533	 * XML <html>
2534	 *
2535	 * @param string  $tag   HTML tag name
2536	 * @param array[] $attrs an array of key value pairs for the attributes
2537	 */
2538	private function htmlStartHandler($tag, $attrs) {
2539		if ($tag === 'tempdoc') {
2540			return;
2541		}
2542		array_push($this->wt_report_stack, $this->wt_report);
2543		$this->wt_report       = $this->report_root->createHTML($tag, $attrs);
2544		$this->current_element = $this->wt_report;
2545
2546		array_push($this->print_data_stack, $this->print_data);
2547		$this->print_data = true;
2548	}
2549
2550	/**
2551	 * XML </html>
2552	 *
2553	 * @param string $tag
2554	 */
2555	private function htmlEndHandler($tag) {
2556		if ($tag === 'tempdoc') {
2557			return;
2558		}
2559
2560		$this->print_data      = array_pop($this->print_data_stack);
2561		$this->current_element = $this->wt_report;
2562		$this->wt_report       = array_pop($this->wt_report_stack);
2563		if (!is_null($this->wt_report)) {
2564			$this->wt_report->addElement($this->current_element);
2565		} else {
2566			$this->wt_report = $this->current_element;
2567		}
2568	}
2569
2570	/**
2571	 * Handle <Input>
2572	 */
2573	private function inputStartHandler() {
2574		// Dummy function, to prevent the default HtmlStartHandler() being called
2575	}
2576
2577	/**
2578	 * Handle </Input>
2579	 */
2580	private function inputEndHandler() {
2581		// Dummy function, to prevent the default HtmlEndHandler() being called
2582	}
2583
2584	/**
2585	 * Handle <Report>
2586	 */
2587	private function reportStartHandler() {
2588		// Dummy function, to prevent the default HtmlStartHandler() being called
2589	}
2590
2591	/**
2592	 * Handle </Report>
2593	 */
2594	private function reportEndHandler() {
2595		// Dummy function, to prevent the default HtmlEndHandler() being called
2596	}
2597
2598	/**
2599	 * XML </titleEndHandler>
2600	 */
2601	private function titleEndHandler() {
2602		$this->report_root->addTitle($this->text);
2603	}
2604
2605	/**
2606	 * XML </descriptionEndHandler>
2607	 */
2608	private function descriptionEndHandler() {
2609		$this->report_root->addDescription($this->text);
2610	}
2611
2612	/**
2613	 * Create a list of all descendants.
2614	 *
2615	 * @param string[] $list
2616	 * @param string   $pid
2617	 * @param bool  $parents
2618	 * @param int  $generations
2619	 */
2620	private function addDescendancy(&$list, $pid, $parents = false, $generations = -1) {
2621		global $WT_TREE;
2622
2623		$person = Individual::getInstance($pid, $WT_TREE);
2624		if ($person === null) {
2625			return;
2626		}
2627		if (!isset($list[$pid])) {
2628			$list[$pid] = $person;
2629		}
2630		if (!isset($list[$pid]->generation)) {
2631			$list[$pid]->generation = 0;
2632		}
2633		foreach ($person->getSpouseFamilies() as $family) {
2634			if ($parents) {
2635				$husband = $family->getHusband();
2636				$wife    = $family->getWife();
2637				if ($husband) {
2638					$list[$husband->getXref()] = $husband;
2639					if (isset($list[$pid]->generation)) {
2640						$list[$husband->getXref()]->generation = $list[$pid]->generation - 1;
2641					} else {
2642						$list[$husband->getXref()]->generation = 1;
2643					}
2644				}
2645				if ($wife) {
2646					$list[$wife->getXref()] = $wife;
2647					if (isset($list[$pid]->generation)) {
2648						$list[$wife->getXref()]->generation = $list[$pid]->generation - 1;
2649					} else {
2650						$list[$wife->getXref()]->generation = 1;
2651					}
2652				}
2653			}
2654			$children = $family->getChildren();
2655			foreach ($children as $child) {
2656				if ($child) {
2657					$list[$child->getXref()] = $child;
2658					if (isset($list[$pid]->generation)) {
2659						$list[$child->getXref()]->generation = $list[$pid]->generation + 1;
2660					} else {
2661						$list[$child->getXref()]->generation = 2;
2662					}
2663				}
2664			}
2665			if ($generations == -1 || $list[$pid]->generation + 1 < $generations) {
2666				foreach ($children as $child) {
2667					$this->addDescendancy($list, $child->getXref(), $parents, $generations); // recurse on the childs family
2668				}
2669			}
2670		}
2671	}
2672
2673	/**
2674	 * Create a list of all ancestors.
2675	 *
2676	 * @param string[] $list
2677	 * @param string   $pid
2678	 * @param bool  $children
2679	 * @param int  $generations
2680	 */
2681	private function addAncestors(&$list, $pid, $children = false, $generations = -1) {
2682		global $WT_TREE;
2683
2684		$genlist                = [$pid];
2685		$list[$pid]->generation = 1;
2686		while (count($genlist) > 0) {
2687			$id = array_shift($genlist);
2688			if (strpos($id, 'empty') === 0) {
2689				continue; // id can be something like “empty7”
2690			}
2691			$person = Individual::getInstance($id, $WT_TREE);
2692			foreach ($person->getChildFamilies() as $family) {
2693				$husband = $family->getHusband();
2694				$wife    = $family->getWife();
2695				if ($husband) {
2696					$list[$husband->getXref()]             = $husband;
2697					$list[$husband->getXref()]->generation = $list[$id]->generation + 1;
2698				}
2699				if ($wife) {
2700					$list[$wife->getXref()]             = $wife;
2701					$list[$wife->getXref()]->generation = $list[$id]->generation + 1;
2702				}
2703				if ($generations == -1 || $list[$id]->generation + 1 < $generations) {
2704					if ($husband) {
2705						array_push($genlist, $husband->getXref());
2706					}
2707					if ($wife) {
2708						array_push($genlist, $wife->getXref());
2709					}
2710				}
2711				if ($children) {
2712					foreach ($family->getChildren() as $child) {
2713						$list[$child->getXref()] = $child;
2714						if (isset($list[$id]->generation)) {
2715							$list[$child->getXref()]->generation = $list[$id]->generation;
2716						} else {
2717							$list[$child->getXref()]->generation = 1;
2718						}
2719					}
2720				}
2721			}
2722		}
2723	}
2724
2725	/**
2726	 * get gedcom tag value
2727	 *
2728	 * @param string  $tag    The tag to find, use : to delineate subtags
2729	 * @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
2730	 * @param string  $gedrec The gedcom record to get the value from
2731	 *
2732	 * @return string the value of a gedcom tag from the given gedcom record
2733	 */
2734	private function getGedcomValue($tag, $level, $gedrec) {
2735		global $WT_TREE;
2736
2737		if (empty($gedrec)) {
2738			return '';
2739		}
2740		$tags      = explode(':', $tag);
2741		$origlevel = $level;
2742		if ($level == 0) {
2743			$level = $gedrec[0] + 1;
2744		}
2745
2746		$subrec = $gedrec;
2747		foreach ($tags as $t) {
2748			$lastsubrec = $subrec;
2749			$subrec     = Functions::getSubRecord($level, "$level $t", $subrec);
2750			if (empty($subrec) && $origlevel == 0) {
2751				$level--;
2752				$subrec = Functions::getSubRecord($level, "$level $t", $lastsubrec);
2753			}
2754			if (empty($subrec)) {
2755				if ($t == 'TITL') {
2756					$subrec = Functions::getSubRecord($level, "$level ABBR", $lastsubrec);
2757					if (!empty($subrec)) {
2758						$t = 'ABBR';
2759					}
2760				}
2761				if (empty($subrec)) {
2762					if ($level > 0) {
2763						$level--;
2764					}
2765					$subrec = Functions::getSubRecord($level, "@ $t", $gedrec);
2766					if (empty($subrec)) {
2767						return '';
2768					}
2769				}
2770			}
2771			$level++;
2772		}
2773		$level--;
2774		$ct = preg_match("/$level $t(.*)/", $subrec, $match);
2775		if ($ct == 0) {
2776			$ct = preg_match("/$level @.+@ (.+)/", $subrec, $match);
2777		}
2778		if ($ct == 0) {
2779			$ct = preg_match("/@ $t (.+)/", $subrec, $match);
2780		}
2781		if ($ct > 0) {
2782			$value = trim($match[1]);
2783			if ($t == 'NOTE' && preg_match('/^@(.+)@$/', $value, $match)) {
2784				$note = Note::getInstance($match[1], $WT_TREE);
2785				if ($note) {
2786					$value = $note->getNote();
2787				} else {
2788					//-- set the value to the id without the @
2789					$value = $match[1];
2790				}
2791			}
2792			if ($level != 0 || $t != 'NOTE') {
2793				$value .= Functions::getCont($level + 1, $subrec);
2794			}
2795
2796			return $value;
2797		}
2798
2799		return '';
2800	}
2801
2802	/**
2803	 * Replace variable identifiers with their values.
2804	 *
2805	 * @param string $expression An expression such as "$foo == 123"
2806	 * @param bool   $quote      Whether to add quotation marks
2807	 *
2808	 * @return string
2809	 */
2810	private function substituteVars($expression, $quote) {
2811		return preg_replace_callback(
2812			'/\$(\w+)/',
2813			function ($matches) use ($quote) {
2814				if (isset($this->vars[$matches[1]]['id'])) {
2815					if ($quote) {
2816						return "'" . addcslashes($this->vars[$matches[1]]['id'], "'") . "'";
2817					} else {
2818						return $this->vars[$matches[1]]['id'];
2819					}
2820				} else {
2821					Log::addErrorLog(sprintf('Undefined variable $%s in report', $matches[1]));
2822
2823					return '$' . $matches[1];
2824				}
2825			},
2826			$expression
2827		);
2828	}
2829}
2830