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