1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2016 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\Controller\PageController; 20use Fisharebest\Webtrees\Database; 21use Fisharebest\Webtrees\Filter; 22use Fisharebest\Webtrees\Functions\FunctionsEdit; 23use Fisharebest\Webtrees\I18N; 24use Fisharebest\Webtrees\Menu; 25use Fisharebest\Webtrees\Module; 26use Fisharebest\Webtrees\Tree; 27 28/** 29 * Class FrequentlyAskedQuestionsModule 30 */ 31class FrequentlyAskedQuestionsModule extends AbstractModule implements ModuleMenuInterface, ModuleConfigInterface { 32 /** {@inheritdoc} */ 33 public function getTitle() { 34 return /* I18N: Name of a module. Abbreviation for “Frequently Asked Questions” */ I18N::translate('FAQ'); 35 } 36 37 /** {@inheritdoc} */ 38 public function getDescription() { 39 return /* I18N: Description of the “FAQ” module */ I18N::translate('A list of frequently asked questions and answers.'); 40 } 41 42 /** 43 * This is a general purpose hook, allowing modules to respond to routes 44 * of the form module.php?mod=FOO&mod_action=BAR 45 * 46 * @param string $mod_action 47 */ 48 public function modAction($mod_action) { 49 switch ($mod_action) { 50 case 'admin_config': 51 $this->config(); 52 break; 53 case 'admin_delete': 54 if (Auth::isAdmin()) { 55 $this->delete(); 56 } 57 header('Location: ' . WT_BASE_URL . 'module.php?mod=faq&mod_action=admin_config'); 58 break; 59 case 'admin_edit': 60 $this->edit(); 61 break; 62 case 'admin_edit_save': 63 if (Auth::isAdmin()) { 64 $this->editSave(); 65 } 66 header('Location: ' . WT_BASE_URL . 'module.php?mod=faq&mod_action=admin_config'); 67 break; 68 case 'admin_movedown': 69 if (Auth::isAdmin()) { 70 $this->movedown(); 71 } 72 header('Location: ' . WT_BASE_URL . 'module.php?mod=faq&mod_action=admin_config'); 73 break; 74 case 'admin_moveup': 75 if (Auth::isAdmin()) { 76 $this->moveup(); 77 } 78 header('Location: ' . WT_BASE_URL . 'module.php?mod=faq&mod_action=admin_config'); 79 break; 80 case 'show': 81 $this->show(); 82 break; 83 default: 84 http_response_code(404); 85 } 86 } 87 88 /** {@inheritdoc} */ 89 public function getConfigLink() { 90 return 'module.php?mod=' . $this->getName() . '&mod_action=admin_config'; 91 } 92 93 /** 94 * Action from the configuration page 95 */ 96 private function editSave() { 97 if (Filter::checkCsrf()) { 98 $block_id = Filter::postInteger('block_id'); 99 if ($block_id) { 100 Database::prepare( 101 "UPDATE `##block` SET gedcom_id = NULLIF(:tree_id, '0'), block_order = :block_order WHERE block_id = :block_id" 102 )->execute(array( 103 'tree_id' => Filter::postInteger('gedcom_id'), 104 'block_order' => Filter::postInteger('block_order'), 105 'block_id' => $block_id, 106 )); 107 } else { 108 Database::prepare( 109 "INSERT INTO `##block` (gedcom_id, module_name, block_order) VALUES (NULLIF(:tree_id, '0'), :module_name, :block_order)" 110 )->execute(array( 111 'tree_id' => Filter::postInteger('gedcom_id'), 112 'module_name' => $this->getName(), 113 'block_order' => Filter::postInteger('block_order'), 114 )); 115 $block_id = Database::getInstance()->lastInsertId(); 116 } 117 $this->setBlockSetting($block_id, 'header', Filter::post('header')); 118 $this->setBlockSetting($block_id, 'faqbody', Filter::post('faqbody')); 119 120 $languages = Filter::postArray('lang'); 121 $this->setBlockSetting($block_id, 'languages', implode(',', $languages)); 122 } 123 } 124 125 /** 126 * Action from the configuration page 127 */ 128 private function edit() { 129 global $WT_TREE; 130 131 $controller = new PageController; 132 $controller->restrictAccess(Auth::isAdmin()); 133 134 $block_id = Filter::getInteger('block_id'); 135 if ($block_id) { 136 $controller->setPageTitle(I18N::translate('Edit the FAQ item')); 137 $header = $this->getBlockSetting($block_id, 'header'); 138 $faqbody = $this->getBlockSetting($block_id, 'faqbody'); 139 $block_order = Database::prepare( 140 "SELECT block_order FROM `##block` WHERE block_id = :block_id" 141 )->execute(array('block_id' => $block_id))->fetchOne(); 142 $gedcom_id = Database::prepare( 143 "SELECT gedcom_id FROM `##block` WHERE block_id = :block_id" 144 )->execute(array('block_id' => $block_id))->fetchOne(); 145 } else { 146 $controller->setPageTitle(I18N::translate('Add an FAQ item')); 147 $header = ''; 148 $faqbody = ''; 149 $block_order = Database::prepare( 150 "SELECT IFNULL(MAX(block_order)+1, 0) FROM `##block` WHERE module_name = :module_name" 151 )->execute(array('module_name' => $this->getName()))->fetchOne(); 152 $gedcom_id = $WT_TREE->getTreeId(); 153 } 154 $controller->pageHeader(); 155 if (Module::getModuleByName('ckeditor')) { 156 CkeditorModule::enableEditor($controller); 157 } 158 159 ?> 160 <ol class="breadcrumb small"> 161 <li><a href="admin.php"><?php echo I18N::translate('Control panel'); ?></a></li> 162 <li><a href="admin_modules.php"><?php echo I18N::translate('Module administration'); ?></a></li> 163 <li><a href="module.php?mod=<?php echo $this->getName(); ?>&mod_action=admin_config"><?php echo I18N::translate('Frequently asked questions'); ?></a> 164 </li> 165 <li class="active"><?php echo $controller->getPageTitle(); ?></li> 166 </ol> 167 <h1><?php echo $controller->getPageTitle(); ?></h1> 168 169 <form name="faq" class="form-horizontal" method="post" action="module.php?mod=<?php echo $this->getName(); ?>&mod_action=admin_edit_save"> 170 <?php echo Filter::getCsrf(); ?> 171 <input type="hidden" name="block_id" value="<?php echo $block_id; ?>"> 172 173 <div class="form-group"> 174 <label for="header" class="col-sm-3 control-label"> 175 <?php echo 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="<?php echo Filter::escapeHtml($header); ?>"> 181 </div> 182 </div> 183 184 <div class="form-group"> 185 <label for="faqbody" class="col-sm-3 control-label"> 186 <?php echo I18N::translate('Answer'); ?> 187 </label> 188 189 <div class="col-sm-9"> 190 <textarea name="faqbody" id="faqbody" class="form-control html-edit" 191 rows="10"><?php echo Filter::escapeHtml($faqbody); ?></textarea> 192 </div> 193 </div> 194 195 <div class="form-group"> 196 <label for="xref" class="col-sm-3 control-label"> 197 <?php echo /* I18N: Label for a configuration option */ I18N::translate('Show this block for which languages'); ?> 198 </label> 199 200 <div class="col-sm-9"> 201 <?php echo FunctionsEdit::editLanguageCheckboxes('lang', explode(',', $this->getBlockSetting($block_id, 'languages'))); ?> 202 </div> 203 </div> 204 205 <div class="form-group"> 206 <label for="block_order" class="col-sm-3 control-label"> 207 <?php echo I18N::translate('FAQ position'); ?> 208 </label> 209 210 <div class="col-sm-9"> 211 <input type="text" name="block_order" id="block_order" class="form-control" value="<?php echo $block_order; ?>"> 212 </div> 213 </div> 214 215 <div class="form-group"> 216 <label for="gedcom_id" class="col-sm-3 control-label"> 217 <?php echo I18N::translate('FAQ visibility'); ?> 218 </label> 219 220 <div class="col-sm-9"> 221 <?php echo FunctionsEdit::selectEditControl('gedcom_id', Tree::getIdList(), I18N::translate('All'), $gedcom_id, 'class="form-control"'); ?> 222 <p class="small text-muted"> 223 <?php echo I18N::translate('A FAQ item 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="form-group"> 229 <div class="col-sm-offset-3 col-sm-9"> 230 <button type="submit" class="btn btn-primary"> 231 <i class="fa fa-check"></i> 232 <?php echo I18N::translate('save'); ?> 233 </button> 234 </div> 235 </div> 236 237 </form> 238 <?php 239 } 240 241 /** 242 * Respond to a request to delete a 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(array('block_id' => $block_id)); 250 251 Database::prepare( 252 "DELETE FROM `##block` WHERE block_id = :block_id" 253 )->execute(array('block_id' => $block_id)); 254 } 255 256 /** 257 * Respond to a request to move a 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(array('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(array( 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(array( 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(array( 288 'block_order' => $block_order, 289 'block_id' => $swap_block->block_id, 290 )); 291 } 292 } 293 294 /** 295 * Respond to a request to move a 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(array( 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(array( 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(array( 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(array( 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(array( 358 'module_name' => $this->getName(), 359 'tree_id_1' => $WT_TREE->getTreeId(), 360 'tree_id_2' => $WT_TREE->getTreeId(), 361 ))->fetchAll(); 362 363 // Define your colors for the alternating rows 364 echo '<h2 class="center">', I18N::translate('Frequently asked questions'), '</h2>'; 365 // Instructions 366 echo '<div class="faq_italic">', I18N::translate('Click on a title to go straight to it, or scroll down to read them all.'); 367 if (Auth::isManager($WT_TREE)) { 368 echo '<div class="faq_edit"><a href="module.php?mod=', $this->getName(), '&mod_action=admin_config">', I18N::translate('Click here to add, edit, or delete'), '</a></div>'; 369 } 370 echo '</div>'; 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(array( 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(array( 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(array( 436 'tree_id' => $WT_TREE->getTreeId(), 437 ))->fetchOne(); 438 439 ?> 440 <ol class="breadcrumb small"> 441 <li><a href="admin.php"><?php echo I18N::translate('Control panel'); ?></a></li> 442 <li><a href="admin_modules.php"><?php echo I18N::translate('Module administration'); ?></a></li> 443 <li class="active"><?php echo $controller->getPageTitle(); ?></li> 444 </ol> 445 <h2><?php echo $controller->getPageTitle(); ?></h2> 446 <p> 447 <?php echo 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 <?php echo I18N::translate('You may use HTML to format the answer and to add links to other websites.'); ?> 449 </p> 450 <form class="form form-inline"> 451 <label for="ged" class="sr-only"> 452 <?php echo I18N::translate('Family tree'); ?> 453 </label> 454 <input type="hidden" name="mod" value="<?php echo $this->getName(); ?>"> 455 <input type="hidden" name="mod_action" value="admin_config"> 456 <?php echo FunctionsEdit::selectEditControl('ged', Tree::getNameList(), null, $WT_TREE->getName(), 'class="form-control"'); ?> 457 <input type="submit" class="btn btn-primary" value="<?php echo I18N::translate('show'); ?>"> 458 </form> 459 460 <p> 461 <a href="module.php?mod=<?php echo $this->getName(); ?>&mod_action=admin_edit" class="btn btn-default"> 462 <i class="fa fa-plus"></i> 463 <?php echo I18N::translate('Add an FAQ item'); ?> 464 </a> 465 </p> 466 467 <?php 468 echo '<table class="table table-bordered">'; 469 if (empty($faqs)) { 470 echo '<tr><td class="error center" colspan="5">', I18N::translate('The FAQ list is empty.'), '</td></tr></table>'; 471 } else { 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 $WT_TREE->getTitleHtml(); 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="fa 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="fa 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="fa fa-pencil"></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, '" 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>'; 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 /** 512 * The user can re-order menus. Until they do, they are shown in this order. 513 * 514 * @return int 515 */ 516 public function defaultMenuOrder() { 517 return 40; 518 } 519 520 /** 521 * A menu, to be added to the main application menu. 522 * 523 * @return Menu|null 524 */ 525 public function getMenu() { 526 global $WT_TREE; 527 528 $faqs = Database::prepare( 529 "SELECT block_id FROM `##block`" . 530 " JOIN `##block_setting` USING (block_id)" . 531 " WHERE module_name = :module_name AND IFNULL(gedcom_id, :tree_id_1) = :tree_id_2" . 532 " AND setting_name='languages' AND (setting_value LIKE CONCAT('%', :locale, '%') OR setting_value='')" 533 )->execute(array( 534 'module_name' => $this->getName(), 535 'tree_id_1' => $WT_TREE->getTreeId(), 536 'tree_id_2' => $WT_TREE->getTreeId(), 537 'locale' => WT_LOCALE, 538 ))->fetchAll(); 539 540 if ($faqs) { 541 return new Menu(I18N::translate('FAQ'), 'module.php?mod=faq&mod_action=show', 'menu-help'); 542 } else { 543 return null; 544 } 545 546 } 547} 548