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 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Module; 21 22use Fisharebest\Localization\Locale\LocaleInterface; 23use Fisharebest\Webtrees\Http\RequestHandlers\ControlPanel; 24use Fisharebest\Webtrees\I18N; 25use Fisharebest\Webtrees\Menu; 26use Fisharebest\Webtrees\Services\HtmlService; 27use Fisharebest\Webtrees\Services\TreeService; 28use Fisharebest\Webtrees\Tree; 29use Illuminate\Database\Capsule\Manager as DB; 30use Illuminate\Database\Query\Builder; 31use Illuminate\Support\Collection; 32use Psr\Http\Message\ResponseInterface; 33use Psr\Http\Message\ServerRequestInterface; 34use stdClass; 35 36use function assert; 37use function redirect; 38use function route; 39 40/** 41 * Class FrequentlyAskedQuestionsModule 42 */ 43class FrequentlyAskedQuestionsModule extends AbstractModule implements ModuleConfigInterface, ModuleMenuInterface 44{ 45 use ModuleConfigTrait; 46 use ModuleMenuTrait; 47 48 /** @var HtmlService */ 49 private $html_service; 50 51 /** @var TreeService */ 52 private $tree_service; 53 54 /** 55 * BatchUpdateModule constructor. 56 * 57 * @param HtmlService $html_service 58 * @param TreeService $tree_service 59 */ 60 public function __construct(HtmlService $html_service, TreeService $tree_service) 61 { 62 $this->html_service = $html_service; 63 $this->tree_service = $tree_service; 64 } 65 66 /** 67 * How should this module be identified in the control panel, etc.? 68 * 69 * @return string 70 */ 71 public function title(): string 72 { 73 /* I18N: Name of a module. Abbreviation for “Frequently Asked Questions” */ 74 return I18N::translate('FAQ'); 75 } 76 77 /** 78 * A sentence describing what this module does. 79 * 80 * @return string 81 */ 82 public function description(): string 83 { 84 /* I18N: Description of the “FAQ” module */ 85 return I18N::translate('A list of frequently asked questions and answers.'); 86 } 87 88 /** 89 * The default position for this menu. It can be changed in the control panel. 90 * 91 * @return int 92 */ 93 public function defaultMenuOrder(): int 94 { 95 return 8; 96 } 97 98 /** 99 * A menu, to be added to the main application menu. 100 * 101 * @param Tree $tree 102 * 103 * @return Menu|null 104 */ 105 public function getMenu(Tree $tree): ?Menu 106 { 107 $locale = app(ServerRequestInterface::class)->getAttribute('locale'); 108 assert($locale instanceof LocaleInterface); 109 110 if ($this->faqsExist($tree, $locale->languageTag())) { 111 return new Menu($this->title(), route('module', [ 112 'module' => $this->name(), 113 'action' => 'Show', 114 'tree' => $tree->name(), 115 ]), 'menu-help'); 116 } 117 118 return null; 119 } 120 121 /** 122 * @param ServerRequestInterface $request 123 * 124 * @return ResponseInterface 125 */ 126 public function getAdminAction(ServerRequestInterface $request): ResponseInterface 127 { 128 $this->layout = 'layouts/administration'; 129 130 // This module can't run without a tree 131 $tree = $request->getAttribute('tree'); 132 133 if (!$tree instanceof Tree) { 134 $tree = $this->tree_service->all()->first(); 135 if ($tree instanceof Tree) { 136 return redirect(route('module', ['module' => $this->name(), 'action' => 'Admin', 'tree' => $tree->name()])); 137 } 138 139 return redirect(route(ControlPanel::class)); 140 } 141 142 $faqs = $this->faqsForTree($tree); 143 144 $min_block_order = DB::table('block') 145 ->where('module_name', '=', $this->name()) 146 ->where(static function (Builder $query) use ($tree): void { 147 $query 148 ->whereNull('gedcom_id') 149 ->orWhere('gedcom_id', '=', $tree->id()); 150 }) 151 ->min('block_order'); 152 153 $max_block_order = DB::table('block') 154 ->where('module_name', '=', $this->name()) 155 ->where(static function (Builder $query) use ($tree): void { 156 $query 157 ->whereNull('gedcom_id') 158 ->orWhere('gedcom_id', '=', $tree->id()); 159 }) 160 ->max('block_order'); 161 162 $title = I18N::translate('Frequently asked questions') . ' — ' . $tree->title(); 163 164 return $this->viewResponse('modules/faq/config', [ 165 'action' => route('module', ['module' => $this->name(), 'action' => 'Admin']), 166 'faqs' => $faqs, 167 'max_block_order' => $max_block_order, 168 'min_block_order' => $min_block_order, 169 'module' => $this->name(), 170 'title' => $title, 171 'tree' => $tree, 172 'tree_names' => $this->tree_service->titles(), 173 ]); 174 } 175 176 /** 177 * @param ServerRequestInterface $request 178 * 179 * @return ResponseInterface 180 */ 181 public function postAdminAction(ServerRequestInterface $request): ResponseInterface 182 { 183 return redirect(route('module', [ 184 'module' => $this->name(), 185 'action' => 'Admin', 186 'tree' => $request->getParsedBody()['tree'] ?? '', 187 ])); 188 } 189 190 /** 191 * @param ServerRequestInterface $request 192 * 193 * @return ResponseInterface 194 */ 195 public function postAdminDeleteAction(ServerRequestInterface $request): ResponseInterface 196 { 197 $block_id = (int) $request->getQueryParams()['block_id']; 198 199 DB::table('block_setting')->where('block_id', '=', $block_id)->delete(); 200 201 DB::table('block')->where('block_id', '=', $block_id)->delete(); 202 203 $url = route('module', [ 204 'module' => $this->name(), 205 'action' => 'Admin', 206 ]); 207 208 return redirect($url); 209 } 210 211 /** 212 * @param ServerRequestInterface $request 213 * 214 * @return ResponseInterface 215 */ 216 public function postAdminMoveDownAction(ServerRequestInterface $request): ResponseInterface 217 { 218 $block_id = (int) $request->getQueryParams()['block_id']; 219 220 $block_order = DB::table('block') 221 ->where('block_id', '=', $block_id) 222 ->value('block_order'); 223 224 $swap_block = DB::table('block') 225 ->where('module_name', '=', $this->name()) 226 ->where('block_order', '>', $block_order) 227 ->orderBy('block_order', 'asc') 228 ->first(); 229 230 if ($block_order !== null && $swap_block !== null) { 231 DB::table('block') 232 ->where('block_id', '=', $block_id) 233 ->update([ 234 'block_order' => $swap_block->block_order, 235 ]); 236 237 DB::table('block') 238 ->where('block_id', '=', $swap_block->block_id) 239 ->update([ 240 'block_order' => $block_order, 241 ]); 242 } 243 244 return response(); 245 } 246 247 /** 248 * @param ServerRequestInterface $request 249 * 250 * @return ResponseInterface 251 */ 252 public function postAdminMoveUpAction(ServerRequestInterface $request): ResponseInterface 253 { 254 $block_id = (int) $request->getQueryParams()['block_id']; 255 256 $block_order = DB::table('block') 257 ->where('block_id', '=', $block_id) 258 ->value('block_order'); 259 260 $swap_block = DB::table('block') 261 ->where('module_name', '=', $this->name()) 262 ->where('block_order', '<', $block_order) 263 ->orderBy('block_order', 'desc') 264 ->first(); 265 266 if ($block_order !== null && $swap_block !== null) { 267 DB::table('block') 268 ->where('block_id', '=', $block_id) 269 ->update([ 270 'block_order' => $swap_block->block_order, 271 ]); 272 273 DB::table('block') 274 ->where('block_id', '=', $swap_block->block_id) 275 ->update([ 276 'block_order' => $block_order, 277 ]); 278 } 279 280 return response(); 281 } 282 283 /** 284 * @param ServerRequestInterface $request 285 * 286 * @return ResponseInterface 287 */ 288 public function getAdminEditAction(ServerRequestInterface $request): ResponseInterface 289 { 290 $this->layout = 'layouts/administration'; 291 292 $block_id = (int) ($request->getQueryParams()['block_id'] ?? 0); 293 294 if ($block_id === 0) { 295 // Creating a new faq 296 $header = ''; 297 $body = ''; 298 $gedcom_id = null; 299 $block_order = 1 + (int) DB::table('block')->where('module_name', '=', $this->name())->max('block_order'); 300 301 $languages = []; 302 303 $title = I18N::translate('Add an FAQ'); 304 } else { 305 // Editing an existing faq 306 $header = $this->getBlockSetting($block_id, 'header'); 307 $body = $this->getBlockSetting($block_id, 'faqbody'); 308 $gedcom_id = DB::table('block')->where('block_id', '=', $block_id)->value('gedcom_id'); 309 $block_order = DB::table('block')->where('block_id', '=', $block_id)->value('block_order'); 310 311 $languages = explode(',', $this->getBlockSetting($block_id, 'languages')); 312 313 $title = I18N::translate('Edit the FAQ'); 314 } 315 316 $gedcom_ids = $this->tree_service->all() 317 ->mapWithKeys(static function (Tree $tree): array { 318 return [$tree->id() => $tree->title()]; 319 }) 320 ->all(); 321 322 $gedcom_ids = ['' => I18N::translate('All')] + $gedcom_ids; 323 324 return $this->viewResponse('modules/faq/edit', [ 325 'block_id' => $block_id, 326 'block_order' => $block_order, 327 'header' => $header, 328 'body' => $body, 329 'languages' => $languages, 330 'title' => $title, 331 'gedcom_id' => $gedcom_id, 332 'gedcom_ids' => $gedcom_ids, 333 ]); 334 } 335 336 /** 337 * @param ServerRequestInterface $request 338 * 339 * @return ResponseInterface 340 */ 341 public function postAdminEditAction(ServerRequestInterface $request): ResponseInterface 342 { 343 $block_id = (int) ($request->getQueryParams()['block_id'] ?? 0); 344 345 $params = $request->getParsedBody(); 346 347 $body = $params['body']; 348 $header = $params['header']; 349 $languages = $params['languages'] ?? []; 350 $gedcom_id = $params['gedcom_id']; 351 $block_order = $params['block_order']; 352 353 if ($gedcom_id === '') { 354 $gedcom_id = null; 355 } 356 357 $body = $this->html_service->sanitize($body); 358 $header = $this->html_service->sanitize($header); 359 360 if ($block_id !== 0) { 361 DB::table('block') 362 ->where('block_id', '=', $block_id) 363 ->update([ 364 'gedcom_id' => $gedcom_id, 365 'block_order' => $block_order, 366 ]); 367 } else { 368 DB::table('block')->insert([ 369 'gedcom_id' => $gedcom_id, 370 'module_name' => $this->name(), 371 'block_order' => $block_order, 372 ]); 373 374 $block_id = (int) DB::connection()->getPdo()->lastInsertId(); 375 } 376 377 $this->setBlockSetting($block_id, 'faqbody', $body); 378 $this->setBlockSetting($block_id, 'header', $header); 379 $this->setBlockSetting($block_id, 'languages', implode(',', $languages)); 380 381 $url = route('module', [ 382 'module' => $this->name(), 383 'action' => 'Admin', 384 ]); 385 386 return redirect($url); 387 } 388 389 /** 390 * @param ServerRequestInterface $request 391 * 392 * @return ResponseInterface 393 */ 394 public function getShowAction(ServerRequestInterface $request): ResponseInterface 395 { 396 $locale = $request->getAttribute('locale'); 397 assert($locale instanceof LocaleInterface); 398 399 $tree = $request->getAttribute('tree'); 400 assert($tree instanceof Tree); 401 402 // Filter foreign languages. 403 $faqs = $this->faqsForTree($tree) 404 ->filter(static function (stdClass $faq) use ($locale): bool { 405 return $faq->languages === '' || in_array($locale->languageTag(), explode(',', $faq->languages), true); 406 }); 407 408 return $this->viewResponse('modules/faq/show', [ 409 'faqs' => $faqs, 410 'title' => I18N::translate('Frequently asked questions'), 411 'tree' => $tree, 412 ]); 413 } 414 415 /** 416 * @param Tree $tree 417 * 418 * @return Collection 419 */ 420 private function faqsForTree(Tree $tree): Collection 421 { 422 return DB::table('block') 423 ->join('block_setting AS bs1', 'bs1.block_id', '=', 'block.block_id') 424 ->join('block_setting AS bs2', 'bs2.block_id', '=', 'block.block_id') 425 ->join('block_setting AS bs3', 'bs3.block_id', '=', 'block.block_id') 426 ->where('module_name', '=', $this->name()) 427 ->where('bs1.setting_name', '=', 'header') 428 ->where('bs2.setting_name', '=', 'faqbody') 429 ->where('bs3.setting_name', '=', 'languages') 430 ->where(static function (Builder $query) use ($tree): void { 431 $query 432 ->whereNull('gedcom_id') 433 ->orWhere('gedcom_id', '=', $tree->id()); 434 }) 435 ->orderBy('block_order') 436 ->select(['block.block_id', 'block_order', 'gedcom_id', 'bs1.setting_value AS header', 'bs2.setting_value AS faqbody', 'bs3.setting_value AS languages']) 437 ->get(); 438 } 439 440 /** 441 * @param Tree $tree 442 * @param string $language 443 * 444 * @return bool 445 */ 446 private function faqsExist(Tree $tree, string $language): bool 447 { 448 return DB::table('block') 449 ->join('block_setting', 'block_setting.block_id', '=', 'block.block_id') 450 ->where('module_name', '=', $this->name()) 451 ->where('setting_name', '=', 'languages') 452 ->where(static function (Builder $query) use ($tree): void { 453 $query 454 ->whereNull('gedcom_id') 455 ->orWhere('gedcom_id', '=', $tree->id()); 456 }) 457 ->select(['setting_value AS languages']) 458 ->get() 459 ->filter(static function (stdClass $faq) use ($language): bool { 460 return $faq->languages === '' || in_array($language, explode(',', $faq->languages), true); 461 }) 462 ->isNotEmpty(); 463 } 464} 465