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