xref: /webtrees/app/Module/FrequentlyAskedQuestionsModule.php (revision 047f239b70b792655dbaa183656298db216028ae)
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() . '&amp;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() ?>&amp;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(), '&amp;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() ?>&amp;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 '&nbsp;';
483			} else {
484				echo '<a href="module.php?mod=', $this->getName(), '&amp;mod_action=admin_moveup&amp;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 '&nbsp;';
489			} else {
490				echo '<a href="module.php?mod=', $this->getName(), '&amp;mod_action=admin_movedown&amp;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(), '&amp;mod_action=admin_edit&amp;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(), '&amp;mod_action=admin_delete&amp;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&amp;mod_action=show', 'menu-help');
538		} else {
539			return null;
540		}
541
542	}
543}
544