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