xref: /webtrees/app/Module/FrequentlyAskedQuestionsModule.php (revision 10e0649788c8d7d4974d81c048ca2b225df8f22e)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2022 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\Tree;
28use Fisharebest\Webtrees\Validator;
29use Illuminate\Database\Capsule\Manager as DB;
30use Illuminate\Database\Query\Builder;
31use Illuminate\Support\Collection;
32use Psr\Http\Message\ResponseInterface;
33use Psr\Http\Message\ServerRequestInterface;
34
35use function in_array;
36use function redirect;
37use function route;
38
39/**
40 * Class FrequentlyAskedQuestionsModule
41 */
42class FrequentlyAskedQuestionsModule extends AbstractModule implements ModuleConfigInterface, ModuleMenuInterface
43{
44    use ModuleConfigTrait;
45    use ModuleMenuTrait;
46
47    private HtmlService $html_service;
48
49    private TreeService $tree_service;
50
51    /**
52     * FrequentlyAskedQuestionsModule constructor.
53     *
54     * @param HtmlService $html_service
55     * @param TreeService $tree_service
56     */
57    public function __construct(HtmlService $html_service, TreeService $tree_service)
58    {
59        $this->html_service = $html_service;
60        $this->tree_service = $tree_service;
61    }
62
63    /**
64     * How should this module be identified in the control panel, etc.?
65     *
66     * @return string
67     */
68    public function title(): string
69    {
70        /* I18N: Name of a module. Abbreviation for “Frequently Asked Questions” */
71        return I18N::translate('FAQ');
72    }
73
74    /**
75     * A sentence describing what this module does.
76     *
77     * @return string
78     */
79    public function description(): string
80    {
81        /* I18N: Description of the “FAQ” module */
82        return I18N::translate('A list of frequently asked questions and answers.');
83    }
84
85    /**
86     * The default position for this menu.  It can be changed in the control panel.
87     *
88     * @return int
89     */
90    public function defaultMenuOrder(): int
91    {
92        return 8;
93    }
94
95    /**
96     * A menu, to be added to the main application menu.
97     *
98     * @param Tree $tree
99     *
100     * @return Menu|null
101     */
102    public function getMenu(Tree $tree): ?Menu
103    {
104        if ($this->faqsExist($tree, I18N::languageTag())) {
105            return new Menu($this->title(), route('module', [
106                'module' => $this->name(),
107                'action' => 'Show',
108                'tree'   => $tree->name(),
109            ]), 'menu-faq');
110        }
111
112        return null;
113    }
114
115    /**
116     * @param ServerRequestInterface $request
117     *
118     * @return ResponseInterface
119     */
120    public function getAdminAction(ServerRequestInterface $request): ResponseInterface
121    {
122        $this->layout = 'layouts/administration';
123
124        // This module can't run without a tree
125        $tree = Validator::attributes($request)->treeOptional();
126
127        if (!$tree instanceof Tree) {
128            $tree = $this->tree_service->all()->first();
129            if ($tree instanceof Tree) {
130                return redirect(route('module', ['module' => $this->name(), 'action' => 'Admin', 'tree' => $tree->name()]));
131            }
132
133            return redirect(route(ControlPanel::class));
134        }
135
136        $faqs = $this->faqsForTree($tree);
137
138        $min_block_order = DB::table('block')
139            ->where('module_name', '=', $this->name())
140            ->where(static function (Builder $query) use ($tree): void {
141                $query
142                    ->whereNull('gedcom_id')
143                    ->orWhere('gedcom_id', '=', $tree->id());
144            })
145            ->min('block_order');
146
147        $max_block_order = DB::table('block')
148            ->where('module_name', '=', $this->name())
149            ->where(static function (Builder $query) use ($tree): void {
150                $query
151                    ->whereNull('gedcom_id')
152                    ->orWhere('gedcom_id', '=', $tree->id());
153            })
154            ->max('block_order');
155
156        $title = I18N::translate('Frequently asked questions') . ' — ' . $tree->title();
157
158        return $this->viewResponse('modules/faq/config', [
159            'action'          => route('module', ['module' => $this->name(), 'action' => 'Admin']),
160            'faqs'            => $faqs,
161            'max_block_order' => $max_block_order,
162            'min_block_order' => $min_block_order,
163            'module'          => $this->name(),
164            'title'           => $title,
165            'tree'            => $tree,
166            'tree_names'      => $this->tree_service->titles(),
167        ]);
168    }
169
170    /**
171     * @param ServerRequestInterface $request
172     *
173     * @return ResponseInterface
174     */
175    public function postAdminAction(ServerRequestInterface $request): ResponseInterface
176    {
177        $params = (array) $request->getParsedBody();
178
179        return redirect(route('module', [
180            'module' => $this->name(),
181            'action' => 'Admin',
182            'tree'   => $params['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 = (int) $request->getQueryParams()['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 = (int) $request->getQueryParams()['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 = (int) $request->getQueryParams()['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 = (int) ($request->getQueryParams()['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 = (int) ($request->getQueryParams()['block_id'] ?? 0);
340
341        $params = (array) $request->getParsedBody();
342
343        $body        = $params['body'];
344        $header      = $params['header'];
345        $languages   = $params['languages'] ?? [];
346        $gedcom_id   = $params['gedcom_id'];
347        $block_order = (int) $params['block_order'];
348
349        if ($gedcom_id === '') {
350            $gedcom_id = null;
351        }
352
353        $body    = $this->html_service->sanitize($body);
354        $header  = $this->html_service->sanitize($header);
355
356        if ($block_id !== 0) {
357            DB::table('block')
358                ->where('block_id', '=', $block_id)
359                ->update([
360                    'gedcom_id'   => $gedcom_id,
361                    'block_order' => $block_order,
362                ]);
363        } else {
364            DB::table('block')->insert([
365                'gedcom_id'   => $gedcom_id,
366                'module_name' => $this->name(),
367                'block_order' => $block_order,
368            ]);
369
370            $block_id = (int) DB::connection()->getPdo()->lastInsertId();
371        }
372
373        $this->setBlockSetting($block_id, 'faqbody', $body);
374        $this->setBlockSetting($block_id, 'header', $header);
375        $this->setBlockSetting($block_id, 'languages', implode(',', $languages));
376
377        $url = route('module', [
378            'module' => $this->name(),
379            'action' => 'Admin',
380        ]);
381
382        return redirect($url);
383    }
384
385    /**
386     * @param ServerRequestInterface $request
387     *
388     * @return ResponseInterface
389     */
390    public function getShowAction(ServerRequestInterface $request): ResponseInterface
391    {
392        $tree = Validator::attributes($request)->tree();
393
394        // Filter foreign languages.
395        $faqs = $this->faqsForTree($tree)
396            ->filter(static function (object $faq): bool {
397                return $faq->languages === '' || in_array(I18N::languageTag(), explode(',', $faq->languages), true);
398            });
399
400        return $this->viewResponse('modules/faq/show', [
401            'faqs'  => $faqs,
402            'title' => I18N::translate('Frequently asked questions'),
403            'tree'  => $tree,
404        ]);
405    }
406
407    /**
408     * @param Tree $tree
409     *
410     * @return Collection<int,object>
411     */
412    private function faqsForTree(Tree $tree): Collection
413    {
414        return DB::table('block')
415            ->join('block_setting AS bs1', 'bs1.block_id', '=', 'block.block_id')
416            ->join('block_setting AS bs2', 'bs2.block_id', '=', 'block.block_id')
417            ->join('block_setting AS bs3', 'bs3.block_id', '=', 'block.block_id')
418            ->where('module_name', '=', $this->name())
419            ->where('bs1.setting_name', '=', 'header')
420            ->where('bs2.setting_name', '=', 'faqbody')
421            ->where('bs3.setting_name', '=', 'languages')
422            ->where(static function (Builder $query) use ($tree): void {
423                $query
424                    ->whereNull('gedcom_id')
425                    ->orWhere('gedcom_id', '=', $tree->id());
426            })
427            ->orderBy('block_order')
428            ->select(['block.block_id', 'block_order', 'gedcom_id', 'bs1.setting_value AS header', 'bs2.setting_value AS faqbody', 'bs3.setting_value AS languages'])
429            ->get();
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 function (object $faq) use ($language): bool {
452                return $faq->languages === '' || in_array($language, explode(',', $faq->languages), true);
453            })
454            ->isNotEmpty();
455    }
456}
457