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