xref: /webtrees/app/Module/ModuleThemeTrait.php (revision 22e73debfe88ce217451588465f714b2b8a966ab)
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\Auth;
23use Fisharebest\Webtrees\Fact;
24use Fisharebest\Webtrees\Gedcom;
25use Fisharebest\Webtrees\GedcomTag;
26use Fisharebest\Webtrees\Http\RequestHandlers\ControlPanel;
27use Fisharebest\Webtrees\Http\RequestHandlers\HomePage;
28use Fisharebest\Webtrees\Http\RequestHandlers\LoginPage;
29use Fisharebest\Webtrees\Http\RequestHandlers\Logout;
30use Fisharebest\Webtrees\Http\RequestHandlers\PendingChanges;
31use Fisharebest\Webtrees\Http\RequestHandlers\SelectLanguage;
32use Fisharebest\Webtrees\Http\RequestHandlers\SelectTheme;
33use Fisharebest\Webtrees\I18N;
34use Fisharebest\Webtrees\Individual;
35use Fisharebest\Webtrees\Menu;
36use Fisharebest\Webtrees\Services\ModuleService;
37use Fisharebest\Webtrees\Tree;
38use Fisharebest\Webtrees\Webtrees;
39use InvalidArgumentException;
40use Psr\Http\Message\ServerRequestInterface;
41
42use function app;
43use function route;
44
45/**
46 * Trait ModuleThemeTrait - default implementation of ModuleThemeInterface
47 */
48trait ModuleThemeTrait
49{
50    /**
51     * @return string
52     */
53    abstract public function name(): string;
54
55    /**
56     * @return string
57     */
58    abstract public function title(): string;
59
60    /**
61     * A sentence describing what this module does.
62     *
63     * @return string
64     */
65    public function description(): string
66    {
67        return I18N::translate('Theme') . ' — ' . $this->title();
68    }
69
70    /**
71     * Display an icon for this fact.
72     *
73     * @TODO use CSS for this
74     *
75     * @param Fact $fact
76     *
77     * @return string
78     */
79    public function icon(Fact $fact): string
80    {
81        $asset = 'public/css/' . $this->name() . '/images/facts/' . $fact->getTag() . '.png';
82        if (file_exists(Webtrees::ROOT_DIR . 'public' . $asset)) {
83            return '<img src="' . e(asset($asset)) . '" title="' . GedcomTag::getLabel($fact->getTag()) . '">';
84        }
85
86        // Spacer image - for alignment - until we move to a sprite.
87        $asset = 'public/css/' . $this->name() . '/images/facts/NULL.png';
88        if (file_exists(Webtrees::ROOT_DIR . 'public' . $asset)) {
89            return '<img src="' . e(asset($asset)) . '">';
90        }
91
92        return '';
93    }
94
95    /**
96     * Generate the facts, for display in charts.
97     *
98     * @param Individual $individual
99     *
100     * @return string
101     */
102    public function individualBoxFacts(Individual $individual): string
103    {
104        $html = '';
105
106        $opt_tags = preg_split('/\W/', $individual->tree()->getPreference('CHART_BOX_TAGS'), 0, PREG_SPLIT_NO_EMPTY);
107        // Show BIRT or equivalent event
108        foreach (Gedcom::BIRTH_EVENTS as $birttag) {
109            if (!in_array($birttag, $opt_tags, true)) {
110                $event = $individual->facts([$birttag])->first();
111                if ($event instanceof Fact) {
112                    $html .= $event->summary();
113                    break;
114                }
115            }
116        }
117        // Show optional events (before death)
118        foreach ($opt_tags as $key => $tag) {
119            if (!in_array($tag, Gedcom::DEATH_EVENTS, true)) {
120                $event = $individual->facts([$tag])->first();
121                if ($event instanceof Fact) {
122                    $html .= $event->summary();
123                    unset($opt_tags[$key]);
124                }
125            }
126        }
127        // Show DEAT or equivalent event
128        foreach (Gedcom::DEATH_EVENTS as $deattag) {
129            $event = $individual->facts([$deattag])->first();
130            if ($event instanceof Fact) {
131                $html .= $event->summary();
132                if (in_array($deattag, $opt_tags, true)) {
133                    unset($opt_tags[array_search($deattag, $opt_tags, true)]);
134                }
135                break;
136            }
137        }
138        // Show remaining optional events (after death)
139        foreach ($opt_tags as $tag) {
140            $event = $individual->facts([$tag])->first();
141            if ($event instanceof Fact) {
142                $html .= $event->summary();
143            }
144        }
145
146        return $html;
147    }
148
149    /**
150     * Links, to show in chart boxes;
151     *
152     * @param Individual $individual
153     *
154     * @return Menu[]
155     */
156    public function individualBoxMenu(Individual $individual): array
157    {
158        $menus = array_merge(
159            $this->individualBoxMenuCharts($individual),
160            $this->individualBoxMenuFamilyLinks($individual)
161        );
162
163        return $menus;
164    }
165
166    /**
167     * Chart links, to show in chart boxes;
168     *
169     * @param Individual $individual
170     *
171     * @return Menu[]
172     */
173    public function individualBoxMenuCharts(Individual $individual): array
174    {
175        $menus = [];
176        foreach (app(ModuleService::class)->findByComponent(ModuleChartInterface::class, $individual->tree(), Auth::user()) as $chart) {
177            $menu = $chart->chartBoxMenu($individual);
178            if ($menu) {
179                $menus[] = $menu;
180            }
181        }
182
183        usort($menus, static function (Menu $x, Menu $y): int {
184            return I18N::strcasecmp($x->getLabel(), $y->getLabel());
185        });
186
187        return $menus;
188    }
189
190    /**
191     * Family links, to show in chart boxes.
192     *
193     * @param Individual $individual
194     *
195     * @return Menu[]
196     */
197    public function individualBoxMenuFamilyLinks(Individual $individual): array
198    {
199        $menus = [];
200
201        foreach ($individual->spouseFamilies() as $family) {
202            $menus[] = new Menu('<strong>' . I18N::translate('Family with spouse') . '</strong>', $family->url());
203            $spouse  = $family->spouse($individual);
204            if ($spouse && $spouse->canShowName()) {
205                $menus[] = new Menu($spouse->fullName(), $spouse->url());
206            }
207            foreach ($family->children() as $child) {
208                if ($child->canShowName()) {
209                    $menus[] = new Menu($child->fullName(), $child->url());
210                }
211            }
212        }
213
214        return $menus;
215    }
216
217    /**
218     * Generate a menu item to change the blocks on the current tree/user page.
219     *
220     * @param Tree $tree
221     *
222     * @return Menu|null
223     */
224    public function menuChangeBlocks(Tree $tree): ?Menu
225    {
226        /** @var ServerRequestInterface $request */
227        $request = app(ServerRequestInterface::class);
228
229        $route = $request->getAttribute('route');
230
231        if (Auth::check() && $route === 'user-page') {
232            return new Menu(I18N::translate('Customize this page'), route('user-page-edit', ['tree' => $tree->name()]), 'menu-change-blocks');
233        }
234
235        if (Auth::isManager($tree) && $route === 'tree-page') {
236            return new Menu(I18N::translate('Customize this page'), route('tree-page-edit', ['tree' => $tree->name()]), 'menu-change-blocks');
237        }
238
239        return null;
240    }
241
242    /**
243     * Generate a menu item for the control panel.
244     *
245     * @param Tree $tree
246     *
247     * @return Menu|null
248     */
249    public function menuControlPanel(Tree $tree): ?Menu
250    {
251        if (Auth::isAdmin()) {
252            return new Menu(I18N::translate('Control panel'), route(ControlPanel::class), 'menu-admin');
253        }
254
255        if (Auth::isManager($tree)) {
256            return new Menu(I18N::translate('Control panel'), route('manage-trees'), 'menu-admin');
257        }
258
259        return null;
260    }
261
262    /**
263     * A menu to show a list of available languages.
264     *
265     * @return Menu|null
266     */
267    public function menuLanguages(): ?Menu
268    {
269        $menu = new Menu(I18N::translate('Language'), '#', 'menu-language');
270
271        foreach (I18N::activeLocales() as $locale) {
272            $language_tag = $locale->languageTag();
273            $class        = 'menu-language-' . $language_tag . (WT_LOCALE === $language_tag ? ' active' : '');
274            $menu->addSubmenu(new Menu($locale->endonym(), '#', $class, [
275                'data-post-url' => route(SelectLanguage::class, ['language' => $language_tag]),
276            ]));
277        }
278
279        if (count($menu->getSubmenus()) > 1) {
280            return $menu;
281        }
282
283        return null;
284    }
285
286    /**
287     * A login menu option (or null if we are already logged in).
288     *
289     * @return Menu|null
290     */
291    public function menuLogin(): ?Menu
292    {
293        if (Auth::check()) {
294            return null;
295        }
296
297        $request = app(ServerRequestInterface::class);
298
299        // Return to this page after login...
300        $redirect = (string) $request->getUri();
301
302        // ...but switch from the tree-page to the user-page
303        if ($request->getAttribute('route') === 'tree-page') {
304            $tree = $request->getAttribute('tree');
305            assert($tree instanceof Tree, new InvalidArgumentException());
306            $redirect  = route('user-page', ['tree' => $tree->name()]);
307        }
308
309        // Stay on the same tree page
310        $tree = $request->getAttribute('tree');
311        $url  = route(LoginPage::class, ['tree' => $tree instanceof Tree ? $tree->name() : null, 'url' => $redirect]);
312
313        return new Menu(I18N::translate('Sign in'), $url, 'menu-login', ['rel' => 'nofollow']);
314    }
315
316    /**
317     * A logout menu option (or null if we are already logged out).
318     *
319     * @return Menu|null
320     */
321    public function menuLogout(): ?Menu
322    {
323        if (Auth::check()) {
324            $parameters = [
325                'data-post-url'   => route(Logout::class),
326                'data-reload-url' => route(HomePage::class),
327            ];
328
329            return new Menu(I18N::translate('Sign out'), '#', 'menu-logout', $parameters);
330        }
331
332        return null;
333    }
334
335    /**
336     * A link to allow users to edit their account settings.
337     *
338     * @return Menu|null
339     */
340    public function menuMyAccount(): ?Menu
341    {
342        if (Auth::check()) {
343            return new Menu(I18N::translate('My account'), route('my-account'));
344        }
345
346        return null;
347    }
348
349    /**
350     * A link to the user's individual record (individual.php).
351     *
352     * @param Tree $tree
353     *
354     * @return Menu|null
355     */
356    public function menuMyIndividualRecord(Tree $tree): ?Menu
357    {
358        $record = Individual::getInstance($tree->getUserPreference(Auth::user(), 'gedcomid'), $tree);
359
360        if ($record) {
361            return new Menu(I18N::translate('My individual record'), $record->url(), 'menu-myrecord');
362        }
363
364        return null;
365    }
366
367    /**
368     * A link to the user's personal home page.
369     *
370     * @param Tree $tree
371     *
372     * @return Menu
373     */
374    public function menuMyPage(Tree $tree): Menu
375    {
376        return new Menu(I18N::translate('My page'), route('user-page', ['tree' => $tree->name()]), 'menu-mypage');
377    }
378
379    /**
380     * A menu for the user's personal pages.
381     *
382     * @param Tree|null $tree
383     *
384     * @return Menu|null
385     */
386    public function menuMyPages(?Tree $tree): ?Menu
387    {
388        if ($tree instanceof Tree && Auth::id()) {
389            return new Menu(I18N::translate('My pages'), '#', 'menu-mymenu', [], array_filter([
390                $this->menuMyPage($tree),
391                $this->menuMyIndividualRecord($tree),
392                $this->menuMyPedigree($tree),
393                $this->menuMyAccount(),
394                $this->menuControlPanel($tree),
395                $this->menuChangeBlocks($tree),
396            ]));
397        }
398
399        return null;
400    }
401
402    /**
403     * A link to the user's individual record.
404     *
405     * @param Tree $tree
406     *
407     * @return Menu|null
408     */
409    public function menuMyPedigree(Tree $tree): ?Menu
410    {
411        $gedcomid = $tree->getUserPreference(Auth::user(), 'gedcomid');
412
413        $pedigree_chart = app(ModuleService::class)->findByComponent(ModuleChartInterface::class, $tree, Auth::user())
414            ->filter(static function (ModuleInterface $module): bool {
415                return $module instanceof PedigreeChartModule;
416            });
417
418        if ($gedcomid !== '' && $pedigree_chart instanceof PedigreeChartModule) {
419            return new Menu(
420                I18N::translate('My pedigree'),
421                route('pedigree', [
422                    'xref' => $gedcomid,
423                    'tree'  => $tree->name(),
424                ]),
425                'menu-mypedigree'
426            );
427        }
428
429        return null;
430    }
431
432    /**
433     * Create a pending changes menu.
434     *
435     * @param Tree|null $tree
436     *
437     * @return Menu|null
438     */
439    public function menuPendingChanges(?Tree $tree): ?Menu
440    {
441        if ($tree instanceof Tree && $tree->hasPendingEdit() && Auth::isModerator($tree)) {
442            $url = route(PendingChanges::class, [
443                'tree' => $tree->name(),
444                'url' => (string) app(ServerRequestInterface::class)->getUri(),
445            ]);
446
447            return new Menu(I18N::translate('Pending changes'), $url, 'menu-pending');
448        }
449
450        return null;
451    }
452
453    /**
454     * Themes menu.
455     *
456     * @return Menu|null
457     */
458    public function menuThemes(): ?Menu
459    {
460        $themes = app(ModuleService::class)->findByInterface(ModuleThemeInterface::class, false, true);
461
462        $current_theme = app(ModuleThemeInterface::class);
463
464        if ($themes->count() > 1) {
465            $submenus = $themes->map(static function (ModuleThemeInterface $theme) use ($current_theme): Menu {
466                $active     = $theme->name() === $current_theme->name();
467                $class      = 'menu-theme-' . $theme->name() . ($active ? ' active' : '');
468
469                return new Menu($theme->title(), '#', $class, [
470                    'data-post-url' => route(SelectTheme::class, ['theme' => $theme->name()]),
471                ]);
472            });
473
474            return new Menu(I18N::translate('Theme'), '#', 'menu-theme', [], $submenus->all());
475        }
476
477        return null;
478    }
479
480    /**
481     * Misecellaneous dimensions, fonts, styles, etc.
482     *
483     * @param string $parameter_name
484     *
485     * @return string|int|float
486     */
487    public function parameter($parameter_name)
488    {
489        return '';
490    }
491
492    /**
493     * Generate a list of items for the main menu.
494     *
495     * @param Tree|null $tree
496     *
497     * @return Menu[]
498     */
499    public function genealogyMenu(?Tree $tree): array
500    {
501        if ($tree === null) {
502            return [];
503        }
504
505        return app(ModuleService::class)->findByComponent(ModuleMenuInterface::class, $tree, Auth::user())
506            ->map(static function (ModuleMenuInterface $menu) use ($tree): ?Menu {
507                return $menu->getMenu($tree);
508            })
509            ->filter()
510            ->all();
511    }
512
513    /**
514     * Create the genealogy menu.
515     *
516     * @param Menu[] $menus
517     *
518     * @return string
519     */
520    public function genealogyMenuContent(array $menus): string
521    {
522        return implode('', array_map(static function (Menu $menu): string {
523            return $menu->bootstrap4();
524        }, $menus));
525    }
526
527    /**
528     * Generate a list of items for the user menu.
529     *
530     * @param Tree|null $tree
531     *
532     * @return Menu[]
533     */
534    public function userMenu(?Tree $tree): array
535    {
536        return array_filter([
537            $this->menuPendingChanges($tree),
538            $this->menuMyPages($tree),
539            $this->menuThemes(),
540            $this->menuLanguages(),
541            $this->menuLogin(),
542            $this->menuLogout(),
543        ]);
544    }
545
546    /**
547     * A list of CSS files to include for this page.
548     *
549     * @return string[]
550     */
551    public function stylesheets(): array
552    {
553        return [];
554    }
555}
556