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