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