xref: /webtrees/app/Module/FrequentlyAskedQuestionsModule.php (revision 81443e3cbe4eef5ccdcf8dae716a7e35f7417b60)
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 redirect;
36use function route;
37
38/**
39 * Class FrequentlyAskedQuestionsModule
40 */
41class FrequentlyAskedQuestionsModule extends AbstractModule implements ModuleConfigInterface, ModuleMenuInterface
42{
43    use ModuleConfigTrait;
44    use ModuleMenuTrait;
45
46    private HtmlService $html_service;
47
48    private TreeService $tree_service;
49
50    /**
51     * FrequentlyAskedQuestionsModule constructor.
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            $tree = $this->tree_service->all()->first();
128            if ($tree instanceof Tree) {
129                return redirect(route('module', ['module' => $this->name(), 'action' => 'Admin', 'tree' => $tree->name()]));
130            }
131
132            return redirect(route(ControlPanel::class));
133        }
134
135        $faqs = $this->faqsForTree($tree);
136
137        $min_block_order = DB::table('block')
138            ->where('module_name', '=', $this->name())
139            ->where(static function (Builder $query) use ($tree): void {
140                $query
141                    ->whereNull('gedcom_id')
142                    ->orWhere('gedcom_id', '=', $tree->id());
143            })
144            ->min('block_order');
145
146        $max_block_order = DB::table('block')
147            ->where('module_name', '=', $this->name())
148            ->where(static function (Builder $query) use ($tree): void {
149                $query
150                    ->whereNull('gedcom_id')
151                    ->orWhere('gedcom_id', '=', $tree->id());
152            })
153            ->max('block_order');
154
155        $title = I18N::translate('Frequently asked questions') . ' — ' . $tree->title();
156
157        return $this->viewResponse('modules/faq/config', [
158            'action'          => route('module', ['module' => $this->name(), 'action' => 'Admin']),
159            'faqs'            => $faqs,
160            'max_block_order' => $max_block_order,
161            'min_block_order' => $min_block_order,
162            'module'          => $this->name(),
163            'title'           => $title,
164            'tree'            => $tree,
165            'tree_names'      => $this->tree_service->titles(),
166        ]);
167    }
168
169    /**
170     * @param ServerRequestInterface $request
171     *
172     * @return ResponseInterface
173     */
174    public function postAdminAction(ServerRequestInterface $request): ResponseInterface
175    {
176        $params = (array) $request->getParsedBody();
177
178        return redirect(route('module', [
179            'module' => $this->name(),
180            'action' => 'Admin',
181            'tree'   => $params['tree'] ?? '',
182        ]));
183    }
184
185    /**
186     * @param ServerRequestInterface $request
187     *
188     * @return ResponseInterface
189     */
190    public function postAdminDeleteAction(ServerRequestInterface $request): ResponseInterface
191    {
192        $block_id = (int) $request->getQueryParams()['block_id'];
193
194        DB::table('block_setting')->where('block_id', '=', $block_id)->delete();
195
196        DB::table('block')->where('block_id', '=', $block_id)->delete();
197
198        $url = route('module', [
199            'module' => $this->name(),
200            'action' => 'Admin',
201        ]);
202
203        return redirect($url);
204    }
205
206    /**
207     * @param ServerRequestInterface $request
208     *
209     * @return ResponseInterface
210     */
211    public function postAdminMoveDownAction(ServerRequestInterface $request): ResponseInterface
212    {
213        $block_id = (int) $request->getQueryParams()['block_id'];
214
215        $block_order = DB::table('block')
216            ->where('block_id', '=', $block_id)
217            ->value('block_order');
218
219        $swap_block = DB::table('block')
220            ->where('module_name', '=', $this->name())
221            ->where('block_order', '>', $block_order)
222            ->orderBy('block_order')
223            ->first();
224
225        if ($block_order !== null && $swap_block !== null) {
226            DB::table('block')
227                ->where('block_id', '=', $block_id)
228                ->update([
229                    'block_order' => $swap_block->block_order,
230                ]);
231
232            DB::table('block')
233                ->where('block_id', '=', $swap_block->block_id)
234                ->update([
235                    'block_order' => $block_order,
236                ]);
237        }
238
239        return response();
240    }
241
242    /**
243     * @param ServerRequestInterface $request
244     *
245     * @return ResponseInterface
246     */
247    public function postAdminMoveUpAction(ServerRequestInterface $request): ResponseInterface
248    {
249        $block_id = (int) $request->getQueryParams()['block_id'];
250
251        $block_order = DB::table('block')
252            ->where('block_id', '=', $block_id)
253            ->value('block_order');
254
255        $swap_block = DB::table('block')
256            ->where('module_name', '=', $this->name())
257            ->where('block_order', '<', $block_order)
258            ->orderBy('block_order', 'desc')
259            ->first();
260
261        if ($block_order !== null && $swap_block !== null) {
262            DB::table('block')
263                ->where('block_id', '=', $block_id)
264                ->update([
265                    'block_order' => $swap_block->block_order,
266                ]);
267
268            DB::table('block')
269                ->where('block_id', '=', $swap_block->block_id)
270                ->update([
271                    'block_order' => $block_order,
272                ]);
273        }
274
275        return response();
276    }
277
278    /**
279     * @param ServerRequestInterface $request
280     *
281     * @return ResponseInterface
282     */
283    public function getAdminEditAction(ServerRequestInterface $request): ResponseInterface
284    {
285        $this->layout = 'layouts/administration';
286
287        $block_id = (int) ($request->getQueryParams()['block_id'] ?? 0);
288
289        if ($block_id === 0) {
290            // Creating a new faq
291            $header      = '';
292            $body        = '';
293            $gedcom_id   = null;
294            $block_order = 1 + (int) DB::table('block')->where('module_name', '=', $this->name())->max('block_order');
295
296            $languages = [];
297
298            $title = I18N::translate('Add an FAQ');
299        } else {
300            // Editing an existing faq
301            $header      = $this->getBlockSetting($block_id, 'header');
302            $body        = $this->getBlockSetting($block_id, 'faqbody');
303            $gedcom_id   = DB::table('block')->where('block_id', '=', $block_id)->value('gedcom_id');
304            $block_order = DB::table('block')->where('block_id', '=', $block_id)->value('block_order');
305
306            $languages = explode(',', $this->getBlockSetting($block_id, 'languages'));
307
308            $title = I18N::translate('Edit the FAQ');
309        }
310
311        $gedcom_ids = $this->tree_service->all()
312            ->mapWithKeys(static function (Tree $tree): array {
313                return [$tree->id() => $tree->title()];
314            })
315            ->all();
316
317        $gedcom_ids = ['' => I18N::translate('All')] + $gedcom_ids;
318
319        return $this->viewResponse('modules/faq/edit', [
320            'block_id'    => $block_id,
321            'block_order' => $block_order,
322            'header'      => $header,
323            'body'        => $body,
324            'languages'   => $languages,
325            'title'       => $title,
326            'gedcom_id'   => $gedcom_id,
327            'gedcom_ids'  => $gedcom_ids,
328        ]);
329    }
330
331    /**
332     * @param ServerRequestInterface $request
333     *
334     * @return ResponseInterface
335     */
336    public function postAdminEditAction(ServerRequestInterface $request): ResponseInterface
337    {
338        $block_id = (int) ($request->getQueryParams()['block_id'] ?? 0);
339
340        $params = (array) $request->getParsedBody();
341
342        $body        = $params['body'];
343        $header      = $params['header'];
344        $languages   = $params['languages'] ?? [];
345        $gedcom_id   = $params['gedcom_id'];
346        $block_order = (int) $params['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    }
430
431    /**
432     * @param Tree   $tree
433     * @param string $language
434     *
435     * @return bool
436     */
437    private function faqsExist(Tree $tree, string $language): bool
438    {
439        return DB::table('block')
440            ->join('block_setting', 'block_setting.block_id', '=', 'block.block_id')
441            ->where('module_name', '=', $this->name())
442            ->where('setting_name', '=', 'languages')
443            ->where(static function (Builder $query) use ($tree): void {
444                $query
445                    ->whereNull('gedcom_id')
446                    ->orWhere('gedcom_id', '=', $tree->id());
447            })
448            ->select(['setting_value AS languages'])
449            ->get()
450            ->filter(static function (object $faq) use ($language): bool {
451                return $faq->languages === '' || in_array($language, explode(',', $faq->languages), true);
452            })
453            ->isNotEmpty();
454    }
455}
456