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