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 * The user can re-order menus. Until they do, they are shown in this order. 60 * 61 * @return int 62 */ 63 public function defaultMenuOrder() { 64 return 40; 65 } 66 67 /** 68 * A menu, to be added to the main application menu. 69 * 70 * @param Tree $tree 71 * 72 * @return Menu|null 73 */ 74 public function getMenu(Tree $tree) { 75 $faqs = Database::prepare( 76 "SELECT block_id FROM `##block`" . 77 " JOIN `##block_setting` USING (block_id)" . 78 " WHERE module_name = :module_name AND IFNULL(gedcom_id, :tree_id_1) = :tree_id_2" . 79 " AND setting_name='languages' AND (setting_value LIKE CONCAT('%', :locale, '%') OR setting_value='')" 80 )->execute([ 81 'module_name' => $this->getName(), 82 'tree_id_1' => $tree->getTreeId(), 83 'tree_id_2' => $tree->getTreeId(), 84 'locale' => WT_LOCALE, 85 ])->fetchAll(); 86 87 if ($faqs) { 88 return new Menu($this->getTitle(), e(route('module', ['module' => 'faq', 'action' => 'Show', 'ged' => $tree->getName()])), 'menu-help'); 89 } else { 90 return null; 91 } 92 } 93 94 /** 95 * @param Request $request 96 * 97 * @return Response 98 */ 99 public function getAdminAction(Request $request): Response { 100 /** @var Tree $tree */ 101 $tree = $request->attributes->get('tree'); 102 103 $this->layout = 'layouts/administration'; 104 105 $faqs = Database::prepare( 106 "SELECT block_id, block_order, gedcom_id, bs1.setting_value AS header, bs2.setting_value AS faqbody" . 107 " FROM `##block` b" . 108 " JOIN `##block_setting` bs1 USING (block_id)" . 109 " JOIN `##block_setting` bs2 USING (block_id)" . 110 " WHERE module_name = :module_name" . 111 " AND bs1.setting_name = 'header'" . 112 " AND bs2.setting_name = 'faqbody'" . 113 " AND IFNULL(gedcom_id, :tree_id_1) = :tree_id_2" . 114 " ORDER BY block_order" 115 )->execute([ 116 'module_name' => $this->getName(), 117 'tree_id_1' => $tree->getTreeId(), 118 'tree_id_2' => $tree->getTreeId(), 119 ])->fetchAll(); 120 121 $min_block_order = Database::prepare( 122 "SELECT MIN(block_order) FROM `##block` WHERE module_name = 'faq' AND (gedcom_id = :tree_id OR gedcom_id IS NULL)" 123 )->execute([ 124 'tree_id' => $tree->getTreeId(), 125 ])->fetchOne(); 126 127 $max_block_order = Database::prepare( 128 "SELECT MAX(block_order) FROM `##block` WHERE module_name = 'faq' AND (gedcom_id = :tree_id OR gedcom_id IS NULL)" 129 )->execute([ 130 'tree_id' => $tree->getTreeId(), 131 ])->fetchOne(); 132 133 $title = I18N::translate('Frequently asked questions') . ' — ' . $tree->getTitle(); 134 135 return $this->viewResponse('modules/faq/config', [ 136 'faqs' => $faqs, 137 'max_block_order' => $max_block_order, 138 'min_block_order' => $min_block_order, 139 'title' => $title, 140 'tree' => $tree, 141 'tree_names' => Tree::getNameList(), 142 ]); 143 } 144 145 /** 146 * @param Request $request 147 * 148 * @return RedirectResponse 149 */ 150 public function postAdminDeleteAction(Request $request): RedirectResponse { 151 /** @var Tree $tree */ 152 $tree = $request->attributes->get('tree'); 153 154 $block_id = (int) $request->get('block_id'); 155 156 Database::prepare( 157 "DELETE FROM `##block_setting` WHERE block_id = :block_id" 158 )->execute(['block_id' => $block_id]); 159 160 Database::prepare( 161 "DELETE FROM `##block` WHERE block_id = :block_id" 162 )->execute(['block_id' => $block_id]); 163 164 165 $url = route('module', ['module' => 'faq', 'action' => 'Admin', 'ged' => $tree->getName()]); 166 167 return new RedirectResponse($url); 168 } 169 170 /** 171 * @param Request $request 172 * 173 * @return RedirectResponse 174 */ 175 public function postAdminMoveDownAction(Request $request): RedirectResponse { 176 /** @var Tree $tree */ 177 $tree = $request->attributes->get('tree'); 178 179 $block_id = (int) $request->get('block_id'); 180 181 $block_order = Database::prepare( 182 "SELECT block_order FROM `##block` WHERE block_id = :block_id" 183 )->execute([ 184 'block_id' => $block_id, 185 ])->fetchOne(); 186 187 $swap_block = Database::prepare( 188 "SELECT block_order, block_id" . 189 " FROM `##block`" . 190 " WHERE block_order=(" . 191 " SELECT MIN(block_order) FROM `##block` WHERE block_order > :block_order AND module_name = :module_name_1" . 192 " ) AND module_name = :module_name_2" . 193 " LIMIT 1" 194 )->execute([ 195 'block_order' => $block_order, 196 'module_name_1' => $this->getName(), 197 'module_name_2' => $this->getName(), 198 ])->fetchOneRow(); 199 200 if ($swap_block !== null) { 201 Database::prepare( 202 "UPDATE `##block` SET block_order = :block_order WHERE block_id = :block_id" 203 )->execute([ 204 'block_order' => $swap_block->block_order, 205 'block_id' => $block_id, 206 ]); 207 Database::prepare( 208 "UPDATE `##block` SET block_order = :block_order WHERE block_id = :block_id" 209 )->execute([ 210 'block_order' => $block_order, 211 'block_id' => $swap_block->block_id, 212 ]); 213 } 214 215 $url = route('module', ['module' => 'faq', 'action' => 'Admin', 'ged' => $tree->getName()]); 216 217 return new RedirectResponse($url); 218 } 219 220 /** 221 * @param Request $request 222 * 223 * @return RedirectResponse 224 */ 225 public function postAdminMoveUpAction(Request $request): RedirectResponse { 226 /** @var Tree $tree */ 227 $tree = $request->attributes->get('tree'); 228 229 $block_id = (int) $request->get('block_id'); 230 231 $block_order = Database::prepare( 232 "SELECT block_order FROM `##block` WHERE block_id = :block_id" 233 )->execute([ 234 'block_id' => $block_id, 235 ])->fetchOne(); 236 237 $swap_block = Database::prepare( 238 "SELECT block_order, block_id" . 239 " FROM `##block`" . 240 " WHERE block_order = (" . 241 " SELECT MAX(block_order) FROM `##block` WHERE block_order < :block_order AND module_name = :module_name_1" . 242 " ) AND module_name = :module_name_2" . 243 " LIMIT 1" 244 )->execute([ 245 'block_order' => $block_order, 246 'module_name_1' => $this->getName(), 247 'module_name_2' => $this->getName(), 248 ])->fetchOneRow(); 249 250 if ($swap_block !== null) { 251 Database::prepare( 252 "UPDATE `##block` SET block_order = :block_order WHERE block_id = :block_id" 253 )->execute([ 254 'block_order' => $swap_block->block_order, 255 'block_id' => $block_id, 256 ]); 257 Database::prepare( 258 "UPDATE `##block` SET block_order = :block_order WHERE block_id = :block_id" 259 )->execute([ 260 'block_order' => $block_order, 261 'block_id' => $swap_block->block_id, 262 ]); 263 } 264 265 $url = route('module', ['module' => 'faq', 'action' => 'Admin', 'ged' => $tree->getName()]); 266 267 return new RedirectResponse($url); 268 } 269 270 /** 271 * @param Request $request 272 * 273 * @return Response 274 */ 275 public function getAdminEditAction(Request $request): Response { 276 /** @var Tree $tree */ 277 $tree = $request->attributes->get('tree'); 278 279 $this->layout = 'layouts/administration'; 280 281 $block_id = (int) $request->get('block_id'); 282 283 if ($block_id === 0) { 284 // Creating a new faq 285 $header = ''; 286 $faqbody = ''; 287 $block_order = Database::prepare( 288 "SELECT IFNULL(MAX(block_order)+1, 0) FROM `##block` WHERE module_name = :module_name" 289 )->execute([ 290 'module_name' => $this->getName(), 291 ])->fetchOne(); 292 $languages = []; 293 294 $title = I18N::translate('Add an FAQ'); 295 } else { 296 // Editing an existing faq 297 $header = $this->getBlockSetting($block_id, 'header'); 298 $faqbody = $this->getBlockSetting($block_id, 'faqbody'); 299 $block_order = Database::prepare( 300 "SELECT block_order FROM `##block` WHERE block_id = :block_id" 301 )->execute(['block_id' => $block_id])->fetchOne(); 302 $languages = explode(',', $this->getBlockSetting($block_id, 'languages')); 303 304 $title = I18N::translate('Edit the FAQ'); 305 } 306 307 // @TODO enable CKEDITOR 308 309 return $this->viewResponse('modules/faq/edit', [ 310 'block_id' => $block_id, 311 'block_order' => $block_order, 312 'header' => $header, 313 'faqbody' => $faqbody, 314 'languages' => $languages, 315 'title' => $title, 316 'tree' => $tree, 317 'tree_names' => Tree::getNameList(), 318 ]); 319 } 320 321 /** 322 * @param Request $request 323 * 324 * @return RedirectResponse 325 */ 326 public function postAdminEditAction(Request $request): RedirectResponse { 327 /** @var Tree $tree */ 328 $tree = $request->attributes->get('tree'); 329 330 $block_id = (int) $request->get('block_id'); 331 $faqbody = $request->get('faqbody', ''); 332 $header = $request->get('header', ''); 333 $languages = $request->get('languages', []); 334 335 if ($block_id !== 0) { 336 Database::prepare( 337 "UPDATE `##block` SET gedcom_id = NULLIF(:tree_id, '0'), block_order = :block_order WHERE block_id = :block_id" 338 )->execute([ 339 'tree_id' => Filter::postInteger('gedcom_id'), 340 'block_order' => Filter::postInteger('block_order'), 341 'block_id' => $block_id, 342 ]); 343 } else { 344 Database::prepare( 345 "INSERT INTO `##block` (gedcom_id, module_name, block_order) VALUES (NULLIF(:tree_id, '0'), :module_name, :block_order)" 346 )->execute([ 347 'tree_id' => Filter::postInteger('gedcom_id'), 348 'module_name' => $this->getName(), 349 'block_order' => Filter::postInteger('block_order'), 350 ]); 351 352 $block_id = Database::getInstance()->lastInsertId(); 353 } 354 355 $this->setBlockSetting($block_id, 'faqbody', $faqbody); 356 $this->setBlockSetting($block_id, 'header', $header); 357 $this->setBlockSetting($block_id, 'languages', implode(',', $languages)); 358 359 $url = route('module', ['module' => 'faq', 'action' => 'Admin', 'ged' => $tree->getName()]); 360 361 return new RedirectResponse($url); 362 } 363 364 /** 365 * @param Request $request 366 * 367 * @return Response 368 */ 369 public function getShowAction(Request $request): Response { 370 /** @var Tree $tree */ 371 $tree = $request->attributes->get('tree'); 372 373 $faqs = Database::prepare( 374 "SELECT block_id, bs1.setting_value AS header, bs2.setting_value AS body, bs3.setting_value AS languages" . 375 " FROM `##block` b" . 376 " JOIN `##block_setting` bs1 USING (block_id)" . 377 " JOIN `##block_setting` bs2 USING (block_id)" . 378 " JOIN `##block_setting` bs3 USING (block_id)" . 379 " WHERE module_name = :module_name" . 380 " AND bs1.setting_name = 'header'" . 381 " AND bs2.setting_name = 'faqbody'" . 382 " AND bs3.setting_name = 'languages'" . 383 " AND IFNULL(gedcom_id, :tree_id_1) = :tree_id_2" . 384 " ORDER BY block_order" 385 )->execute([ 386 'module_name' => $this->getName(), 387 'tree_id_1' => $tree->getTreeId(), 388 'tree_id_2' => $tree->getTreeId(), 389 ])->fetchAll(); 390 391 echo '<h2 class="wt-page-title">', I18N::translate('Frequently asked questions'); 392 if (Auth::isManager($tree)) { 393 echo ' — <a href="module.php?mod=', $this->getName(), '&mod_action=admin_config">', I18N::translate('edit'), '</a>'; 394 } 395 echo '</h2>'; 396 $row_count = 0; 397 echo '<table class="faq">'; 398 // List of titles 399 foreach ($faqs as $id => $faq) { 400 if (!$faq->languages || in_array(WT_LOCALE, explode(',', $faq->languages))) { 401 $row_color = ($row_count % 2) ? 'odd' : 'even'; 402 // NOTE: Print the header of the current item 403 echo '<tr class="', $row_color, '"><td style="padding: 5px;">'; 404 echo '<a href="#faq', $id, '">', $faq->header, '</a>'; 405 echo '</td></tr>'; 406 $row_count++; 407 } 408 } 409 echo '</table><hr>'; 410 // Detailed entries 411 foreach ($faqs as $id => $faq) { 412 if (!$faq->languages || in_array(WT_LOCALE, explode(',', $faq->languages))) { 413 echo '<div class="faq_title" id="faq', $id, '">', $faq->header; 414 echo '<div class="faq_top faq_italic">'; 415 echo '<a href="#content">', I18N::translate('back to top'), '</a>'; 416 echo '</div>'; 417 echo '</div>'; 418 echo '<div class="faq_body">', substr($faq->body, 0, 1) == '<' ? $faq->body : nl2br($faq->body, false), '</div>'; 419 echo '<hr>'; 420 } 421 } 422 423 return $this->viewResponse('modules/faq/show', [ 424 'title' => I18N::translate('Frequently asked questions'), 425 ]); 426 } 427} 428