1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2018 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 */ 16namespace Fisharebest\Webtrees\Module; 17 18use Fisharebest\Webtrees\Auth; 19use Fisharebest\Webtrees\Bootstrap4; 20use Fisharebest\Webtrees\Controller\PageController; 21use Fisharebest\Webtrees\Database; 22use Fisharebest\Webtrees\Filter; 23use Fisharebest\Webtrees\Functions\FunctionsEdit; 24use Fisharebest\Webtrees\Html; 25use Fisharebest\Webtrees\I18N; 26use Fisharebest\Webtrees\Menu; 27use Fisharebest\Webtrees\Module; 28use Fisharebest\Webtrees\Tree; 29use Symfony\Component\HttpFoundation\RedirectResponse; 30use Symfony\Component\HttpFoundation\Request; 31use Symfony\Component\HttpFoundation\Response; 32 33/** 34 * Class FrequentlyAskedQuestionsModule 35 */ 36class FrequentlyAskedQuestionsModule extends AbstractModule implements ModuleMenuInterface, ModuleConfigInterface { 37 /** {@inheritdoc} */ 38 public function getTitle() { 39 return /* I18N: Name of a module. Abbreviation for “Frequently Asked Questions” */ 40 I18N::translate('FAQ'); 41 } 42 43 /** {@inheritdoc} */ 44 public function getDescription() { 45 return /* I18N: Description of the “FAQ” module */ 46 I18N::translate('A list of frequently asked questions and answers.'); 47 } 48 49 /** 50 * The URL to a page where the user can modify the configuration of this module. 51 * 52 * @return string 53 */ 54 public function getConfigLink() { 55 return route('module', ['module' => $this->getName(), 'action' => 'Admin']); 56 } 57 58 /** 59 * Provide a form to manage the FAQs. 60 */ 61 private function config() { 62 global $WT_TREE; 63 64 $controller = new PageController; 65 $controller 66 ->restrictAccess(Auth::isAdmin()) 67 ->setPageTitle(I18N::translate('Frequently asked questions')) 68 ->pageHeader(); 69 70 $faqs = Database::prepare( 71 "SELECT block_id, block_order, gedcom_id, bs1.setting_value AS header, bs2.setting_value AS faqbody" . 72 " FROM `##block` b" . 73 " JOIN `##block_setting` bs1 USING (block_id)" . 74 " JOIN `##block_setting` bs2 USING (block_id)" . 75 " WHERE module_name = :module_name" . 76 " AND bs1.setting_name = 'header'" . 77 " AND bs2.setting_name = 'faqbody'" . 78 " AND IFNULL(gedcom_id, :tree_id_1) = :tree_id_2" . 79 " ORDER BY block_order" 80 )->execute([ 81 'module_name' => $this->getName(), 82 'tree_id_1' => $WT_TREE->getTreeId(), 83 'tree_id_2' => $WT_TREE->getTreeId(), 84 ])->fetchAll(); 85 86 $min_block_order = Database::prepare( 87 "SELECT MIN(block_order) FROM `##block` WHERE module_name = 'faq' AND (gedcom_id = :tree_id OR gedcom_id IS NULL)" 88 )->execute([ 89 'tree_id' => $WT_TREE->getTreeId(), 90 ])->fetchOne(); 91 92 $max_block_order = Database::prepare( 93 "SELECT MAX(block_order) FROM `##block` WHERE module_name = 'faq' AND (gedcom_id = :tree_id OR gedcom_id IS NULL)" 94 )->execute([ 95 'tree_id' => $WT_TREE->getTreeId(), 96 ])->fetchOne(); 97 98 echo Bootstrap4::breadcrumbs([ 99 route('admin-control-panel') => I18N::translate('Control panel'), 100 route('admin-modules') => I18N::translate('Module administration'), 101 ], $controller->getPageTitle()); 102 ?> 103 104 <h1><?= $controller->getPageTitle() ?></h1> 105 <p> 106 <?= /* I18N: FAQ = “Frequently Asked Question” */ 107 I18N::translate('FAQs are lists of questions and answers, which allow you to explain the site’s rules, policies, and procedures to your visitors. Questions are typically concerned with privacy, copyright, user-accounts, unsuitable content, requirement for source-citations, etc.') ?> 108 <?= I18N::translate('You may use HTML to format the answer and to add links to other websites.') ?> 109 </p> 110 111 <p> 112 <form class="form form-inline"> 113 <label for="ged" class="sr-only"> 114 <?= I18N::translate('Family tree') ?> 115 </label> 116 <input type="hidden" name="mod" value="<?= $this->getName() ?>"> 117 <input type="hidden" name="mod_action" value="admin_config"> 118 <?= Bootstrap4::select(Tree::getNameList(), $WT_TREE->getName(), ['id' => 'ged', 'name' => 'ged']) ?> 119 <input type="submit" class="btn btn-primary" value="<?= I18N::translate('show') ?>"> 120 </form> 121 </p> 122 123 <p> 124 <a href="module.php?mod=<?= $this->getName() ?>&mod_action=admin_edit" class="btn btn-default"> 125 <i class="fas fa-plus"></i> 126 <?= /* I18N: FAQ = “Frequently Asked Question” */ 127 I18N::translate('Add an FAQ') ?> 128 </a> 129 </p> 130 131 <?php 132 echo '<table class="table table-bordered">'; 133 foreach ($faqs as $faq) { 134 // NOTE: Print the position of the current item 135 echo '<tr class="faq_edit_pos"><td>'; 136 echo I18N::translate('#%s', $faq->block_order + 1), ' '; 137 if ($faq->gedcom_id === null) { 138 echo I18N::translate('All'); 139 } else { 140 echo e($WT_TREE->getTitle()); 141 } 142 echo '</td>'; 143 // NOTE: Print the edit options of the current item 144 echo '<td>'; 145 if ($faq->block_order == $min_block_order) { 146 echo ' '; 147 } else { 148 echo '<a href="module.php?mod=', $this->getName(), '&mod_action=admin_moveup&block_id=', $faq->block_id, '"><i class="fas fa-arrow-up"></i></i> ', I18N::translate('Move up'), '</a>'; 149 } 150 echo '</td><td>'; 151 if ($faq->block_order == $max_block_order) { 152 echo ' '; 153 } else { 154 echo '<a href="module.php?mod=', $this->getName(), '&mod_action=admin_movedown&block_id=', $faq->block_id, '"><i class="fas fa-arrow-down"></i></i> ', I18N::translate('Move down'), '</a>'; 155 } 156 echo '</td><td>'; 157 echo '<a href="module.php?mod=', $this->getName(), '&mod_action=admin_edit&block_id=', $faq->block_id, '"><i class="fas fa-pencil-alt"></i> ', I18N::translate('Edit'), '</a>'; 158 echo '</td><td>'; 159 echo '<a href="module.php?mod=', $this->getName(), '&mod_action=admin_delete&block_id=', $faq->block_id, '" data-confirm="\', I18N::translate(\'Are you sure you want to delete “%s”?\', e($faq->header)), \'" onclick="return confirm(this.dataset.confirm);"><i class="fas fa-trash-alt"></i> ', I18N::translate('Delete'), '</a>'; 160 echo '</td></tr>'; 161 // NOTE: Print the title text of the current item 162 echo '<tr><td colspan="5">'; 163 echo '<div class="faq_edit_item">'; 164 echo '<div class="faq_edit_title">', $faq->header, '</div>'; 165 // NOTE: Print the body text of the current item 166 echo '<div class="faq_edit_content">', substr($faq->faqbody, 0, 1) == '<' ? $faq->faqbody : nl2br($faq->faqbody, false), '</div></div></td></tr>'; 167 } 168 echo '</table>'; 169 } 170 171 /** 172 * The user can re-order menus. Until they do, they are shown in this order. 173 * 174 * @return int 175 */ 176 public function defaultMenuOrder() { 177 return 40; 178 } 179 180 /** 181 * A menu, to be added to the main application menu. 182 * 183 * @param Tree $tree 184 * 185 * @return Menu|null 186 */ 187 public function getMenu(Tree $tree) { 188 $faqs = Database::prepare( 189 "SELECT block_id FROM `##block`" . 190 " JOIN `##block_setting` USING (block_id)" . 191 " WHERE module_name = :module_name AND IFNULL(gedcom_id, :tree_id_1) = :tree_id_2" . 192 " AND setting_name='languages' AND (setting_value LIKE CONCAT('%', :locale, '%') OR setting_value='')" 193 )->execute([ 194 'module_name' => $this->getName(), 195 'tree_id_1' => $tree->getTreeId(), 196 'tree_id_2' => $tree->getTreeId(), 197 'locale' => WT_LOCALE, 198 ])->fetchAll(); 199 200 if ($faqs) { 201 return new Menu($this->getTitle(), e(route('module', ['module' => 'faq', 'action' => 'Show', 'ged' => $tree->getName()])), 'menu-help'); 202 } else { 203 return null; 204 } 205 } 206 207 /** 208 * @param Request $request 209 * 210 * @return Response 211 */ 212 public function getAdminAction(Request $request): Response { 213 /** @var Tree $tree */ 214 $tree = $request->attributes->get('tree'); 215 216 $this->layout = 'layouts/administration'; 217 218 $faqs = Database::prepare( 219 "SELECT block_id, block_order, gedcom_id, bs1.setting_value AS header, bs2.setting_value AS faqbody" . 220 " FROM `##block` b" . 221 " JOIN `##block_setting` bs1 USING (block_id)" . 222 " JOIN `##block_setting` bs2 USING (block_id)" . 223 " WHERE module_name = :module_name" . 224 " AND bs1.setting_name = 'header'" . 225 " AND bs2.setting_name = 'faqbody'" . 226 " AND IFNULL(gedcom_id, :tree_id_1) = :tree_id_2" . 227 " ORDER BY block_order" 228 )->execute([ 229 'module_name' => $this->getName(), 230 'tree_id_1' => $tree->getTreeId(), 231 'tree_id_2' => $tree->getTreeId(), 232 ])->fetchAll(); 233 234 $min_block_order = Database::prepare( 235 "SELECT MIN(block_order) FROM `##block` WHERE module_name = 'faq' AND (gedcom_id = :tree_id OR gedcom_id IS NULL)" 236 )->execute([ 237 'tree_id' => $tree->getTreeId(), 238 ])->fetchOne(); 239 240 $max_block_order = Database::prepare( 241 "SELECT MAX(block_order) FROM `##block` WHERE module_name = 'faq' AND (gedcom_id = :tree_id OR gedcom_id IS NULL)" 242 )->execute([ 243 'tree_id' => $tree->getTreeId(), 244 ])->fetchOne(); 245 246 $title = I18N::translate('Frequently asked questions') . ' — ' . $tree->getTitle(); 247 248 return $this->viewResponse('modules/faq/config', [ 249 'faqs' => $faqs, 250 'max_block_order' => $max_block_order, 251 'min_block_order' => $min_block_order, 252 'title' => $title, 253 'tree' => $tree, 254 'tree_names' => Tree::getNameList(), 255 ]); 256 } 257 258 /** 259 * @param Request $request 260 * 261 * @return RedirectResponse 262 */ 263 public function postAdminDeleteAction(Request $request): RedirectResponse { 264 /** @var Tree $tree */ 265 $tree = $request->attributes->get('tree'); 266 267 $block_id = (int) $request->get('block_id'); 268 269 Database::prepare( 270 "DELETE FROM `##block_setting` WHERE block_id = :block_id" 271 )->execute(['block_id' => $block_id]); 272 273 Database::prepare( 274 "DELETE FROM `##block` WHERE block_id = :block_id" 275 )->execute(['block_id' => $block_id]); 276 277 278 $url = route('module', ['module' => 'faq', 'action' => 'Admin', 'ged' => $tree->getName()]); 279 280 return new RedirectResponse($url); 281 } 282 283 /** 284 * @param Request $request 285 * 286 * @return RedirectResponse 287 */ 288 public function postAdminMoveDownAction(Request $request): RedirectResponse { 289 /** @var Tree $tree */ 290 $tree = $request->attributes->get('tree'); 291 292 $block_id = (int) $request->get('block_id'); 293 294 $block_order = Database::prepare( 295 "SELECT block_order FROM `##block` WHERE block_id = :block_id" 296 )->execute([ 297 'block_id' => $block_id, 298 ])->fetchOne(); 299 300 $swap_block = Database::prepare( 301 "SELECT block_order, block_id" . 302 " FROM `##block`" . 303 " WHERE block_order=(" . 304 " SELECT MIN(block_order) FROM `##block` WHERE block_order > :block_order AND module_name = :module_name_1" . 305 " ) AND module_name = :module_name_2" . 306 " LIMIT 1" 307 )->execute([ 308 'block_order' => $block_order, 309 'module_name_1' => $this->getName(), 310 'module_name_2' => $this->getName(), 311 ])->fetchOneRow(); 312 313 if ($swap_block !== null) { 314 Database::prepare( 315 "UPDATE `##block` SET block_order = :block_order WHERE block_id = :block_id" 316 )->execute([ 317 'block_order' => $swap_block->block_order, 318 'block_id' => $block_id, 319 ]); 320 Database::prepare( 321 "UPDATE `##block` SET block_order = :block_order WHERE block_id = :block_id" 322 )->execute([ 323 'block_order' => $block_order, 324 'block_id' => $swap_block->block_id, 325 ]); 326 } 327 328 $url = route('module', ['module' => 'faq', 'action' => 'Admin', 'ged' => $tree->getName()]); 329 330 return new RedirectResponse($url); 331 } 332 333 /** 334 * @param Request $request 335 * 336 * @return RedirectResponse 337 */ 338 public function postAdminMoveUpAction(Request $request): RedirectResponse { 339 /** @var Tree $tree */ 340 $tree = $request->attributes->get('tree'); 341 342 $block_id = (int) $request->get('block_id'); 343 344 $block_order = Database::prepare( 345 "SELECT block_order FROM `##block` WHERE block_id = :block_id" 346 )->execute([ 347 'block_id' => $block_id, 348 ])->fetchOne(); 349 350 $swap_block = Database::prepare( 351 "SELECT block_order, block_id" . 352 " FROM `##block`" . 353 " WHERE block_order = (" . 354 " SELECT MAX(block_order) FROM `##block` WHERE block_order < :block_order AND module_name = :module_name_1" . 355 " ) AND module_name = :module_name_2" . 356 " LIMIT 1" 357 )->execute([ 358 'block_order' => $block_order, 359 'module_name_1' => $this->getName(), 360 'module_name_2' => $this->getName(), 361 ])->fetchOneRow(); 362 363 if ($swap_block !== null) { 364 Database::prepare( 365 "UPDATE `##block` SET block_order = :block_order WHERE block_id = :block_id" 366 )->execute([ 367 'block_order' => $swap_block->block_order, 368 'block_id' => $block_id, 369 ]); 370 Database::prepare( 371 "UPDATE `##block` SET block_order = :block_order WHERE block_id = :block_id" 372 )->execute([ 373 'block_order' => $block_order, 374 'block_id' => $swap_block->block_id, 375 ]); 376 } 377 378 $url = route('module', ['module' => 'faq', 'action' => 'Admin', 'ged' => $tree->getName()]); 379 380 return new RedirectResponse($url); 381 } 382 383 /** 384 * @param Request $request 385 * 386 * @return Response 387 */ 388 public function getAdminEditAction(Request $request): Response { 389 /** @var Tree $tree */ 390 $tree = $request->attributes->get('tree'); 391 392 $this->layout = 'layouts/administration'; 393 394 $block_id = (int) $request->get('block_id'); 395 396 if ($block_id === 0) { 397 // Creating a new faq 398 $header = ''; 399 $faqbody = ''; 400 $block_order = Database::prepare( 401 "SELECT IFNULL(MAX(block_order)+1, 0) FROM `##block` WHERE module_name = :module_name" 402 )->execute([ 403 'module_name' => $this->getName(), 404 ])->fetchOne(); 405 $languages = []; 406 407 $title = I18N::translate('Add an FAQ'); 408 } else { 409 // Editing an existing faq 410 $header = $this->getBlockSetting($block_id, 'header'); 411 $faqbody = $this->getBlockSetting($block_id, 'faqbody'); 412 $block_order = Database::prepare( 413 "SELECT block_order FROM `##block` WHERE block_id = :block_id" 414 )->execute(['block_id' => $block_id])->fetchOne(); 415 $languages = explode(',', $this->getBlockSetting($block_id, 'languages')); 416 417 $title = I18N::translate('Edit the FAQ'); 418 } 419 420 // @TODO enable CKEDITOR 421 422 return $this->viewResponse('modules/faq/edit', [ 423 'block_id' => $block_id, 424 'block_order' => $block_order, 425 'header' => $header, 426 'faqbody' => $faqbody, 427 'languages' => $languages, 428 'title' => $title, 429 'tree' => $tree, 430 'tree_names' => Tree::getNameList(), 431 ]); 432 } 433 434 /** 435 * @param Request $request 436 * 437 * @return RedirectResponse 438 */ 439 public function postAdminEditAction(Request $request): RedirectResponse { 440 /** @var Tree $tree */ 441 $tree = $request->attributes->get('tree'); 442 443 $block_id = (int) $request->get('block_id'); 444 $faqbody = $request->get('faqbody', ''); 445 $header = $request->get('header', ''); 446 $languages = $request->get('languages', []); 447 448 if ($block_id !== 0) { 449 Database::prepare( 450 "UPDATE `##block` SET gedcom_id = NULLIF(:tree_id, '0'), block_order = :block_order WHERE block_id = :block_id" 451 )->execute([ 452 'tree_id' => Filter::postInteger('gedcom_id'), 453 'block_order' => Filter::postInteger('block_order'), 454 'block_id' => $block_id, 455 ]); 456 } else { 457 Database::prepare( 458 "INSERT INTO `##block` (gedcom_id, module_name, block_order) VALUES (NULLIF(:tree_id, '0'), :module_name, :block_order)" 459 )->execute([ 460 'tree_id' => Filter::postInteger('gedcom_id'), 461 'module_name' => $this->getName(), 462 'block_order' => Filter::postInteger('block_order'), 463 ]); 464 465 $block_id = Database::getInstance()->lastInsertId(); 466 } 467 468 $this->setBlockSetting($block_id, 'faqbody', $faqbody); 469 $this->setBlockSetting($block_id, 'header', $header); 470 $this->setBlockSetting($block_id, 'languages', implode(',', $languages)); 471 472 $url = route('module', ['module' => 'faq', 'action' => 'Admin', 'ged' => $tree->getName()]); 473 474 return new RedirectResponse($url); 475 } 476 477 public function getShowAction(Request $request): Response { 478 /** @var Tree $tree */ 479 $tree = $request->attributes->get('tree'); 480 481 $faqs = Database::prepare( 482 "SELECT block_id, bs1.setting_value AS header, bs2.setting_value AS body, bs3.setting_value AS languages" . 483 " FROM `##block` b" . 484 " JOIN `##block_setting` bs1 USING (block_id)" . 485 " JOIN `##block_setting` bs2 USING (block_id)" . 486 " JOIN `##block_setting` bs3 USING (block_id)" . 487 " WHERE module_name = :module_name" . 488 " AND bs1.setting_name = 'header'" . 489 " AND bs2.setting_name = 'faqbody'" . 490 " AND bs3.setting_name = 'languages'" . 491 " AND IFNULL(gedcom_id, :tree_id_1) = :tree_id_2" . 492 " ORDER BY block_order" 493 )->execute([ 494 'module_name' => $this->getName(), 495 'tree_id_1' => $tree->getTreeId(), 496 'tree_id_2' => $tree->getTreeId(), 497 ])->fetchAll(); 498 499 echo '<h2 class="wt-page-title">', I18N::translate('Frequently asked questions'); 500 if (Auth::isManager($tree)) { 501 echo ' — <a href="module.php?mod=', $this->getName(), '&mod_action=admin_config">', I18N::translate('edit'), '</a>'; 502 } 503 echo '</h2>'; 504 $row_count = 0; 505 echo '<table class="faq">'; 506 // List of titles 507 foreach ($faqs as $id => $faq) { 508 if (!$faq->languages || in_array(WT_LOCALE, explode(',', $faq->languages))) { 509 $row_color = ($row_count % 2) ? 'odd' : 'even'; 510 // NOTE: Print the header of the current item 511 echo '<tr class="', $row_color, '"><td style="padding: 5px;">'; 512 echo '<a href="#faq', $id, '">', $faq->header, '</a>'; 513 echo '</td></tr>'; 514 $row_count++; 515 } 516 } 517 echo '</table><hr>'; 518 // Detailed entries 519 foreach ($faqs as $id => $faq) { 520 if (!$faq->languages || in_array(WT_LOCALE, explode(',', $faq->languages))) { 521 echo '<div class="faq_title" id="faq', $id, '">', $faq->header; 522 echo '<div class="faq_top faq_italic">'; 523 echo '<a href="#content">', I18N::translate('back to top'), '</a>'; 524 echo '</div>'; 525 echo '</div>'; 526 echo '<div class="faq_body">', substr($faq->body, 0, 1) == '<' ? $faq->body : nl2br($faq->body, false), '</div>'; 527 echo '<hr>'; 528 } 529 } 530 531 return $this->viewResponse('modules/faq/show', [ 532 'title' => I18N::translate('Frequently asked questions'), 533 ]); 534 } 535} 536