xref: /webtrees/app/Module/FrequentlyAskedQuestionsModule.php (revision 6bd19c8ce39244f770147102c818d9c7d9e13667)
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
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 function (Tree $tree): array {
314                return [$tree->id() => $tree->title()];
315            })
316            ->all();
317
318        $gedcom_ids = ['' => I18N::translate('All')] + $gedcom_ids;
319
320        return $this->viewResponse('modules/faq/edit', [
321            'block_id'    => $block_id,
322            'block_order' => $block_order,
323            'header'      => $header,
324            'body'        => $body,
325            'languages'   => $languages,
326            'title'       => $title,
327            'gedcom_id'   => $gedcom_id,
328            'gedcom_ids'  => $gedcom_ids,
329        ]);
330    }
331
332    /**
333     * @param ServerRequestInterface $request
334     *
335     * @return ResponseInterface
336     */
337    public function postAdminEditAction(ServerRequestInterface $request): ResponseInterface
338    {
339        $block_id    = Validator::queryParams($request)->integer('block_id', 0);
340        $body        = Validator::parsedBody($request)->string('body');
341        $header      = Validator::parsedBody($request)->string('header');
342        $languages   = Validator::parsedBody($request)->array('languages');
343        $gedcom_id   = Validator::parsedBody($request)->string('gedcom_id');
344        $block_order = Validator::parsedBody($request)->integer('block_order');
345
346        if ($gedcom_id === '') {
347            $gedcom_id = null;
348        }
349
350        $body    = $this->html_service->sanitize($body);
351        $header  = $this->html_service->sanitize($header);
352
353        if ($block_id !== 0) {
354            DB::table('block')
355                ->where('block_id', '=', $block_id)
356                ->update([
357                    'gedcom_id'   => $gedcom_id,
358                    'block_order' => $block_order,
359                ]);
360        } else {
361            DB::table('block')->insert([
362                'gedcom_id'   => $gedcom_id,
363                'module_name' => $this->name(),
364                'block_order' => $block_order,
365            ]);
366
367            $block_id = (int) DB::connection()->getPdo()->lastInsertId();
368        }
369
370        $this->setBlockSetting($block_id, 'faqbody', $body);
371        $this->setBlockSetting($block_id, 'header', $header);
372        $this->setBlockSetting($block_id, 'languages', implode(',', $languages));
373
374        $url = route('module', [
375            'module' => $this->name(),
376            'action' => 'Admin',
377        ]);
378
379        return redirect($url);
380    }
381
382    /**
383     * @param ServerRequestInterface $request
384     *
385     * @return ResponseInterface
386     */
387    public function getShowAction(ServerRequestInterface $request): ResponseInterface
388    {
389        $tree = Validator::attributes($request)->tree();
390
391        // Filter foreign languages.
392        $faqs = $this->faqsForTree($tree)
393            ->filter(static function (object $faq): bool {
394                return $faq->languages === '' || in_array(I18N::languageTag(), explode(',', $faq->languages), true);
395            });
396
397        return $this->viewResponse('modules/faq/show', [
398            'faqs'  => $faqs,
399            'title' => I18N::translate('Frequently asked questions'),
400            'tree'  => $tree,
401        ]);
402    }
403
404    /**
405     * @param Tree $tree
406     *
407     * @return Collection<int,object>
408     */
409    private function faqsForTree(Tree $tree): Collection
410    {
411        return DB::table('block')
412            ->join('block_setting AS bs1', 'bs1.block_id', '=', 'block.block_id')
413            ->join('block_setting AS bs2', 'bs2.block_id', '=', 'block.block_id')
414            ->join('block_setting AS bs3', 'bs3.block_id', '=', 'block.block_id')
415            ->where('module_name', '=', $this->name())
416            ->where('bs1.setting_name', '=', 'header')
417            ->where('bs2.setting_name', '=', 'faqbody')
418            ->where('bs3.setting_name', '=', 'languages')
419            ->where(static function (Builder $query) use ($tree): void {
420                $query
421                    ->whereNull('gedcom_id')
422                    ->orWhere('gedcom_id', '=', $tree->id());
423            })
424            ->orderBy('block_order')
425            ->select(['block.block_id', 'block_order', 'gedcom_id', 'bs1.setting_value AS header', 'bs2.setting_value AS faqbody', 'bs3.setting_value AS languages'])
426            ->get()
427            ->map(static function (object $row): object {
428                $row->block_id    = (int) $row->block_id;
429                $row->block_order = (int) $row->block_order;
430                $row->gedcom_id   = (int) $row->gedcom_id;
431
432                return $row;
433            });
434    }
435
436    /**
437     * @param Tree   $tree
438     * @param string $language
439     *
440     * @return bool
441     */
442    private function faqsExist(Tree $tree, string $language): bool
443    {
444        return DB::table('block')
445            ->join('block_setting', 'block_setting.block_id', '=', 'block.block_id')
446            ->where('module_name', '=', $this->name())
447            ->where('setting_name', '=', 'languages')
448            ->where(static function (Builder $query) use ($tree): void {
449                $query
450                    ->whereNull('gedcom_id')
451                    ->orWhere('gedcom_id', '=', $tree->id());
452            })
453            ->select(['setting_value AS languages'])
454            ->get()
455            ->filter(static function (object $faq) use ($language): bool {
456                return $faq->languages === '' || in_array($language, explode(',', $faq->languages), true);
457            })
458            ->isNotEmpty();
459    }
460}
461