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