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