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