1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2019 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17declare(strict_types=1); 18 19namespace Fisharebest\Webtrees\Module; 20 21use Fisharebest\Webtrees\Auth; 22use Fisharebest\Webtrees\Http\RequestHandlers\ModuleAction; 23use Fisharebest\Webtrees\I18N; 24use Fisharebest\Webtrees\Individual; 25use Fisharebest\Webtrees\Menu; 26use Fisharebest\Webtrees\Services\HtmlService; 27use Fisharebest\Webtrees\Tree; 28use Illuminate\Database\Capsule\Manager as DB; 29use Psr\Http\Message\ResponseInterface; 30use Psr\Http\Message\ServerRequestInterface; 31use stdClass; 32 33/** 34 * Class StoriesModule 35 */ 36class StoriesModule extends AbstractModule implements ModuleConfigInterface, ModuleMenuInterface, ModuleTabInterface 37{ 38 use ModuleTabTrait; 39 use ModuleConfigTrait; 40 use ModuleMenuTrait; 41 42 /** @var HtmlService */ 43 private $html_service; 44 45 /** 46 * HtmlBlockModule bootstrap. 47 * 48 * @param HtmlService $html_service 49 */ 50 public function boot(HtmlService $html_service) 51 { 52 $this->html_service = $html_service; 53 } 54 55 /** @var int The default access level for this module. It can be changed in the control panel. */ 56 protected $access_level = Auth::PRIV_HIDE; 57 58 /** 59 * A sentence describing what this module does. 60 * 61 * @return string 62 */ 63 public function description(): string 64 { 65 /* I18N: Description of the “Stories” module */ 66 return I18N::translate('Add narrative stories to individuals in the family tree.'); 67 } 68 69 /** 70 * The default position for this menu. It can be changed in the control panel. 71 * 72 * @return int 73 */ 74 public function defaultMenuOrder(): int 75 { 76 return 7; 77 } 78 79 /** 80 * The default position for this tab. It can be changed in the control panel. 81 * 82 * @return int 83 */ 84 public function defaultTabOrder(): int 85 { 86 return 9; 87 } 88 89 /** 90 * Generate the HTML content of this tab. 91 * 92 * @param Individual $individual 93 * 94 * @return string 95 */ 96 public function getTabContent(Individual $individual): string 97 { 98 return view('modules/stories/tab', [ 99 'is_admin' => Auth::isAdmin(), 100 'individual' => $individual, 101 'stories' => $this->getStoriesForIndividual($individual), 102 ]); 103 } 104 105 /** 106 * @param Individual $individual 107 * 108 * @return stdClass[] 109 */ 110 private function getStoriesForIndividual(Individual $individual): array 111 { 112 $block_ids = DB::table('block') 113 ->where('module_name', '=', $this->name()) 114 ->where('xref', '=', $individual->xref()) 115 ->where('gedcom_id', '=', $individual->tree()->id()) 116 ->pluck('block_id'); 117 118 $stories = []; 119 foreach ($block_ids as $block_id) { 120 $block_id = (int) $block_id; 121 122 // Only show this block for certain languages 123 $languages = $this->getBlockSetting($block_id, 'languages'); 124 if ($languages === '' || in_array(WT_LOCALE, explode(',', $languages), true)) { 125 $stories[] = (object) [ 126 'block_id' => $block_id, 127 'title' => $this->getBlockSetting($block_id, 'title'), 128 'story_body' => $this->getBlockSetting($block_id, 'story_body'), 129 ]; 130 } 131 } 132 133 return $stories; 134 } 135 136 /** 137 * Is this tab empty? If so, we don't always need to display it. 138 * 139 * @param Individual $individual 140 * 141 * @return bool 142 */ 143 public function hasTabContent(Individual $individual): bool 144 { 145 return Auth::isManager($individual->tree()) || !empty($this->getStoriesForIndividual($individual)); 146 } 147 148 /** 149 * A greyed out tab has no actual content, but may perhaps have 150 * options to create content. 151 * 152 * @param Individual $individual 153 * 154 * @return bool 155 */ 156 public function isGrayedOut(Individual $individual): bool 157 { 158 return !empty($this->getStoriesForIndividual($individual)); 159 } 160 161 /** 162 * Can this tab load asynchronously? 163 * 164 * @return bool 165 */ 166 public function canLoadAjax(): bool 167 { 168 return false; 169 } 170 171 /** 172 * A menu, to be added to the main application menu. 173 * 174 * @param Tree $tree 175 * 176 * @return Menu|null 177 */ 178 public function getMenu(Tree $tree): ?Menu 179 { 180 $menu = new Menu($this->title(), route('module', [ 181 'module' => $this->name(), 182 'action' => 'ShowList', 183 'ged' => $tree->name(), 184 ]), 'menu-story'); 185 186 return $menu; 187 } 188 189 /** 190 * How should this module be identified in the control panel, etc.? 191 * 192 * @return string 193 */ 194 public function title(): string 195 { 196 /* I18N: Name of a module */ 197 return I18N::translate('Stories'); 198 } 199 200 /** 201 * @param ServerRequestInterface $request 202 * 203 * @return ResponseInterface 204 */ 205 public function getAdminAction(ServerRequestInterface $request): ResponseInterface 206 { 207 $this->layout = 'layouts/administration'; 208 209 $tree = $request->getAttribute('tree'); 210 211 $stories = DB::table('block') 212 ->where('module_name', '=', $this->name()) 213 ->where('gedcom_id', '=', $tree->id()) 214 ->orderBy('xref') 215 ->get(); 216 217 foreach ($stories as $story) { 218 $block_id = (int) $story->block_id; 219 220 $story->individual = Individual::getInstance($story->xref, $tree); 221 $story->title = $this->getBlockSetting($block_id, 'title'); 222 $story->languages = $this->getBlockSetting($block_id, 'languages'); 223 } 224 225 return $this->viewResponse('modules/stories/config', [ 226 'module' => $this->name(), 227 'stories' => $stories, 228 'title' => $this->title() . ' — ' . $tree->title(), 229 'tree' => $tree, 230 'tree_names' => Tree::getNameList(), 231 ]); 232 } 233 234 /** 235 * @param ServerRequestInterface $request 236 * 237 * @return ResponseInterface 238 */ 239 public function getAdminEditAction(ServerRequestInterface $request): ResponseInterface 240 { 241 $this->layout = 'layouts/administration'; 242 243 $tree = $request->getAttribute('tree'); 244 $block_id = (int) ($request->getQueryParams()['block_id'] ?? 0); 245 246 if ($block_id === 0) { 247 // Creating a new story 248 $individual = null; 249 $story_title = ''; 250 $story_body = ''; 251 $languages = []; 252 253 $title = I18N::translate('Add a story') . ' — ' . e($tree->title()); 254 } else { 255 // Editing an existing story 256 $xref = (string) DB::table('block') 257 ->where('block_id', '=', $block_id) 258 ->value('xref'); 259 260 $individual = Individual::getInstance($xref, $tree); 261 $story_title = $this->getBlockSetting($block_id, 'title'); 262 $story_body = $this->getBlockSetting($block_id, 'story_body'); 263 $languages = explode(',', $this->getBlockSetting($block_id, 'languages')); 264 265 $title = I18N::translate('Edit the story') . ' — ' . e($tree->title()); 266 } 267 268 return $this->viewResponse('modules/stories/edit', [ 269 'block_id' => $block_id, 270 'languages' => $languages, 271 'story_body' => $story_body, 272 'story_title' => $story_title, 273 'title' => $title, 274 'tree' => $tree, 275 'individual' => $individual, 276 ]); 277 } 278 279 /** 280 * @param ServerRequestInterface $request 281 * 282 * @return ResponseInterface 283 */ 284 public function postAdminEditAction(ServerRequestInterface $request): ResponseInterface 285 { 286 $tree = $request->getAttribute('tree'); 287 $block_id = (int) ($request->getQueryParams()['block_id'] ?? 0); 288 289 $params = $request->getParsedBody(); 290 291 $xref = $params['xref']; 292 $story_body = $params['story_body']; 293 $story_title = $params['story_title']; 294 $languages = $params['languages'] ?? []; 295 296 $story_body = $this->html_service->sanitize($story_body); 297 $story_title = $this->html_service->sanitize($story_title); 298 299 if ($block_id !== 0) { 300 DB::table('block') 301 ->where('block_id', '=', $block_id) 302 ->update([ 303 'gedcom_id' => $tree->id(), 304 'xref' => $xref, 305 ]); 306 } else { 307 DB::table('block')->insert([ 308 'gedcom_id' => $tree->id(), 309 'xref' => $xref, 310 'module_name' => $this->name(), 311 'block_order' => 0, 312 ]); 313 314 $block_id = (int) DB::connection()->getPdo()->lastInsertId(); 315 } 316 317 $this->setBlockSetting($block_id, 'story_body', $story_body); 318 $this->setBlockSetting($block_id, 'title', $story_title); 319 $this->setBlockSetting($block_id, 'languages', implode(',', $languages)); 320 321 $url = route('module', [ 322 'module' => $this->name(), 323 'action' => 'Admin', 324 'ged' => $tree->name(), 325 ]); 326 327 return redirect($url); 328 } 329 330 /** 331 * @param ServerRequestInterface $request 332 * 333 * @return ResponseInterface 334 */ 335 public function postAdminDeleteAction(ServerRequestInterface $request): ResponseInterface 336 { 337 $tree = $request->getAttribute('tree'); 338 $block_id = $request->getQueryParams()['block_id']; 339 340 DB::table('block_setting') 341 ->where('block_id', '=', $block_id) 342 ->delete(); 343 344 DB::table('block') 345 ->where('block_id', '=', $block_id) 346 ->delete(); 347 348 $url = route('module', [ 349 'module' => $this->name(), 350 'action' => 'Admin', 351 'ged' => $tree->name(), 352 ]); 353 354 return redirect($url); 355 } 356 357 /** 358 * @param ServerRequestInterface $request 359 * 360 * @return ResponseInterface 361 */ 362 public function getShowListAction(ServerRequestInterface $request): ResponseInterface 363 { 364 $tree = $request->getAttribute('tree'); 365 366 $stories = DB::table('block') 367 ->where('module_name', '=', $this->name()) 368 ->where('gedcom_id', '=', $tree->id()) 369 ->get() 370 ->map(function (stdClass $story) use ($tree): stdClass { 371 $block_id = (int) $story->block_id; 372 373 $story->individual = Individual::getInstance($story->xref, $tree); 374 $story->title = $this->getBlockSetting($block_id, 'title'); 375 $story->languages = $this->getBlockSetting($block_id, 'languages'); 376 377 return $story; 378 })->filter(static function (stdClass $story): bool { 379 // Filter non-existant and private individuals. 380 return $story->individual instanceof Individual && $story->individual->canShow(); 381 })->filter(static function (stdClass $story): bool { 382 // Filter foreign languages. 383 return $story->languages === '' || in_array(WT_LOCALE, explode(',', $story->languages), true); 384 }); 385 386 return $this->viewResponse('modules/stories/list', [ 387 'stories' => $stories, 388 'title' => $this->title(), 389 ]); 390 } 391} 392