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