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