xref: /webtrees/app/Module/ModuleThemeTrait.php (revision 14981035a86b1c0b3df76e6eccff5bd50d0359df)
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\Localization\Locale\LocaleInterface;
23use Fisharebest\Webtrees\Auth;
24use Fisharebest\Webtrees\Fact;
25use Fisharebest\Webtrees\Gedcom;
26use Fisharebest\Webtrees\GedcomTag;
27use Fisharebest\Webtrees\Http\RequestHandlers\AccountEdit;
28use Fisharebest\Webtrees\Http\RequestHandlers\ControlPanel;
29use Fisharebest\Webtrees\Http\RequestHandlers\HomePage;
30use Fisharebest\Webtrees\Http\RequestHandlers\LoginPage;
31use Fisharebest\Webtrees\Http\RequestHandlers\Logout;
32use Fisharebest\Webtrees\Http\RequestHandlers\PendingChanges;
33use Fisharebest\Webtrees\Http\RequestHandlers\SelectLanguage;
34use Fisharebest\Webtrees\Http\RequestHandlers\SelectTheme;
35use Fisharebest\Webtrees\I18N;
36use Fisharebest\Webtrees\Individual;
37use Fisharebest\Webtrees\Menu;
38use Fisharebest\Webtrees\Services\ModuleService;
39use Fisharebest\Webtrees\Tree;
40use Fisharebest\Webtrees\Webtrees;
41use Psr\Http\Message\ServerRequestInterface;
42
43use function app;
44use function assert;
45use function route;
46
47/**
48 * Trait ModuleThemeTrait - default implementation of ModuleThemeInterface
49 */
50trait ModuleThemeTrait
51{
52    /**
53     * @return string
54     */
55    abstract public function name(): string;
56
57    /**
58     * @return string
59     */
60    abstract public function title(): string;
61
62    /**
63     * A sentence describing what this module does.
64     *
65     * @return string
66     */
67    public function description(): string
68    {
69        return I18N::translate('Theme') . ' — ' . $this->title();
70    }
71
72    /**
73     * Display an icon for this fact.
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        $locale = app(ServerRequestInterface::class)->getAttribute('locale');
270        assert($locale instanceof LocaleInterface);
271
272        $menu = new Menu(I18N::translate('Language'), '#', 'menu-language');
273
274        foreach (I18N::activeLocales() as $active_locale) {
275            $language_tag = $active_locale->languageTag();
276            $class        = 'menu-language-' . $language_tag . ($locale->languageTag() === $language_tag ? ' active' : '');
277            $menu->addSubmenu(new Menu($active_locale->endonym(), '#', $class, [
278                'data-post-url' => route(SelectLanguage::class, ['language' => $language_tag]),
279            ]));
280        }
281
282        if (count($menu->getSubmenus()) > 1) {
283            return $menu;
284        }
285
286        return null;
287    }
288
289    /**
290     * A login menu option (or null if we are already logged in).
291     *
292     * @return Menu|null
293     */
294    public function menuLogin(): ?Menu
295    {
296        if (Auth::check()) {
297            return null;
298        }
299
300        $request = app(ServerRequestInterface::class);
301
302        // Return to this page after login...
303        $redirect = $request->getQueryParams()['url'] ?? (string) $request->getUri();
304
305        // ...but switch from the tree-page to the user-page
306        if ($request->getAttribute('route') === 'tree-page') {
307            $tree = $request->getAttribute('tree');
308            assert($tree instanceof Tree);
309            $redirect = route('user-page', ['tree' => $tree->name()]);
310        }
311
312        // Stay on the same tree page
313        $tree = $request->getAttribute('tree');
314        $url  = route(LoginPage::class, ['tree' => $tree instanceof Tree ? $tree->name() : null, 'url' => $redirect]);
315
316        return new Menu(I18N::translate('Sign in'), $url, 'menu-login', ['rel' => 'nofollow']);
317    }
318
319    /**
320     * A logout menu option (or null if we are already logged out).
321     *
322     * @return Menu|null
323     */
324    public function menuLogout(): ?Menu
325    {
326        if (Auth::check()) {
327            $parameters = [
328                'data-post-url'   => route(Logout::class),
329            ];
330
331            return new Menu(I18N::translate('Sign out'), '#', 'menu-logout', $parameters);
332        }
333
334        return null;
335    }
336
337    /**
338     * A link to allow users to edit their account settings.
339     *
340     * @param Tree|null $tree
341     *
342     * @return Menu
343     */
344    public function menuMyAccount(?Tree $tree): Menu
345    {
346        $url = route(AccountEdit::class, ['tree' => $tree instanceof Tree ? $tree->name() : null]);
347
348        return new Menu(I18N::translate('My account'), $url);
349    }
350
351    /**
352     * A link to the user's individual record (individual.php).
353     *
354     * @param Tree $tree
355     *
356     * @return Menu|null
357     */
358    public function menuMyIndividualRecord(Tree $tree): ?Menu
359    {
360        $record = Individual::getInstance($tree->getUserPreference(Auth::user(), 'gedcomid'), $tree);
361
362        if ($record) {
363            return new Menu(I18N::translate('My individual record'), $record->url(), 'menu-myrecord');
364        }
365
366        return null;
367    }
368
369    /**
370     * A link to the user's personal home page.
371     *
372     * @param Tree $tree
373     *
374     * @return Menu
375     */
376    public function menuMyPage(Tree $tree): Menu
377    {
378        return new Menu(I18N::translate('My page'), route('user-page', ['tree' => $tree->name()]), 'menu-mypage');
379    }
380
381    /**
382     * A menu for the user's personal pages.
383     *
384     * @param Tree|null $tree
385     *
386     * @return Menu|null
387     */
388    public function menuMyPages(?Tree $tree): ?Menu
389    {
390        if (Auth::id()) {
391            if ($tree instanceof Tree) {
392                return new Menu(I18N::translate('My pages'), '#', 'menu-mymenu', [], array_filter([
393                    $this->menuMyPage($tree),
394                    $this->menuMyIndividualRecord($tree),
395                    $this->menuMyPedigree($tree),
396                    $this->menuMyAccount($tree),
397                    $this->menuControlPanel($tree),
398                    $this->menuChangeBlocks($tree),
399                ]));
400            }
401
402            return $this->menuMyAccount($tree);
403        }
404
405        return null;
406    }
407
408    /**
409     * A link to the user's individual record.
410     *
411     * @param Tree $tree
412     *
413     * @return Menu|null
414     */
415    public function menuMyPedigree(Tree $tree): ?Menu
416    {
417        $gedcomid = $tree->getUserPreference(Auth::user(), 'gedcomid');
418
419        $pedigree_chart = app(ModuleService::class)->findByComponent(ModuleChartInterface::class, $tree, Auth::user())
420            ->filter(static function (ModuleInterface $module): bool {
421                return $module instanceof PedigreeChartModule;
422            });
423
424        if ($gedcomid !== '' && $pedigree_chart instanceof PedigreeChartModule) {
425            return new Menu(
426                I18N::translate('My pedigree'),
427                route('pedigree', [
428                    'xref' => $gedcomid,
429                    'tree'  => $tree->name(),
430                ]),
431                'menu-mypedigree'
432            );
433        }
434
435        return null;
436    }
437
438    /**
439     * Create a pending changes menu.
440     *
441     * @param Tree|null $tree
442     *
443     * @return Menu|null
444     */
445    public function menuPendingChanges(?Tree $tree): ?Menu
446    {
447        if ($tree instanceof Tree && $tree->hasPendingEdit() && Auth::isModerator($tree)) {
448            $url = route(PendingChanges::class, [
449                'tree' => $tree->name(),
450                'url' => (string) app(ServerRequestInterface::class)->getUri(),
451            ]);
452
453            return new Menu(I18N::translate('Pending changes'), $url, 'menu-pending');
454        }
455
456        return null;
457    }
458
459    /**
460     * Themes menu.
461     *
462     * @return Menu|null
463     */
464    public function menuThemes(): ?Menu
465    {
466        $themes = app(ModuleService::class)->findByInterface(ModuleThemeInterface::class, false, true);
467
468        $current_theme = app(ModuleThemeInterface::class);
469
470        if ($themes->count() > 1) {
471            $submenus = $themes->map(static function (ModuleThemeInterface $theme) use ($current_theme): Menu {
472                $active     = $theme->name() === $current_theme->name();
473                $class      = 'menu-theme-' . $theme->name() . ($active ? ' active' : '');
474
475                return new Menu($theme->title(), '#', $class, [
476                    'data-post-url' => route(SelectTheme::class, ['theme' => $theme->name()]),
477                ]);
478            });
479
480            return new Menu(I18N::translate('Theme'), '#', 'menu-theme', [], $submenus->all());
481        }
482
483        return null;
484    }
485
486    /**
487     * Misecellaneous dimensions, fonts, styles, etc.
488     *
489     * @param string $parameter_name
490     *
491     * @return string|int|float
492     */
493    public function parameter($parameter_name)
494    {
495        return '';
496    }
497
498    /**
499     * Generate a list of items for the main menu.
500     *
501     * @param Tree|null $tree
502     *
503     * @return Menu[]
504     */
505    public function genealogyMenu(?Tree $tree): array
506    {
507        if ($tree === null) {
508            return [];
509        }
510
511        return app(ModuleService::class)->findByComponent(ModuleMenuInterface::class, $tree, Auth::user())
512            ->map(static function (ModuleMenuInterface $menu) use ($tree): ?Menu {
513                return $menu->getMenu($tree);
514            })
515            ->filter()
516            ->all();
517    }
518
519    /**
520     * Create the genealogy menu.
521     *
522     * @param Menu[] $menus
523     *
524     * @return string
525     */
526    public function genealogyMenuContent(array $menus): string
527    {
528        return implode('', array_map(static function (Menu $menu): string {
529            return $menu->bootstrap4();
530        }, $menus));
531    }
532
533    /**
534     * Generate a list of items for the user menu.
535     *
536     * @param Tree|null $tree
537     *
538     * @return Menu[]
539     */
540    public function userMenu(?Tree $tree): array
541    {
542        return array_filter([
543            $this->menuPendingChanges($tree),
544            $this->menuMyPages($tree),
545            $this->menuThemes(),
546            $this->menuLanguages(),
547            $this->menuLogin(),
548            $this->menuLogout(),
549        ]);
550    }
551
552    /**
553     * A list of CSS files to include for this page.
554     *
555     * @return string[]
556     */
557    public function stylesheets(): array
558    {
559        return [];
560    }
561}
562