xref: /webtrees/app/Module/FamilyTreeFavoritesModule.php (revision 727f238cd22e70e36392127f0b2bce4f7aec649a)
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\Module;
17
18use Fisharebest\Webtrees\Auth;
19use Fisharebest\Webtrees\Database;
20use Fisharebest\Webtrees\Filter;
21use Fisharebest\Webtrees\FlashMessages;
22use Fisharebest\Webtrees\Functions\FunctionsEdit;
23use Fisharebest\Webtrees\Functions\FunctionsPrint;
24use Fisharebest\Webtrees\GedcomRecord;
25use Fisharebest\Webtrees\GedcomTag;
26use Fisharebest\Webtrees\I18N;
27use Fisharebest\Webtrees\Individual;
28use Fisharebest\Webtrees\Theme;
29use PDO;
30use PDOException;
31use Rhumsaa\Uuid\Uuid;
32
33/**
34 * Class FamilyTreeFavoritesModule
35 *
36 * Note that the user favorites module simply extends this module, so ensure that the
37 * logic works for both.
38 */
39class FamilyTreeFavoritesModule extends AbstractModule implements ModuleBlockInterface {
40	/**
41	 * Create a new module.
42	 *
43	 * @param string $directory Where is this module installed
44	 */
45	public function __construct($directory) {
46		parent::__construct($directory);
47
48		// Create/update the database tables.
49		Database::updateSchema('\Fisharebest\Webtrees\Module\FamilyTreeFavorites\Schema', 'FV_SCHEMA_VERSION', 4);
50	}
51
52	/**
53	 * How should this module be labelled on tabs, menus, etc.?
54	 *
55	 * @return string
56	 */
57	public function getTitle() {
58		return /* I18N: Name of a module */ I18N::translate('Favorites');
59	}
60
61	/**
62	 * A sentence describing what this module does.
63	 *
64	 * @return string
65	 */
66	public function getDescription() {
67		return /* I18N: Description of the “Favorites” module */ I18N::translate('Display and manage a family tree’s favorite pages.');
68	}
69
70	/**
71	 * Generate the HTML content of this block.
72	 *
73	 * @param int      $block_id
74	 * @param bool     $template
75	 * @param string[] $cfg
76	 *
77	 * @return string
78	 */
79	public function getBlock($block_id, $template = true, $cfg = array()) {
80		global $ctype, $controller, $WT_TREE;
81
82		$action = Filter::get('action');
83		switch ($action) {
84		case 'deletefav':
85			$favorite_id = Filter::getInteger('favorite_id');
86			if ($favorite_id) {
87				self::deleteFavorite($favorite_id);
88			}
89			break;
90		case 'addfav':
91			$gid      = Filter::get('gid', WT_REGEX_XREF);
92			$favnote  = Filter::get('favnote');
93			$url      = Filter::getUrl('url');
94			$favtitle = Filter::get('favtitle');
95
96			if ($gid) {
97				$record = GedcomRecord::getInstance($gid, $WT_TREE);
98				if ($record && $record->canShow()) {
99					self::addFavorite(array(
100						'user_id'   => $ctype === 'user' ? Auth::id() : null,
101						'gedcom_id' => $WT_TREE->getTreeId(),
102						'gid'       => $record->getXref(),
103						'type'      => $record::RECORD_TYPE,
104						'url'       => null,
105						'note'      => $favnote,
106						'title'     => $favtitle,
107					));
108				}
109			} elseif ($url) {
110				self::addFavorite(array(
111					'user_id'   => $ctype === 'user' ? Auth::id() : null,
112					'gedcom_id' => $WT_TREE->getTreeId(),
113					'gid'       => null,
114					'type'      => 'URL',
115					'url'       => $url,
116					'note'      => $favnote,
117					'title'     => $favtitle ? $favtitle : $url,
118				));
119			}
120			break;
121		}
122
123		$block = $this->getBlockSetting($block_id, 'block', '0');
124
125		foreach (array('block') as $name) {
126			if (array_key_exists($name, $cfg)) {
127				$$name = $cfg[$name];
128			}
129		}
130
131		$userfavs = $this->getFavorites($ctype === 'user' ? Auth::id() : $WT_TREE->getTreeId());
132		if (!is_array($userfavs)) {
133			$userfavs = array();
134		}
135
136		$id    = $this->getName() . $block_id;
137		$class = $this->getName() . '_block';
138		$title = $this->getTitle();
139
140		if (Auth::check()) {
141			$controller
142				->addExternalJavascript(WT_AUTOCOMPLETE_JS_URL)
143				->addInlineJavascript('autocomplete();');
144		}
145
146		$content = '';
147		if ($userfavs) {
148			foreach ($userfavs as $key => $favorite) {
149				if (isset($favorite['id'])) {
150					$key = $favorite['id'];
151				}
152				$removeFavourite = '<a class="font9" href="index.php?ctype=' . $ctype . '&amp;action=deletefav&amp;favorite_id=' . $key . '" onclick="return confirm(\'' . I18N::translate('Are you sure you want to remove this item from your list of favorites?') . '\');">' . I18N::translate('Remove') . '</a> ';
153				if ($favorite['type'] == 'URL') {
154					$content .= '<div id="boxurl' . $key . '.0" class="person_box">';
155					if ($ctype == 'user' || Auth::isManager($WT_TREE)) {
156						$content .= $removeFavourite;
157					}
158					$content .= '<a href="' . $favorite['url'] . '"><b>' . $favorite['title'] . '</b></a>';
159					$content .= '<br>' . $favorite['note'];
160					$content .= '</div>';
161				} else {
162					$record = GedcomRecord::getInstance($favorite['gid'], $WT_TREE);
163					if ($record && $record->canShow()) {
164						if ($record instanceof Individual) {
165							$content .= '<div id="box' . $favorite["gid"] . '.0" class="person_box action_header';
166							switch ($record->getsex()) {
167							case 'M':
168								break;
169							case 'F':
170								$content .= 'F';
171								break;
172							default:
173								$content .= 'NN';
174								break;
175							}
176							$content .= '">';
177							if ($ctype == "user" || Auth::isManager($WT_TREE)) {
178								$content .= $removeFavourite;
179							}
180							$content .= Theme::theme()->individualBoxLarge($record);
181							$content .= $favorite['note'];
182							$content .= '</div>';
183						} else {
184							$content .= '<div id="box' . $favorite['gid'] . '.0" class="person_box">';
185							if ($ctype == 'user' || Auth::isManager($WT_TREE)) {
186								$content .= $removeFavourite;
187							}
188							$content .= $record->formatList('span');
189							$content .= '<br>' . $favorite['note'];
190							$content .= '</div>';
191						}
192					}
193				}
194			}
195		}
196		if ($ctype == 'user' || Auth::isManager($WT_TREE)) {
197			$uniqueID = Uuid::uuid4(); // This block can theoretically appear multiple times, so use a unique ID.
198			$content .= '<div class="add_fav_head">';
199			$content .= '<a href="#" onclick="return expand_layer(\'add_fav' . $uniqueID . '\');">' . I18N::translate('Add a new favorite') . '<i id="add_fav' . $uniqueID . '_img" class="icon-plus"></i></a>';
200			$content .= '</div>';
201			$content .= '<div id="add_fav' . $uniqueID . '" style="display: none;">';
202			$content .= '<form name="addfavform" method="get" action="index.php">';
203			$content .= '<input type="hidden" name="action" value="addfav">';
204			$content .= '<input type="hidden" name="ctype" value="' . $ctype . '">';
205			$content .= '<input type="hidden" name="ged" value="' . $WT_TREE->getNameHtml() . '">';
206			$content .= '<div class="add_fav_ref">';
207			$content .= '<input type="radio" name="fav_category" value="record" checked onclick="jQuery(\'#gid' . $uniqueID . '\').removeAttr(\'disabled\'); jQuery(\'#url, #favtitle\').attr(\'disabled\',\'disabled\').val(\'\');">';
208			$content .= '<label for="gid' . $uniqueID . '">' . I18N::translate('Enter an individual, family, or source ID') . '</label>';
209			$content .= '<input class="pedigree_form" data-autocomplete-type="IFSRO" type="text" name="gid" id="gid' . $uniqueID . '" size="5" value="">';
210			$content .= ' ' . FunctionsPrint::printFindIndividualLink('gid' . $uniqueID);
211			$content .= ' ' . FunctionsPrint::printFindFamilyLink('gid' . $uniqueID);
212			$content .= ' ' . FunctionsPrint::printFindSourceLink('gid' . $uniqueID);
213			$content .= ' ' . FunctionsPrint::printFindRepositoryLink('gid' . $uniqueID);
214			$content .= ' ' . FunctionsPrint::printFindNoteLink('gid' . $uniqueID);
215			$content .= ' ' . FunctionsPrint::printFindMediaLink('gid' . $uniqueID);
216			$content .= '</div>';
217			$content .= '<div class="add_fav_url">';
218			$content .= '<input type="radio" name="fav_category" value="url" onclick="jQuery(\'#url, #favtitle\').removeAttr(\'disabled\'); jQuery(\'#gid' . $uniqueID . '\').attr(\'disabled\',\'disabled\').val(\'\');">';
219			$content .= '<input type="text" name="url" id="url" size="20" value="" placeholder="' . GedcomTag::getLabel('URL') . '" disabled> ';
220			$content .= '<input type="text" name="favtitle" id="favtitle" size="20" value="" placeholder="' . I18N::translate('Title') . '" disabled>';
221			$content .= '<p>' . I18N::translate('Enter an optional note about this favorite') . '</p>';
222			$content .= '<textarea name="favnote" rows="6" cols="50"></textarea>';
223			$content .= '</div>';
224			$content .= '<input type="submit" value="' . I18N::translate('Add') . '">';
225			$content .= '</form></div>';
226		}
227
228		if ($template) {
229			if ($block) {
230				$class .= ' small_inner_block';
231			}
232
233			return Theme::theme()->formatBlock($id, $title, $class, $content);
234		} else {
235			return $content;
236		}
237	}
238
239	/**
240	 * Should this block load asynchronously using AJAX?
241	 *
242	 * Simple blocks are faster in-line, more comples ones
243	 * can be loaded later.
244	 *
245	 * @return bool
246	 */
247	public function loadAjax() {
248		return false;
249	}
250
251	/**
252	 * Can this block be shown on the user’s home page?
253	 *
254	 * @return bool
255	 */
256	public function isUserBlock() {
257		return false;
258	}
259
260	/**
261	 * Can this block be shown on the tree’s home page?
262	 *
263	 * @return bool
264	 */
265	public function isGedcomBlock() {
266		return true;
267	}
268
269	/**
270	 * An HTML form to edit block settings
271	 *
272	 * @param int $block_id
273	 */
274	public function configureBlock($block_id) {
275		if (Filter::postBool('save') && Filter::checkCsrf()) {
276			$this->setBlockSetting($block_id, 'block', Filter::postBool('block'));
277		}
278
279		$block = $this->getBlockSetting($block_id, 'block', '0');
280
281		echo '<tr><td class="descriptionbox wrap width33">';
282		echo /* I18N: label for a yes/no option */ I18N::translate('Add a scrollbar when block contents grow');
283		echo '</td><td class="optionbox">';
284		echo FunctionsEdit::editFieldYesNo('block', $block);
285		echo '</td></tr>';
286	}
287
288	/**
289	 * Delete a favorite from the database
290	 *
291	 * @param int $favorite_id
292	 *
293	 * @return bool
294	 */
295	public static function deleteFavorite($favorite_id) {
296		return (bool)
297			Database::prepare("DELETE FROM `##favorite` WHERE favorite_id=?")
298			->execute(array($favorite_id));
299	}
300
301	/**
302	 * Store a new favorite in the database
303	 *
304	 * @param $favorite
305	 *
306	 * @return bool
307	 */
308	public static function addFavorite($favorite) {
309		// -- make sure a favorite is added
310		if (empty($favorite['gid']) && empty($favorite['url'])) {
311			return false;
312		}
313
314		//-- make sure this is not a duplicate entry
315		$sql = "SELECT SQL_NO_CACHE 1 FROM `##favorite` WHERE";
316		if (!empty($favorite['gid'])) {
317			$sql .= " xref=?";
318			$vars = array($favorite['gid']);
319		} else {
320			$sql .= " url=?";
321			$vars = array($favorite['url']);
322		}
323		$sql .= " AND gedcom_id=?";
324		$vars[] = $favorite['gedcom_id'];
325		if ($favorite['user_id']) {
326			$sql .= " AND user_id=?";
327			$vars[] = $favorite['user_id'];
328		} else {
329			$sql .= " AND user_id IS NULL";
330		}
331
332		if (Database::prepare($sql)->execute($vars)->fetchOne()) {
333			return false;
334		}
335
336		//-- add the favorite to the database
337		return (bool)
338			Database::prepare("INSERT INTO `##favorite` (user_id, gedcom_id, xref, favorite_type, url, title, note) VALUES (? ,? ,? ,? ,? ,? ,?)")
339				->execute(array($favorite['user_id'], $favorite['gedcom_id'], $favorite['gid'], $favorite['type'], $favorite['url'], $favorite['title'], $favorite['note']));
340	}
341
342	/**
343	 * Get favorites for a user or family tree
344	 *
345	 * @param int $gedcom_id
346	 *
347	 * @return string[][]
348	 */
349	public static function getFavorites($gedcom_id) {
350		return
351			Database::prepare(
352				"SELECT SQL_CACHE favorite_id AS id, user_id, gedcom_id, xref AS gid, favorite_type AS type, title, note, url" .
353				" FROM `##favorite` WHERE gedcom_id=? AND user_id IS NULL")
354			->execute(array($gedcom_id))
355			->fetchAll(PDO::FETCH_ASSOC);
356	}
357
358	/**
359	 * Make sure the database structure is up-to-date.
360	 */
361	protected static function updateSchema() {
362		// Create tables, if not already present
363		try {
364			Database::updateSchema(WT_ROOT . WT_MODULES_DIR . 'gedcom_favorites/db_schema/', 'FV_SCHEMA_VERSION', 4);
365		} catch (PDOException $ex) {
366			// The schema update scripts should never fail.  If they do, there is no clean recovery.
367			FlashMessages::addMessage($ex->getMessage(), 'danger');
368			header('Location: ' . WT_BASE_URL . 'site-unavailable.php');
369			throw $ex;
370		}
371	}
372}
373