xref: /webtrees/app/Module/StoriesModule.php (revision d11be7027e34e3121be11cc025421873364403f9)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2023 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\Auth;
23use Fisharebest\Webtrees\Http\RequestHandlers\ControlPanel;
24use Fisharebest\Webtrees\I18N;
25use Fisharebest\Webtrees\Individual;
26use Fisharebest\Webtrees\Menu;
27use Fisharebest\Webtrees\Registry;
28use Fisharebest\Webtrees\Services\HtmlService;
29use Fisharebest\Webtrees\Services\TreeService;
30use Fisharebest\Webtrees\Tree;
31use Fisharebest\Webtrees\Validator;
32use Illuminate\Database\Capsule\Manager as DB;
33use Psr\Http\Message\ResponseInterface;
34use Psr\Http\Message\ServerRequestInterface;
35
36use function in_array;
37use function redirect;
38use function route;
39
40/**
41 * Class StoriesModule
42 */
43class StoriesModule extends AbstractModule implements ModuleConfigInterface, ModuleMenuInterface, ModuleTabInterface
44{
45    use ModuleTabTrait;
46    use ModuleConfigTrait;
47    use ModuleMenuTrait;
48
49    private HtmlService $html_service;
50
51    private TreeService $tree_service;
52
53    /**
54     * StoriesModule 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    /** @var int The default access level for this module.  It can be changed in the control panel. */
66    protected int $access_level = Auth::PRIV_HIDE;
67
68    /**
69     * A sentence describing what this module does.
70     *
71     * @return string
72     */
73    public function description(): string
74    {
75        /* I18N: Description of the “Stories” module */
76        return I18N::translate('Add narrative stories to individuals in the family tree.');
77    }
78
79    /**
80     * The default position for this menu.  It can be changed in the control panel.
81     *
82     * @return int
83     */
84    public function defaultMenuOrder(): int
85    {
86        return 7;
87    }
88
89    /**
90     * The default position for this tab.  It can be changed in the control panel.
91     *
92     * @return int
93     */
94    public function defaultTabOrder(): int
95    {
96        return 9;
97    }
98
99    /**
100     * Generate the HTML content of this tab.
101     *
102     * @param Individual $individual
103     *
104     * @return string
105     */
106    public function getTabContent(Individual $individual): string
107    {
108        return view('modules/stories/tab', [
109            'is_admin'   => Auth::isAdmin(),
110            'individual' => $individual,
111            'stories'    => $this->getStoriesForIndividual($individual),
112            'tree'       => $individual->tree(),
113        ]);
114    }
115
116    /**
117     * @param Individual $individual
118     *
119     * @return array<object>
120     */
121    private function getStoriesForIndividual(Individual $individual): array
122    {
123        $block_ids = DB::table('block')
124            ->where('module_name', '=', $this->name())
125            ->where('xref', '=', $individual->xref())
126            ->where('gedcom_id', '=', $individual->tree()->id())
127            ->pluck('block_id');
128
129        $stories = [];
130        foreach ($block_ids as $block_id) {
131            $block_id = (int) $block_id;
132
133            // Only show this block for certain languages
134            $languages = $this->getBlockSetting($block_id, 'languages');
135            if ($languages === '' || in_array(I18N::languageTag(), explode(',', $languages), true)) {
136                $stories[] = (object) [
137                    'block_id'   => $block_id,
138                    'title'      => $this->getBlockSetting($block_id, 'title'),
139                    'story_body' => $this->getBlockSetting($block_id, 'story_body'),
140                ];
141            }
142        }
143
144        return $stories;
145    }
146
147    /**
148     * Is this tab empty? If so, we don't always need to display it.
149     *
150     * @param Individual $individual
151     *
152     * @return bool
153     */
154    public function hasTabContent(Individual $individual): bool
155    {
156        return Auth::isManager($individual->tree()) || $this->getStoriesForIndividual($individual) !== [];
157    }
158
159    /**
160     * A greyed out tab has no actual content, but may perhaps have
161     * options to create content.
162     *
163     * @param Individual $individual
164     *
165     * @return bool
166     */
167    public function isGrayedOut(Individual $individual): bool
168    {
169        return $this->getStoriesForIndividual($individual) === [];
170    }
171
172    /**
173     * Can this tab load asynchronously?
174     *
175     * @return bool
176     */
177    public function canLoadAjax(): bool
178    {
179        return false;
180    }
181
182    /**
183     * A menu, to be added to the main application menu.
184     *
185     * @param Tree $tree
186     *
187     * @return Menu|null
188     */
189    public function getMenu(Tree $tree): ?Menu
190    {
191        return new Menu($this->title(), route('module', [
192            'module' => $this->name(),
193            'action' => 'ShowList',
194            'tree'    => $tree->name(),
195        ]), 'menu-story');
196    }
197
198    /**
199     * How should this module be identified in the control panel, etc.?
200     *
201     * @return string
202     */
203    public function title(): string
204    {
205        /* I18N: Name of a module */
206        return I18N::translate('Stories');
207    }
208
209    /**
210     * @param ServerRequestInterface $request
211     *
212     * @return ResponseInterface
213     */
214    public function getAdminAction(ServerRequestInterface $request): ResponseInterface
215    {
216        $this->layout = 'layouts/administration';
217
218        // This module can't run without a tree
219        $tree = Validator::attributes($request)->treeOptional();
220
221        if (!$tree instanceof Tree) {
222            $tree = $this->tree_service->all()->first();
223            if ($tree instanceof Tree) {
224                return redirect(route('module', ['module' => $this->name(), 'action' => 'Admin', 'tree' => $tree->name()]));
225            }
226
227            return redirect(route(ControlPanel::class));
228        }
229
230        $stories = DB::table('block')
231            ->where('module_name', '=', $this->name())
232            ->where('gedcom_id', '=', $tree->id())
233            ->orderBy('xref')
234            ->get();
235
236        foreach ($stories as $story) {
237            $block_id = (int) $story->block_id;
238            $xref     = (string) $story->xref;
239
240            $story->individual = Registry::individualFactory()->make($xref, $tree);
241            $story->title      = $this->getBlockSetting($block_id, 'title');
242            $story->languages  = $this->getBlockSetting($block_id, 'languages');
243        }
244
245        $tree_names = $this->tree_service->all()->map(static function (Tree $tree): string {
246            return $tree->title();
247        });
248
249        return $this->viewResponse('modules/stories/config', [
250            'module'     => $this->name(),
251            'stories'    => $stories,
252            'title'      => $this->title() . ' — ' . $tree->title(),
253            'tree'       => $tree,
254            'tree_names' => $tree_names,
255        ]);
256    }
257
258    /**
259     * @param ServerRequestInterface $request
260     *
261     * @return ResponseInterface
262     */
263    public function postAdminAction(ServerRequestInterface $request): ResponseInterface
264    {
265        return redirect(route('module', [
266            'module' => $this->name(),
267            'action' => 'Admin',
268            'tree'   => Validator::parsedBody($request)->string('tree'),
269        ]));
270    }
271
272    /**
273     * @param ServerRequestInterface $request
274     *
275     * @return ResponseInterface
276     */
277    public function getAdminEditAction(ServerRequestInterface $request): ResponseInterface
278    {
279        $this->layout = 'layouts/administration';
280
281        $tree     = Validator::attributes($request)->tree();
282        $block_id = Validator::queryParams($request)->integer('block_id', 0);
283        $url      = Validator::queryParams($request)->string('url', '');
284
285        if ($block_id === 0) {
286            // Creating a new story
287            $story_title = '';
288            $story_body  = '';
289            $languages   = [];
290            $xref        = Validator::queryParams($request)->isXref()->string('xref', '');
291            $title       = I18N::translate('Add a story') . ' — ' . e($tree->title());
292        } else {
293            // Editing an existing story
294            $xref = (string) DB::table('block')
295                ->where('block_id', '=', $block_id)
296                ->value('xref');
297
298            $story_title = $this->getBlockSetting($block_id, 'title');
299            $story_body  = $this->getBlockSetting($block_id, 'story_body');
300            $languages   = explode(',', $this->getBlockSetting($block_id, 'languages'));
301            $title       = I18N::translate('Edit the story') . ' — ' . e($tree->title());
302        }
303
304        $individual = Registry::individualFactory()->make($xref, $tree);
305
306        return $this->viewResponse('modules/stories/edit', [
307            'block_id'    => $block_id,
308            'languages'   => $languages,
309            'story_body'  => $story_body,
310            'story_title' => $story_title,
311            'title'       => $title,
312            'tree'        => $tree,
313            'url'         => $url,
314            'individual'  => $individual,
315        ]);
316    }
317
318    /**
319     * @param ServerRequestInterface $request
320     *
321     * @return ResponseInterface
322     */
323    public function postAdminEditAction(ServerRequestInterface $request): ResponseInterface
324    {
325        $tree        = Validator::attributes($request)->tree();
326        $block_id    = Validator::queryParams($request)->integer('block_id', 0);
327        $xref        = Validator::parsedBody($request)->string('xref');
328        $story_body  = Validator::parsedBody($request)->string('story_body');
329        $story_title = Validator::parsedBody($request)->string('story_title');
330        $languages   = Validator::parsedBody($request)->array('languages');
331        $default_url = route('module', ['module' => $this->name(), 'action' => 'Admin', 'tree' => $tree->name()]);
332        $url         = Validator::parsedBody($request)->isLocalUrl()->string('url', $default_url);
333        $story_body  = $this->html_service->sanitize($story_body);
334
335        if ($block_id !== 0) {
336            DB::table('block')
337                ->where('block_id', '=', $block_id)
338                ->update([
339                    'gedcom_id' => $tree->id(),
340                    'xref'      => $xref,
341                ]);
342        } else {
343            DB::table('block')->insert([
344                'gedcom_id'   => $tree->id(),
345                'xref'        => $xref,
346                'module_name' => $this->name(),
347                'block_order' => 0,
348            ]);
349
350            $block_id = (int) DB::connection()->getPdo()->lastInsertId();
351        }
352
353        $this->setBlockSetting($block_id, 'story_body', $story_body);
354        $this->setBlockSetting($block_id, 'title', $story_title);
355        $this->setBlockSetting($block_id, 'languages', implode(',', $languages));
356
357        return redirect($url);
358    }
359
360    /**
361     * @param ServerRequestInterface $request
362     *
363     * @return ResponseInterface
364     */
365    public function postAdminDeleteAction(ServerRequestInterface $request): ResponseInterface
366    {
367        $tree     = Validator::attributes($request)->tree();
368        $block_id = Validator::queryParams($request)->integer('block_id');
369
370        DB::table('block_setting')
371            ->where('block_id', '=', $block_id)
372            ->delete();
373
374        DB::table('block')
375            ->where('block_id', '=', $block_id)
376            ->delete();
377
378        $url = route('module', [
379            'module' => $this->name(),
380            'action' => 'Admin',
381            'tree'    => $tree->name(),
382        ]);
383
384        return redirect($url);
385    }
386
387    /**
388     * @param ServerRequestInterface $request
389     *
390     * @return ResponseInterface
391     */
392    public function getShowListAction(ServerRequestInterface $request): ResponseInterface
393    {
394        $tree = Validator::attributes($request)->tree();
395
396        $stories = DB::table('block')
397            ->where('module_name', '=', $this->name())
398            ->where('gedcom_id', '=', $tree->id())
399            ->get()
400            ->map(function (object $story) use ($tree): object {
401                $block_id = (int) $story->block_id;
402                $xref     = (string) $story->xref;
403
404                $story->individual = Registry::individualFactory()->make($xref, $tree);
405                $story->title      = $this->getBlockSetting($block_id, 'title');
406                $story->languages  = $this->getBlockSetting($block_id, 'languages');
407
408                return $story;
409            })->filter(static function (object $story): bool {
410                // Filter non-existent and private individuals.
411                return $story->individual instanceof Individual && $story->individual->canShow();
412            })->filter(static function (object $story): bool {
413                // Filter foreign languages.
414                return $story->languages === '' || in_array(I18N::languageTag(), explode(',', $story->languages), true);
415            });
416
417        return $this->viewResponse('modules/stories/list', [
418            'stories' => $stories,
419            'title'   => $this->title(),
420            'tree'    => $tree,
421        ]);
422    }
423}
424