xref: /webtrees/app/Module/FrequentlyAskedQuestionsModule.php (revision 571e6fcac743cc9b048486db27eb10ae509eb172)
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;
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 Html::url('module.php', [
93			'mod'        => $this->getName(),
94			'mod_action' => 'admin_config',
95		]);
96	}
97
98	/**
99	 * Action from the configuration page
100	 */
101	private function editSave() {
102		if (Filter::checkCsrf()) {
103			$block_id = Filter::postInteger('block_id');
104			if ($block_id) {
105				Database::prepare(
106					"UPDATE `##block` SET gedcom_id = NULLIF(:tree_id, '0'), block_order = :block_order WHERE block_id = :block_id"
107				)->execute([
108					'tree_id'     => Filter::postInteger('gedcom_id'),
109					'block_order' => Filter::postInteger('block_order'),
110					'block_id'    => $block_id,
111				]);
112			} else {
113				Database::prepare(
114					"INSERT INTO `##block` (gedcom_id, module_name, block_order) VALUES (NULLIF(:tree_id, '0'), :module_name, :block_order)"
115				)->execute([
116					'tree_id'     => Filter::postInteger('gedcom_id'),
117					'module_name' => $this->getName(),
118					'block_order' => Filter::postInteger('block_order'),
119				]);
120				$block_id = Database::getInstance()->lastInsertId();
121			}
122			$this->setBlockSetting($block_id, 'header', Filter::post('header'));
123			$this->setBlockSetting($block_id, 'faqbody', Filter::post('faqbody'));
124
125			$languages = Filter::postArray('lang');
126			$this->setBlockSetting($block_id, 'languages', implode(',', $languages));
127		}
128	}
129
130	/**
131	 * Action from the configuration page
132	 */
133	private function edit() {
134		global $WT_TREE;
135
136		$controller = new PageController;
137		$controller->restrictAccess(Auth::isAdmin());
138
139		$block_id = Filter::getInteger('block_id');
140		if ($block_id) {
141			$controller->setPageTitle(/* I18N: FAQ = “Frequently Asked Question” */ I18N::translate('Edit the FAQ'));
142			$header      = $this->getBlockSetting($block_id, 'header');
143			$faqbody     = $this->getBlockSetting($block_id, 'faqbody');
144			$block_order = Database::prepare(
145				"SELECT block_order FROM `##block` WHERE block_id = :block_id"
146			)->execute(['block_id' => $block_id])->fetchOne();
147			$gedcom_id = Database::prepare(
148				"SELECT gedcom_id FROM `##block` WHERE block_id = :block_id"
149			)->execute(['block_id' => $block_id])->fetchOne();
150		} else {
151			$controller->setPageTitle(/* I18N: FAQ = “Frequently Asked Question” */ I18N::translate('Add an FAQ'));
152			$header      = '';
153			$faqbody     = '';
154			$block_order = Database::prepare(
155				"SELECT IFNULL(MAX(block_order)+1, 0) FROM `##block` WHERE module_name = :module_name"
156			)->execute(['module_name' => $this->getName()])->fetchOne();
157			$gedcom_id = $WT_TREE->getTreeId();
158		}
159		$controller->pageHeader();
160		if (Module::getModuleByName('ckeditor')) {
161			CkeditorModule::enableEditor($controller);
162		}
163
164		echo Bootstrap4::breadcrumbs([
165			route('admin-control-panel')                                      => I18N::translate('Control panel'),
166			route('admin-modules')                                            => I18N::translate('Module administration'),
167			'module.php?mod=' . $this->getName() . '&mod_action=admin_config' => $this->getTitle(),
168		], $controller->getPageTitle());
169		?>
170
171		<h1><?= $controller->getPageTitle() ?></h1>
172
173		<form name="faq" class="form-horizontal" method="post" action="module.php?mod=<?= $this->getName() ?>&amp;mod_action=admin_edit_save">
174		<?= Filter::getCsrf() ?>
175		<input type="hidden" name="block_id" value="<?= $block_id ?>">
176
177		<div class="row form-group">
178			<label for="header" class="col-sm-3 col-form-label">
179				<?= I18N::translate('Question') ?>
180			</label>
181
182			<div class="col-sm-9">
183				<input type="text" class="form-control" name="header" id="header"
184				       value="<?= e($header) ?>">
185			</div>
186		</div>
187
188		<div class="row form-group">
189			<label for="faqbody" class="col-sm-3 col-form-label">
190				<?= I18N::translate('Answer') ?>
191			</label>
192
193			<div class="col-sm-9">
194				<textarea name="faqbody" id="faqbody" class="form-control html-edit" rows="10"><?= e($faqbody) ?></textarea>
195			</div>
196		</div>
197
198		<div class="row form-group">
199			<label for="xref" class="col-sm-3 col-form-label">
200				<?= /* I18N: Label for a configuration option */ I18N::translate('Show this block for which languages') ?>
201			</label>
202
203			<div class="col-sm-9">
204				<?= FunctionsEdit::editLanguageCheckboxes('lang', explode(',', $this->getBlockSetting($block_id, 'languages'))) ?>
205			</div>
206		</div>
207
208		<div class="row form-group">
209			<label for="block_order" class="col-sm-3 col-form-label">
210				<?= I18N::translate('Sort order') ?>
211			</label>
212
213			<div class="col-sm-9">
214				<input type="text" name="block_order" id="block_order" class="form-control" value="<?= $block_order ?>">
215			</div>
216		</div>
217
218		<div class="row form-group">
219			<label for="gedcom_id" class="col-sm-3 col-form-label">
220				<?= I18N::translate('Family tree') ?>
221			</label>
222
223			<div class="col-sm-9">
224				<?= Bootstrap4::select(['' => I18N::translate('All')] + Tree::getIdList(), $gedcom_id, ['id' => 'gedcom_id', 'name' => 'gedcom_id']) ?>
225				<p class="small text-muted">
226					<?= /* 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.') ?>
227				</p>
228			</div>
229		</div>
230
231		<div class="row form-group">
232			<div class="offset-sm-3 col-sm-9">
233				<button type="submit" class="btn btn-primary">
234					<i class="fas fa-check"></i>
235					<?= I18N::translate('save') ?>
236				</button>
237			</div>
238		</div>
239
240	</form>
241	<?php
242	}
243
244	/**
245	 * Delete an FAQ.
246	 */
247	private function delete() {
248		$block_id = Filter::getInteger('block_id');
249
250		Database::prepare(
251			"DELETE FROM `##block_setting` WHERE block_id = :block_id"
252		)->execute(['block_id' => $block_id]);
253
254		Database::prepare(
255			"DELETE FROM `##block` WHERE block_id = :block_id"
256		)->execute(['block_id' => $block_id]);
257	}
258
259	/**
260	 * Move an FAQ up the list.
261	 */
262	private function moveup() {
263		$block_id = Filter::getInteger('block_id');
264
265		$block_order = Database::prepare(
266			"SELECT block_order FROM `##block` WHERE block_id = :block_id"
267		)->execute(['block_id' => $block_id])->fetchOne();
268
269		$swap_block = Database::prepare(
270			"SELECT block_order, block_id" .
271			" FROM `##block`" .
272			" WHERE block_order = (" .
273			"  SELECT MAX(block_order) FROM `##block` WHERE block_order < :block_order AND module_name = :module_name_1" .
274			" ) AND module_name = :module_name_2" .
275			" LIMIT 1"
276		)->execute([
277			'block_order'   => $block_order,
278			'module_name_1' => $this->getName(),
279			'module_name_2' => $this->getName(),
280		])->fetchOneRow();
281		if ($swap_block) {
282			Database::prepare(
283				"UPDATE `##block` SET block_order = :block_order WHERE block_id = :block_id"
284			)->execute([
285				'block_order' => $swap_block->block_order,
286				'block_id'    => $block_id,
287			]);
288			Database::prepare(
289				"UPDATE `##block` SET block_order = :block_order WHERE block_id = :block_id"
290			)->execute([
291				'block_order' => $block_order,
292				'block_id'    => $swap_block->block_id,
293			]);
294		}
295	}
296
297	/**
298	 * Move an FAQ down the list.
299	 */
300	private function movedown() {
301		$block_id = Filter::get('block_id');
302
303		$block_order = Database::prepare(
304			"SELECT block_order FROM `##block` WHERE block_id = :block_id"
305		)->execute([
306			'block_id' => $block_id,
307		])->fetchOne();
308
309		$swap_block = Database::prepare(
310			"SELECT block_order, block_id" .
311			" FROM `##block`" .
312			" WHERE block_order=(" .
313			"  SELECT MIN(block_order) FROM `##block` WHERE block_order > :block_order AND module_name = :module_name_1" .
314			" ) AND module_name = :module_name_2" .
315			" LIMIT 1"
316		)->execute([
317			'block_order'   => $block_order,
318			'module_name_1' => $this->getName(),
319			'module_name_2' => $this->getName(),
320			])->fetchOneRow();
321		if ($swap_block) {
322			Database::prepare(
323				"UPDATE `##block` SET block_order = :block_order WHERE block_id = :block_id"
324			)->execute([
325				'block_order' => $swap_block->block_order,
326				'block_id'    => $block_id,
327			]);
328			Database::prepare(
329				"UPDATE `##block` SET block_order = :block_order WHERE block_id = :block_id"
330			)->execute([
331				'block_order' => $block_order,
332				'block_id'    => $swap_block->block_id,
333			]);
334		}
335	}
336
337	/**
338	 * Show a list of FAQs
339	 */
340	private function show() {
341		global $controller, $WT_TREE;
342
343		$controller = new PageController;
344		$controller
345			->setPageTitle(I18N::translate('Frequently asked questions'))
346			->pageHeader();
347
348		$faqs = Database::prepare(
349			"SELECT block_id, bs1.setting_value AS header, bs2.setting_value AS body, bs3.setting_value AS languages" .
350			" FROM `##block` b" .
351			" JOIN `##block_setting` bs1 USING (block_id)" .
352			" JOIN `##block_setting` bs2 USING (block_id)" .
353			" JOIN `##block_setting` bs3 USING (block_id)" .
354			" WHERE module_name = :module_name" .
355			" AND bs1.setting_name = 'header'" .
356			" AND bs2.setting_name = 'faqbody'" .
357			" AND bs3.setting_name = 'languages'" .
358			" AND IFNULL(gedcom_id, :tree_id_1) = :tree_id_2" .
359			" ORDER BY block_order"
360		)->execute([
361			'module_name' => $this->getName(),
362			'tree_id_1'   => $WT_TREE->getTreeId(),
363			'tree_id_2'   => $WT_TREE->getTreeId(),
364		])->fetchAll();
365
366		echo '<h2 class="wt-page-title">', I18N::translate('Frequently asked questions');
367		if (Auth::isManager($WT_TREE)) {
368			echo ' — <a href="module.php?mod=', $this->getName(), '&amp;mod_action=admin_config">', I18N::translate('edit'), '</a>';
369		}
370		echo '</h2>';
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([
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([
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([
436			'tree_id' => $WT_TREE->getTreeId(),
437		])->fetchOne();
438
439		echo Bootstrap4::breadcrumbs([
440			route('admin-control-panel') => I18N::translate('Control panel'),
441			route('admin-modules')       => I18N::translate('Module administration'),
442		], $controller->getPageTitle());
443		?>
444
445		<h1><?= $controller->getPageTitle() ?></h1>
446		<p>
447			<?= /* 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.') ?>
448			<?= I18N::translate('You may use HTML to format the answer and to add links to other websites.') ?>
449		</p>
450
451		<p>
452			<form class="form form-inline">
453				<label for="ged" class="sr-only">
454					<?= I18N::translate('Family tree') ?>
455				</label>
456				<input type="hidden" name="mod" value="<?=  $this->getName() ?>">
457				<input type="hidden" name="mod_action" value="admin_config">
458				<?= Bootstrap4::select(Tree::getNameList(), $WT_TREE->getName(), ['id' => 'ged', 'name' => 'ged']) ?>
459				<input type="submit" class="btn btn-primary" value="<?= I18N::translate('show') ?>">
460			</form>
461		</p>
462
463		<p>
464			<a href="module.php?mod=<?= $this->getName() ?>&amp;mod_action=admin_edit" class="btn btn-default">
465				<i class="fas fa-plus"></i>
466				<?= /* I18N: FAQ = “Frequently Asked Question” */ I18N::translate('Add an FAQ') ?>
467			</a>
468		</p>
469
470		<?php
471		echo '<table class="table table-bordered">';
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 '&nbsp;';
486			} else {
487				echo '<a href="module.php?mod=', $this->getName(), '&amp;mod_action=admin_moveup&amp;block_id=', $faq->block_id, '"><i class="fas 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 '&nbsp;';
492			} else {
493				echo '<a href="module.php?mod=', $this->getName(), '&amp;mod_action=admin_movedown&amp;block_id=', $faq->block_id, '"><i class="fas fa-arrow-down"></i></i> ', I18N::translate('Move down'), '</a>';
494			}
495			echo '</td><td>';
496			echo '<a href="module.php?mod=', $this->getName(), '&amp;mod_action=admin_edit&amp;block_id=', $faq->block_id, '"><i class="fas fa-pencil-alt"></i> ', I18N::translate('Edit'), '</a>';
497			echo '</td><td>';
498			echo '<a href="module.php?mod=', $this->getName(), '&amp;mod_action=admin_delete&amp;block_id=', $faq->block_id, '" data-confirm="\', I18N::translate(\'Are you sure you want to delete “%s”?\', e($faq->header)), \'" onclick="return confirm(this.dataset.confirm);"><i class="fas fa-trash-alt"></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	 * The user can re-order menus. Until they do, they are shown in this order.
512	 *
513	 * @return int
514	 */
515	public function defaultMenuOrder() {
516		return 40;
517	}
518
519	/**
520	 * A menu, to be added to the main application menu.
521	 *
522	 * @return Menu|null
523	 */
524	public function getMenu() {
525		global $WT_TREE;
526
527		$faqs = Database::prepare(
528			"SELECT block_id FROM `##block`" .
529			" JOIN `##block_setting` USING (block_id)" .
530			" WHERE module_name = :module_name AND IFNULL(gedcom_id, :tree_id_1) = :tree_id_2" .
531			" AND setting_name='languages' AND (setting_value LIKE CONCAT('%', :locale, '%') OR setting_value='')"
532		)->execute([
533			'module_name' => $this->getName(),
534			'tree_id_1'   => $WT_TREE->getTreeId(),
535			'tree_id_2'   => $WT_TREE->getTreeId(),
536			'locale'      => WT_LOCALE,
537		])->fetchAll();
538
539		if ($faqs) {
540			return new Menu($this->getTitle(), 'module.php?mod=faq&amp;mod_action=show', 'menu-help');
541		} else {
542			return null;
543		}
544	}
545}
546