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