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