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