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