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">&times;</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 . '&amp;view=day', 'menu-calendar-day', array('rel' => 'nofollow')),
1122            // Month view
1123            new Menu(I18N::translate('Month'), 'calendar.php?' . $this->tree_url . '&amp;view=month', 'menu-calendar-month', array('rel' => 'nofollow')),
1124            //Year view
1125            new Menu(I18N::translate('Year'), 'calendar.php?' . $this->tree_url . '&amp;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&amp;' . $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&amp;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() . '&amp;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() . '&amp;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() . '&amp;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 . '&amp;' . $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&amp;' . $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 . '&amp;rootid=' . $gedcomid . '&amp;show_full=' . $showFull . '&amp;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 . '&amp;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 . '&amp;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