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