xref: /webtrees/app/Module/FrequentlyAskedQuestionsModule.php (revision 81b514b4672980e5db010e9d89b55eaf131e798f)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2023 webtrees development team
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees\Module;
21
22use Fisharebest\Webtrees\Http\RequestHandlers\ControlPanel;
23use Fisharebest\Webtrees\I18N;
24use Fisharebest\Webtrees\Menu;
25use Fisharebest\Webtrees\Services\HtmlService;
26use Fisharebest\Webtrees\Services\TreeService;
27use Fisharebest\Webtrees\Site;
28use Fisharebest\Webtrees\Tree;
29use Fisharebest\Webtrees\Validator;
30use Illuminate\Database\Capsule\Manager as DB;
31use Illuminate\Database\Query\Builder;
32use Illuminate\Support\Collection;
33use Psr\Http\Message\ResponseInterface;
34use Psr\Http\Message\ServerRequestInterface;
35
36use function in_array;
37use function redirect;
38use function route;
39
40/**
41 * Class FrequentlyAskedQuestionsModule
42 */
43class FrequentlyAskedQuestionsModule extends AbstractModule implements ModuleConfigInterface, ModuleMenuInterface
44{
45    use ModuleConfigTrait;
46    use ModuleMenuTrait;
47
48    private HtmlService $html_service;
49
50    private TreeService $tree_service;
51
52    /**
53     * FrequentlyAskedQuestionsModule constructor.
54     *
55     * @param HtmlService $html_service
56     * @param TreeService $tree_service
57     */
58    public function __construct(HtmlService $html_service, TreeService $tree_service)
59    {
60        $this->html_service = $html_service;
61        $this->tree_service = $tree_service;
62    }
63
64    /**
65     * How should this module be identified in the control panel, etc.?
66     *
67     * @return string
68     */
69    public function title(): string
70    {
71        /* I18N: Name of a module. Abbreviation for “Frequently Asked Questions” */
72        return I18N::translate('FAQ');
73    }
74
75    /**
76     * A sentence describing what this module does.
77     *
78     * @return string
79     */
80    public function description(): string
81    {
82        /* I18N: Description of the “FAQ” module */
83        return I18N::translate('A list of frequently asked questions and answers.');
84    }
85
86    /**
87     * The default position for this menu.  It can be changed in the control panel.
88     *
89     * @return int
90     */
91    public function defaultMenuOrder(): int
92    {
93        return 8;
94    }
95
96    /**
97     * A menu, to be added to the main application menu.
98     *
99     * @param Tree $tree
100     *
101     * @return Menu|null
102     */
103    public function getMenu(Tree $tree): ?Menu
104    {
105        if ($this->faqsExist($tree, I18N::languageTag())) {
106            return new Menu($this->title(), route('module', [
107                'module' => $this->name(),
108                'action' => 'Show',
109                'tree'   => $tree->name(),
110            ]), 'menu-faq');
111        }
112
113        return null;
114    }
115
116    /**
117     * @param ServerRequestInterface $request
118     *
119     * @return ResponseInterface
120     */
121    public function getAdminAction(ServerRequestInterface $request): ResponseInterface
122    {
123        $this->layout = 'layouts/administration';
124
125        // This module can't run without a tree
126        $tree = Validator::attributes($request)->treeOptional();
127
128        if (!$tree instanceof Tree) {
129            $trees = $this->tree_service->all();
130
131            $tree = $trees->get(Site::getPreference('DEFAULT_GEDCOM')) ?? $trees->first();
132
133            if ($tree instanceof Tree) {
134                return redirect(route('module', ['module' => $this->name(), 'action' => 'Admin', 'tree' => $tree->name()]));
135            }
136
137            return redirect(route(ControlPanel::class));
138        }
139
140        $faqs = $this->faqsForTree($tree);
141
142        $min_block_order = (int) DB::table('block')
143            ->where('module_name', '=', $this->name())
144            ->where(static function (Builder $query) use ($tree): void {
145                $query
146                    ->whereNull('gedcom_id')
147                    ->orWhere('gedcom_id', '=', $tree->id());
148            })
149            ->min('block_order');
150
151        $max_block_order = (int) DB::table('block')
152            ->where('module_name', '=', $this->name())
153            ->where(static function (Builder $query) use ($tree): void {
154                $query
155                    ->whereNull('gedcom_id')
156                    ->orWhere('gedcom_id', '=', $tree->id());
157            })
158            ->max('block_order');
159
160        $title = I18N::translate('Frequently asked questions') . ' — ' . $tree->title();
161
162        return $this->viewResponse('modules/faq/config', [
163            'action'          => route('module', ['module' => $this->name(), 'action' => 'Admin']),
164            'faqs'            => $faqs,
165            'max_block_order' => $max_block_order,
166            'min_block_order' => $min_block_order,
167            'module'          => $this->name(),
168            'title'           => $title,
169            'tree'            => $tree,
170            'tree_names'      => $this->tree_service->titles(),
171        ]);
172    }
173
174    /**
175     * @param ServerRequestInterface $request
176     *
177     * @return ResponseInterface
178     */
179    public function postAdminAction(ServerRequestInterface $request): ResponseInterface
180    {
181        return redirect(route('module', [
182            'module' => $this->name(),
183            'action' => 'Admin',
184            'tree'   => Validator::parsedBody($request)->string('tree'),
185        ]));
186    }
187
188    /**
189     * @param ServerRequestInterface $request
190     *
191     * @return ResponseInterface
192     */
193    public function postAdminDeleteAction(ServerRequestInterface $request): ResponseInterface
194    {
195        $block_id = Validator::queryParams($request)->integer('block_id');
196
197        DB::table('block_setting')->where('block_id', '=', $block_id)->delete();
198
199        DB::table('block')->where('block_id', '=', $block_id)->delete();
200
201        $url = route('module', [
202            'module' => $this->name(),
203            'action' => 'Admin',
204        ]);
205
206        return redirect($url);
207    }
208
209    /**
210     * @param ServerRequestInterface $request
211     *
212     * @return ResponseInterface
213     */
214    public function postAdminMoveDownAction(ServerRequestInterface $request): ResponseInterface
215    {
216        $block_id = Validator::queryParams($request)->integer('block_id');
217
218        $block_order = DB::table('block')
219            ->where('block_id', '=', $block_id)
220            ->value('block_order');
221
222        $swap_block = DB::table('block')
223            ->where('module_name', '=', $this->name())
224            ->where('block_order', '>', $block_order)
225            ->orderBy('block_order')
226            ->first();
227
228        if ($block_order !== null && $swap_block !== null) {
229            DB::table('block')
230                ->where('block_id', '=', $block_id)
231                ->update([
232                    'block_order' => $swap_block->block_order,
233                ]);
234
235            DB::table('block')
236                ->where('block_id', '=', $swap_block->block_id)
237                ->update([
238                    'block_order' => $block_order,
239                ]);
240        }
241
242        return response();
243    }
244
245    /**
246     * @param ServerRequestInterface $request
247     *
248     * @return ResponseInterface
249     */
250    public function postAdminMoveUpAction(ServerRequestInterface $request): ResponseInterface
251    {
252        $block_id = Validator::queryParams($request)->integer('block_id');
253
254        $block_order = DB::table('block')
255            ->where('block_id', '=', $block_id)
256            ->value('block_order');
257
258        $swap_block = DB::table('block')
259            ->where('module_name', '=', $this->name())
260            ->where('block_order', '<', $block_order)
261            ->orderBy('block_order', 'desc')
262            ->first();
263
264        if ($block_order !== null && $swap_block !== null) {
265            DB::table('block')
266                ->where('block_id', '=', $block_id)
267                ->update([
268                    'block_order' => $swap_block->block_order,
269                ]);
270
271            DB::table('block')
272                ->where('block_id', '=', $swap_block->block_id)
273                ->update([
274                    'block_order' => $block_order,
275                ]);
276        }
277
278        return response();
279    }
280
281    /**
282     * @param ServerRequestInterface $request
283     *
284     * @return ResponseInterface
285     */
286    public function getAdminEditAction(ServerRequestInterface $request): ResponseInterface
287    {
288        $this->layout = 'layouts/administration';
289
290        $block_id = Validator::queryParams($request)->integer('block_id', 0);
291
292        if ($block_id === 0) {
293            // Creating a new faq
294            $header      = '';
295            $body        = '';
296            $gedcom_id   = null;
297            $block_order = 1 + (int) DB::table('block')->where('module_name', '=', $this->name())->max('block_order');
298
299            $languages = [];
300
301            $title = I18N::translate('Add an FAQ');
302        } else {
303            // Editing an existing faq
304            $header      = $this->getBlockSetting($block_id, 'header');
305            $body        = $this->getBlockSetting($block_id, 'faqbody');
306            $gedcom_id   = DB::table('block')->where('block_id', '=', $block_id)->value('gedcom_id');
307            $block_order = DB::table('block')->where('block_id', '=', $block_id)->value('block_order');
308
309            $languages = explode(',', $this->getBlockSetting($block_id, 'languages'));
310
311            $title = I18N::translate('Edit the FAQ');
312        }
313
314        $gedcom_ids = $this->tree_service->all()
315            ->mapWithKeys(static function (Tree $tree): array {
316                return [$tree->id() => $tree->title()];
317            })
318            ->all();
319
320        $gedcom_ids = ['' => I18N::translate('All')] + $gedcom_ids;
321
322        return $this->viewResponse('modules/faq/edit', [
323            'block_id'    => $block_id,
324            'block_order' => $block_order,
325            'header'      => $header,
326            'body'        => $body,
327            'languages'   => $languages,
328            'title'       => $title,
329            'gedcom_id'   => $gedcom_id,
330            'gedcom_ids'  => $gedcom_ids,
331        ]);
332    }
333
334    /**
335     * @param ServerRequestInterface $request
336     *
337     * @return ResponseInterface
338     */
339    public function postAdminEditAction(ServerRequestInterface $request): ResponseInterface
340    {
341        $block_id    = Validator::queryParams($request)->integer('block_id', 0);
342        $body        = Validator::parsedBody($request)->string('body');
343        $header      = Validator::parsedBody($request)->string('header');
344        $languages   = Validator::parsedBody($request)->array('languages');
345        $gedcom_id   = Validator::parsedBody($request)->string('gedcom_id');
346        $block_order = Validator::parsedBody($request)->integer('block_order');
347
348        if ($gedcom_id === '') {
349            $gedcom_id = null;
350        }
351
352        $body    = $this->html_service->sanitize($body);
353        $header  = $this->html_service->sanitize($header);
354
355        if ($block_id !== 0) {
356            DB::table('block')
357                ->where('block_id', '=', $block_id)
358                ->update([
359                    'gedcom_id'   => $gedcom_id,
360                    'block_order' => $block_order,
361                ]);
362        } else {
363            DB::table('block')->insert([
364                'gedcom_id'   => $gedcom_id,
365                'module_name' => $this->name(),
366                'block_order' => $block_order,
367            ]);
368
369            $block_id = (int) DB::connection()->getPdo()->lastInsertId();
370        }
371
372        $this->setBlockSetting($block_id, 'faqbody', $body);
373        $this->setBlockSetting($block_id, 'header', $header);
374        $this->setBlockSetting($block_id, 'languages', implode(',', $languages));
375
376        $url = route('module', [
377            'module' => $this->name(),
378            'action' => 'Admin',
379        ]);
380
381        return redirect($url);
382    }
383
384    /**
385     * @param ServerRequestInterface $request
386     *
387     * @return ResponseInterface
388     */
389    public function getShowAction(ServerRequestInterface $request): ResponseInterface
390    {
391        $tree = Validator::attributes($request)->tree();
392
393        // Filter foreign languages.
394        $faqs = $this->faqsForTree($tree)
395            ->filter(static function (object $faq): bool {
396                return $faq->languages === '' || in_array(I18N::languageTag(), explode(',', $faq->languages), true);
397            });
398
399        return $this->viewResponse('modules/faq/show', [
400            'faqs'  => $faqs,
401            'title' => I18N::translate('Frequently asked questions'),
402            'tree'  => $tree,
403        ]);
404    }
405
406    /**
407     * @param Tree $tree
408     *
409     * @return Collection<int,object>
410     */
411    private function faqsForTree(Tree $tree): Collection
412    {
413        return DB::table('block')
414            ->join('block_setting AS bs1', 'bs1.block_id', '=', 'block.block_id')
415            ->join('block_setting AS bs2', 'bs2.block_id', '=', 'block.block_id')
416            ->join('block_setting AS bs3', 'bs3.block_id', '=', 'block.block_id')
417            ->where('module_name', '=', $this->name())
418            ->where('bs1.setting_name', '=', 'header')
419            ->where('bs2.setting_name', '=', 'faqbody')
420            ->where('bs3.setting_name', '=', 'languages')
421            ->where(static function (Builder $query) use ($tree): void {
422                $query
423                    ->whereNull('gedcom_id')
424                    ->orWhere('gedcom_id', '=', $tree->id());
425            })
426            ->orderBy('block_order')
427            ->select(['block.block_id', 'block_order', 'gedcom_id', 'bs1.setting_value AS header', 'bs2.setting_value AS faqbody', 'bs3.setting_value AS languages'])
428            ->get()
429            ->map(static function (object $row): object {
430                $row->block_id    = (int) $row->block_id;
431                $row->block_order = (int) $row->block_order;
432                $row->gedcom_id   = (int) $row->gedcom_id;
433
434                return $row;
435            });
436    }
437
438    /**
439     * @param Tree   $tree
440     * @param string $language
441     *
442     * @return bool
443     */
444    private function faqsExist(Tree $tree, string $language): bool
445    {
446        return DB::table('block')
447            ->join('block_setting', 'block_setting.block_id', '=', 'block.block_id')
448            ->where('module_name', '=', $this->name())
449            ->where('setting_name', '=', 'languages')
450            ->where(static function (Builder $query) use ($tree): void {
451                $query
452                    ->whereNull('gedcom_id')
453                    ->orWhere('gedcom_id', '=', $tree->id());
454            })
455            ->select(['setting_value AS languages'])
456            ->get()
457            ->filter(static function (object $faq) use ($language): bool {
458                return $faq->languages === '' || in_array($language, explode(',', $faq->languages), true);
459            })
460            ->isNotEmpty();
461    }
462}
463