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