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