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