xref: /webtrees/app/Module/FrequentlyAskedQuestionsModule.php (revision 033999b09ece322217e4407c67eafd75e03718d3)
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 = (int) 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 = (int) 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        return redirect(route('module', [
178            'module' => $this->name(),
179            'action' => 'Admin',
180            'tree'   => Validator::parsedBody($request)->string('tree'),
181        ]));
182    }
183
184    /**
185     * @param ServerRequestInterface $request
186     *
187     * @return ResponseInterface
188     */
189    public function postAdminDeleteAction(ServerRequestInterface $request): ResponseInterface
190    {
191        $block_id = Validator::queryParams($request)->integer('block_id');
192
193        DB::table('block_setting')->where('block_id', '=', $block_id)->delete();
194
195        DB::table('block')->where('block_id', '=', $block_id)->delete();
196
197        $url = route('module', [
198            'module' => $this->name(),
199            'action' => 'Admin',
200        ]);
201
202        return redirect($url);
203    }
204
205    /**
206     * @param ServerRequestInterface $request
207     *
208     * @return ResponseInterface
209     */
210    public function postAdminMoveDownAction(ServerRequestInterface $request): ResponseInterface
211    {
212        $block_id = Validator::queryParams($request)->integer('block_id');
213
214        $block_order = DB::table('block')
215            ->where('block_id', '=', $block_id)
216            ->value('block_order');
217
218        $swap_block = DB::table('block')
219            ->where('module_name', '=', $this->name())
220            ->where('block_order', '>', $block_order)
221            ->orderBy('block_order')
222            ->first();
223
224        if ($block_order !== null && $swap_block !== null) {
225            DB::table('block')
226                ->where('block_id', '=', $block_id)
227                ->update([
228                    'block_order' => $swap_block->block_order,
229                ]);
230
231            DB::table('block')
232                ->where('block_id', '=', $swap_block->block_id)
233                ->update([
234                    'block_order' => $block_order,
235                ]);
236        }
237
238        return response();
239    }
240
241    /**
242     * @param ServerRequestInterface $request
243     *
244     * @return ResponseInterface
245     */
246    public function postAdminMoveUpAction(ServerRequestInterface $request): ResponseInterface
247    {
248        $block_id = Validator::queryParams($request)->integer('block_id');
249
250        $block_order = DB::table('block')
251            ->where('block_id', '=', $block_id)
252            ->value('block_order');
253
254        $swap_block = DB::table('block')
255            ->where('module_name', '=', $this->name())
256            ->where('block_order', '<', $block_order)
257            ->orderBy('block_order', 'desc')
258            ->first();
259
260        if ($block_order !== null && $swap_block !== null) {
261            DB::table('block')
262                ->where('block_id', '=', $block_id)
263                ->update([
264                    'block_order' => $swap_block->block_order,
265                ]);
266
267            DB::table('block')
268                ->where('block_id', '=', $swap_block->block_id)
269                ->update([
270                    'block_order' => $block_order,
271                ]);
272        }
273
274        return response();
275    }
276
277    /**
278     * @param ServerRequestInterface $request
279     *
280     * @return ResponseInterface
281     */
282    public function getAdminEditAction(ServerRequestInterface $request): ResponseInterface
283    {
284        $this->layout = 'layouts/administration';
285
286        $block_id = Validator::queryParams($request)->integer('block_id', 0);
287
288        if ($block_id === 0) {
289            // Creating a new faq
290            $header      = '';
291            $body        = '';
292            $gedcom_id   = null;
293            $block_order = 1 + (int) DB::table('block')->where('module_name', '=', $this->name())->max('block_order');
294
295            $languages = [];
296
297            $title = I18N::translate('Add an FAQ');
298        } else {
299            // Editing an existing faq
300            $header      = $this->getBlockSetting($block_id, 'header');
301            $body        = $this->getBlockSetting($block_id, 'faqbody');
302            $gedcom_id   = DB::table('block')->where('block_id', '=', $block_id)->value('gedcom_id');
303            $block_order = DB::table('block')->where('block_id', '=', $block_id)->value('block_order');
304
305            $languages = explode(',', $this->getBlockSetting($block_id, 'languages'));
306
307            $title = I18N::translate('Edit the FAQ');
308        }
309
310        $gedcom_ids = $this->tree_service->all()
311            ->mapWithKeys(static function (Tree $tree): array {
312                return [$tree->id() => $tree->title()];
313            })
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 = (int) DB::connection()->getPdo()->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 function (object $faq): bool {
392                return $faq->languages === '' || in_array(I18N::languageTag(), explode(',', $faq->languages), true);
393            });
394
395        return $this->viewResponse('modules/faq/show', [
396            'faqs'  => $faqs,
397            'title' => I18N::translate('Frequently asked questions'),
398            'tree'  => $tree,
399        ]);
400    }
401
402    /**
403     * @param Tree $tree
404     *
405     * @return Collection<int,object>
406     */
407    private function faqsForTree(Tree $tree): Collection
408    {
409        return DB::table('block')
410            ->join('block_setting AS bs1', 'bs1.block_id', '=', 'block.block_id')
411            ->join('block_setting AS bs2', 'bs2.block_id', '=', 'block.block_id')
412            ->join('block_setting AS bs3', 'bs3.block_id', '=', 'block.block_id')
413            ->where('module_name', '=', $this->name())
414            ->where('bs1.setting_name', '=', 'header')
415            ->where('bs2.setting_name', '=', 'faqbody')
416            ->where('bs3.setting_name', '=', 'languages')
417            ->where(static function (Builder $query) use ($tree): void {
418                $query
419                    ->whereNull('gedcom_id')
420                    ->orWhere('gedcom_id', '=', $tree->id());
421            })
422            ->orderBy('block_order')
423            ->select(['block.block_id', 'block_order', 'gedcom_id', 'bs1.setting_value AS header', 'bs2.setting_value AS faqbody', 'bs3.setting_value AS languages'])
424            ->get()
425            ->map(static function (object $row): object {
426                $row->block_id    = (int) $row->block_id;
427                $row->block_order = (int) $row->block_order;
428                $row->gedcom_id   = (int) $row->gedcom_id;
429
430                return $row;
431            });
432    }
433
434    /**
435     * @param Tree   $tree
436     * @param string $language
437     *
438     * @return bool
439     */
440    private function faqsExist(Tree $tree, string $language): bool
441    {
442        return DB::table('block')
443            ->join('block_setting', 'block_setting.block_id', '=', 'block.block_id')
444            ->where('module_name', '=', $this->name())
445            ->where('setting_name', '=', 'languages')
446            ->where(static function (Builder $query) use ($tree): void {
447                $query
448                    ->whereNull('gedcom_id')
449                    ->orWhere('gedcom_id', '=', $tree->id());
450            })
451            ->select(['setting_value AS languages'])
452            ->get()
453            ->filter(static function (object $faq) use ($language): bool {
454                return $faq->languages === '' || in_array($language, explode(',', $faq->languages), true);
455            })
456            ->isNotEmpty();
457    }
458}
459