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