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