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