xref: /webtrees/app/Module/FrequentlyAskedQuestionsModule.php (revision e80729a14bd6eb5653b213bcf9d3d977ef32136e)
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: 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: 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: 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: 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() . '&amp;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() ?>&amp;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(), '&amp;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() ?>&amp;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 '&nbsp;';
482			} else {
483				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>';
484			}
485			echo '</td><td>';
486			if ($faq->block_order == $max_block_order) {
487				echo '&nbsp;';
488			} else {
489				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>';
490			}
491			echo '</td><td>';
492			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>';
493			echo '</td><td>';
494			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”?', 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&amp;mod_action=show', 'menu-help');
537		} else {
538			return null;
539		}
540
541	}
542}
543