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