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