xref: /webtrees/app/Module/StoriesModule.php (revision 10e0649788c8d7d4974d81c048ca2b225df8f22e)
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\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        $params = (array) $request->getParsedBody();
266
267        return redirect(route('module', [
268            'module' => $this->name(),
269            'action' => 'Admin',
270            'tree'   => $params['tree'] ?? '',
271        ]));
272    }
273
274    /**
275     * @param ServerRequestInterface $request
276     *
277     * @return ResponseInterface
278     */
279    public function getAdminEditAction(ServerRequestInterface $request): ResponseInterface
280    {
281        $this->layout = 'layouts/administration';
282
283        $tree     = Validator::attributes($request)->tree();
284        $block_id = Validator::queryParams($request)->integer('block_id', 0);
285        $url      = Validator::queryParams($request)->string('url', '');
286
287        if ($block_id === 0) {
288            // Creating a new story
289            $story_title = '';
290            $story_body  = '';
291            $languages   = [];
292            $xref        = $request->getQueryParams()['xref'] ?? '';
293            $title       = I18N::translate('Add a story') . ' — ' . e($tree->title());
294        } else {
295            // Editing an existing story
296            $xref = (string) DB::table('block')
297                ->where('block_id', '=', $block_id)
298                ->value('xref');
299
300            $story_title = $this->getBlockSetting($block_id, 'title');
301            $story_body  = $this->getBlockSetting($block_id, 'story_body');
302            $languages   = explode(',', $this->getBlockSetting($block_id, 'languages'));
303            $title       = I18N::translate('Edit the story') . ' — ' . e($tree->title());
304        }
305
306        $individual = Registry::individualFactory()->make($xref, $tree);
307
308        return $this->viewResponse('modules/stories/edit', [
309            'block_id'    => $block_id,
310            'languages'   => $languages,
311            'story_body'  => $story_body,
312            'story_title' => $story_title,
313            'title'       => $title,
314            'tree'        => $tree,
315            'url'         => $url,
316            'individual'  => $individual,
317        ]);
318    }
319
320    /**
321     * @param ServerRequestInterface $request
322     *
323     * @return ResponseInterface
324     */
325    public function postAdminEditAction(ServerRequestInterface $request): ResponseInterface
326    {
327        $tree        = Validator::attributes($request)->tree();
328        $block_id    = Validator::queryParams($request)->integer('block_id', 0);
329        $xref        = Validator::parsedBody($request)->string('xref');
330        $story_body  = Validator::parsedBody($request)->string('story_body');
331        $story_title = Validator::parsedBody($request)->string('story_title');
332        $languages   = Validator::parsedBody($request)->array('languages');
333        $default_url = route('module', ['module' => $this->name(), 'action' => 'Admin', 'tree' => $tree->name()]);
334        $url         = Validator::parsedBody($request)->isLocalUrl()->string('url', $default_url);
335        $story_body  = $this->html_service->sanitize($story_body);
336
337        if ($block_id !== 0) {
338            DB::table('block')
339                ->where('block_id', '=', $block_id)
340                ->update([
341                    'gedcom_id' => $tree->id(),
342                    'xref'      => $xref,
343                ]);
344        } else {
345            DB::table('block')->insert([
346                'gedcom_id'   => $tree->id(),
347                'xref'        => $xref,
348                'module_name' => $this->name(),
349                'block_order' => 0,
350            ]);
351
352            $block_id = (int) DB::connection()->getPdo()->lastInsertId();
353        }
354
355        $this->setBlockSetting($block_id, 'story_body', $story_body);
356        $this->setBlockSetting($block_id, 'title', $story_title);
357        $this->setBlockSetting($block_id, 'languages', implode(',', $languages));
358
359        return redirect($url);
360    }
361
362    /**
363     * @param ServerRequestInterface $request
364     *
365     * @return ResponseInterface
366     */
367    public function postAdminDeleteAction(ServerRequestInterface $request): ResponseInterface
368    {
369        $tree     = Validator::attributes($request)->tree();
370        $block_id = $request->getQueryParams()['block_id'];
371
372        DB::table('block_setting')
373            ->where('block_id', '=', $block_id)
374            ->delete();
375
376        DB::table('block')
377            ->where('block_id', '=', $block_id)
378            ->delete();
379
380        $url = route('module', [
381            'module' => $this->name(),
382            'action' => 'Admin',
383            'tree'    => $tree->name(),
384        ]);
385
386        return redirect($url);
387    }
388
389    /**
390     * @param ServerRequestInterface $request
391     *
392     * @return ResponseInterface
393     */
394    public function getShowListAction(ServerRequestInterface $request): ResponseInterface
395    {
396        $tree = Validator::attributes($request)->tree();
397
398        $stories = DB::table('block')
399            ->where('module_name', '=', $this->name())
400            ->where('gedcom_id', '=', $tree->id())
401            ->get()
402            ->map(function (object $story) use ($tree): object {
403                $block_id = (int) $story->block_id;
404                $xref     = (string) $story->xref;
405
406                $story->individual = Registry::individualFactory()->make($xref, $tree);
407                $story->title      = $this->getBlockSetting($block_id, 'title');
408                $story->languages  = $this->getBlockSetting($block_id, 'languages');
409
410                return $story;
411            })->filter(static function (object $story): bool {
412                // Filter non-existent and private individuals.
413                return $story->individual instanceof Individual && $story->individual->canShow();
414            })->filter(static function (object $story): bool {
415                // Filter foreign languages.
416                return $story->languages === '' || in_array(I18N::languageTag(), explode(',', $story->languages), true);
417            });
418
419        return $this->viewResponse('modules/stories/list', [
420            'stories' => $stories,
421            'title'   => $this->title(),
422            'tree'    => $tree,
423        ]);
424    }
425}
426