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