xref: /webtrees/app/Module/FrequentlyAskedQuestionsModule.php (revision 5ea23dc88949638e49adc824fca07edb4c4f4bc7)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2019 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 <http://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     * BatchUpdateModule 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-help');
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        return redirect(route('module', [
180            'module' => $this->name(),
181            'action' => 'Admin',
182            'tree'   => $request->getParsedBody()['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', 'asc')
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 = $request->getParsedBody();
342
343        $body        = $params['body'];
344        $header      = $params['header'];
345        $languages   = $params['languages'] ?? [];
346        $gedcom_id   = $params['gedcom_id'];
347        $block_order = $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 = $request->getAttribute('tree');
393        assert($tree instanceof Tree);
394
395        // Filter foreign languages.
396        $faqs = $this->faqsForTree($tree)
397            ->filter(static function (stdClass $faq): bool {
398                return $faq->languages === '' || in_array(I18N::languageTag(), explode(',', $faq->languages), true);
399            });
400
401        return $this->viewResponse('modules/faq/show', [
402            'faqs'  => $faqs,
403            'title' => I18N::translate('Frequently asked questions'),
404            'tree'  => $tree,
405        ]);
406    }
407
408    /**
409     * @param Tree $tree
410     *
411     * @return Collection
412     */
413    private function faqsForTree(Tree $tree): Collection
414    {
415        return DB::table('block')
416            ->join('block_setting AS bs1', 'bs1.block_id', '=', 'block.block_id')
417            ->join('block_setting AS bs2', 'bs2.block_id', '=', 'block.block_id')
418            ->join('block_setting AS bs3', 'bs3.block_id', '=', 'block.block_id')
419            ->where('module_name', '=', $this->name())
420            ->where('bs1.setting_name', '=', 'header')
421            ->where('bs2.setting_name', '=', 'faqbody')
422            ->where('bs3.setting_name', '=', 'languages')
423            ->where(static function (Builder $query) use ($tree): void {
424                $query
425                    ->whereNull('gedcom_id')
426                    ->orWhere('gedcom_id', '=', $tree->id());
427            })
428            ->orderBy('block_order')
429            ->select(['block.block_id', 'block_order', 'gedcom_id', 'bs1.setting_value AS header', 'bs2.setting_value AS faqbody', 'bs3.setting_value AS languages'])
430            ->get();
431    }
432
433    /**
434     * @param Tree   $tree
435     * @param string $language
436     *
437     * @return bool
438     */
439    private function faqsExist(Tree $tree, string $language): bool
440    {
441        return DB::table('block')
442            ->join('block_setting', 'block_setting.block_id', '=', 'block.block_id')
443            ->where('module_name', '=', $this->name())
444            ->where('setting_name', '=', 'languages')
445            ->where(static function (Builder $query) use ($tree): void {
446                $query
447                    ->whereNull('gedcom_id')
448                    ->orWhere('gedcom_id', '=', $tree->id());
449            })
450            ->select(['setting_value AS languages'])
451            ->get()
452            ->filter(static function (stdClass $faq) use ($language): bool {
453                return $faq->languages === '' || in_array($language, explode(',', $faq->languages), true);
454            })
455            ->isNotEmpty();
456    }
457}
458