xref: /webtrees/app/Report/ReportParserGenerate.php (revision 6664b4a34cf6b2d1fc123cfb8f05bb5dda4a7f25)
1<?php
2/**
3 * webtrees: online genealogy
4 * Copyright (C) 2016 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 = array();
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 = array();
61
62	/** @var array[] Nested repeating data */
63	private $repeats_stack = array();
64
65	/** @var ReportBase[] Nested repeating data */
66	private $wt_report_stack = array();
67
68	/** @var resource Nested repeating data */
69	private $parser;
70
71	/** @var resource[] Nested repeating data */
72	private $parser_stack = array();
73
74	/** @var string The current GEDCOM record */
75	private $gedrec = '';
76
77	/** @var string[] Nested GEDCOM records */
78	private $gedrec_stack = array();
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 = array();
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	/** @todo This attribute is public to support the PHP5.3 closure workaround. */
114	/** @var string[][] Variables defined in the report at run-time */
115	public $vars;
116
117	/**
118	 * Create a parser for a report
119	 *
120	 * @param string     $report     The XML filename
121	 * @param ReportBase $report_root
122	 * @param string[][] $vars
123	 */
124	public function __construct($report, ReportBase $report_root = null, array $vars = array()) {
125		$this->report_root     = $report_root;
126		$this->wt_report       = $report_root;
127		$this->current_element = new ReportBaseElement;
128		$this->vars            = $vars;
129		parent::__construct($report);
130	}
131
132	/**
133	 * XML start element handler
134	 *
135	 * This function is called whenever a starting element is reached
136	 * The element handler will be called if found, otherwise it must be HTML
137	 *
138	 * @param resource $parser the resource handler for the XML parser
139	 * @param string   $name   the name of the XML element parsed
140	 * @param array    $attrs  an array of key value pairs for the attributes
141	 */
142	protected function startElement($parser, $name, $attrs) {
143		$newattrs = array();
144
145		foreach ($attrs as $key => $value) {
146			if (preg_match("/^\\$(\w+)$/", $value, $match)) {
147				if ((isset($this->vars[$match[1]]['id'])) && (!isset($this->vars[$match[1]]['gedcom']))) {
148					$value = $this->vars[$match[1]]['id'];
149				}
150			}
151			$newattrs[$key] = $value;
152		}
153		$attrs = $newattrs;
154		if ($this->process_footnote && ($this->process_ifs === 0 || $name === "if") && ($this->process_gedcoms === 0 || $name === "Gedcom") && ($this->process_repeats === 0 || $name === "Facts" || $name === "RepeatTag")) {
155			$start_method = $name . 'StartHandler';
156			$end_method   = $name . 'EndHandler';
157			if (method_exists($this, $start_method)) {
158				$this->$start_method($attrs);
159			} elseif (!method_exists($this, $end_method)) {
160				$this->htmlStartHandler($name, $attrs);
161			}
162		}
163	}
164
165	/**
166	 * XML end element handler
167	 *
168	 * This function is called whenever an ending element is reached
169	 * The element handler will be called if found, otherwise it must be HTML
170	 *
171	 * @param resource $parser the resource handler for the XML parser
172	 * @param string   $name   the name of the XML element parsed
173	 */
174	protected function endElement($parser, $name) {
175		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")) {
176			$start_method = $name . 'StartHandler';
177			$end_method   = $name . 'EndHandler';
178			if (method_exists($this, $end_method)) {
179				$this->$end_method();
180			} elseif (!method_exists($this, $start_method)) {
181				$this->htmlEndHandler($name);
182			}
183		}
184	}
185
186	/**
187	 * XML character data handler
188	 *
189	 * @param resource $parser the resource handler for the XML parser
190	 * @param string   $data   the name of the XML element parsed
191	 */
192	protected function characterData($parser, $data) {
193		if ($this->print_data && $this->process_gedcoms === 0 && $this->process_ifs === 0 && $this->process_repeats === 0) {
194			$this->current_element->addText($data);
195		}
196	}
197
198	/**
199	 * XML <style>
200	 *
201	 * @param array $attrs an array of key value pairs for the attributes
202	 */
203	private function styleStartHandler($attrs) {
204		if (empty($attrs['name'])) {
205			throw new \DomainException('REPORT ERROR Style: The "name" of the style is missing or not set in the XML file.');
206		}
207
208		// array Style that will be passed on
209		$s = array();
210
211		// string Name af the style
212		$s['name'] = $attrs['name'];
213
214		// string Name of the DEFAULT font
215		$s['font'] = $this->wt_report->defaultFont;
216		if (!empty($attrs['font'])) {
217			$s['font'] = $attrs['font'];
218		}
219
220		// int The size of the font in points
221		$s['size'] = $this->wt_report->defaultFontSize;
222		if (!empty($attrs['size'])) {
223			$s['size'] = (int) $attrs['size'];
224		} // Get it as int to ignore all decimal points or text (if any text then int(0))
225
226		// string B: bold, I: italic, U: underline, D: line trough, The default value is regular.
227		$s['style'] = "";
228		if (!empty($attrs['style'])) {
229			$s['style'] = $attrs['style'];
230		}
231
232		$this->wt_report->addStyle($s);
233	}
234
235	/**
236	 * XML <Doc>
237	 *
238	 * Sets up the basics of the document proparties
239	 *
240	 * @param array $attrs an array of key value pairs for the attributes
241	 */
242	private function docStartHandler($attrs) {
243		$this->parser = $this->xml_parser;
244
245		// Custom page width
246		if (!empty($attrs['customwidth'])) {
247			$this->wt_report->pagew = (int) $attrs['customwidth'];
248		} // Get it as int to ignore all decimal points or text (if any text then int(0))
249		// Custom Page height
250		if (!empty($attrs['customheight'])) {
251			$this->wt_report->pageh = (int) $attrs['customheight'];
252		} // Get it as int to ignore all decimal points or text (if any text then int(0))
253
254		// Left Margin
255		if (isset($attrs['leftmargin'])) {
256			if ($attrs['leftmargin'] === "0") {
257				$this->wt_report->leftmargin = 0;
258			} elseif (!empty($attrs['leftmargin'])) {
259				$this->wt_report->leftmargin = (int) $attrs['leftmargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
260			}
261		}
262		// Right Margin
263		if (isset($attrs['rightmargin'])) {
264			if ($attrs['rightmargin'] === "0") {
265				$this->wt_report->rightmargin = 0;
266			} elseif (!empty($attrs['rightmargin'])) {
267				$this->wt_report->rightmargin = (int) $attrs['rightmargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
268			}
269		}
270		// Top Margin
271		if (isset($attrs['topmargin'])) {
272			if ($attrs['topmargin'] === "0") {
273				$this->wt_report->topmargin = 0;
274			} elseif (!empty($attrs['topmargin'])) {
275				$this->wt_report->topmargin = (int) $attrs['topmargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
276			}
277		}
278		// Bottom Margin
279		if (isset($attrs['bottommargin'])) {
280			if ($attrs['bottommargin'] === "0") {
281				$this->wt_report->bottommargin = 0;
282			} elseif (!empty($attrs['bottommargin'])) {
283				$this->wt_report->bottommargin = (int) $attrs['bottommargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
284			}
285		}
286		// Header Margin
287		if (isset($attrs['headermargin'])) {
288			if ($attrs['headermargin'] === "0") {
289				$this->wt_report->headermargin = 0;
290			} elseif (!empty($attrs['headermargin'])) {
291				$this->wt_report->headermargin = (int) $attrs['headermargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
292			}
293		}
294		// Footer Margin
295		if (isset($attrs['footermargin'])) {
296			if ($attrs['footermargin'] === "0") {
297				$this->wt_report->footermargin = 0;
298			} elseif (!empty($attrs['footermargin'])) {
299				$this->wt_report->footermargin = (int) $attrs['footermargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
300			}
301		}
302
303		// Page Orientation
304		if (!empty($attrs['orientation'])) {
305			if ($attrs['orientation'] == "landscape") {
306				$this->wt_report->orientation = "landscape";
307			} elseif ($attrs['orientation'] == "portrait") {
308				$this->wt_report->orientation = "portrait";
309			}
310		}
311		// Page Size
312		if (!empty($attrs['pageSize'])) {
313			$this->wt_report->pageFormat = strtoupper($attrs['pageSize']);
314		}
315
316		// Show Generated By...
317		if (isset($attrs['showGeneratedBy'])) {
318			if ($attrs['showGeneratedBy'] === "0") {
319				$this->wt_report->showGenText = false;
320			} elseif ($attrs['showGeneratedBy'] === "1") {
321				$this->wt_report->showGenText = true;
322			}
323		}
324
325		$this->wt_report->setup();
326	}
327
328	/**
329	 * XML </Doc>
330	 */
331	private function docEndHandler() {
332		$this->wt_report->run();
333	}
334
335	/**
336	 * XML <Header>
337	 */
338	private function headerStartHandler() {
339		// Clear the Header before any new elements are added
340		$this->wt_report->clearHeader();
341		$this->wt_report->setProcessing("H");
342	}
343
344	/**
345	 * XML <PageHeader>
346	 */
347	private function pageHeaderStartHandler() {
348		array_push($this->print_data_stack, $this->print_data);
349		$this->print_data = false;
350		array_push($this->wt_report_stack, $this->wt_report);
351		$this->wt_report = $this->report_root->createPageHeader();
352	}
353
354	/**
355	 * XML <pageHeaderEndHandler>
356	 */
357	private function pageHeaderEndHandler() {
358		$this->print_data        = array_pop($this->print_data_stack);
359		$this->current_element   = $this->wt_report;
360		$this->wt_report         = array_pop($this->wt_report_stack);
361		$this->wt_report->addElement($this->current_element);
362	}
363
364	/**
365	 * XML <bodyStartHandler>
366	 */
367	private function bodyStartHandler() {
368		$this->wt_report->setProcessing("B");
369	}
370
371	/**
372	 * XML <footerStartHandler>
373	 */
374	private function footerStartHandler() {
375		$this->wt_report->setProcessing("F");
376	}
377
378	/**
379	 * XML <Cell>
380	 *
381	 * @param array $attrs an array of key value pairs for the attributes
382	 */
383	private function cellStartHandler($attrs) {
384		// string The text alignment of the text in this box.
385		$align = "";
386		if (!empty($attrs['align'])) {
387			$align = $attrs['align'];
388			// RTL supported left/right alignment
389			if ($align == "rightrtl") {
390				if ($this->wt_report->rtl) {
391					$align = "left";
392				} else {
393					$align = "right";
394				}
395			} elseif ($align == "leftrtl") {
396				if ($this->wt_report->rtl) {
397					$align = "right";
398				} else {
399					$align = "left";
400				}
401			}
402		}
403
404		// string The color to fill the background of this cell
405		$bgcolor = "";
406		if (!empty($attrs['bgcolor'])) {
407			$bgcolor = $attrs['bgcolor'];
408		}
409
410		// int Whether or not the background should be painted
411		$fill = 1;
412		if (isset($attrs['fill'])) {
413			if ($attrs['fill'] === "0") {
414				$fill = 0;
415			} elseif ($attrs['fill'] === "1") {
416				$fill = 1;
417			}
418		}
419
420		$reseth = true;
421		// boolean   if true reset the last cell height (default true)
422		if (isset($attrs['reseth'])) {
423			if ($attrs['reseth'] === "0") {
424				$reseth = false;
425			} elseif ($attrs['reseth'] === "1") {
426				$reseth = true;
427			}
428		}
429
430		// mixed Whether or not a border should be printed around this box
431		$border = 0;
432		if (!empty($attrs['border'])) {
433			$border = $attrs['border'];
434		}
435		// string Border color in HTML code
436		$bocolor = "";
437		if (!empty($attrs['bocolor'])) {
438			$bocolor = $attrs['bocolor'];
439		}
440
441		// int Cell height (expressed in points) The starting height of this cell. If the text wraps the height will automatically be adjusted.
442		$height = 0;
443		if (!empty($attrs['height'])) {
444			$height = (int) $attrs['height'];
445		}
446		// int Cell width (expressed in points) Setting the width to 0 will make it the width from the current location to the right margin.
447		$width = 0;
448		if (!empty($attrs['width'])) {
449			$width = (int) $attrs['width'];
450		}
451
452		// int Stretch carachter mode
453		$stretch = 0;
454		if (!empty($attrs['stretch'])) {
455			$stretch = (int) $attrs['stretch'];
456		}
457
458		// mixed Position the left corner of this box on the page. The default is the current position.
459		$left = ".";
460		if (isset($attrs['left'])) {
461			if ($attrs['left'] === ".") {
462				$left = ".";
463			} elseif (!empty($attrs['left'])) {
464				$left = (int) $attrs['left'];
465			} elseif ($attrs['left'] === "0") {
466				$left = 0;
467			}
468		}
469		// mixed Position the top corner of this box on the page. the default is the current position
470		$top = ".";
471		if (isset($attrs['top'])) {
472			if ($attrs['top'] === ".") {
473				$top = ".";
474			} elseif (!empty($attrs['top'])) {
475				$top = (int) $attrs['top'];
476			} elseif ($attrs['top'] === "0") {
477				$top = 0;
478			}
479		}
480
481		// string The name of the Style that should be used to render the text.
482		$style = "";
483		if (!empty($attrs['style'])) {
484			$style = $attrs['style'];
485		}
486
487		// string Text color in html code
488		$tcolor = "";
489		if (!empty($attrs['tcolor'])) {
490			$tcolor = $attrs['tcolor'];
491		}
492
493		// int Indicates where the current position should go after the call.
494		$ln = 0;
495		if (isset($attrs['newline'])) {
496			if (!empty($attrs['newline'])) {
497				$ln = (int) $attrs['newline'];
498			} elseif ($attrs['newline'] === "0") {
499				$ln = 0;
500			}
501		}
502
503		if ($align == "left") {
504			$align = "L";
505		} elseif ($align == "right") {
506			$align = "R";
507		} elseif ($align == "center") {
508			$align = "C";
509		} elseif ($align == "justify") {
510			$align = "J";
511		}
512
513		array_push($this->print_data_stack, $this->print_data);
514		$this->print_data = true;
515
516		$this->current_element = $this->report_root->createCell(
517			$width,
518			$height,
519			$border,
520			$align,
521			$bgcolor,
522			$style,
523			$ln,
524			$top,
525			$left,
526			$fill,
527			$stretch,
528			$bocolor,
529			$tcolor,
530			$reseth
531		);
532	}
533
534	/**
535	 * XML </Cell>
536	 */
537	private function cellEndHandler() {
538		$this->print_data = array_pop($this->print_data_stack);
539		$this->wt_report->addElement($this->current_element);
540	}
541
542	/**
543	 * XML <Now /> element handler
544	 */
545	private function nowStartHandler() {
546		$g = FunctionsDate::timestampToGedcomDate(WT_TIMESTAMP + WT_TIMESTAMP_OFFSET);
547		$this->current_element->addText($g->display());
548	}
549
550	/**
551	 * XML <PageNum /> element handler
552	 */
553	private function pageNumStartHandler() {
554		$this->current_element->addText("#PAGENUM#");
555	}
556
557	/**
558	 * XML <TotalPages /> element handler
559	 */
560	private function totalPagesStartHandler() {
561		$this->current_element->addText("{{:ptp:}}");
562	}
563
564	/**
565	 * Called at the start of an element.
566	 *
567	 * @param array $attrs an array of key value pairs for the attributes
568	 */
569	private function gedcomStartHandler($attrs) {
570		global $WT_TREE;
571
572		if ($this->process_gedcoms > 0) {
573			$this->process_gedcoms++;
574
575			return;
576		}
577
578		$tag       = $attrs['id'];
579		$tag       = str_replace("@fact", $this->fact, $tag);
580		$tags      = explode(":", $tag);
581		$newgedrec = '';
582		if (count($tags) < 2) {
583			$tmp       = GedcomRecord::getInstance($attrs['id'], $WT_TREE);
584			$newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($WT_TREE)) : '';
585		}
586		if (empty($newgedrec)) {
587			$tgedrec   = $this->gedrec;
588			$newgedrec = '';
589			foreach ($tags as $tag) {
590				if (preg_match("/\\$(.+)/", $tag, $match)) {
591					if (isset($this->vars[$match[1]]['gedcom'])) {
592						$newgedrec = $this->vars[$match[1]]['gedcom'];
593					} else {
594						$tmp       = GedcomRecord::getInstance($match[1], $WT_TREE);
595						$newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($WT_TREE)) : '';
596					}
597				} else {
598					if (preg_match("/@(.+)/", $tag, $match)) {
599						$gmatch = array();
600						if (preg_match("/\d $match[1] @([^@]+)@/", $tgedrec, $gmatch)) {
601							$tmp       = GedcomRecord::getInstance($gmatch[1], $WT_TREE);
602							$newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($WT_TREE)) : '';
603							$tgedrec   = $newgedrec;
604						} else {
605							$newgedrec = '';
606							break;
607						}
608					} else {
609						$temp      = explode(" ", trim($tgedrec));
610						$level     = $temp[0] + 1;
611						$newgedrec = Functions::getSubRecord($level, "$level $tag", $tgedrec);
612						$tgedrec   = $newgedrec;
613					}
614				}
615			}
616		}
617		if (!empty($newgedrec)) {
618			array_push($this->gedrec_stack, array($this->gedrec, $this->fact, $this->desc));
619			$this->gedrec = $newgedrec;
620			if (preg_match("/(\d+) (_?[A-Z0-9]+) (.*)/", $this->gedrec, $match)) {
621				$this->fact = $match[2];
622				$this->desc = trim($match[3]);
623			}
624		} else {
625			$this->process_gedcoms++;
626		}
627	}
628
629	/**
630	 * Called at the end of an element.
631	 */
632	private function gedcomEndHandler() {
633		if ($this->process_gedcoms > 0) {
634			$this->process_gedcoms--;
635		} else {
636			list($this->gedrec, $this->fact, $this->desc) = array_pop($this->gedrec_stack);
637		}
638	}
639
640	/**
641	 * XML <textBoxStartHandler>
642	 *
643	 * @param array $attrs an array of key value pairs for the attributes
644	 */
645	private function textBoxStartHandler($attrs) {
646		// string Background color code
647		$bgcolor = "";
648		if (!empty($attrs['bgcolor'])) {
649			$bgcolor = $attrs['bgcolor'];
650		}
651
652		// boolean Wether or not fill the background color
653		$fill = true;
654		if (isset($attrs['fill'])) {
655			if ($attrs['fill'] === "0") {
656				$fill = false;
657			} elseif ($attrs['fill'] === "1") {
658				$fill = true;
659			}
660		}
661
662		// var boolean Whether or not a border should be printed around this box. 0 = no border, 1 = border. Default is 0
663		$border = false;
664		if (isset($attrs['border'])) {
665			if ($attrs['border'] === "1") {
666				$border = true;
667			} elseif ($attrs['border'] === "0") {
668				$border = false;
669			}
670		}
671
672		// int The starting height of this cell. If the text wraps the height will automatically be adjusted
673		$height = 0;
674		if (!empty($attrs['height'])) {
675			$height = (int) $attrs['height'];
676		}
677		// int Setting the width to 0 will make it the width from the current location to the margin
678		$width = 0;
679		if (!empty($attrs['width'])) {
680			$width = (int) $attrs['width'];
681		}
682
683		// mixed Position the left corner of this box on the page. The default is the current position.
684		$left = ".";
685		if (isset($attrs['left'])) {
686			if ($attrs['left'] === ".") {
687				$left = ".";
688			} elseif (!empty($attrs['left'])) {
689				$left = (int) $attrs['left'];
690			} elseif ($attrs['left'] === "0") {
691				$left = 0;
692			}
693		}
694		// mixed Position the top corner of this box on the page. the default is the current position
695		$top = ".";
696		if (isset($attrs['top'])) {
697			if ($attrs['top'] === ".") {
698				$top = ".";
699			} elseif (!empty($attrs['top'])) {
700				$top = (int) $attrs['top'];
701			} elseif ($attrs['top'] === "0") {
702				$top = 0;
703			}
704		}
705		// 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
706		$newline = false;
707		if (isset($attrs['newline'])) {
708			if ($attrs['newline'] === "1") {
709				$newline = true;
710			} elseif ($attrs['newline'] === "0") {
711				$newline = false;
712			}
713		}
714		// boolean
715		$pagecheck = true;
716		if (isset($attrs['pagecheck'])) {
717			if ($attrs['pagecheck'] === "0") {
718				$pagecheck = false;
719			} elseif ($attrs['pagecheck'] === "1") {
720				$pagecheck = true;
721			}
722		}
723		// boolean Cell padding
724		$padding = true;
725		if (isset($attrs['padding'])) {
726			if ($attrs['padding'] === "0") {
727				$padding = false;
728			} elseif ($attrs['padding'] === "1") {
729				$padding = true;
730			}
731		}
732		// boolean Reset this box Height
733		$reseth = false;
734		if (isset($attrs['reseth'])) {
735			if ($attrs['reseth'] === "1") {
736				$reseth = true;
737			} elseif ($attrs['reseth'] === "0") {
738				$reseth = false;
739			}
740		}
741
742		// string Style of rendering
743		$style = "";
744
745		array_push($this->print_data_stack, $this->print_data);
746		$this->print_data = false;
747
748		array_push($this->wt_report_stack, $this->wt_report);
749		$this->wt_report = $this->report_root->createTextBox(
750			$width,
751			$height,
752			$border,
753			$bgcolor,
754			$newline,
755			$left,
756			$top,
757			$pagecheck,
758			$style,
759			$fill,
760			$padding,
761			$reseth
762		);
763	}
764
765	/**
766	 * XML <textBoxEndHandler>
767	 */
768	private function textBoxEndHandler() {
769		$this->print_data      = array_pop($this->print_data_stack);
770		$this->current_element = $this->wt_report;
771		$this->wt_report       = array_pop($this->wt_report_stack);
772		$this->wt_report->addElement($this->current_element);
773	}
774
775	/**
776	 * XLM <Text>.
777	 *
778	 * @param array $attrs an array of key value pairs for the attributes
779	 */
780	private function textStartHandler($attrs) {
781		array_push($this->print_data_stack, $this->print_data);
782		$this->print_data = true;
783
784		// string The name of the Style that should be used to render the text.
785		$style = "";
786		if (!empty($attrs['style'])) {
787			$style = $attrs['style'];
788		}
789
790		// string  The color of the text - Keep the black color as default
791		$color = "";
792		if (!empty($attrs['color'])) {
793			$color = $attrs['color'];
794		}
795
796		$this->current_element = $this->report_root->createText($style, $color);
797	}
798
799	/**
800	 * XML </Text>
801	 */
802	private function textEndHandler() {
803		$this->print_data = array_pop($this->print_data_stack);
804		$this->wt_report->addElement($this->current_element);
805	}
806
807	/**
808	 * XML <GetPersonName/>
809	 *
810	 * Get the name
811	 * 1. id is empty - current GEDCOM record
812	 * 2. id is set with a record id
813	 *
814	 * @param array $attrs an array of key value pairs for the attributes
815	 */
816	private function getPersonNameStartHandler($attrs) {
817		global $WT_TREE;
818
819		$id    = "";
820		$match = array();
821		if (empty($attrs['id'])) {
822			if (preg_match("/0 @(.+)@/", $this->gedrec, $match)) {
823				$id = $match[1];
824			}
825		} else {
826			if (preg_match("/\\$(.+)/", $attrs['id'], $match)) {
827				if (isset($this->vars[$match[1]]['id'])) {
828					$id = $this->vars[$match[1]]['id'];
829				}
830			} else {
831				if (preg_match("/@(.+)/", $attrs['id'], $match)) {
832					$gmatch = array();
833					if (preg_match("/\d $match[1] @([^@]+)@/", $this->gedrec, $gmatch)) {
834						$id = $gmatch[1];
835					}
836				} else {
837					$id = $attrs['id'];
838				}
839			}
840		}
841		if (!empty($id)) {
842			$record = GedcomRecord::getInstance($id, $WT_TREE);
843			if (is_null($record)) {
844				return;
845			}
846			if (!$record->canShowName()) {
847				$this->current_element->addText(I18N::translate('Private'));
848			} else {
849				$name = $record->getFullName();
850				$name = preg_replace(
851					array('/<span class="starredname">/', '/<\/span><\/span>/', '/<\/span>/'),
852					array('«', '', '»'),
853					$name
854				);
855				$name = strip_tags($name);
856				if (!empty($attrs['truncate'])) {
857					if (mb_strlen($name) > $attrs['truncate']) {
858						$name  = preg_replace("/\(.*\) ?/", '', $name); //removes () and text inbetween - what about ", [ and { etc?
859						$words = preg_split('/[, -]+/', $name); // names separated with space, comma or hyphen - any others?
860						$name  = $words[count($words) - 1];
861						for ($i = count($words) - 2; $i >= 0; $i--) {
862							$len = mb_strlen($name);
863							for ($j = count($words) - 3; $j >= 0; $j--) {
864								$len += mb_strlen($words[$j]);
865							}
866							if ($len > $attrs['truncate']) {
867								$first_letter = mb_substr($words[$i], 0, 1);
868								// Do not show " of nick-names
869								if ($first_letter != "\"") {
870									$name = mb_substr($words[$i], 0, 1) . '. ' . $name;
871								}
872							} else {
873								$name = $words[$i] . ' ' . $name;
874							}
875						}
876					}
877				} else {
878					$addname = $record->getAddName();
879					$addname = preg_replace(
880						array('/<span class="starredname">/', '/<\/span><\/span>/', '/<\/span>/'),
881						array('«', '', '»'),
882						$addname
883					);
884					$addname = strip_tags($addname);
885					if (!empty($addname)) {
886						$name .= " " . $addname;
887					}
888				}
889				$this->current_element->addText(trim($name));
890			}
891		}
892	}
893
894	/**
895	 * XML <GedcomValue/>
896	 *
897	 * @param array $attrs an array of key value pairs for the attributes
898	 */
899	private function gedcomValueStartHandler($attrs) {
900		global $WT_TREE;
901
902		$id    = "";
903		$match = array();
904		if (preg_match("/0 @(.+)@/", $this->gedrec, $match)) {
905			$id = $match[1];
906		}
907
908		if (isset($attrs['newline']) && $attrs['newline'] == "1") {
909			$useBreak = "1";
910		} else {
911			$useBreak = "0";
912		}
913
914		$tag = $attrs['tag'];
915		if (!empty($tag)) {
916			if ($tag == "@desc") {
917				$value = $this->desc;
918				$value = trim($value);
919				$this->current_element->addText($value);
920			}
921			if ($tag == "@id") {
922				$this->current_element->addText($id);
923			} else {
924				$tag = str_replace("@fact", $this->fact, $tag);
925				if (empty($attrs['level'])) {
926					$temp  = explode(" ", trim($this->gedrec));
927					$level = $temp[0];
928					if ($level == 0) {
929						$level++;
930					}
931				} else {
932					$level = $attrs['level'];
933				}
934				$tags  = preg_split('/[: ]/', $tag);
935				$value = $this->getGedcomValue($tag, $level, $this->gedrec);
936				switch (end($tags)) {
937				case 'DATE':
938					$tmp   = new Date($value);
939					$value = $tmp->display();
940					break;
941				case 'PLAC':
942					$tmp   = new Place($value, $WT_TREE);
943					$value = $tmp->getShortName();
944					break;
945				}
946				if ($useBreak == "1") {
947					// Insert <br> when multiple dates exist.
948					// This works around a TCPDF bug that incorrectly wraps RTL dates on LTR pages
949					$value = str_replace('(', '<br>(', $value);
950					$value = str_replace('<span dir="ltr"><br>', '<br><span dir="ltr">', $value);
951					$value = str_replace('<span dir="rtl"><br>', '<br><span dir="rtl">', $value);
952					if (substr($value, 0, 6) == '<br>') {
953						$value = substr($value, 6);
954					}
955				}
956				$tmp = explode(':', $tag);
957				if (in_array(end($tmp), array('NOTE', 'TEXT'))) {
958					$value = Filter::formatText($value, $WT_TREE); // We'll strip HTML in addText()
959				}
960				$this->current_element->addText($value);
961			}
962		}
963	}
964
965	/**
966	 * XML <RepeatTag>
967	 *
968	 * @param array $attrs an array of key value pairs for the attributes
969	 */
970	private function repeatTagStartHandler($attrs) {
971		global $WT_TREE;
972
973		$this->process_repeats++;
974		if ($this->process_repeats > 1) {
975			return;
976		}
977
978		array_push($this->repeats_stack, array($this->repeats, $this->repeat_bytes));
979		$this->repeats      = array();
980		$this->repeat_bytes = xml_get_current_line_number($this->parser);
981
982		$tag = "";
983		if (isset($attrs['tag'])) {
984			$tag = $attrs['tag'];
985		}
986		if (!empty($tag)) {
987			if ($tag == "@desc") {
988				$value = $this->desc;
989				$value = trim($value);
990				$this->current_element->addText($value);
991			} else {
992				$tag   = str_replace("@fact", $this->fact, $tag);
993				$tags  = explode(":", $tag);
994				$temp  = explode(" ", trim($this->gedrec));
995				$level = $temp[0];
996				if ($level == 0) {
997					$level++;
998				}
999				$subrec = $this->gedrec;
1000				$t      = $tag;
1001				$count  = count($tags);
1002				$i      = 0;
1003				while ($i < $count) {
1004					$t = $tags[$i];
1005					if (!empty($t)) {
1006						if ($i < ($count - 1)) {
1007							$subrec = Functions::getSubRecord($level, "$level $t", $subrec);
1008							if (empty($subrec)) {
1009								$level--;
1010								$subrec = Functions::getSubRecord($level, "@ $t", $this->gedrec);
1011								if (empty($subrec)) {
1012									return;
1013								}
1014							}
1015						}
1016						$level++;
1017					}
1018					$i++;
1019				}
1020				$level--;
1021				$count = preg_match_all("/$level $t(.*)/", $subrec, $match, PREG_SET_ORDER);
1022				$i     = 0;
1023				while ($i < $count) {
1024					$i++;
1025					// Privacy check - is this a link, and are we allowed to view the linked object?
1026					$subrecord = Functions::getSubRecord($level, "$level $t", $subrec, $i);
1027					if (preg_match('/^\d ' . WT_REGEX_TAG . ' @(' . WT_REGEX_XREF . ')@/', $subrecord, $xref_match)) {
1028						$linked_object = GedcomRecord::getInstance($xref_match[1], $WT_TREE);
1029						if ($linked_object && !$linked_object->canShow()) {
1030							continue;
1031						}
1032					}
1033					$this->repeats[] = $subrecord;
1034				}
1035			}
1036		}
1037	}
1038
1039	/**
1040	 * XML </ RepeatTag>
1041	 */
1042	private function repeatTagEndHandler() {
1043		global $report;
1044
1045		$this->process_repeats--;
1046		if ($this->process_repeats > 0) {
1047			return;
1048		}
1049
1050		// Check if there is anything to repeat
1051		if (count($this->repeats) > 0) {
1052			// No need to load them if not used...
1053
1054			$lineoffset = 0;
1055			foreach ($this->repeats_stack as $rep) {
1056				$lineoffset += $rep[1];
1057			}
1058			//-- read the xml from the file
1059			$lines = file($report);
1060			while (strpos($lines[$lineoffset + $this->repeat_bytes], "<RepeatTag") === false) {
1061				$lineoffset--;
1062			}
1063			$lineoffset++;
1064			$reportxml = "<tempdoc>\n";
1065			$line_nr   = $lineoffset + $this->repeat_bytes;
1066			// RepeatTag Level counter
1067			$count = 1;
1068			while (0 < $count) {
1069				if (strstr($lines[$line_nr], "<RepeatTag") !== false) {
1070					$count++;
1071				} elseif (strstr($lines[$line_nr], "</RepeatTag") !== false) {
1072					$count--;
1073				}
1074				if (0 < $count) {
1075					$reportxml .= $lines[$line_nr];
1076				}
1077				$line_nr++;
1078			}
1079			// No need to drag this
1080			unset($lines);
1081			$reportxml .= "</tempdoc>\n";
1082			// Save original values
1083			array_push($this->parser_stack, $this->parser);
1084			$oldgedrec = $this->gedrec;
1085			foreach ($this->repeats as $gedrec) {
1086				$this->gedrec  = $gedrec;
1087				$repeat_parser = xml_parser_create();
1088				$this->parser  = $repeat_parser;
1089				xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false);
1090				xml_set_element_handler($repeat_parser, array($this, 'startElement'), array($this, 'endElement'));
1091				xml_set_character_data_handler($repeat_parser, array($this, 'characterData'));
1092				if (!xml_parse($repeat_parser, $reportxml, true)) {
1093					throw new \DomainException(sprintf(
1094						'RepeatTagEHandler XML error: %s at line %d',
1095						xml_error_string(xml_get_error_code($repeat_parser)),
1096						xml_get_current_line_number($repeat_parser)
1097					));
1098				}
1099				xml_parser_free($repeat_parser);
1100			}
1101			// Restore original values
1102			$this->gedrec = $oldgedrec;
1103			$this->parser = array_pop($this->parser_stack);
1104		}
1105		list($this->repeats, $this->repeat_bytes) = array_pop($this->repeats_stack);
1106	}
1107
1108	/**
1109	 * Variable lookup
1110	 *
1111	 * Retrieve predefined variables :
1112	 *
1113	 * @ desc GEDCOM fact description, example:
1114	 *        1 EVEN This is a description
1115	 * @ fact GEDCOM fact tag, such as BIRT, DEAT etc.
1116	 * $ I18N::translate('....')
1117	 * $ language_settings[]
1118	 *
1119	 * @param array $attrs an array of key value pairs for the attributes
1120	 */
1121	private function varStartHandler($attrs) {
1122		if (empty($attrs['var'])) {
1123			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));
1124		}
1125
1126		$var = $attrs['var'];
1127		// SetVar element preset variables
1128		if (!empty($this->vars[$var]['id'])) {
1129			$var = $this->vars[$var]['id'];
1130		} else {
1131			$tfact = $this->fact;
1132			if (($this->fact === "EVEN" || $this->fact === "FACT") && $this->type !== " ") {
1133				// Use :
1134				// n TYPE This text if string
1135				$tfact = $this->type;
1136			}
1137			$var = str_replace(array("@fact", "@desc"), array(GedcomTag::getLabel($tfact), $this->desc), $var);
1138			if (preg_match('/^I18N::number\((.+)\)$/', $var, $match)) {
1139				$var = I18N::number($match[1]);
1140			} elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $var, $match)) {
1141				$var = I18N::translate($match[1]);
1142			} elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $var, $match)) {
1143				$var = I18N::translateContext($match[1], $match[2]);
1144			}
1145		}
1146		// Check if variable is set as a date and reformat the date
1147		if (isset($attrs['date'])) {
1148			if ($attrs['date'] === "1") {
1149				$g   = new Date($var);
1150				$var = $g->display();
1151			}
1152		}
1153		$this->current_element->addText($var);
1154		$this->text = $var; // Used for title/descriptio
1155	}
1156
1157	/**
1158	 * XML <Facts>
1159	 *
1160	 * @param array $attrs an array of key value pairs for the attributes
1161	 */
1162	private function factsStartHandler($attrs) {
1163		global $WT_TREE;
1164
1165		$this->process_repeats++;
1166		if ($this->process_repeats > 1) {
1167			return;
1168		}
1169
1170		array_push($this->repeats_stack, array($this->repeats, $this->repeat_bytes));
1171		$this->repeats      = array();
1172		$this->repeat_bytes = xml_get_current_line_number($this->parser);
1173
1174		$id    = "";
1175		$match = array();
1176		if (preg_match("/0 @(.+)@/", $this->gedrec, $match)) {
1177			$id = $match[1];
1178		}
1179		$tag = "";
1180		if (isset($attrs['ignore'])) {
1181			$tag .= $attrs['ignore'];
1182		}
1183		if (preg_match("/\\$(.+)/", $tag, $match)) {
1184			$tag = $this->vars[$match[1]]['id'];
1185		}
1186
1187		$record = GedcomRecord::getInstance($id, $WT_TREE);
1188		if (empty($attrs['diff']) && !empty($id)) {
1189			$facts = $record->getFacts();
1190			Functions::sortFacts($facts);
1191			$this->repeats  = array();
1192			$nonfacts       = explode(',', $tag);
1193			foreach ($facts as $event) {
1194				if (!in_array($event->getTag(), $nonfacts)) {
1195					$this->repeats[] = $event->getGedcom();
1196				}
1197			}
1198		} else {
1199			foreach ($record->getFacts() as $fact) {
1200				if ($fact->isPendingAddition() && $fact->getTag() !== 'CHAN') {
1201					$this->repeats[] = $fact->getGedcom();
1202				}
1203			}
1204		}
1205	}
1206
1207	/**
1208	 * XML </Facts>
1209	 */
1210	private function factsEndHandler() {
1211		global $report;
1212
1213		$this->process_repeats--;
1214		if ($this->process_repeats > 0) {
1215			return;
1216		}
1217
1218		// Check if there is anything to repeat
1219		if (count($this->repeats) > 0) {
1220
1221			$line       = xml_get_current_line_number($this->parser) - 1;
1222			$lineoffset = 0;
1223			foreach ($this->repeats_stack as $rep) {
1224				$lineoffset += $rep[1];
1225			}
1226
1227			//-- read the xml from the file
1228			$lines = file($report);
1229			while ($lineoffset + $this->repeat_bytes > 0 && strpos($lines[$lineoffset + $this->repeat_bytes], '<Facts ') === false) {
1230				$lineoffset--;
1231			}
1232			$lineoffset++;
1233			$reportxml = "<tempdoc>\n";
1234			$i         = $line + $lineoffset;
1235			$line_nr   = $this->repeat_bytes + $lineoffset;
1236			while ($line_nr < $i) {
1237				$reportxml .= $lines[$line_nr];
1238				$line_nr++;
1239			}
1240			// No need to drag this
1241			unset($lines);
1242			$reportxml .= "</tempdoc>\n";
1243			// Save original values
1244			array_push($this->parser_stack, $this->parser);
1245			$oldgedrec = $this->gedrec;
1246			$count     = count($this->repeats);
1247			$i         = 0;
1248			while ($i < $count) {
1249				$this->gedrec = $this->repeats[$i];
1250				$this->fact   = '';
1251				$this->desc   = '';
1252				if (preg_match('/1 (\w+)(.*)/', $this->gedrec, $match)) {
1253					$this->fact = $match[1];
1254					if ($this->fact === 'EVEN' || $this->fact === 'FACT') {
1255						$tmatch = array();
1256						if (preg_match('/2 TYPE (.+)/', $this->gedrec, $tmatch)) {
1257							$this->type = trim($tmatch[1]);
1258						} else {
1259							$this->type = ' ';
1260						}
1261					}
1262					$this->desc = trim($match[2]);
1263					$this->desc .= Functions::getCont(2, $this->gedrec);
1264				}
1265				$repeat_parser = xml_parser_create();
1266				$this->parser  = $repeat_parser;
1267				xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false);
1268				xml_set_element_handler($repeat_parser, array($this, 'startElement'), array($this, 'endElement'));
1269				xml_set_character_data_handler($repeat_parser, array($this, 'characterData'));
1270				if (!xml_parse($repeat_parser, $reportxml, true)) {
1271					throw new \DomainException(sprintf(
1272						'FactsEHandler XML error: %s at line %d',
1273						xml_error_string(xml_get_error_code($repeat_parser)),
1274						xml_get_current_line_number($repeat_parser)
1275					));
1276				}
1277				xml_parser_free($repeat_parser);
1278				$i++;
1279			}
1280			// Restore original values
1281			$this->parser = array_pop($this->parser_stack);
1282			$this->gedrec = $oldgedrec;
1283		}
1284		list($this->repeats, $this->repeat_bytes) = array_pop($this->repeats_stack);
1285	}
1286
1287	/**
1288	 * Setting upp or changing variables in the XML
1289	 * The XML variable name and value is stored in $this->vars
1290	 *
1291	 * @param array $attrs an array of key value pairs for the attributes
1292	 */
1293	private function setVarStartHandler($attrs) {
1294		if (empty($attrs['name'])) {
1295			throw new \DomainException('REPORT ERROR var: The attribute "name" is missing or not set in the XML file');
1296		}
1297
1298		$name  = $attrs['name'];
1299		$value = $attrs['value'];
1300		$match = array();
1301		// Current GEDCOM record strings
1302		if ($value == "@ID") {
1303			if (preg_match("/0 @(.+)@/", $this->gedrec, $match)) {
1304				$value = $match[1];
1305			}
1306		} elseif ($value == "@fact") {
1307			$value = $this->fact;
1308		} elseif ($value == "@desc") {
1309			$value = $this->desc;
1310		} elseif ($value == "@generation") {
1311			$value = $this->generation;
1312		} elseif (preg_match("/@(\w+)/", $value, $match)) {
1313			$gmatch = array();
1314			if (preg_match("/\d $match[1] (.+)/", $this->gedrec, $gmatch)) {
1315				$value = str_replace("@", "", trim($gmatch[1]));
1316			}
1317		}
1318		if (preg_match("/\\$(\w+)/", $name, $match)) {
1319			$name = $this->vars["'" . $match[1] . "'"]['id'];
1320		}
1321		$count = preg_match_all("/\\$(\w+)/", $value, $match, PREG_SET_ORDER);
1322		$i     = 0;
1323		while ($i < $count) {
1324			$t     = $this->vars[$match[$i][1]]['id'];
1325			$value = preg_replace("/\\$" . $match[$i][1] . "/", $t, $value, 1);
1326			$i++;
1327		}
1328		if (preg_match('/^I18N::number\((.+)\)$/', $value, $match)) {
1329			$value = I18N::number($match[1]);
1330		} elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $value, $match)) {
1331			$value = I18N::translate($match[1]);
1332		} elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $value, $match)) {
1333			$value = I18N::translateContext($match[1], $match[2]);
1334		}
1335		// Arithmetic functions
1336		if (preg_match("/(\d+)\s*([\-\+\*\/])\s*(\d+)/", $value, $match)) {
1337			switch ($match[2]) {
1338			case "+":
1339				$t     = $match[1] + $match[3];
1340				$value = preg_replace("/" . $match[1] . "\s*([\-\+\*\/])\s*" . $match[3] . "/", $t, $value);
1341				break;
1342			case "-":
1343				$t     = $match[1] - $match[3];
1344				$value = preg_replace("/" . $match[1] . "\s*([\-\+\*\/])\s*" . $match[3] . "/", $t, $value);
1345				break;
1346			case "*":
1347				$t     = $match[1] * $match[3];
1348				$value = preg_replace("/" . $match[1] . "\s*([\-\+\*\/])\s*" . $match[3] . "/", $t, $value);
1349				break;
1350			case "/":
1351				$t     = $match[1] / $match[3];
1352				$value = preg_replace("/" . $match[1] . "\s*([\-\+\*\/])\s*" . $match[3] . "/", $t, $value);
1353				break;
1354			}
1355		}
1356		if (strpos($value, "@") !== false) {
1357			$value = "";
1358		}
1359		$this->vars[$name]['id'] = $value;
1360	}
1361
1362	/**
1363	 * XML <if > start element
1364	 *
1365	 * @param array $attrs an array of key value pairs for the attributes
1366	 */
1367	private function ifStartHandler($attrs) {
1368		if ($this->process_ifs > 0) {
1369			$this->process_ifs++;
1370
1371			return;
1372		}
1373
1374		$condition = $attrs['condition'];
1375		$condition = $this->substituteVars($condition, true);
1376		$condition = str_replace(array(" LT ", " GT "), array("<", ">"), $condition);
1377		// Replace the first accurance only once of @fact:DATE or in any other combinations to the current fact, such as BIRT
1378		$condition = str_replace("@fact:", $this->fact . ':', $condition);
1379		$match     = array();
1380		$count     = preg_match_all("/@([\w:\.]+)/", $condition, $match, PREG_SET_ORDER);
1381		$i         = 0;
1382		while ($i < $count) {
1383			$id    = $match[$i][1];
1384			$value = '""';
1385			if ($id == "ID") {
1386				if (preg_match("/0 @(.+)@/", $this->gedrec, $match)) {
1387					$value = "'" . $match[1] . "'";
1388				}
1389			} elseif ($id === "fact") {
1390				$value = '"' . $this->fact . '"';
1391			} elseif ($id === "desc") {
1392				$value = '"' . addslashes($this->desc) . '"';
1393			} elseif ($id === "generation") {
1394				$value = '"' . $this->generation . '"';
1395			} else {
1396
1397				$temp  = explode(" ", trim($this->gedrec));
1398				$level = $temp[0];
1399				if ($level == 0) {
1400					$level++;
1401				}
1402				$value = $this->getGedcomValue($id, $level, $this->gedrec);
1403				if (empty($value)) {
1404					$level++;
1405					$value = $this->getGedcomValue($id, $level, $this->gedrec);
1406				}
1407				$value = preg_replace('/^@(' . WT_REGEX_XREF . ')@$/', '$1', $value);
1408				$value = '"' . addslashes($value) . '"';
1409			}
1410			$condition = str_replace("@$id", $value, $condition);
1411			$i++;
1412		}
1413		$ret = eval("return (bool) ($condition);");
1414		if (!$ret) {
1415			$this->process_ifs++;
1416		}
1417	}
1418
1419	/**
1420	 * XML <if /> end element
1421	 */
1422	private function ifEndHandler() {
1423		if ($this->process_ifs > 0) {
1424			$this->process_ifs--;
1425		}
1426	}
1427
1428	/**
1429	 * XML <Footnote > start element
1430	 * Collect the Footnote links
1431	 * GEDCOM Records that are protected by Privacy setting will be ignore
1432	 *
1433	 * @param array $attrs an array of key value pairs for the attributes
1434	 */
1435	private function footnoteStartHandler($attrs) {
1436		global $WT_TREE;
1437
1438		$id = "";
1439		if (preg_match("/[0-9] (.+) @(.+)@/", $this->gedrec, $match)) {
1440			$id = $match[2];
1441		}
1442		$record = GedcomRecord::getInstance($id, $WT_TREE);
1443		if ($record && $record->canShow()) {
1444			array_push($this->print_data_stack, $this->print_data);
1445			$this->print_data = true;
1446			$style            = "";
1447			if (!empty($attrs['style'])) {
1448				$style = $attrs['style'];
1449			}
1450			$this->footnote_element = $this->current_element;
1451			$this->current_element  = $this->report_root->createFootnote($style);
1452		} else {
1453			$this->print_data       = false;
1454			$this->process_footnote = false;
1455		}
1456	}
1457
1458	/**
1459	 * XML <Footnote /> end element
1460	 * Print the collected Footnote data
1461	 */
1462	private function footnoteEndHandler() {
1463		if ($this->process_footnote) {
1464			$this->print_data = array_pop($this->print_data_stack);
1465			$temp             = trim($this->current_element->getValue());
1466			if (strlen($temp) > 3) {
1467				$this->wt_report->addElement($this->current_element);
1468			}
1469			$this->current_element = $this->footnote_element;
1470		} else {
1471			$this->process_footnote = true;
1472		}
1473	}
1474
1475	/**
1476	 * XML <FootnoteTexts /> element
1477	 */
1478	private function footnoteTextsStartHandler() {
1479		$temp = "footnotetexts";
1480		$this->wt_report->addElement($temp);
1481	}
1482
1483	/**
1484	 * XML <AgeAtDeath /> element handler
1485	 */
1486	private function ageAtDeathStartHandler() {
1487		// This duplicates functionality in FunctionsPrint::format_fact_date()
1488		global $factrec, $WT_TREE;
1489
1490		$match = array();
1491		if (preg_match("/0 @(.+)@/", $this->gedrec, $match)) {
1492			$person = Individual::getInstance($match[1], $WT_TREE);
1493			// Recorded age
1494			if (preg_match('/\n2 AGE (.+)/', $factrec, $match)) {
1495				$fact_age = $match[1];
1496			} else {
1497				$fact_age = '';
1498			}
1499			if (preg_match('/\n2 HUSB\n3 AGE (.+)/', $factrec, $match)) {
1500				$husb_age = $match[1];
1501			} else {
1502				$husb_age = '';
1503			}
1504			if (preg_match('/\n2 WIFE\n3 AGE (.+)/', $factrec, $match)) {
1505				$wife_age = $match[1];
1506			} else {
1507				$wife_age = '';
1508			}
1509
1510			// Calculated age
1511			$birth_date = $person->getBirthDate();
1512			// Can't use getDeathDate(), as this also gives BURI/CREM events, which
1513			// wouldn't give the correct "days after death" result for people with
1514			// no DEAT.
1515			$death_event = $person->getFirstFact('DEAT');
1516			if ($death_event) {
1517				$death_date = $death_event->getDate();
1518			} else {
1519				$death_date = new Date('');
1520			}
1521			$value = '';
1522			if (Date::compare($birth_date, $death_date) <= 0 || !$person->isDead()) {
1523				$age = Date::getAgeGedcom($birth_date, $death_date);
1524				// Only show calculated age if it differs from recorded age
1525				if ($age != '' && $age != "0d") {
1526					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
1527					) {
1528						$value  = FunctionsDate::getAgeAtEvent($age, false);
1529						$abbrev = substr($value, 0, strpos($value, ' ') + 5);
1530						if ($value !== $abbrev) {
1531							$value = $abbrev . '.';
1532						}
1533					}
1534				}
1535			}
1536			$this->current_element->addText($value);
1537		}
1538	}
1539
1540	/**
1541	 * XML element Forced line break handler - HTML code
1542	 */
1543	private function brStartHandler() {
1544		if ($this->print_data && $this->process_gedcoms === 0) {
1545			$this->current_element->addText('<br>');
1546		}
1547	}
1548
1549	/**
1550	 * XML <sp />element Forced space handler
1551	 */
1552	private function spStartHandler() {
1553		if ($this->print_data && $this->process_gedcoms === 0) {
1554			$this->current_element->addText(' ');
1555		}
1556	}
1557
1558	/**
1559	 * XML <HighlightedImage/>
1560	 *
1561	 * @param array $attrs an array of key value pairs for the attributes
1562	 */
1563	private function highlightedImageStartHandler($attrs) {
1564		global $WT_TREE;
1565
1566		$id    = '';
1567		$match = array();
1568		if (preg_match("/0 @(.+)@/", $this->gedrec, $match)) {
1569			$id = $match[1];
1570		}
1571
1572		// mixed Position the top corner of this box on the page. the default is the current position
1573		$top = '.';
1574		if (isset($attrs['top'])) {
1575			if ($attrs['top'] === '0') {
1576				$top = 0;
1577			} elseif ($attrs['top'] === '.') {
1578				$top = '.';
1579			} elseif (!empty($attrs['top'])) {
1580				$top = (int) $attrs['top'];
1581			}
1582		}
1583
1584		// mixed Position the left corner of this box on the page. the default is the current position
1585		$left = '.';
1586		if (isset($attrs['left'])) {
1587			if ($attrs['left'] === '0') {
1588				$left = 0;
1589			} elseif ($attrs['left'] === '.') {
1590				$left = '.';
1591			} elseif (!empty($attrs['left'])) {
1592				$left = (int) $attrs['left'];
1593			}
1594		}
1595
1596		// string Align the image in left, center, right
1597		$align = '';
1598		if (!empty($attrs['align'])) {
1599			$align = $attrs['align'];
1600		}
1601
1602		// string Next Line should be T:next to the image, N:next line
1603		$ln = '';
1604		if (!empty($attrs['ln'])) {
1605			$ln = $attrs['ln'];
1606		}
1607
1608		$width  = 0;
1609		$height = 0;
1610		if (!empty($attrs['width'])) {
1611			$width = (int) $attrs['width'];
1612		}
1613		if (!empty($attrs['height'])) {
1614			$height = (int) $attrs['height'];
1615		}
1616
1617		$person      = Individual::getInstance($id, $WT_TREE);
1618		$mediaobject = $person->findHighlightedMedia();
1619		if ($mediaobject) {
1620			$attributes = $mediaobject->getImageAttributes('thumb');
1621			if (in_array(
1622					$attributes['ext'],
1623					array(
1624						'GIF',
1625						'JPG',
1626						'PNG',
1627						'SWF',
1628						'PSD',
1629						'BMP',
1630						'TIFF',
1631						'TIFF',
1632						'JPC',
1633						'JP2',
1634						'JPX',
1635						'JB2',
1636						'SWC',
1637						'IFF',
1638						'WBMP',
1639						'XBM',
1640					)
1641				) && $mediaobject->canShow() && $mediaobject->fileExists('thumb')
1642			) {
1643				if ($width > 0 && $height == 0) {
1644					$perc   = $width / $attributes['adjW'];
1645					$height = round($attributes['adjH'] * $perc);
1646				} elseif ($height > 0 && $width == 0) {
1647					$perc  = $height / $attributes['adjH'];
1648					$width = round($attributes['adjW'] * $perc);
1649				} else {
1650					$width  = $attributes['adjW'];
1651					$height = $attributes['adjH'];
1652				}
1653				$image = $this->report_root->createImageFromObject($mediaobject, $left, $top, $width, $height, $align, $ln);
1654				$this->wt_report->addElement($image);
1655			}
1656		}
1657	}
1658
1659	/**
1660	 * XML <Image/>
1661	 *
1662	 * @param array $attrs an array of key value pairs for the attributes
1663	 */
1664	private function imageStartHandler($attrs) {
1665		global $WT_TREE;
1666
1667		// mixed Position the top corner of this box on the page. the default is the current position
1668		$top = '.';
1669		if (isset($attrs['top'])) {
1670			if ($attrs['top'] === "0") {
1671				$top = 0;
1672			} elseif ($attrs['top'] === '.') {
1673				$top = '.';
1674			} elseif (!empty($attrs['top'])) {
1675				$top = (int) $attrs['top'];
1676			}
1677		}
1678
1679		// mixed Position the left corner of this box on the page. the default is the current position
1680		$left = '.';
1681		if (isset($attrs['left'])) {
1682			if ($attrs['left'] === '0') {
1683				$left = 0;
1684			} elseif ($attrs['left'] === '.') {
1685				$left = '.';
1686			} elseif (!empty($attrs['left'])) {
1687				$left = (int) $attrs['left'];
1688			}
1689		}
1690
1691		// string Align the image in left, center, right
1692		$align = '';
1693		if (!empty($attrs['align'])) {
1694			$align = $attrs['align'];
1695		}
1696
1697		// string Next Line should be T:next to the image, N:next line
1698		$ln = 'T';
1699		if (!empty($attrs['ln'])) {
1700			$ln = $attrs['ln'];
1701		}
1702
1703		$width  = 0;
1704		$height = 0;
1705		if (!empty($attrs['width'])) {
1706			$width = (int) $attrs['width'];
1707		}
1708		if (!empty($attrs['height'])) {
1709			$height = (int) $attrs['height'];
1710		}
1711
1712		$file = '';
1713		if (!empty($attrs['file'])) {
1714			$file = $attrs['file'];
1715		}
1716		if ($file == "@FILE") {
1717			$match = array();
1718			if (preg_match("/\d OBJE @(.+)@/", $this->gedrec, $match)) {
1719				$mediaobject = Media::getInstance($match[1], $WT_TREE);
1720				$attributes  = $mediaobject->getImageAttributes('thumb');
1721				if (in_array(
1722						$attributes['ext'],
1723						array(
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('thumb')
1742				) {
1743					if ($width > 0 && $height == 0) {
1744						$perc   = $width / $attributes['adjW'];
1745						$height = round($attributes['adjH'] * $perc);
1746					} elseif ($height > 0 && $width == 0) {
1747						$perc  = $height / $attributes['adjH'];
1748						$width = round($attributes['adjW'] * $perc);
1749					} else {
1750						$width  = $attributes['adjW'];
1751						$height = $attributes['adjH'];
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 = array();
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(array(
1872				'tree_id' => $WT_TREE->getTreeId(),
1873			))->fetchAll();
1874			$this->list = array();
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   = array('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 = array();
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   = array('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 = array();
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  = array();
2039		$filters2 = array();
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[] = array("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 = array();
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, array($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, array($this, 'startElement'), array($this, 'endElement'));
2267					xml_set_character_data_handler($repeat_parser, array($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   = array();
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 = array();
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 = array();
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 = array();
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, array($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, array($this, 'startElement'), array($this, 'endElement'));
2501				xml_set_character_data_handler($repeat_parser, array($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   = array();
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                = array($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		$that = $this; // PHP5.3 cannot access $this inside a closure
2815		return preg_replace_callback(
2816			'/\$(\w+)/',
2817			function ($matches) use ($that, $quote) {
2818				if (isset($that->vars[$matches[1]]['id'])) {
2819					if ($quote) {
2820						return "'" . addcslashes($that->vars[$matches[1]]['id'], "'") . "'";
2821					} else {
2822						return $that->vars[$matches[1]]['id'];
2823					}
2824				} else {
2825					Log::addErrorLog(sprintf('Undefined variable $%s in report', $matches[1]));
2826
2827					return '$' . $matches[1];
2828				}
2829			},
2830			$expression
2831		);
2832	}
2833}
2834