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