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