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