1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2019 webtrees development team 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * You should have received a copy of the GNU General Public License 14 * along with this program. If not, see <http://www.gnu.org/licenses/>. 15 */ 16namespace Fisharebest\Webtrees\Theme; 17 18use Fisharebest\Webtrees\Auth; 19use Fisharebest\Webtrees\Controller\PageController; 20use Fisharebest\Webtrees\Database; 21use Fisharebest\Webtrees\Fact; 22use Fisharebest\Webtrees\Filter; 23use Fisharebest\Webtrees\FlashMessages; 24use Fisharebest\Webtrees\Functions\Functions; 25use Fisharebest\Webtrees\GedcomRecord; 26use Fisharebest\Webtrees\GedcomTag; 27use Fisharebest\Webtrees\HitCounter; 28use Fisharebest\Webtrees\I18N; 29use Fisharebest\Webtrees\Individual; 30use Fisharebest\Webtrees\Menu; 31use Fisharebest\Webtrees\Module; 32use Fisharebest\Webtrees\Module\AncestorsChartModule; 33use Fisharebest\Webtrees\Module\CompactTreeChartModule; 34use Fisharebest\Webtrees\Module\DescendancyChartModule; 35use Fisharebest\Webtrees\Module\FamilyBookChartModule; 36use Fisharebest\Webtrees\Module\FamilyTreeFavoritesModule; 37use Fisharebest\Webtrees\Module\FanChartModule; 38use Fisharebest\Webtrees\Module\GoogleMapsModule; 39use Fisharebest\Webtrees\Module\HourglassChartModule; 40use Fisharebest\Webtrees\Module\InteractiveTreeModule; 41use Fisharebest\Webtrees\Module\LifespansChartModule; 42use Fisharebest\Webtrees\Module\PedigreeChartModule; 43use Fisharebest\Webtrees\Module\RelationshipsChartModule; 44use Fisharebest\Webtrees\Module\StatisticsChartModule; 45use Fisharebest\Webtrees\Module\TimelineChartModule; 46use Fisharebest\Webtrees\Module\UserFavoritesModule; 47use Fisharebest\Webtrees\Site; 48use Fisharebest\Webtrees\Theme; 49use Fisharebest\Webtrees\Tree; 50use Fisharebest\Webtrees\User; 51 52/** 53 * Common functions for all themes. 54 */ 55abstract class AbstractTheme 56{ 57 /** @var Tree The current tree */ 58 protected $tree; 59 60 /** @var string An escaped version of the "ged=XXX" URL parameter */ 61 protected $tree_url; 62 63 /** @var int The number of times this page has been shown */ 64 protected $page_views; 65 66 /** 67 * Custom themes should place their initialization code in the function hookAfterInit(), not in 68 * the constructor, as all themes get constructed - whether they are used or not. 69 */ 70 final public function __construct() 71 { 72 } 73 74 /** 75 * Create accessibility links for the header. 76 * 77 * "Skip to content" allows keyboard only users to navigate over the headers without 78 * pressing TAB many times. 79 * 80 * @return string 81 */ 82 protected function accessibilityLinks() 83 { 84 return 85 '<div class="accessibility-links">' . 86 '<a class="sr-only sr-only-focusable btn btn-info btn-sm" href="#content">' . 87 /* I18N: Skip over the headers and menus, to the main content of the page */ I18N::translate('Skip to content') . 88 '</a>' . 89 '</div>'; 90 } 91 92 /** 93 * Create scripts for analytics and tracking. 94 * 95 * @return string 96 */ 97 protected function analytics() 98 { 99 if ($this->themeId() === '_administration' || !empty($_SERVER['HTTP_DNT'])) { 100 return ''; 101 } else { 102 return 103 $this->analyticsBingWebmaster( 104 Site::getPreference('BING_WEBMASTER_ID') 105 ) . 106 $this->analyticsGoogleWebmaster( 107 Site::getPreference('GOOGLE_WEBMASTER_ID') 108 ) . 109 $this->analyticsGoogleTracker( 110 Site::getPreference('GOOGLE_ANALYTICS_ID') 111 ) . 112 $this->analyticsPiwikTracker( 113 Site::getPreference('PIWIK_URL'), 114 Site::getPreference('PIWIK_SITE_ID') 115 ) . 116 $this->analyticsStatcounterTracker( 117 Site::getPreference('STATCOUNTER_PROJECT_ID'), 118 Site::getPreference('STATCOUNTER_SECURITY_ID') 119 ); 120 } 121 } 122 123 /** 124 * Create the verification code for Google Webmaster Tools. 125 * 126 * @param string $verification_id 127 * 128 * @return string 129 */ 130 protected function analyticsBingWebmaster($verification_id) 131 { 132 // Only need to add this to the home page. 133 if (WT_SCRIPT_NAME === 'index.php' && $verification_id) { 134 return '<meta name="msvalidate.01" content="' . $verification_id . '">'; 135 } else { 136 return ''; 137 } 138 } 139 140 /** 141 * Create the verification code for Google Webmaster Tools. 142 * 143 * @param string $verification_id 144 * 145 * @return string 146 */ 147 protected function analyticsGoogleWebmaster($verification_id) 148 { 149 // Only need to add this to the home page. 150 if (WT_SCRIPT_NAME === 'index.php' && $verification_id) { 151 return '<meta name="google-site-verification" content="' . $verification_id . '">'; 152 } else { 153 return ''; 154 } 155 } 156 157 /** 158 * Create the tracking code for Google Analytics. 159 * 160 * See https://developers.google.com/analytics/devguides/collection/analyticsjs/advanced 161 * 162 * @param string $analytics_id 163 * 164 * @return string 165 */ 166 protected function analyticsGoogleTracker($analytics_id) 167 { 168 if ($analytics_id) { 169 // Add extra dimensions (i.e. filtering categories) 170 $dimensions = (object) array( 171 'dimension1' => $this->tree ? $this->tree->getName() : '-', 172 'dimension2' => $this->tree ? Auth::accessLevel($this->tree) : '-', 173 ); 174 175 return 176 '<script async src="https://www.google-analytics.com/analytics.js"></script>' . 177 '<script>' . 178 'window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;' . 179 'ga("create","' . $analytics_id . '","auto");' . 180 'ga("send", "pageview", ' . json_encode($dimensions) . ');' . 181 '</script>'; 182 } else { 183 return ''; 184 } 185 } 186 187 /** 188 * Create the tracking code for Piwik Analytics. 189 * 190 * @param string $url - The domain/path to Piwik 191 * @param string $site_id - The Piwik site identifier 192 * 193 * @return string 194 */ 195 protected function analyticsPiwikTracker($url, $site_id) 196 { 197 $url = preg_replace(array('/^https?:\/\//', '/\/$/'), '', $url); 198 199 if ($url && $site_id) { 200 return 201 '<script>' . 202 'var _paq=_paq||[];' . 203 '(function(){var u=(("https:"==document.location.protocol)?"https://' . $url . '/":"http://' . $url . '/");' . 204 '_paq.push(["setSiteId",' . $site_id . ']);' . 205 '_paq.push(["setTrackerUrl",u+"piwik.php"]);' . 206 '_paq.push(["trackPageView"]);' . 207 '_paq.push(["enableLinkTracking"]);' . 208 'var d=document,g=d.createElement("script"),s=d.getElementsByTagName("script")[0];g.defer=true;g.async=true;g.src=u+"piwik.js";' . 209 's.parentNode.insertBefore(g,s);})();' . 210 '</script>'; 211 } else { 212 return ''; 213 } 214 } 215 216 /** 217 * Create the tracking code for Statcounter. 218 * 219 * @param string $project_id - The statcounter project ID 220 * @param string $security_id - The statcounter security ID 221 * 222 * @return string 223 */ 224 protected function analyticsStatcounterTracker($project_id, $security_id) 225 { 226 if ($project_id && $security_id) { 227 return 228 '<script>' . 229 'var sc_project=' . (int) $project_id . ',sc_invisible=1,sc_security="' . $security_id . 230 '",scJsHost = (("https:"===document.location.protocol)?"https://secure.":"http://www.");' . 231 'document.write("<sc"+"ript src=\'"+scJsHost+"statcounter.com/counter/counter.js\'></"+"script>");' . 232 '</script>'; 233 } else { 234 return ''; 235 } 236 } 237 238 /** 239 * Create the top of the <body>. 240 * 241 * @return string 242 */ 243 public function bodyHeader() 244 { 245 return 246 '<body class="container">' . 247 '<header>' . 248 $this->headerContent() . 249 $this->primaryMenuContainer($this->primaryMenu()) . 250 '</header>' . 251 '<main id="content">' . 252 $this->flashMessagesContainer(FlashMessages::getMessages()); 253 } 254 255 /** 256 * Create the top of the <body> (for popup windows). 257 * 258 * @return string 259 */ 260 public function bodyHeaderPopupWindow() 261 { 262 return 263 '<body class="container container-popup">' . 264 '<main id="content">' . 265 $this->flashMessagesContainer(FlashMessages::getMessages()); 266 } 267 268 /** 269 * Create a contact link for a user. 270 * 271 * @param User $user 272 * 273 * @return string 274 */ 275 public function contactLink(User $user) 276 { 277 $method = $user->getPreference('contactmethod'); 278 279 switch ($method) { 280 case 'none': 281 return ''; 282 case 'mailto': 283 return '<a href="mailto:' . Filter::escapeHtml($user->getEmail()) . '">' . $user->getRealNameHtml() . '</a>'; 284 default: 285 return "<a href='#' onclick='message(\"" . Filter::escapeHtml($user->getUserName()) . "\", \"" . $method . "\", \"" . WT_BASE_URL . Filter::escapeHtml(Functions::getQueryUrl()) . "\", \"\");return false;'>" . $user->getRealNameHtml() . '</a>'; 286 } 287 } 288 289 /** 290 * Create contact link for both technical and genealogy support. 291 * 292 * @param User $user 293 * 294 * @return string 295 */ 296 protected function contactLinkEverything(User $user) 297 { 298 return I18N::translate('For technical support or genealogy questions contact %s.', $this->contactLink($user)); 299 } 300 301 /** 302 * Create contact link for genealogy support. 303 * 304 * @param User $user 305 * 306 * @return string 307 */ 308 protected function contactLinkGenealogy(User $user) 309 { 310 return I18N::translate('For help with genealogy questions contact %s.', $this->contactLink($user)); 311 } 312 313 /** 314 * Create contact link for technical support. 315 * 316 * @param User $user 317 * 318 * @return string 319 */ 320 protected function contactLinkTechnical(User $user) 321 { 322 return I18N::translate('For technical support and information contact %s.', $this->contactLink($user)); 323 } 324 325 /** 326 * Create contact links for the page footer. 327 * 328 * @return string 329 */ 330 protected function contactLinks() 331 { 332 $contact_user = User::find($this->tree->getPreference('CONTACT_USER_ID')); 333 $webmaster_user = User::find($this->tree->getPreference('WEBMASTER_USER_ID')); 334 335 if ($contact_user && $contact_user === $webmaster_user) { 336 return $this->contactLinkEverything($contact_user); 337 } elseif ($contact_user && $webmaster_user) { 338 return $this->contactLinkGenealogy($contact_user) . '<br>' . $this->contactLinkTechnical($webmaster_user); 339 } elseif ($contact_user) { 340 return $this->contactLinkGenealogy($contact_user); 341 } elseif ($webmaster_user) { 342 return $this->contactLinkTechnical($webmaster_user); 343 } else { 344 return ''; 345 } 346 } 347 348 /** 349 * Create a cookie warning. 350 * 351 * @return string 352 */ 353 public function cookieWarning() 354 { 355 if ( 356 empty($_SERVER['HTTP_DNT']) && 357 empty($_COOKIE['cookie']) && 358 (Site::getPreference('GOOGLE_ANALYTICS_ID') || Site::getPreference('PIWIK_SITE_ID') || Site::getPreference('STATCOUNTER_PROJECT_ID')) 359 ) { 360 return 361 '<div class="cookie-warning">' . 362 I18N::translate('Cookies') . ' - ' . 363 I18N::translate('This website uses cookies to learn about visitor behaviour.') . ' ' . 364 '<button onclick="document.cookie=\'cookie=1\'; this.parentNode.classList.add(\'hidden\');">' . I18N::translate('continue') . '</button>' . 365 '</div>'; 366 } else { 367 return ''; 368 } 369 } 370 371 /** 372 * Create the <DOCTYPE> tag. 373 * 374 * @return string 375 */ 376 public function doctype() 377 { 378 return '<!DOCTYPE html>'; 379 } 380 381 /** 382 * HTML link to a "favorites icon". 383 * 384 * @return string 385 */ 386 protected function favicon() 387 { 388 return 389 '<link rel="icon" href="' . $this->assetUrl() . 'favicon.png" type="image/png">' . 390 '<link rel="icon" type="image/png" href="' . $this->assetUrl() . 'favicon192.png" sizes="192x192">' . 391 '<link rel="apple-touch-icon" sizes="180x180" href="' . $this->assetUrl() . 'favicon180.png">'; 392 } 393 394 /** 395 * Add markup to a flash message. 396 * 397 * @param \stdClass $message 398 * 399 * @return string 400 */ 401 protected function flashMessageContainer(\stdClass $message) 402 { 403 return $this->htmlAlert($message->text, $message->status, true); 404 } 405 406 /** 407 * Create a container for messages that are "flashed" to the session 408 * on one request, and displayed on another. If there are many messages, 409 * the container may need a max-height and scroll-bar. 410 * 411 * @param \stdClass[] $messages 412 * 413 * @return string 414 */ 415 protected function flashMessagesContainer(array $messages) 416 { 417 $html = ''; 418 foreach ($messages as $message) { 419 $html .= $this->flashMessageContainer($message); 420 } 421 422 if ($html) { 423 return '<div class="flash-messages">' . $html . '</div>'; 424 } else { 425 return ''; 426 } 427 } 428 429 /** 430 * Close the main content and create the <footer> tag. 431 * 432 * @return string 433 */ 434 public function footerContainer() 435 { 436 return '</main><footer>' . $this->footerContent() . '</footer>'; 437 } 438 439 /** 440 * Close the main content. 441 * Note that popup windows are deprecated 442 * 443 * @return string 444 */ 445 public function footerContainerPopupWindow() 446 { 447 return '</main>'; 448 } 449 450 /** 451 * Create the contents of the <footer> tag. 452 * 453 * @return string 454 */ 455 protected function footerContent() 456 { 457 return 458 $this->formatContactLinks() . 459 $this->logoPoweredBy() . 460 $this->formatPageViews($this->page_views) . 461 $this->cookieWarning(); 462 } 463 464 /** 465 * Format the contents of a variable-height home-page block. 466 * 467 * @param string $id 468 * @param string $title 469 * @param string $class 470 * @param string $content 471 * 472 * @return string 473 */ 474 public function formatBlock($id, $title, $class, $content) 475 { 476 return 477 '<div id="' . $id . '" class="block" >' . 478 '<div class="blockheader">' . $title . '</div>' . 479 '<div class="blockcontent ' . $class . '">' . $content . '</div>' . 480 '</div>'; 481 } 482 483 /** 484 * Add markup to the contact links. 485 * 486 * @return string 487 */ 488 protected function formatContactLinks() 489 { 490 if ($this->tree) { 491 return '<div class="contact-links">' . $this->contactLinks() . '</div>'; 492 } else { 493 return ''; 494 } 495 } 496 497 /** 498 * Add markup to the hit counter. 499 * 500 * @param int $count 501 * 502 * @return string 503 */ 504 protected function formatPageViews($count) 505 { 506 if ($count > 0) { 507 return 508 '<div class="page-views">' . 509 I18N::plural('This page has been viewed %s time.', 'This page has been viewed %s times.', $count, 510 '<span class="odometer">' . I18N::digits($count) . '</span>') . 511 '</div>'; 512 } else { 513 return ''; 514 } 515 } 516 517 /** 518 * Create a pending changes link for the page footer. 519 * 520 * @return string 521 */ 522 protected function formatPendingChangesLink() 523 { 524 if ($this->pendingChangesExist()) { 525 return '<div class="pending-changes-link">' . $this->pendingChangesLink() . '</div>'; 526 } else { 527 return ''; 528 } 529 } 530 531 /** 532 * Create a quick search form for the header. 533 * 534 * @return string 535 */ 536 protected function formQuickSearch() 537 { 538 if ($this->tree) { 539 return 540 '<form action="search.php" class="header-search" role="search">' . 541 '<input type="hidden" name="action" value="header">' . 542 '<input type="hidden" name="ged" value="' . $this->tree->getNameHtml() . '">' . 543 $this->formQuickSearchFields() . 544 '</form>'; 545 } else { 546 return ''; 547 } 548 } 549 550 /** 551 * Create a search field and submit button for the quick search form in the header. 552 * 553 * @return string 554 */ 555 protected function formQuickSearchFields() 556 { 557 return 558 '<input type="search" name="query" size="15" placeholder="' . I18N::translate('Search') . '">' . 559 '<input type="image" src="' . $this->assetUrl() . 'images/go.png" alt="' . I18N::translate('Search') . '">'; 560 } 561 562 /** 563 * Add markup to the tree title. 564 * 565 * @return string 566 */ 567 protected function formatTreeTitle() 568 { 569 if ($this->tree) { 570 return '<h1 class="header-title">' . $this->tree->getTitleHtml() . '</h1>'; 571 } else { 572 return ''; 573 } 574 } 575 576 /** 577 * Add markup to the secondary menu. 578 * 579 * @return string 580 */ 581 protected function formatSecondaryMenu() 582 { 583 return 584 '<ul class="secondary-menu">' . 585 implode('', $this->secondaryMenu()) . 586 '</ul>'; 587 } 588 589 /** 590 * Add markup to an item in the secondary menu. 591 * 592 * @param Menu $menu 593 * 594 * @return string 595 */ 596 protected function formatSecondaryMenuItem(Menu $menu) 597 { 598 return $menu->getMenuAsList(); 599 } 600 601 /** 602 * Create the <head> tag. 603 * 604 * @param PageController $controller The current controller 605 * 606 * @return string 607 */ 608 public function head(PageController $controller) 609 { 610 // Record this now. By the time we render the footer, $controller no longer exists. 611 $this->page_views = $this->pageViews($controller); 612 613 return 614 '<head>' . 615 $this->headContents($controller) . 616 $this->hookHeaderExtraContent() . 617 $this->analytics() . 618 '</head>'; 619 } 620 621 /** 622 * Create the contents of the <head> tag. 623 * 624 * @param PageController $controller The current controller 625 * 626 * @return string 627 */ 628 protected function headContents(PageController $controller) 629 { 630 // The title often includes the names of records, which may include HTML markup. 631 $title = Filter::unescapeHtml($controller->getPageTitle()); 632 633 // If an extra (site) title is specified, append it. 634 if ($this->tree && $this->tree->getPreference('META_TITLE')) { 635 $title .= ' – ' . $this->tree->getPreference('META_TITLE'); 636 } 637 638 $html = 639 // modernizr.js and respond.js need to be loaded before the <body> to avoid FOUC 640 '<!--[if IE 8]><script src="' . WT_MODERNIZR_JS_URL . '"></script><![endif]-->' . 641 '<!--[if IE 8]><script src="' . WT_RESPOND_JS_URL . '"></script><![endif]-->' . 642 $this->metaCharset() . 643 $this->title($title) . 644 $this->favicon() . 645 $this->metaViewport() . 646 $this->metaRobots($controller->getMetaRobots()) . 647 $this->metaUaCompatible() . 648 $this->metaGenerator(WT_WEBTREES . ' ' . WT_VERSION . ' - ' . WT_WEBTREES_URL); 649 650 if ($this->tree) { 651 $html .= $this->metaDescription($this->tree->getPreference('META_DESCRIPTION')); 652 } 653 654 // CSS files 655 foreach ($this->stylesheets() as $css) { 656 $html .= '<link rel="stylesheet" type="text/css" href="' . $css . '">'; 657 } 658 659 return $html; 660 } 661 662 /** 663 * Create the contents of the <header> tag. 664 * 665 * @return string 666 */ 667 protected function headerContent() 668 { 669 return 670 //$this->accessibilityLinks() . 671 $this->logoHeader() . 672 $this->secondaryMenuContainer($this->secondaryMenu()) . 673 $this->formatTreeTitle() . 674 $this->formQuickSearch(); 675 } 676 677 /** 678 * Create the <header> tag for a popup window. 679 * 680 * @return string 681 */ 682 protected function headerSimple() 683 { 684 return 685 $this->flashMessagesContainer(FlashMessages::getMessages()) . 686 '<div id="content">'; 687 } 688 689 /** 690 * Allow themes to do things after initialization (since they cannot use 691 * the constructor). 692 */ 693 public function hookAfterInit() 694 { 695 } 696 697 /** 698 * Allow themes to add extra scripts to the page footer. 699 * 700 * @return string 701 */ 702 public function hookFooterExtraJavascript() 703 { 704 return ''; 705 } 706 707 /** 708 * Allow themes to add extra content to the page header. 709 * Typically this will be additional CSS. 710 * 711 * @return string 712 */ 713 public function hookHeaderExtraContent() 714 { 715 return ''; 716 } 717 718 /** 719 * Create the <html> tag. 720 * 721 * @return string 722 */ 723 public function html() 724 { 725 return '<html ' . I18N::htmlAttributes() . '>'; 726 } 727 728 /** 729 * Add HTML markup to create an alert 730 * 731 * @param string $html The content of the alert 732 * @param string $level One of 'success', 'info', 'warning', 'danger' 733 * @param bool $dismissible If true, add a close button. 734 * 735 * @return string 736 */ 737 public function htmlAlert($html, $level, $dismissible) 738 { 739 if ($dismissible) { 740 return 741 '<div class="alert alert-' . $level . ' alert-dismissible" role="alert">' . 742 '<button type="button" class="close" data-dismiss="alert" aria-label="' . I18N::translate('close') . '">' . 743 '<span aria-hidden="true">×</span>' . 744 '</button>' . 745 $html . 746 '</div>'; 747 } else { 748 return 749 '<div class="alert alert-' . $level . '" role="alert">' . 750 $html . 751 '</div>'; 752 } 753 } 754 755 /** 756 * Display an icon for this fact. 757 * 758 * @param Fact $fact 759 * 760 * @return string 761 */ 762 public function icon(Fact $fact) 763 { 764 $icon = 'images/facts/' . $fact->getTag() . '.png'; 765 $dir = substr($this->assetUrl(), strlen(WT_STATIC_URL)); 766 if (file_exists($dir . $icon)) { 767 return '<img src="' . $this->assetUrl() . $icon . '" title="' . GedcomTag::getLabel($fact->getTag()) . '">'; 768 } elseif (file_exists($dir . 'images/facts/NULL.png')) { 769 // Spacer image - for alignment - until we move to a sprite. 770 return '<img src="' . Theme::theme()->assetUrl() . 'images/facts/NULL.png">'; 771 } else { 772 return ''; 773 } 774 } 775 776 /** 777 * Display an individual in a box - for charts, etc. 778 * 779 * @param Individual $individual 780 * 781 * @return string 782 */ 783 public function individualBox(Individual $individual) 784 { 785 $personBoxClass = array_search($individual->getSex(), array('person_box' => 'M', 'person_boxF' => 'F', 'person_boxNN' => 'U')); 786 if ($individual->canShow() && $individual->getTree()->getPreference('SHOW_HIGHLIGHT_IMAGES')) { 787 $thumbnail = $individual->displayImage(); 788 } else { 789 $thumbnail = ''; 790 } 791 792 $content = '<span class="namedef name1">' . $individual->getFullName() . '</span>'; 793 $icons = ''; 794 if ($individual->canShow()) { 795 $content = 796 '<a href="' . $individual->getHtmlUrl() . '">' . $content . '</a>' . 797 '<div class="namedef name1">' . $individual->getAddName() . '</div>'; 798 $icons = 799 '<div class="noprint icons">' . 800 '<span class="iconz icon-zoomin" title="' . I18N::translate('Zoom in/out on this box.') . '"></span>' . 801 '<div class="itr"><i class="icon-pedigree"></i><div class="popup">' . 802 '<ul class="' . $personBoxClass . '">' . implode('', $this->individualBoxMenu($individual)) . '</ul>' . 803 '</div>' . 804 '</div>' . 805 '</div>'; 806 } 807 808 return 809 '<div data-pid="' . $individual->getXref() . '" class="person_box_template ' . $personBoxClass . ' box-style1" style="width: ' . $this->parameter('chart-box-x') . 'px; min-height: ' . $this->parameter('chart-box-y') . 'px">' . 810 $icons . 811 '<div class="chart_textbox" style="max-height:' . $this->parameter('chart-box-y') . 'px;">' . 812 $thumbnail . 813 $content . 814 '<div class="inout2 details1">' . $this->individualBoxFacts($individual) . '</div>' . 815 '</div>' . 816 '<div class="inout"></div>' . 817 '</div>'; 818 } 819 820 /** 821 * Display an empty box - for a missing individual in a chart. 822 * 823 * @return string 824 */ 825 public function individualBoxEmpty() 826 { 827 return '<div class="person_box_template person_boxNN box-style1" style="width: ' . $this->parameter('chart-box-x') . 'px; min-height: ' . $this->parameter('chart-box-y') . 'px"></div>'; 828 } 829 830 /** 831 * Display an individual in a box - for charts, etc. 832 * 833 * @param Individual $individual 834 * 835 * @return string 836 */ 837 public function individualBoxLarge(Individual $individual) 838 { 839 $personBoxClass = array_search($individual->getSex(), array('person_box' => 'M', 'person_boxF' => 'F', 'person_boxNN' => 'U')); 840 if ($individual->getTree()->getPreference('SHOW_HIGHLIGHT_IMAGES')) { 841 $thumbnail = $individual->displayImage(); 842 } else { 843 $thumbnail = ''; 844 } 845 846 $content = '<span class="namedef name1">' . $individual->getFullName() . '</span>'; 847 $icons = ''; 848 if ($individual->canShow()) { 849 $content = 850 '<a href="' . $individual->getHtmlUrl() . '">' . $content . '</a>' . 851 '<div class="namedef name2">' . $individual->getAddName() . '</div>'; 852 $icons = 853 '<div class="noprint icons">' . 854 '<span class="iconz icon-zoomin" title="' . I18N::translate('Zoom in/out on this box.') . '"></span>' . 855 '<div class="itr"><i class="icon-pedigree"></i><div class="popup">' . 856 '<ul class="' . $personBoxClass . '">' . implode('', $this->individualBoxMenu($individual)) . '</ul>' . 857 '</div>' . 858 '</div>' . 859 '</div>'; 860 } 861 862 return 863 '<div data-pid="' . $individual->getXref() . '" class="person_box_template ' . $personBoxClass . ' box-style2">' . 864 $icons . 865 '<div class="chart_textbox" style="max-height:' . $this->parameter('chart-box-y') . 'px;">' . 866 $thumbnail . 867 $content . 868 '<div class="inout2 details2">' . $this->individualBoxFacts($individual) . '</div>' . 869 '</div>' . 870 '<div class="inout"></div>' . 871 '</div>'; 872 } 873 874 /** 875 * Display an individual in a box - for charts, etc. 876 * 877 * @param Individual $individual 878 * 879 * @return string 880 */ 881 public function individualBoxSmall(Individual $individual) 882 { 883 $personBoxClass = array_search($individual->getSex(), array('person_box' => 'M', 'person_boxF' => 'F', 'person_boxNN' => 'U')); 884 if ($individual->getTree()->getPreference('SHOW_HIGHLIGHT_IMAGES')) { 885 $thumbnail = $individual->displayImage(); 886 } else { 887 $thumbnail = ''; 888 } 889 890 return 891 '<div data-pid="' . $individual->getXref() . '" class="person_box_template ' . $personBoxClass . ' iconz box-style0" style="width: ' . $this->parameter('compact-chart-box-x') . 'px; min-height: ' . $this->parameter('compact-chart-box-y') . 'px">' . 892 '<div class="compact_view">' . 893 $thumbnail . 894 '<a href="' . $individual->getHtmlUrl() . '">' . 895 '<span class="namedef name0">' . $individual->getFullName() . '</span>' . 896 '</a>' . 897 '<div class="inout2 details0">' . $individual->getLifeSpan() . '</div>' . 898 '</div>' . 899 '<div class="inout"></div>' . 900 '</div>'; 901 } 902 903 /** 904 * Display an individual in a box - for charts, etc. 905 * 906 * @return string 907 */ 908 public function individualBoxSmallEmpty() 909 { 910 return '<div class="person_box_template person_boxNN box-style1" style="width: ' . $this->parameter('compact-chart-box-x') . 'px; min-height: ' . $this->parameter('compact-chart-box-y') . 'px"></div>'; 911 } 912 913 /** 914 * Generate the facts, for display in charts. 915 * 916 * @param Individual $individual 917 * 918 * @return string 919 */ 920 protected function individualBoxFacts(Individual $individual) 921 { 922 $html = ''; 923 924 $opt_tags = preg_split('/\W/', $individual->getTree()->getPreference('CHART_BOX_TAGS'), 0, PREG_SPLIT_NO_EMPTY); 925 // Show BIRT or equivalent event 926 foreach (explode('|', WT_EVENTS_BIRT) as $birttag) { 927 if (!in_array($birttag, $opt_tags)) { 928 $event = $individual->getFirstFact($birttag); 929 if ($event) { 930 $html .= $event->summary(); 931 break; 932 } 933 } 934 } 935 // Show optional events (before death) 936 foreach ($opt_tags as $key => $tag) { 937 if (!preg_match('/^(' . WT_EVENTS_DEAT . ')$/', $tag)) { 938 $event = $individual->getFirstFact($tag); 939 if ($event instanceof Fact) { 940 $html .= $event->summary(); 941 unset($opt_tags[$key]); 942 } 943 } 944 } 945 // Show DEAT or equivalent event 946 foreach (explode('|', WT_EVENTS_DEAT) as $deattag) { 947 $event = $individual->getFirstFact($deattag); 948 if ($event) { 949 $html .= $event->summary(); 950 if (in_array($deattag, $opt_tags)) { 951 unset($opt_tags[array_search($deattag, $opt_tags)]); 952 } 953 break; 954 } 955 } 956 // Show remaining optional events (after death) 957 foreach ($opt_tags as $tag) { 958 $event = $individual->getFirstFact($tag); 959 if ($event) { 960 $html .= $event->summary(); 961 } 962 } 963 964 return $html; 965 } 966 967 /** 968 * Generate the LDS summary, for display in charts. 969 * 970 * @param Individual $individual 971 * 972 * @return string 973 */ 974 protected function individualBoxLdsSummary(Individual $individual) 975 { 976 if ($individual->getTree()->getPreference('SHOW_LDS_AT_GLANCE')) { 977 $BAPL = $individual->getFacts('BAPL') ? 'B' : '_'; 978 $ENDL = $individual->getFacts('ENDL') ? 'E' : '_'; 979 $SLGC = $individual->getFacts('SLGC') ? 'C' : '_'; 980 $SLGS = '_'; 981 982 foreach ($individual->getSpouseFamilies() as $family) { 983 if ($family->getFacts('SLGS')) { 984 $SLGS = ''; 985 } 986 } 987 988 return $BAPL . $ENDL . $SLGS . $SLGC; 989 } else { 990 return ''; 991 } 992 } 993 994 /** 995 * Links, to show in chart boxes; 996 * 997 * @param Individual $individual 998 * 999 * @return Menu[] 1000 */ 1001 public function individualBoxMenu(Individual $individual) 1002 { 1003 $menus = array_merge( 1004 $this->individualBoxMenuCharts($individual), 1005 $this->individualBoxMenuFamilyLinks($individual) 1006 ); 1007 1008 return $menus; 1009 } 1010 1011 /** 1012 * Chart links, to show in chart boxes; 1013 * 1014 * @param Individual $individual 1015 * 1016 * @return Menu[] 1017 */ 1018 protected function individualBoxMenuCharts(Individual $individual) 1019 { 1020 $menus = array(); 1021 foreach (Module::getActiveCharts($this->tree) as $chart) { 1022 $menu = $chart->getBoxChartMenu($individual); 1023 if ($menu) { 1024 $menus[] = $menu; 1025 } 1026 } 1027 1028 usort($menus, function (Menu $x, Menu $y) { 1029 return I18N::strcasecmp($x->getLabel(), $y->getLabel()); 1030 }); 1031 1032 return $menus; 1033 } 1034 1035 /** 1036 * Family links, to show in chart boxes. 1037 * 1038 * @param Individual $individual 1039 * 1040 * @return Menu[] 1041 */ 1042 protected function individualBoxMenuFamilyLinks(Individual $individual) 1043 { 1044 $menus = array(); 1045 1046 foreach ($individual->getSpouseFamilies() as $family) { 1047 $menus[] = new Menu('<strong>' . I18N::translate('Family with spouse') . '</strong>', $family->getHtmlUrl()); 1048 $spouse = $family->getSpouse($individual); 1049 if ($spouse && $spouse->canShowName()) { 1050 $menus[] = new Menu($spouse->getFullName(), $spouse->getHtmlUrl()); 1051 } 1052 foreach ($family->getChildren() as $child) { 1053 if ($child->canShowName()) { 1054 $menus[] = new Menu($child->getFullName(), $child->getHtmlUrl()); 1055 } 1056 } 1057 } 1058 1059 return $menus; 1060 } 1061 1062 /** 1063 * Create part of an individual box 1064 * 1065 * @param Individual $individual 1066 * 1067 * @return string 1068 */ 1069 protected function individualBoxSexSymbol(Individual $individual) 1070 { 1071 if ($individual->getTree()->getPreference('PEDIGREE_SHOW_GENDER')) { 1072 return $individual->sexImage('large'); 1073 } else { 1074 return ''; 1075 } 1076 } 1077 1078 /** 1079 * Initialise the theme. We cannot pass these in a constructor, as the construction 1080 * happens in a theme file, and we need to be able to change it. 1081 * 1082 * @param Tree|null $tree The current tree (if there is one). 1083 */ 1084 final public function init(Tree $tree = null) 1085 { 1086 $this->tree = $tree; 1087 $this->tree_url = $tree ? 'ged=' . $tree->getNameUrl() : ''; 1088 1089 $this->hookAfterInit(); 1090 } 1091 1092 /** 1093 * A large webtrees logo, for the header. 1094 * 1095 * @return string 1096 */ 1097 protected function logoHeader() 1098 { 1099 return '<div class="header-logo"></div>'; 1100 } 1101 1102 /** 1103 * A small "powered by webtrees" logo for the footer. 1104 * 1105 * @return string 1106 */ 1107 protected function logoPoweredBy() 1108 { 1109 return '<a href="' . WT_WEBTREES_URL . '" class="powered-by-webtrees" title="' . WT_WEBTREES_URL . '"></a>'; 1110 } 1111 1112 /** 1113 * A menu for the day/month/year calendar views. 1114 * 1115 * @return Menu 1116 */ 1117 protected function menuCalendar() 1118 { 1119 return new Menu(I18N::translate('Calendar'), '#', 'menu-calendar', array('rel' => 'nofollow'), array( 1120 // Day view 1121 new Menu(I18N::translate('Day'), 'calendar.php?' . $this->tree_url . '&view=day', 'menu-calendar-day', array('rel' => 'nofollow')), 1122 // Month view 1123 new Menu(I18N::translate('Month'), 'calendar.php?' . $this->tree_url . '&view=month', 'menu-calendar-month', array('rel' => 'nofollow')), 1124 //Year view 1125 new Menu(I18N::translate('Year'), 'calendar.php?' . $this->tree_url . '&view=year', 'menu-calendar-year', array('rel' => 'nofollow')), 1126 )); 1127 } 1128 1129 /** 1130 * Generate a menu item to change the blocks on the current (index.php) page. 1131 * 1132 * @return Menu|null 1133 */ 1134 protected function menuChangeBlocks() 1135 { 1136 if (WT_SCRIPT_NAME === 'index.php' && Auth::check() && Filter::get('ctype', 'gedcom|user', 'user') === 'user') { 1137 return new Menu(I18N::translate('Customize this page'), 'index_edit.php?user_id=' . Auth::id(), 'menu-change-blocks'); 1138 } elseif (WT_SCRIPT_NAME === 'index.php' && Auth::isManager($this->tree)) { 1139 return new Menu(I18N::translate('Customize this page'), 'index_edit.php?gedcom_id=' . $this->tree->getTreeId(), 'menu-change-blocks'); 1140 } else { 1141 return null; 1142 } 1143 } 1144 1145 /** 1146 * Generate a menu for each of the different charts. 1147 * 1148 * @param Individual $individual 1149 * 1150 * @return Menu|null 1151 */ 1152 protected function menuChart(Individual $individual) 1153 { 1154 $submenus = array(); 1155 foreach (Module::getActiveCharts($this->tree) as $chart) { 1156 $menu = $chart->getChartMenu($individual); 1157 if ($menu) { 1158 $submenus[] = $menu; 1159 } 1160 } 1161 1162 if ($submenus) { 1163 usort($submenus, function (Menu $x, Menu $y) { 1164 return I18N::strcasecmp($x->getLabel(), $y->getLabel()); 1165 }); 1166 1167 return new Menu(I18N::translate('Charts'), '#', 'menu-chart', array('rel' => 'nofollow'), $submenus); 1168 } else { 1169 return null; 1170 } 1171 } 1172 1173 /** 1174 * Generate a menu item for the ancestors chart. 1175 * 1176 * @param Individual $individual 1177 * 1178 * @return Menu|null 1179 * 1180 * @deprecated 1181 */ 1182 protected function menuChartAncestors(Individual $individual) 1183 { 1184 $chart = new AncestorsChartModule(WT_ROOT . WT_MODULES_DIR . 'ancestors_chart'); 1185 1186 return $chart->getChartMenu($individual); 1187 } 1188 1189 /** 1190 * Generate a menu item for the compact tree. 1191 * 1192 * @param Individual $individual 1193 * 1194 * @return Menu|null 1195 * 1196 * @deprecated 1197 */ 1198 protected function menuChartCompact(Individual $individual) 1199 { 1200 $chart = new CompactTreeChartModule(WT_ROOT . WT_MODULES_DIR . 'compact_tree_chart'); 1201 1202 return $chart->getChartMenu($individual); 1203 } 1204 1205 /** 1206 * Generate a menu item for the descendants chart. 1207 * 1208 * @param Individual $individual 1209 * 1210 * @return Menu|null 1211 * 1212 * @deprecated 1213 */ 1214 protected function menuChartDescendants(Individual $individual) 1215 { 1216 $chart = new DescendancyChartModule(WT_ROOT . WT_MODULES_DIR . 'descendancy_chart'); 1217 1218 return $chart->getChartMenu($individual); 1219 } 1220 1221 /** 1222 * Generate a menu item for the family-book chart. 1223 * 1224 * @param Individual $individual 1225 * 1226 * @return Menu|null 1227 * 1228 * @deprecated 1229 */ 1230 protected function menuChartFamilyBook(Individual $individual) 1231 { 1232 $chart = new FamilyBookChartModule(WT_ROOT . WT_MODULES_DIR . 'family_book_chart'); 1233 1234 return $chart->getChartMenu($individual); 1235 } 1236 1237 /** 1238 * Generate a menu item for the fan chart. 1239 * 1240 * We can only do this if the GD2 library is installed with TrueType support. 1241 * 1242 * @param Individual $individual 1243 * 1244 * @return Menu|null 1245 * 1246 * @deprecated 1247 */ 1248 protected function menuChartFanChart(Individual $individual) 1249 { 1250 $chart = new FanChartModule(WT_ROOT . WT_MODULES_DIR . 'fan_chart'); 1251 1252 return $chart->getChartMenu($individual); 1253 } 1254 1255 /** 1256 * Generate a menu item for the interactive tree. 1257 * 1258 * @param Individual $individual 1259 * 1260 * @return Menu|null 1261 * 1262 * @deprecated 1263 */ 1264 protected function menuChartInteractiveTree(Individual $individual) 1265 { 1266 $chart = new InteractiveTreeModule(WT_ROOT . WT_MODULES_DIR . 'tree'); 1267 1268 return $chart->getChartMenu($individual); 1269 } 1270 1271 /** 1272 * Generate a menu item for the hourglass chart. 1273 * 1274 * @param Individual $individual 1275 * 1276 * @return Menu|null 1277 * 1278 * @deprecated 1279 */ 1280 protected function menuChartHourglass(Individual $individual) 1281 { 1282 $chart = new HourglassChartModule(WT_ROOT . WT_MODULES_DIR . 'hourglass_chart'); 1283 1284 return $chart->getChartMenu($individual); 1285 } 1286 1287 /** 1288 * Generate a menu item for the lifepsan chart. 1289 * 1290 * @param Individual $individual 1291 * 1292 * @return Menu|null 1293 * 1294 * @deprecated 1295 */ 1296 protected function menuChartLifespan(Individual $individual) 1297 { 1298 $chart = new LifespansChartModule(WT_ROOT . WT_MODULES_DIR . 'lifespans_chart'); 1299 1300 return $chart->getChartMenu($individual); 1301 } 1302 1303 /** 1304 * Generate a menu item for the pedigree chart. 1305 * 1306 * @param Individual $individual 1307 * 1308 * @return Menu|null 1309 * 1310 * @deprecated 1311 */ 1312 protected function menuChartPedigree(Individual $individual) 1313 { 1314 $chart = new PedigreeChartModule(WT_ROOT . WT_MODULES_DIR . 'pedigree_chart'); 1315 1316 return $chart->getChartMenu($individual); 1317 } 1318 1319 /** 1320 * Generate a menu item for the pedigree map. 1321 * 1322 * @param Individual $individual 1323 * 1324 * @return Menu|null 1325 * 1326 * @deprecated 1327 */ 1328 protected function menuChartPedigreeMap(Individual $individual) 1329 { 1330 $chart = new GoogleMapsModule(WT_ROOT . WT_MODULES_DIR . 'googlemap'); 1331 1332 return $chart->getChartMenu($individual); 1333 } 1334 1335 /** 1336 * Generate a menu item for the relationship chart. 1337 * 1338 * @param Individual $individual 1339 * 1340 * @return Menu|null 1341 * 1342 * @deprecated 1343 */ 1344 protected function menuChartRelationship(Individual $individual) 1345 { 1346 $chart = new RelationshipsChartModule(WT_ROOT . WT_MODULES_DIR . 'relationships_chart'); 1347 1348 return $chart->getChartMenu($individual); 1349 } 1350 1351 /** 1352 * Generate a menu item for the statistics charts. 1353 * 1354 * @return Menu|null 1355 * 1356 * @deprecated 1357 */ 1358 protected function menuChartStatistics() 1359 { 1360 $chart = new StatisticsChartModule(WT_ROOT . WT_MODULES_DIR . 'statistics_chart'); 1361 1362 return $chart->getChartMenu(null); 1363 } 1364 1365 /** 1366 * Generate a menu item for the timeline chart. 1367 * 1368 * @param Individual $individual 1369 * 1370 * @return Menu|null 1371 * 1372 * @deprecated 1373 */ 1374 protected function menuChartTimeline(Individual $individual) 1375 { 1376 $chart = new TimelineChartModule(WT_ROOT . WT_MODULES_DIR . 'timeline_chart'); 1377 1378 return $chart->getChartMenu($individual); 1379 } 1380 1381 /** 1382 * Generate a menu item for the control panel. 1383 * 1384 * @return Menu|null 1385 */ 1386 protected function menuControlPanel() 1387 { 1388 if (Auth::isManager($this->tree)) { 1389 return new Menu(I18N::translate('Control panel'), 'admin.php', 'menu-admin'); 1390 } else { 1391 return null; 1392 } 1393 } 1394 1395 /** 1396 * Favorites menu. 1397 * 1398 * @return Menu|null 1399 */ 1400 protected function menuFavorites() 1401 { 1402 global $controller; 1403 1404 $show_user_favorites = $this->tree && Module::getModuleByName('user_favorites') && Auth::check(); 1405 $show_tree_favorites = $this->tree && Module::getModuleByName('gedcom_favorites'); 1406 1407 if ($show_user_favorites && $show_tree_favorites) { 1408 $favorites = array_merge( 1409 FamilyTreeFavoritesModule::getFavorites($this->tree->getTreeId()), 1410 UserFavoritesModule::getFavorites(Auth::id()) 1411 ); 1412 } elseif ($show_user_favorites) { 1413 $favorites = UserFavoritesModule::getFavorites(Auth::id()); 1414 } elseif ($show_tree_favorites) { 1415 $favorites = FamilyTreeFavoritesModule::getFavorites($this->tree->getTreeId()); 1416 } else { 1417 $favorites = array(); 1418 } 1419 1420 $submenus = array(); 1421 $records = array(); 1422 foreach ($favorites as $favorite) { 1423 switch ($favorite['type']) { 1424 case 'URL': 1425 $submenus[] = new Menu($favorite['title'], $favorite['url']); 1426 break; 1427 case 'INDI': 1428 case 'FAM': 1429 case 'SOUR': 1430 case 'OBJE': 1431 case 'NOTE': 1432 $record = GedcomRecord::getInstance($favorite['gid'], $this->tree); 1433 if ($record && $record->canShowName()) { 1434 $submenus[] = new Menu($record->getFullName(), $record->getHtmlUrl()); 1435 $records[] = $record; 1436 } 1437 break; 1438 } 1439 } 1440 1441 if ($show_user_favorites && isset($controller->record) && $controller->record instanceof GedcomRecord && !in_array($controller->record, $records, true)) { 1442 $submenus[] = new Menu(I18N::translate('Add to favorites'), '#', '', array( 1443 'onclick' => 'jQuery.post("module.php?mod=user_favorites&mod_action=menu-add-favorite", {xref:"' . $controller->record->getXref() . '"},function(){location.reload();})', 1444 )); 1445 } 1446 1447 if (empty($submenus)) { 1448 return null; 1449 } else { 1450 return new Menu(I18N::translate('Favorites'), '#', 'menu-favorites', array(), $submenus); 1451 } 1452 } 1453 1454 /** 1455 * A menu for the home (family tree) pages. 1456 * 1457 * @return Menu 1458 */ 1459 protected function menuHomePage() 1460 { 1461 if (count(Tree::getAll()) === 1 || Site::getPreference('ALLOW_CHANGE_GEDCOM') === '0') { 1462 return new Menu(I18N::translate('Family tree'), 'index.php?ctype=gedcom&' . $this->tree_url, 'menu-tree'); 1463 } else { 1464 $submenus = array(); 1465 foreach (Tree::getAll() as $tree) { 1466 if ($tree == $this->tree) { 1467 $active = 'active '; 1468 } else { 1469 $active = ''; 1470 } 1471 $submenus[] = new Menu($tree->getTitleHtml(), 'index.php?ctype=gedcom&ged=' . $tree->getNameUrl(), $active . 'menu-tree-' . $tree->getTreeId()); 1472 } 1473 1474 return new Menu(I18N::translate('Family trees'), '#', 'menu-tree', array(), $submenus); 1475 } 1476 } 1477 1478 /** 1479 * A menu to show a list of available languages. 1480 * 1481 * @return Menu|null 1482 */ 1483 protected function menuLanguages() 1484 { 1485 $menu = new Menu(I18N::translate('Language'), '#', 'menu-language'); 1486 1487 foreach (I18N::activeLocales() as $locale) { 1488 $language_tag = $locale->languageTag(); 1489 $class = 'menu-language-' . $language_tag . (WT_LOCALE === $language_tag ? ' active' : ''); 1490 $menu->addSubmenu(new Menu($locale->endonym(), '#', $class, array( 1491 'onclick' => 'return false;', 1492 'data-language' => $language_tag, 1493 ))); 1494 } 1495 1496 if (count($menu->getSubmenus()) > 1) { 1497 return $menu; 1498 } else { 1499 return null; 1500 } 1501 } 1502 1503 /** 1504 * Create a menu to show lists of individuals, families, sources, etc. 1505 * 1506 * @param string $surname The significant surname on the page 1507 * 1508 * @return Menu 1509 */ 1510 protected function menuLists($surname) 1511 { 1512 // Do not show empty lists 1513 $row = Database::prepare( 1514 "SELECT" . 1515 " EXISTS(SELECT 1 FROM `##sources` WHERE s_file = ?) AS sour," . 1516 " EXISTS(SELECT 1 FROM `##other` WHERE o_file = ? AND o_type='REPO') AS repo," . 1517 " EXISTS(SELECT 1 FROM `##other` WHERE o_file = ? AND o_type='NOTE') AS note," . 1518 " EXISTS(SELECT 1 FROM `##media` WHERE m_file = ?) AS obje" 1519 )->execute(array( 1520 $this->tree->getTreeId(), 1521 $this->tree->getTreeId(), 1522 $this->tree->getTreeId(), 1523 $this->tree->getTreeId(), 1524 ))->fetchOneRow(); 1525 1526 $submenus = array( 1527 $this->menuListsIndividuals($surname), 1528 $this->menuListsFamilies($surname), 1529 $this->menuListsBranches($surname), 1530 $this->menuListsPlaces(), 1531 ); 1532 if ($row->obje) { 1533 $submenus[] = $this->menuListsMedia(); 1534 } 1535 if ($row->repo) { 1536 $submenus[] = $this->menuListsRepositories(); 1537 } 1538 if ($row->sour) { 1539 $submenus[] = $this->menuListsSources(); 1540 } 1541 if ($row->note) { 1542 $submenus[] = $this->menuListsNotes(); 1543 } 1544 1545 uasort($submenus, function (Menu $x, Menu $y) { 1546 return I18N::strcasecmp($x->getLabel(), $y->getLabel()); 1547 }); 1548 1549 return new Menu(I18N::translate('Lists'), '#', 'menu-list', array(), $submenus); 1550 } 1551 1552 /** 1553 * A menu for the list of branches 1554 * 1555 * @param string $surname The significant surname on the page 1556 * 1557 * @return Menu 1558 */ 1559 protected function menuListsBranches($surname) 1560 { 1561 return new Menu(I18N::translate('Branches'), 'branches.php?ged=' . $this->tree->getNameUrl() . '&surname=' . rawurlencode($surname), 'menu-branches', array('rel' => 'nofollow')); 1562 } 1563 1564 /** 1565 * A menu for the list of families 1566 * 1567 * @param string $surname The significant surname on the page 1568 * 1569 * @return Menu 1570 */ 1571 protected function menuListsFamilies($surname) 1572 { 1573 return new Menu(I18N::translate('Families'), 'famlist.php?ged=' . $this->tree->getNameUrl() . '&surname=' . rawurlencode($surname), 'menu-list-fam', array('rel' => 'nofollow')); 1574 } 1575 1576 /** 1577 * A menu for the list of individuals 1578 * 1579 * @param string $surname The significant surname on the page 1580 * 1581 * @return Menu 1582 */ 1583 protected function menuListsIndividuals($surname) 1584 { 1585 return new Menu(I18N::translate('Individuals'), 'indilist.php?ged=' . $this->tree->getNameUrl() . '&surname=' . rawurlencode($surname), 'menu-list-indi'); 1586 } 1587 1588 /** 1589 * A menu for the list of media objects 1590 * 1591 * @return Menu 1592 */ 1593 protected function menuListsMedia() 1594 { 1595 return new Menu(I18N::translate('Media objects'), 'medialist.php?' . $this->tree_url, 'menu-list-obje', array('rel' => 'nofollow')); 1596 } 1597 1598 /** 1599 * A menu for the list of notes 1600 * 1601 * @return Menu 1602 */ 1603 protected function menuListsNotes() 1604 { 1605 return new Menu(I18N::translate('Shared notes'), 'notelist.php?' . $this->tree_url, 'menu-list-note', array('rel' => 'nofollow')); 1606 } 1607 1608 /** 1609 * A menu for the list of individuals 1610 * 1611 * @return Menu 1612 */ 1613 protected function menuListsPlaces() 1614 { 1615 return new Menu(I18N::translate('Place hierarchy'), 'placelist.php?ged=' . $this->tree->getNameUrl(), 'menu-list-plac', array('rel' => 'nofollow')); 1616 } 1617 1618 /** 1619 * A menu for the list of repositories 1620 * 1621 * @return Menu 1622 */ 1623 protected function menuListsRepositories() 1624 { 1625 return new Menu(I18N::translate('Repositories'), 'repolist.php?' . $this->tree_url, 'menu-list-repo', array('rel' => 'nofollow')); 1626 } 1627 1628 /** 1629 * A menu for the list of sources 1630 * 1631 * @return Menu 1632 */ 1633 protected function menuListsSources() 1634 { 1635 return new Menu(I18N::translate('Sources'), 'sourcelist.php?' . $this->tree_url, 'menu-list-sour', array('rel' => 'nofollow')); 1636 } 1637 1638 /** 1639 * A login menu option (or null if we are already logged in). 1640 * 1641 * @return Menu|null 1642 */ 1643 protected function menuLogin() 1644 { 1645 if (Auth::check() || WT_SCRIPT_NAME === 'login.php') { 1646 return null; 1647 } else { 1648 return new Menu(I18N::translate('Sign in'), WT_LOGIN_URL . '?url=' . rawurlencode(Functions::getQueryUrl()), 'menu-login', array('rel' => 'nofollow')); 1649 } 1650 } 1651 1652 /** 1653 * A logout menu option (or null if we are already logged out). 1654 * 1655 * @return Menu|null 1656 */ 1657 protected function menuLogout() 1658 { 1659 if (Auth::check()) { 1660 return new Menu(I18N::translate('Sign out'), 'logout.php', 'menu-logout'); 1661 } else { 1662 return null; 1663 } 1664 } 1665 1666 /** 1667 * Get the additional menus created by each of the modules 1668 * 1669 * @return Menu[] 1670 */ 1671 protected function menuModules() 1672 { 1673 $menus = array(); 1674 foreach (Module::getActiveMenus($this->tree) as $module) { 1675 $menus[] = $module->getMenu(); 1676 } 1677 1678 return array_filter($menus); 1679 } 1680 1681 /** 1682 * A link to allow users to edit their account settings (edituser.php). 1683 * 1684 * @return Menu|null 1685 */ 1686 protected function menuMyAccount() 1687 { 1688 if (Auth::check()) { 1689 return new Menu(I18N::translate('My account'), 'edituser.php'); 1690 } else { 1691 return null; 1692 } 1693 } 1694 1695 /** 1696 * A link to the user's individual record (individual.php). 1697 * 1698 * @return Menu|null 1699 */ 1700 protected function menuMyIndividualRecord() 1701 { 1702 $gedcomid = $this->tree->getUserPreference(Auth::user(), 'gedcomid'); 1703 1704 if ($gedcomid) { 1705 return new Menu(I18N::translate('My individual record'), 'individual.php?pid=' . $gedcomid . '&' . $this->tree_url, 'menu-myrecord'); 1706 } else { 1707 return null; 1708 } 1709 } 1710 1711 /** 1712 * A link to the user's personal home page. 1713 * 1714 * @return Menu 1715 */ 1716 protected function menuMyPage() 1717 { 1718 return new Menu(I18N::translate('My page'), 'index.php?ctype=user&' . $this->tree_url, 'menu-mypage'); 1719 } 1720 1721 /** 1722 * A menu for the user's personal pages. 1723 * 1724 * @return Menu|null 1725 */ 1726 protected function menuMyPages() 1727 { 1728 if (Auth::id()) { 1729 return new Menu(I18N::translate('My pages'), '#', 'menu-mymenu', array(), array_filter(array( 1730 $this->menuMyPage(), 1731 $this->menuMyIndividualRecord(), 1732 $this->menuMyPedigree(), 1733 $this->menuMyAccount(), 1734 $this->menuControlPanel(), 1735 $this->menuChangeBlocks(), 1736 ))); 1737 } else { 1738 return null; 1739 } 1740 } 1741 1742 /** 1743 * A link to the user's individual record. 1744 * 1745 * @return Menu|null 1746 */ 1747 protected function menuMyPedigree() 1748 { 1749 $gedcomid = $this->tree->getUserPreference(Auth::user(), 'gedcomid'); 1750 1751 if ($gedcomid && Module::isActiveChart($this->tree, 'pedigree_chart')) { 1752 $showFull = $this->tree->getPreference('PEDIGREE_FULL_DETAILS') ? 1 : 0; 1753 $showLayout = $this->tree->getPreference('PEDIGREE_LAYOUT') ? 1 : 0; 1754 1755 return new Menu( 1756 I18N::translate('My pedigree'), 1757 'pedigree.php?' . $this->tree_url . '&rootid=' . $gedcomid . '&show_full=' . $showFull . '&talloffset=' . $showLayout, 1758 'menu-mypedigree' 1759 ); 1760 } else { 1761 return null; 1762 } 1763 } 1764 1765 /** 1766 * Create a pending changes menu. 1767 * 1768 * @return Menu|null 1769 */ 1770 protected function menuPendingChanges() 1771 { 1772 if ($this->pendingChangesExist()) { 1773 $menu = new Menu(I18N::translate('Pending changes'), '#', 'menu-pending', array('onclick' => 'window.open("edit_changes.php", "_blank", chan_window_specs); return false;')); 1774 1775 return $menu; 1776 } else { 1777 return null; 1778 } 1779 } 1780 1781 /** 1782 * A menu with a list of reports. 1783 * 1784 * @return Menu|null 1785 */ 1786 protected function menuReports() 1787 { 1788 $submenus = array(); 1789 foreach (Module::getActiveReports($this->tree) as $report) { 1790 $submenus[] = $report->getReportMenu(); 1791 } 1792 1793 if ($submenus) { 1794 return new Menu(I18N::translate('Reports'), '#', 'menu-report', array('rel' => 'nofollow'), $submenus); 1795 } else { 1796 return null; 1797 } 1798 } 1799 1800 /** 1801 * Create the search menu. 1802 * 1803 * @return Menu 1804 */ 1805 protected function menuSearch() 1806 { 1807 return new Menu(I18N::translate('Search'), '#', 'menu-search', array('rel' => 'nofollow'), array_filter(array( 1808 $this->menuSearchGeneral(), 1809 $this->menuSearchPhonetic(), 1810 $this->menuSearchAdvanced(), 1811 $this->menuSearchAndReplace(), 1812 ))); 1813 } 1814 1815 /** 1816 * Create the general search sub-menu. 1817 * 1818 * @return Menu 1819 */ 1820 protected function menuSearchGeneral() 1821 { 1822 return new Menu(I18N::translate('General search'), 'search.php?' . $this->tree_url, 'menu-search-general', array('rel' => 'nofollow')); 1823 } 1824 1825 /** 1826 * Create the phonetic search sub-menu. 1827 * 1828 * @return Menu 1829 */ 1830 protected function menuSearchPhonetic() 1831 { 1832 return new Menu(/* I18N: search using “sounds like”, rather than exact spelling */ I18N::translate('Phonetic search'), 'search.php?' . $this->tree_url . '&action=soundex', 'menu-search-soundex', array('rel' => 'nofollow')); 1833 } 1834 1835 /** 1836 * Create the advanced search sub-menu. 1837 * 1838 * @return Menu 1839 */ 1840 protected function menuSearchAdvanced() 1841 { 1842 return new Menu(I18N::translate('Advanced search'), 'search_advanced.php?' . $this->tree_url, 'menu-search-advanced', array('rel' => 'nofollow')); 1843 } 1844 1845 /** 1846 * Create the advanced search sub-menu. 1847 * 1848 * @return Menu 1849 */ 1850 protected function menuSearchAndReplace() 1851 { 1852 if (Auth::isEditor($this->tree)) { 1853 return new Menu(I18N::translate('Search and replace'), 'search.php?' . $this->tree_url . '&action=replace', 'menu-search-replace'); 1854 } else { 1855 return null; 1856 } 1857 } 1858 1859 /** 1860 * Themes menu. 1861 * 1862 * @return Menu|null 1863 */ 1864 public function menuThemes() 1865 { 1866 if ($this->tree && Site::getPreference('ALLOW_USER_THEMES') && $this->tree->getPreference('ALLOW_THEME_DROPDOWN')) { 1867 $submenus = array(); 1868 foreach (Theme::installedThemes() as $theme) { 1869 $class = 'menu-theme-' . $theme->themeId() . ($theme === $this ? ' active' : ''); 1870 $submenus[] = new Menu($theme->themeName(), '#', $class, array( 1871 'onclick' => 'return false;', 1872 'data-theme' => $theme->themeId(), 1873 )); 1874 } 1875 1876 usort($submenus, function (Menu $x, Menu $y) { 1877 return I18N::strcasecmp($x->getLabel(), $y->getLabel()); 1878 }); 1879 1880 $menu = new Menu(I18N::translate('Theme'), '#', 'menu-theme', array(), $submenus); 1881 1882 return $menu; 1883 } else { 1884 return null; 1885 } 1886 } 1887 1888 /** 1889 * Create the <meta charset=""> tag. 1890 * 1891 * @return string 1892 */ 1893 protected function metaCharset() 1894 { 1895 return '<meta charset="UTF-8">'; 1896 } 1897 1898 /** 1899 * Create the <meta name="description"> tag. 1900 * 1901 * @param string $description 1902 * 1903 * @return string 1904 */ 1905 protected function metaDescription($description) 1906 { 1907 if ($description) { 1908 return '<meta name="description" content="' . $description . '">'; 1909 } else { 1910 return ''; 1911 } 1912 } 1913 1914 /** 1915 * Create the <meta name="generator"> tag. 1916 * 1917 * @param string $generator 1918 * 1919 * @return string 1920 */ 1921 protected function metaGenerator($generator) 1922 { 1923 if ($generator) { 1924 return '<meta name="generator" content="' . $generator . '">'; 1925 } else { 1926 return ''; 1927 } 1928 } 1929 1930 /** 1931 * Create the <meta name="robots"> tag. 1932 * 1933 * @param string $robots 1934 * 1935 * @return string 1936 */ 1937 protected function metaRobots($robots) 1938 { 1939 if ($robots) { 1940 return '<meta name="robots" content="' . $robots . '">'; 1941 } else { 1942 return ''; 1943 } 1944 } 1945 1946 /** 1947 * Create the <meta http-equiv="X-UA-Compatible"> tag. 1948 * 1949 * @return string 1950 */ 1951 protected function metaUaCompatible() 1952 { 1953 return '<meta http-equiv="X-UA-Compatible" content="IE=edge">'; 1954 } 1955 1956 /** 1957 * Create the <meta name="viewport" content="width=device-width, initial-scale=1"> tag. 1958 * 1959 * @return string 1960 */ 1961 protected function metaViewport() 1962 { 1963 return '<meta name="viewport" content="width=device-width, initial-scale=1">'; 1964 } 1965 1966 /** 1967 * How many times has the current page been shown? 1968 * 1969 * @param PageController $controller 1970 * 1971 * @return int Number of views, or zero for pages that aren't logged. 1972 */ 1973 protected function pageViews(PageController $controller) 1974 { 1975 if ($this->tree && $this->tree->getPreference('SHOW_COUNTER')) { 1976 if (isset($controller->record) && $controller->record instanceof GedcomRecord) { 1977 return HitCounter::countHit($this->tree, WT_SCRIPT_NAME, $controller->record->getXref()); 1978 } elseif (isset($controller->root) && $controller->root instanceof GedcomRecord) { 1979 return HitCounter::countHit($this->tree, WT_SCRIPT_NAME, $controller->root->getXref()); 1980 } elseif (WT_SCRIPT_NAME === 'index.php') { 1981 if (Auth::check() && Filter::get('ctype') !== 'gedcom') { 1982 return HitCounter::countHit($this->tree, WT_SCRIPT_NAME, 'user:' . Auth::id()); 1983 } else { 1984 return HitCounter::countHit($this->tree, WT_SCRIPT_NAME, 'gedcom:' . $this->tree->getTreeId()); 1985 } 1986 } 1987 } 1988 1989 return 0; 1990 } 1991 1992 /** 1993 * Misecellaneous dimensions, fonts, styles, etc. 1994 * 1995 * @param string $parameter_name 1996 * 1997 * @return string|int|float 1998 */ 1999 public function parameter($parameter_name) 2000 { 2001 $parameters = array( 2002 'chart-background-f' => 'dddddd', 2003 'chart-background-m' => 'cccccc', 2004 'chart-background-u' => 'eeeeee', 2005 'chart-box-x' => 250, 2006 'chart-box-y' => 80, 2007 'chart-descendancy-indent' => 15, 2008 'chart-font-color' => '000000', 2009 'chart-font-name' => WT_ROOT . 'packages/dejavu-fonts-ttf-2.35/ttf/DejaVuSans.ttf', 2010 'chart-font-size' => 7, 2011 'chart-spacing-x' => 5, 2012 'chart-spacing-y' => 10, 2013 'compact-chart-box-x' => 240, 2014 'compact-chart-box-y' => 50, 2015 'distribution-chart-high-values' => '555555', 2016 'distribution-chart-low-values' => 'cccccc', 2017 'distribution-chart-no-values' => 'ffffff', 2018 'distribution-chart-x' => 440, 2019 'distribution-chart-y' => 220, 2020 'line-width' => 1.5, 2021 'shadow-blur' => 0, 2022 'shadow-color' => '', 2023 'shadow-offset-x' => 0, 2024 'shadow-offset-y' => 0, 2025 'stats-small-chart-x' => 440, 2026 'stats-small-chart-y' => 125, 2027 'stats-large-chart-x' => 900, 2028 'image-dline' => $this->assetUrl() . 'images/dline.png', 2029 'image-dline2' => $this->assetUrl() . 'images/dline2.png', 2030 'image-hline' => $this->assetUrl() . 'images/hline.png', 2031 'image-spacer' => $this->assetUrl() . 'images/spacer.png', 2032 'image-vline' => $this->assetUrl() . 'images/vline.png', 2033 'image-minus' => $this->assetUrl() . 'images/minus.png', 2034 'image-plus' => $this->assetUrl() . 'images/plus.png', 2035 ); 2036 2037 if (array_key_exists($parameter_name, $parameters)) { 2038 return $parameters[$parameter_name]; 2039 } else { 2040 throw new \InvalidArgumentException($parameter_name); 2041 } 2042 } 2043 2044 /** 2045 * Are there any pending changes for us to approve? 2046 * 2047 * @return bool 2048 */ 2049 protected function pendingChangesExist() 2050 { 2051 return $this->tree && $this->tree->hasPendingEdit() && Auth::isModerator($this->tree); 2052 } 2053 2054 /** 2055 * Create a pending changes link. Some themes prefer an alert/banner to a menu. 2056 * 2057 * @return string 2058 */ 2059 protected function pendingChangesLink() 2060 { 2061 return 2062 '<a href="#" onclick="window.open(\'edit_changes.php\', \'_blank\', chan_window_specs); return false;">' . 2063 $this->pendingChangesLinkText() . 2064 '</a>'; 2065 } 2066 2067 /** 2068 * Text to use in the pending changes link. 2069 * 2070 * @return string 2071 */ 2072 protected function pendingChangesLinkText() 2073 { 2074 return I18N::translate('There are pending changes for you to moderate.'); 2075 } 2076 2077 /** 2078 * Generate a list of items for the main menu. 2079 * 2080 * @return Menu[] 2081 */ 2082 protected function primaryMenu() 2083 { 2084 global $controller; 2085 2086 if ($this->tree) { 2087 $individual = $controller->getSignificantIndividual(); 2088 2089 return array_filter(array_merge(array( 2090 $this->menuHomePage(), 2091 $this->menuChart($individual), 2092 $this->menuLists($controller->getSignificantSurname()), 2093 $this->menuCalendar(), 2094 $this->menuReports(), 2095 $this->menuSearch(), 2096 ), $this->menuModules())); 2097 } else { 2098 // No public trees? No genealogy menu! 2099 return array(); 2100 } 2101 } 2102 2103 /** 2104 * Add markup to the primary menu. 2105 * 2106 * @param Menu[] $menus 2107 * 2108 * @return string 2109 */ 2110 protected function primaryMenuContainer(array $menus) 2111 { 2112 return '<nav><ul class="primary-menu">' . $this->primaryMenuContent($menus) . '</ul></nav>'; 2113 } 2114 2115 /** 2116 * Create the primary menu. 2117 * 2118 * @param Menu[] $menus 2119 * 2120 * @return string 2121 */ 2122 protected function primaryMenuContent(array $menus) 2123 { 2124 return implode('', array_map(function (Menu $menu) { 2125 return $menu->getMenuAsList(); 2126 }, $menus)); 2127 } 2128 2129 /** 2130 * Generate a list of items for the user menu. 2131 * 2132 * @return Menu[] 2133 */ 2134 protected function secondaryMenu() 2135 { 2136 return array_filter(array( 2137 $this->menuPendingChanges(), 2138 $this->menuMyPages(), 2139 $this->menuFavorites(), 2140 $this->menuThemes(), 2141 $this->menuLanguages(), 2142 $this->menuLogin(), 2143 $this->menuLogout(), 2144 )); 2145 } 2146 2147 /** 2148 * Add markup to the secondary menu. 2149 * 2150 * @param Menu[] $menus 2151 * 2152 * @return string 2153 */ 2154 protected function secondaryMenuContainer(array $menus) 2155 { 2156 return '<ul class="nav nav-pills secondary-menu">' . $this->secondaryMenuContent($menus) . '</ul>'; 2157 } 2158 2159 /** 2160 * Format the secondary menu. 2161 * 2162 * @param Menu[] $menus 2163 * 2164 * @return string 2165 */ 2166 protected function secondaryMenuContent(array $menus) 2167 { 2168 return implode('', array_map(function (Menu $menu) { 2169 return $menu->getMenuAsList(); 2170 }, $menus)); 2171 } 2172 2173 /** 2174 * Send any HTTP headers. 2175 */ 2176 public function sendHeaders() 2177 { 2178 header('Content-Type: text/html; charset=UTF-8'); 2179 } 2180 2181 /** 2182 * A list of CSS files to include for this page. 2183 * 2184 * @return string[] 2185 */ 2186 protected function stylesheets() 2187 { 2188 $stylesheets = array( 2189 WT_BOOTSTRAP_CSS_URL, 2190 WT_FONT_AWESOME_CSS_URL, 2191 WT_FONT_AWESOME_RTL_CSS_URL, 2192 ); 2193 2194 if (I18N::direction() === 'rtl') { 2195 $stylesheets[] = WT_BOOTSTRAP_RTL_CSS_URL; 2196 } 2197 2198 return $stylesheets; 2199 } 2200 2201 /** 2202 * Create the <title> tag. 2203 * 2204 * @param string $title 2205 * 2206 * @return string 2207 */ 2208 protected function title($title) 2209 { 2210 return '<title>' . Filter::escapeHtml($title) . '</title>'; 2211 } 2212} 2213