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