xref: /webtrees/app/Tree.php (revision e6057e0705735ac10481f708d3688ecdd2c75af4)
1<?php
2namespace Webtrees;
3
4/**
5 * webtrees: online genealogy
6 * Copyright (C) 2015 webtrees development team
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19use PDOException;
20
21/**
22 * Class Tree - Provide an interface to the wt_gedcom table
23 */
24class Tree {
25	/** @var integer The tree's ID number */
26	private $id;
27	/** @var string The tree's name */
28	private $name;
29	/** @var string The tree's title */
30	private $title;
31
32	/** @var Tree[] All trees that we have permission to see. */
33	private static $trees;
34
35	/** @var string[] Cached copy of the wt_gedcom_setting table. */
36	private $preferences;
37
38	/** @var string[][] Cached copy of the wt_user_gedcom_setting table. */
39	private $user_preferences = array();
40
41	/**
42	 * Create a tree object.  This is a private constructor - it can only
43	 * be called from Tree::getAll() to ensure proper initialisation.
44	 *
45	 * @param integer $id
46	 * @param string  $tree_name
47	 * @param string  $tree_title
48	 */
49	private function __construct($id, $tree_name, $tree_title) {
50		$this->id    = $id;
51		$this->name  = $tree_name;
52		$this->title = $tree_title;
53	}
54
55	/**
56	 * The ID of this tree
57	 *
58	 * @return integer
59	 */
60	public function id() {
61		return $this->id;
62	}
63
64	/**
65	 * The name of this tree
66	 *
67	 * @return string
68	 */
69	public function name() {
70		return $this->name;
71	}
72
73	/**
74	 * The name of this tree
75	 *
76	 * @return string
77	 */
78	public function nameHtml() {
79		return Filter::escapeHtml($this->name);
80	}
81
82	/**
83	 * The name of this tree
84	 *
85	 * @return string
86	 */
87	public function nameUrl() {
88		return Filter::escapeUrl($this->name);
89	}
90
91	/**
92	 * The title of this tree
93	 *
94	 * @return string
95	 */
96	public function title() {
97		return $this->title;
98	}
99
100	/**
101	 * The title of this tree, with HTML markup
102	 *
103	 * @return string
104	 */
105	public function titleHtml() {
106		return Filter::escapeHtml($this->title);
107	}
108
109	/**
110	 * Get the tree’s configuration settings.
111	 *
112	 * @param string      $setting_name
113	 * @param string|null $default
114	 *
115	 * @return string|null
116	 */
117	public function getPreference($setting_name, $default = null) {
118		if ($this->preferences === null) {
119			$this->preferences = Database::prepare(
120				"SELECT SQL_CACHE setting_name, setting_value FROM `##gedcom_setting` WHERE gedcom_id = ?"
121			)->execute(array($this->id))->fetchAssoc();
122		}
123
124		if (array_key_exists($setting_name, $this->preferences)) {
125			return $this->preferences[$setting_name];
126		} else {
127			return $default;
128		}
129	}
130
131	/**
132	 * Set the tree’s configuration settings.
133	 *
134	 * @param string $setting_name
135	 * @param string $setting_value
136	 *
137	 * @return $this
138	 */
139	public function setPreference($setting_name, $setting_value) {
140		if ($setting_value !== $this->getPreference($setting_name)) {
141			// Update the database
142			if ($setting_value === null) {
143				Database::prepare(
144					"DELETE FROM `##gedcom_setting` WHERE gedcom_id = :tree_id AND setting_name = :setting_name"
145				)->execute(array(
146					'tree_id'      => $this->id,
147					'setting_name' => $setting_name,
148				));
149			} else {
150				Database::prepare(
151					"REPLACE INTO `##gedcom_setting` (gedcom_id, setting_name, setting_value)" .
152					" VALUES (:tree_id, :setting_name, LEFT(:setting_value, 255))"
153				)->execute(array(
154					'tree_id'       => $this->id,
155					'setting_name'  => $setting_name,
156					'setting_value' => $setting_value,
157				));
158			}
159			// Update our cache
160			$this->preferences[$setting_name] = $setting_value;
161			// Audit log of changes
162			Log::addConfigurationLog('Tree setting "' . $setting_name . '" set to "' . $setting_value . '"', $this);
163		}
164
165		return $this;
166	}
167
168	/**
169	 * Get the tree’s user-configuration settings.
170	 *
171	 * @param User        $user
172	 * @param string      $setting_name
173	 * @param string|null $default
174	 *
175	 * @return string
176	 */
177	public function getUserPreference(User $user, $setting_name, $default = null) {
178		// There are lots of settings, and we need to fetch lots of them on every page
179		// so it is quicker to fetch them all in one go.
180		if (!array_key_exists($user->getUserId(), $this->user_preferences)) {
181			$this->user_preferences[$user->getUserId()] = Database::prepare(
182				"SELECT SQL_CACHE setting_name, setting_value FROM `##user_gedcom_setting` WHERE user_id = ? AND gedcom_id = ?"
183			)->execute(array($user->getUserId(), $this->id))->fetchAssoc();
184		}
185
186		if (array_key_exists($setting_name, $this->user_preferences[$user->getUserId()])) {
187			return $this->user_preferences[$user->getUserId()][$setting_name];
188		} else {
189			return $default;
190		}
191	}
192
193	/**
194	 * Set the tree’s user-configuration settings.
195	 *
196	 * @param User    $user
197	 * @param string  $setting_name
198	 * @param string  $setting_value
199	 *
200	 * @return $this
201	 */
202	public function setUserPreference(User $user, $setting_name, $setting_value) {
203		if ($this->getUserPreference($user, $setting_name) !== $setting_value) {
204			// Update the database
205			if ($setting_value === null) {
206				Database::prepare(
207					"DELETE FROM `##user_gedcom_setting` WHERE gedcom_id = :tree_id AND user_id = :user_id AND setting_name = :setting_name"
208				)->execute(array(
209					'tree_id'      => $this->id,
210					'user_id'      => $user->getUserId(),
211					'setting_name' => $setting_name,
212				));
213			} else {
214				Database::prepare(
215					"REPLACE INTO `##user_gedcom_setting` (user_id, gedcom_id, setting_name, setting_value) VALUES (:user_id, :tree_id, :setting_name, LEFT(:setting_value, 255))"
216				)->execute(array(
217					'user_id'       => $user->getUserId(),
218					'tree_id'       => $this->id,
219					'setting_name'  => $setting_name,
220					'setting_value' => $setting_value
221				));
222			}
223			// Update our cache
224			$this->user_preferences[$user->getUserId()][$setting_name] = $setting_value;
225			// Audit log of changes
226			Log::addConfigurationLog('Tree setting "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->getUserName() . '"', $this);
227		}
228
229		return $this;
230	}
231
232	/**
233	 * Can a user accept changes for this tree?
234	 *
235	 * @param User $user
236	 *
237	 * @return boolean
238	 */
239	public function canAcceptChanges(User $user) {
240		return Auth::isModerator($this, $user);
241	}
242
243	/**
244	 * Fetch all the trees that we have permission to access.
245	 *
246	 * @return Tree[]
247	 */
248	public static function getAll() {
249		if (self::$trees === null) {
250			self::$trees = array();
251			$rows = Database::prepare(
252				"SELECT SQL_CACHE g.gedcom_id AS tree_id, g.gedcom_name AS tree_name, gs1.setting_value AS tree_title" .
253				" FROM `##gedcom` g" .
254				" LEFT JOIN `##gedcom_setting`      gs1 ON (g.gedcom_id=gs1.gedcom_id AND gs1.setting_name='title')" .
255				" LEFT JOIN `##gedcom_setting`      gs2 ON (g.gedcom_id=gs2.gedcom_id AND gs2.setting_name='imported')" .
256				" LEFT JOIN `##gedcom_setting`      gs3 ON (g.gedcom_id=gs3.gedcom_id AND gs3.setting_name='REQUIRE_AUTHENTICATION')" .
257				" LEFT JOIN `##user_gedcom_setting` ugs ON (g.gedcom_id=ugs.gedcom_id AND ugs.setting_name='canedit' AND ugs.user_id=?)" .
258				" WHERE " .
259				"  g.gedcom_id>0 AND (" . // exclude the "template" tree
260				"    EXISTS (SELECT 1 FROM `##user_setting` WHERE user_id=? AND setting_name='canadmin' AND setting_value=1)" . // Admin sees all
261				"   ) OR (" .
262				"    gs2.setting_value = 1 AND (" . // Allow imported trees, with either:
263				"     gs3.setting_value <> 1 OR" . // visitor access
264				"     IFNULL(ugs.setting_value, 'none')<>'none'" . // explicit access
265				"   )" .
266				"  )" .
267				" ORDER BY g.sort_order, 3"
268			)->execute(array(Auth::id(), Auth::id()))->fetchAll();
269			foreach ($rows as $row) {
270				self::$trees[$row->tree_id] = new self($row->tree_id, $row->tree_name, $row->tree_title);
271			}
272		}
273
274		return self::$trees;
275	}
276
277	/**
278	 * Get the tree with a specific ID.
279	 *
280	 * @param integer $tree_id
281	 *
282	 * @return Tree
283	 */
284	public static function get($tree_id) {
285		$trees = self::getAll();
286
287		return $trees[$tree_id];
288	}
289
290	/**
291	 * Create arguments to select_edit_control()
292	 * Note - these will be escaped later
293	 *
294	 * @return string[]
295	 */
296	public static function getIdList() {
297		$list = array();
298		foreach (self::getAll() as $tree) {
299			$list[$tree->id] = $tree->title;
300		}
301
302		return $list;
303	}
304
305	/**
306	 * Create arguments to select_edit_control()
307	 * Note - these will be escaped later
308	 *
309	 * @return string[]
310	 */
311	public static function getNameList() {
312		$list = array();
313		foreach (self::getAll() as $tree) {
314			$list[$tree->name] = $tree->title;
315		}
316
317		return $list;
318	}
319
320	/**
321	 * Find the ID number for a tree name
322	 *
323	 * @param integer $tree_name
324	 *
325	 * @return integer|null
326	 */
327	public static function getIdFromName($tree_name) {
328		foreach (self::getAll() as $tree_id => $tree) {
329			if ($tree->name == $tree_name) {
330				return $tree_id;
331			}
332		}
333
334		return null;
335	}
336
337	/**
338	 * Find the tree name from a numeric ID.
339	 * @param integer $tree_id
340	 *
341	 * @return string
342	 */
343	public static function getNameFromId($tree_id) {
344		return self::get($tree_id)->name;
345	}
346
347	/**
348	 * Create a new tree
349	 *
350	 * @param string $tree_name
351	 * @param string $tree_title
352	 *
353	 * @return Tree
354	 */
355	public static function create($tree_name, $tree_title) {
356		try {
357			// Create a new tree
358			Database::prepare(
359				"INSERT INTO `##gedcom` (gedcom_name) VALUES (?)"
360			)->execute(array($tree_name));
361			$tree_id = Database::prepare("SELECT LAST_INSERT_ID()")->fetchOne();
362		} catch (PDOException $ex) {
363			// A tree with that name already exists?
364			return self::get(self::getIdFromName($tree_name));
365		}
366
367		// Update the list of trees - to include this new one
368		self::$trees = null;
369		$tree        = self::get($tree_id);
370
371		$tree->setPreference('imported', '0');
372		$tree->setPreference('title', $tree_title);
373
374		// Module privacy
375		Module::setDefaultAccess($tree_id);
376
377		// Gedcom and privacy settings
378		$tree->setPreference('ADVANCED_NAME_FACTS', 'NICK,_AKA');
379		$tree->setPreference('ADVANCED_PLAC_FACTS', '');
380		$tree->setPreference('ALLOW_THEME_DROPDOWN', '1');
381		$tree->setPreference('CALENDAR_FORMAT', 'gregorian');
382		$tree->setPreference('CHART_BOX_TAGS', '');
383		$tree->setPreference('COMMON_NAMES_ADD', '');
384		$tree->setPreference('COMMON_NAMES_REMOVE', '');
385		$tree->setPreference('COMMON_NAMES_THRESHOLD', '40');
386		$tree->setPreference('CONTACT_USER_ID', Auth::id());
387		$tree->setPreference('DEFAULT_PEDIGREE_GENERATIONS', '4');
388		$tree->setPreference('EXPAND_RELATIVES_EVENTS', '0');
389		$tree->setPreference('EXPAND_SOURCES', '0');
390		$tree->setPreference('FAM_FACTS_ADD', 'CENS,MARR,RESI,SLGS,MARR_CIVIL,MARR_RELIGIOUS,MARR_PARTNERS,RESN');
391		$tree->setPreference('FAM_FACTS_QUICK', 'MARR,DIV,_NMR');
392		$tree->setPreference('FAM_FACTS_UNIQUE', 'NCHI,MARL,DIV,ANUL,DIVF,ENGA,MARB,MARC,MARS');
393		$tree->setPreference('FAM_ID_PREFIX', 'F');
394		$tree->setPreference('FORMAT_TEXT', 'markdown');
395		$tree->setPreference('FULL_SOURCES', '0');
396		$tree->setPreference('GEDCOM_ID_PREFIX', 'I');
397		$tree->setPreference('GEDCOM_MEDIA_PATH', '');
398		$tree->setPreference('GENERATE_UIDS', '0');
399		$tree->setPreference('HIDE_GEDCOM_ERRORS', '1');
400		$tree->setPreference('HIDE_LIVE_PEOPLE', '1');
401		$tree->setPreference('INDI_FACTS_ADD', 'AFN,BIRT,DEAT,BURI,CREM,ADOP,BAPM,BARM,BASM,BLES,CHRA,CONF,FCOM,ORDN,NATU,EMIG,IMMI,CENS,PROB,WILL,GRAD,RETI,DSCR,EDUC,IDNO,NATI,NCHI,NMR,OCCU,PROP,RELI,RESI,SSN,TITL,BAPL,CONL,ENDL,SLGC,_MILI,ASSO,RESN');
402		$tree->setPreference('INDI_FACTS_QUICK', 'BIRT,BURI,BAPM,CENS,DEAT,OCCU,RESI');
403		$tree->setPreference('INDI_FACTS_UNIQUE', '');
404		$tree->setPreference('KEEP_ALIVE_YEARS_BIRTH', '');
405		$tree->setPreference('KEEP_ALIVE_YEARS_DEATH', '');
406		$tree->setPreference('LANGUAGE', WT_LOCALE); // Default to the current admin’s language
407		$tree->setPreference('MAX_ALIVE_AGE', 120);
408		$tree->setPreference('MAX_DESCENDANCY_GENERATIONS', '15');
409		$tree->setPreference('MAX_PEDIGREE_GENERATIONS', '10');
410		$tree->setPreference('MEDIA_DIRECTORY', 'media/');
411		$tree->setPreference('MEDIA_ID_PREFIX', 'M');
412		$tree->setPreference('MEDIA_UPLOAD', WT_PRIV_USER);
413		$tree->setPreference('META_DESCRIPTION', '');
414		$tree->setPreference('META_TITLE', WT_WEBTREES);
415		$tree->setPreference('NOTE_FACTS_ADD', 'SOUR,RESN');
416		$tree->setPreference('NOTE_FACTS_QUICK', '');
417		$tree->setPreference('NOTE_FACTS_UNIQUE', '');
418		$tree->setPreference('NOTE_ID_PREFIX', 'N');
419		$tree->setPreference('NO_UPDATE_CHAN', '0');
420		$tree->setPreference('PEDIGREE_FULL_DETAILS', '1');
421		$tree->setPreference('PEDIGREE_LAYOUT', '1');
422		$tree->setPreference('PEDIGREE_ROOT_ID', '');
423		$tree->setPreference('PEDIGREE_SHOW_GENDER', '0');
424		$tree->setPreference('PREFER_LEVEL2_SOURCES', '1');
425		$tree->setPreference('QUICK_REQUIRED_FACTS', 'BIRT,DEAT');
426		$tree->setPreference('QUICK_REQUIRED_FAMFACTS', 'MARR');
427		$tree->setPreference('REPO_FACTS_ADD', 'PHON,EMAIL,FAX,WWW,RESN');
428		$tree->setPreference('REPO_FACTS_QUICK', '');
429		$tree->setPreference('REPO_FACTS_UNIQUE', 'NAME,ADDR');
430		$tree->setPreference('REPO_ID_PREFIX', 'R');
431		$tree->setPreference('REQUIRE_AUTHENTICATION', '0');
432		$tree->setPreference('SAVE_WATERMARK_IMAGE', '0');
433		$tree->setPreference('SAVE_WATERMARK_THUMB', '0');
434		$tree->setPreference('SHOW_AGE_DIFF', '0');
435		$tree->setPreference('SHOW_COUNTER', '1');
436		$tree->setPreference('SHOW_DEAD_PEOPLE', WT_PRIV_PUBLIC);
437		$tree->setPreference('SHOW_EST_LIST_DATES', '0');
438		$tree->setPreference('SHOW_FACT_ICONS', '1');
439		$tree->setPreference('SHOW_GEDCOM_RECORD', '0');
440		$tree->setPreference('SHOW_HIGHLIGHT_IMAGES', '1');
441		$tree->setPreference('SHOW_LDS_AT_GLANCE', '0');
442		$tree->setPreference('SHOW_LEVEL2_NOTES', '1');
443		$tree->setPreference('SHOW_LIVING_NAMES', WT_PRIV_USER);
444		$tree->setPreference('SHOW_MEDIA_DOWNLOAD', '0');
445		$tree->setPreference('SHOW_NO_WATERMARK', WT_PRIV_USER);
446		$tree->setPreference('SHOW_PARENTS_AGE', '1');
447		$tree->setPreference('SHOW_PEDIGREE_PLACES', '9');
448		$tree->setPreference('SHOW_PEDIGREE_PLACES_SUFFIX', '0');
449		$tree->setPreference('SHOW_PRIVATE_RELATIONSHIPS', '1');
450		$tree->setPreference('SHOW_RELATIVES_EVENTS', '_BIRT_CHIL,_BIRT_SIBL,_MARR_CHIL,_MARR_PARE,_DEAT_CHIL,_DEAT_PARE,_DEAT_GPAR,_DEAT_SIBL,_DEAT_SPOU');
451		$tree->setPreference('SHOW_STATS', '0');
452		$tree->setPreference('SOURCE_ID_PREFIX', 'S');
453		$tree->setPreference('SOUR_FACTS_ADD', 'NOTE,REPO,SHARED_NOTE,RESN');
454		$tree->setPreference('SOUR_FACTS_QUICK', 'TEXT,NOTE,REPO');
455		$tree->setPreference('SOUR_FACTS_UNIQUE', 'AUTH,ABBR,TITL,PUBL,TEXT');
456		$tree->setPreference('SUBLIST_TRIGGER_I', '200');
457		$tree->setPreference('SURNAME_LIST_STYLE', 'style2');
458		switch (WT_LOCALE) {
459		case 'es':
460			$tree->setPreference('SURNAME_TRADITION', 'spanish');
461			break;
462		case 'is':
463			$tree->setPreference('SURNAME_TRADITION', 'icelandic');
464			break;
465		case 'lt':
466			$tree->setPreference('SURNAME_TRADITION', 'lithuanian');
467			break;
468		case 'pl':
469			$tree->setPreference('SURNAME_TRADITION', 'polish');
470			break;
471		case 'pt':
472		case 'pt-BR':
473			$tree->setPreference('SURNAME_TRADITION', 'portuguese');
474			break;
475		default:
476			$tree->setPreference('SURNAME_TRADITION', 'paternal');
477			break;
478		}
479		$tree->setPreference('THEME_DIR', 'webtrees');
480		$tree->setPreference('THUMBNAIL_WIDTH', '100');
481		$tree->setPreference('USE_RIN', '0');
482		$tree->setPreference('USE_SILHOUETTE', '1');
483		$tree->setPreference('WATERMARK_THUMB', '0');
484		$tree->setPreference('WEBMASTER_USER_ID', Auth::id());
485		$tree->setPreference('WEBTREES_EMAIL', '');
486		$tree->setPreference('WORD_WRAPPED_NOTES', '0');
487
488		// Default restriction settings
489		$statement = Database::prepare(
490			"INSERT INTO `##default_resn` (gedcom_id, xref, tag_type, resn) VALUES (?, NULL, ?, ?)"
491		);
492		$statement->execute(array($tree_id, 'SSN', 'confidential'));
493		$statement->execute(array($tree_id, 'SOUR', 'privacy'));
494		$statement->execute(array($tree_id, 'REPO', 'privacy'));
495		$statement->execute(array($tree_id, 'SUBM', 'confidential'));
496		$statement->execute(array($tree_id, 'SUBN', 'confidential'));
497
498		// Genealogy data
499		// It is simpler to create a temporary/unimported GEDCOM than to populate all the tables...
500		$john_doe = /* I18N: This should be a common/default/placeholder name of an individual.  Put slashes around the surname. */
501			I18N::translate('John /DOE/');
502		$note     = I18N::translate('Edit this individual and replace their details with your own');
503		Database::prepare("INSERT INTO `##gedcom_chunk` (gedcom_id, chunk_data) VALUES (?, ?)")->execute(array(
504			$tree_id,
505			"0 HEAD\n1 CHAR UTF-8\n0 @I1@ INDI\n1 NAME {$john_doe}\n1 SEX M\n1 BIRT\n2 DATE 01 JAN 1850\n2 NOTE {$note}\n0 TRLR\n"
506		));
507
508		// Set the initial blocks
509		Database::prepare(
510			"INSERT INTO `##block` (gedcom_id, location, block_order, module_name)" .
511			" SELECT ?, location, block_order, module_name" .
512			" FROM `##block`" .
513			" WHERE gedcom_id = -1"
514		)->execute(array($tree_id));
515
516		// Update our cache
517		self::$trees[$tree->id] = $tree;
518
519		return $tree;
520	}
521
522	/**
523	 * Delete all the genealogy data from a tree - in preparation for importing
524	 * new data.  Optionally retain the media data, for when the user has been
525	 * editing their data offline using an application which deletes (or does not
526	 * support) media data.
527	 *
528	 * @param bool $keep_media
529	 */
530	public function deleteGenealogyData($keep_media) {
531		Database::prepare("DELETE FROM `##gedcom_chunk` WHERE gedcom_id = ?")->execute(array($this->id));
532		Database::prepare("DELETE FROM `##individuals`  WHERE i_file    = ?")->execute(array($this->id));
533		Database::prepare("DELETE FROM `##families`     WHERE f_file    = ?")->execute(array($this->id));
534		Database::prepare("DELETE FROM `##sources`      WHERE s_file    = ?")->execute(array($this->id));
535		Database::prepare("DELETE FROM `##other`        WHERE o_file    = ?")->execute(array($this->id));
536		Database::prepare("DELETE FROM `##places`       WHERE p_file    = ?")->execute(array($this->id));
537		Database::prepare("DELETE FROM `##placelinks`   WHERE pl_file   = ?")->execute(array($this->id));
538		Database::prepare("DELETE FROM `##name`         WHERE n_file    = ?")->execute(array($this->id));
539		Database::prepare("DELETE FROM `##dates`        WHERE d_file    = ?")->execute(array($this->id));
540		Database::prepare("DELETE FROM `##change`       WHERE gedcom_id = ?")->execute(array($this->id));
541
542		if ($keep_media) {
543			Database::prepare("DELETE FROM `##link` WHERE l_file =? AND l_type<>'OBJE'")->execute(array($this->id));
544		} else {
545			Database::prepare("DELETE FROM `##link`  WHERE l_file =?")->execute(array($this->id));
546			Database::prepare("DELETE FROM `##media` WHERE m_file =?")->execute(array($this->id));
547		}
548	}
549
550	/**
551	 * Delete everything relating to a tree
552	 */
553	public function delete() {
554		// If this is the default tree, then unset it
555		if (Site::getPreference('DEFAULT_GEDCOM') === self::getNameFromId($this->id)) {
556			Site::setPreference('DEFAULT_GEDCOM', '');
557		}
558
559		$this->deleteGenealogyData(false);
560
561		Database::prepare("DELETE `##block_setting` FROM `##block_setting` JOIN `##block` USING (block_id) WHERE gedcom_id=?")->execute(array($this->id));
562		Database::prepare("DELETE FROM `##block`               WHERE gedcom_id = ?")->execute(array($this->id));
563		Database::prepare("DELETE FROM `##user_gedcom_setting` WHERE gedcom_id = ?")->execute(array($this->id));
564		Database::prepare("DELETE FROM `##gedcom_setting`      WHERE gedcom_id = ?")->execute(array($this->id));
565		Database::prepare("DELETE FROM `##module_privacy`      WHERE gedcom_id = ?")->execute(array($this->id));
566		Database::prepare("DELETE FROM `##next_id`             WHERE gedcom_id = ?")->execute(array($this->id));
567		Database::prepare("DELETE FROM `##hit_counter`         WHERE gedcom_id = ?")->execute(array($this->id));
568		Database::prepare("DELETE FROM `##default_resn`        WHERE gedcom_id = ?")->execute(array($this->id));
569		Database::prepare("DELETE FROM `##gedcom_chunk`        WHERE gedcom_id = ?")->execute(array($this->id));
570		Database::prepare("DELETE FROM `##log`                 WHERE gedcom_id = ?")->execute(array($this->id));
571		Database::prepare("DELETE FROM `##gedcom`              WHERE gedcom_id = ?")->execute(array($this->id));
572
573		// After updating the database, we need to fetch a new (sorted) copy
574		self::$trees = null;
575	}
576
577	/**
578	 * Export the tree to a GEDCOM file
579	 *
580	 * @param $gedcom_file
581	 *
582	 * @return bool
583	 */
584	public function exportGedcom($gedcom_file) {
585		// To avoid partial trees on timeout/diskspace/etc, write to a temporary file first
586		$tmp_file = $gedcom_file . '.tmp';
587
588		$file_pointer = @fopen($tmp_file, 'w');
589		if ($file_pointer === false) {
590			return false;
591		}
592
593		$buffer = reformat_record_export(gedcom_header($this->name));
594
595		$stmt = Database::prepare(
596			"SELECT i_gedcom AS gedcom FROM `##individuals` WHERE i_file = ?" .
597			" UNION ALL " .
598			"SELECT f_gedcom AS gedcom FROM `##families`    WHERE f_file = ?" .
599			" UNION ALL " .
600			"SELECT s_gedcom AS gedcom FROM `##sources`     WHERE s_file = ?" .
601			" UNION ALL " .
602			"SELECT o_gedcom AS gedcom FROM `##other`       WHERE o_file = ? AND o_type NOT IN ('HEAD', 'TRLR')" .
603			" UNION ALL " .
604			"SELECT m_gedcom AS gedcom FROM `##media`       WHERE m_file = ?"
605		)->execute(array($this->id, $this->id, $this->id, $this->id, $this->id));
606
607		while ($row = $stmt->fetch()) {
608			$buffer .= reformat_record_export($row->gedcom);
609			if (strlen($buffer) > 65535) {
610				fwrite($file_pointer, $buffer);
611				$buffer = '';
612			}
613		}
614
615		fwrite($file_pointer, $buffer . '0 TRLR' . WT_EOL);
616		fclose($file_pointer);
617
618		return @rename($tmp_file, $gedcom_file);
619	}
620
621	/**
622	 * Import data from a gedcom file into this tree.
623	 *
624	 * @param string  $path       The full path to the (possibly temporary) file.
625	 * @param string  $filename   The preferred filename, for export/download.
626	 * @param boolean $keep_media Whether to retain any existing media records
627	 *
628	 * @throws \Exception
629	 */
630	public function importGedcomFile($path, $filename, $keep_media) {
631		// Read the file in blocks of roughly 64K.  Ensure that each block
632		// contains complete gedcom records.  This will ensure we don’t split
633		// multi-byte characters, as well as simplifying the code to import
634		// each block.
635
636		$file_data = '';
637		$fp = fopen($path, 'rb');
638
639		// Don’t allow the user to cancel the request.  We do not want to be left with an incomplete transaction.
640		ignore_user_abort(true);
641
642		Database::beginTransaction();
643		$this->deleteGenealogyData($keep_media);
644		$this->setPreference('gedcom_filename', $filename);
645		$this->setPreference('imported', '0');
646
647		while (!feof($fp)) {
648			$file_data .= fread($fp, 65536);
649			// There is no strrpos() function that searches for substrings :-(
650			for ($pos = strlen($file_data) - 1; $pos > 0; --$pos) {
651				if ($file_data[$pos] === '0' && ($file_data[$pos - 1] === "\n" || $file_data[$pos - 1] === "\r")) {
652					// We’ve found the last record boundary in this chunk of data
653					break;
654				}
655			}
656			if ($pos) {
657				Database::prepare(
658					"INSERT INTO `##gedcom_chunk` (gedcom_id, chunk_data) VALUES (?, ?)"
659				)->execute(array($this->id, substr($file_data, 0, $pos)));
660				$file_data = substr($file_data, $pos);
661			}
662		}
663		Database::prepare(
664			"INSERT INTO `##gedcom_chunk` (gedcom_id, chunk_data) VALUES (?, ?)"
665		)->execute(array($this->id, $file_data));
666
667		Database::commit();
668		fclose($fp);
669	}
670}
671