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