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