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