Completed
Push — develop ( c314ec...310a97 )
by Greg
13:42
created

AbstractTheme::menuChart()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 23
rs 9.0856
cc 1
eloc 18
nc 1
nop 1
1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2015 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
 */
16
namespace Fisharebest\Webtrees\Theme;
17
18
use Fisharebest\Webtrees\Auth;
19
use Fisharebest\Webtrees\Controller\PageController;
20
use Fisharebest\Webtrees\Database;
21
use Fisharebest\Webtrees\Fact;
22
use Fisharebest\Webtrees\Filter;
23
use Fisharebest\Webtrees\FlashMessages;
24
use Fisharebest\Webtrees\Functions\Functions;
25
use Fisharebest\Webtrees\GedcomRecord;
26
use Fisharebest\Webtrees\GedcomTag;
27
use Fisharebest\Webtrees\HitCounter;
28
use Fisharebest\Webtrees\I18N;
29
use Fisharebest\Webtrees\Individual;
30
use Fisharebest\Webtrees\Menu;
31
use Fisharebest\Webtrees\Module;
32
use Fisharebest\Webtrees\Module\FamilyTreeFavoritesModule;
33
use Fisharebest\Webtrees\Module\UserFavoritesModule;
34
use Fisharebest\Webtrees\Site;
35
use Fisharebest\Webtrees\Theme;
36
use Fisharebest\Webtrees\Tree;
37
use Fisharebest\Webtrees\User;
38
39
/**
40
 * Common functions for all themes.
41
 */
42
abstract class AbstractTheme {
43
	/** @var Tree The current tree */
44
	protected $tree;
45
46
	/** @var string An escaped version of the "ged=XXX" URL parameter */
47
	protected $tree_url;
48
49
	/** @var int The number of times this page has been shown */
50
	protected $page_views;
51
52
	/**
53
	 * Custom themes should place their initialization code in the function hookAfterInit(), not in
54
	 * the constructor, as all themes get constructed - whether they are used or not.
55
	 */
56
	final public function __construct() {
57
	}
58
59
	/**
60
	 * Create accessibility links for the header.
61
	 *
62
	 * "Skip to content" allows keyboard only users to navigate over the headers without
63
	 * pressing TAB many times.
64
	 *
65
	 * @return string
66
	 */
67
	protected function accessibilityLinks() {
68
		return
69
			'<div class="accessibility-links">' .
70
			'<a class="sr-only sr-only-focusable btn btn-info btn-sm" href="#content">' .
71
			/* I18N: Skip over the headers and menus, to the main content of the page */ I18N::translate('Skip to content') .
72
			'</a>' .
73
			'</div>';
74
	}
75
76
	/**
77
	 * Create scripts for analytics and tracking.
78
	 *
79
	 * @return string
80
	 */
81
	protected function analytics() {
82
		if ($this->themeId() === '_administration') {
83
			return '';
84
		} else {
85
			return
86
				$this->analyticsGoogleWebmaster(
87
					Site::getPreference('GOOGLE_WEBMASTER_ID')
88
				) .
89
				$this->analyticsGoogleTracker(
90
					Site::getPreference('GOOGLE_ANALYTICS_ID')
91
				) .
92
				$this->analyticsPiwikTracker(
93
					Site::getPreference('PIWIK_URL'),
94
					Site::getPreference('PIWIK_SITE_ID')
95
				) .
96
				$this->analyticsStatcounterTracker(
97
					Site::getPreference('STATCOUNTER_PROJECT_ID'),
98
					Site::getPreference('STATCOUNTER_SECURITY_ID')
99
				);
100
		}
101
	}
102
103
	/**
104
	 * Create the verification code for Google Webmaster Tools.
105
	 *
106
	 * @param string $verification_id
107
	 *
108
	 * @return string
109
	 */
110 View Code Duplication
	protected function analyticsBingWebmaster($verification_id) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
111
		// Only need to add this to the home page.
112
		if (WT_SCRIPT_NAME === 'index.php' && $verification_id) {
113
			return '<meta name="msvalidate.01" content="' . $verification_id . '">';
114
		} else {
115
			return '';
116
		}
117
	}
118
119
	/**
120
	 * Create the verification code for Google Webmaster Tools.
121
	 *
122
	 * @param string $verification_id
123
	 *
124
	 * @return string
125
	 */
126 View Code Duplication
	protected function analyticsGoogleWebmaster($verification_id) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
127
		// Only need to add this to the home page.
128
		if (WT_SCRIPT_NAME === 'index.php' && $verification_id) {
129
			return '<meta name="google-site-verification" content="' . $verification_id . '">';
130
		} else {
131
			return '';
132
		}
133
	}
134
135
	/**
136
	 * Create the tracking code for Google Analytics.
137
	 *
138
	 * See https://developers.google.com/analytics/devguides/collection/analyticsjs/advanced
139
	 *
140
	 * @param string $analytics_id
141
	 *
142
	 * @return string
143
	 */
144
	protected function analyticsGoogleTracker($analytics_id) {
145
		if ($analytics_id) {
146
			return
147
				'<script async src="https://www.google-analytics.com/analytics.js"></script>' .
148
				'<script>' .
149
				'window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;' .
150
				'ga("create","' . $analytics_id . '","auto");' .
151
				'ga("send", "pageview");' .
152
				'</script>';
153
		} else {
154
			return '';
155
		}
156
	}
157
158
	/**
159
	 * Create the tracking code for Piwik Analytics.
160
	 *
161
	 * @param string $url     - The domain/path to Piwik
162
	 * @param string $site_id - The Piwik site identifier
163
	 *
164
	 * @return string
165
	 */
166
	protected function analyticsPiwikTracker($url, $site_id) {
167
		$url = preg_replace(array('/^https?:\/\//', '/\/$/'), '', $url);
168
169
		if ($url && $site_id) {
170
			return
171
				'<script>' .
172
				'var _paq=_paq||[];' .
173
				'(function(){var u=(("https:"==document.location.protocol)?"https://' . $url . '/":"http://' . $url . '/");' .
174
				'_paq.push(["setSiteId",' . $site_id . ']);' .
175
				'_paq.push(["setTrackerUrl",u+"piwik.php"]);' .
176
				'_paq.push(["trackPageView"]);' .
177
				'_paq.push(["enableLinkTracking"]);' .
178
				'var d=document,g=d.createElement("script"),s=d.getElementsByTagName("script")[0];g.defer=true;g.async=true;g.src=u+"piwik.js";' .
179
				's.parentNode.insertBefore(g,s);})();' .
180
				'</script>';
181
		} else {
182
			return '';
183
		}
184
	}
185
186
	/**
187
	 * Create the tracking code for Statcounter.
188
	 *
189
	 * @param string $project_id  - The statcounter project ID
190
	 * @param string $security_id - The statcounter security ID
191
	 *
192
	 * @return string
193
	 */
194
	protected function analyticsStatcounterTracker($project_id, $security_id) {
195
		if ($project_id && $security_id) {
196
			return
197
				'<script>' .
198
				'var sc_project=' . (int) $project_id . ',sc_invisible=1,sc_security="' . $security_id .
199
				'",scJsHost = (("https:"===document.location.protocol)?"https://secure.":"http://www.");' .
200
				'document.write("<sc"+"ript src=\'"+scJsHost+"statcounter.com/counter/counter.js\'></"+"script>");' .
201
				'</script>';
202
		} else {
203
			return '';
204
		}
205
	}
206
207
	/**
208
	 * Create the top of the <body>.
209
	 *
210
	 * @return string
211
	 */
212
	public function bodyHeader() {
213
		return
214
			'<body class="container">' .
215
			'<header>' .
216
			$this->headerContent() .
217
			$this->primaryMenuContainer($this->primaryMenu()) .
218
			'</header>' .
219
			'<main id="content" role="main">' .
220
			$this->flashMessagesContainer(FlashMessages::getMessages());
0 ignored issues
show
Documentation introduced by
\Fisharebest\Webtrees\FlashMessages::getMessages() is of type array<integer,string>, but the function expects a array<integer,object<stdClass>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
221
	}
222
223
	/**
224
	 * Create the top of the <body> (for popup windows).
225
	 *
226
	 * @return string
227
	 */
228
	public function bodyHeaderPopupWindow() {
229
		return
230
			'<body class="container container-popup">' .
231
			'<main id="content" role="main">' .
232
			$this->flashMessagesContainer(FlashMessages::getMessages());
0 ignored issues
show
Documentation introduced by
\Fisharebest\Webtrees\FlashMessages::getMessages() is of type array<integer,string>, but the function expects a array<integer,object<stdClass>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
233
	}
234
235
	/**
236
	 * Create a contact link for a user.
237
	 *
238
	 * @param User $user
239
	 *
240
	 * @return string
241
	 */
242
	public function contactLink(User $user) {
243
		$method = $user->getPreference('contactmethod');
244
245
		switch ($method) {
246
		case 'none':
247
			return '';
248
		case 'mailto':
249
			return '<a href="mailto:' . Filter::escapeHtml($user->getEmail()) . '">' . $user->getRealNameHtml() . '</a>';
250
		default:
251
			return "<a href='#' onclick='message(\"" . Filter::escapeHtml($user->getUserName()) . "\", \"" . $method . "\", \"" . WT_BASE_URL . Filter::escapeHtml(Functions::getQueryUrl()) . "\", \"\");return false;'>" . $user->getRealNameHtml() . '</a>';
252
		}
253
	}
254
255
	/**
256
	 * Create contact link for both technical and genealogy support.
257
	 *
258
	 * @param User $user
259
	 *
260
	 * @return string
261
	 */
262
	protected function contactLinkEverything(User $user) {
263
		return I18N::translate('For technical support or genealogy questions, please contact') . ' ' . $this->contactLink($user);
264
	}
265
266
	/**
267
	 * Create contact link for genealogy support.
268
	 *
269
	 * @param User $user
270
	 *
271
	 * @return string
272
	 */
273
	protected function contactLinkGenealogy(User $user) {
274
		return I18N::translate('For help with genealogy questions contact') . ' ' . $this->contactLink($user);
275
	}
276
277
	/**
278
	 * Create contact link for technical support.
279
	 *
280
	 * @param User $user
281
	 *
282
	 * @return string
283
	 */
284
	protected function contactLinkTechnical(User $user) {
285
		return I18N::translate('For technical support and information contact') . ' ' . $this->contactLink($user);
286
	}
287
288
	/**
289
	 * Create contact links for the page footer.
290
	 *
291
	 * @return string
292
	 */
293
	protected function contactLinks() {
294
		$contact_user   = User::find($this->tree->getPreference('CONTACT_USER_ID'));
295
		$webmaster_user = User::find($this->tree->getPreference('WEBMASTER_USER_ID'));
296
297
		if ($contact_user && $contact_user === $webmaster_user) {
298
			return $this->contactLinkEverything($contact_user);
299
		} elseif ($contact_user && $webmaster_user) {
300
			return $this->contactLinkGenealogy($contact_user) . '<br>' . $this->contactLinkTechnical($webmaster_user);
301
		} elseif ($contact_user) {
302
			return $this->contactLinkGenealogy($contact_user);
303
		} elseif ($webmaster_user) {
304
			return $this->contactLinkTechnical($webmaster_user);
305
		} else {
306
			return '';
307
		}
308
	}
309
310
	/**
311
	 * Create the <DOCTYPE> tag.
312
	 *
313
	 * @return string
314
	 */
315
	public function doctype() {
316
		return '<!DOCTYPE html>';
317
	}
318
319
	/**
320
	 * HTML link to a "favorites icon".
321
	 *
322
	 * @return string
323
	 */
324
	protected function favicon() {
325
		return
326
			'<link rel="icon" href="' . $this->assetUrl() . 'favicon.png" type="image/png">' .
327
			'<link rel="icon" type="image/png" href="' . $this->assetUrl() . 'favicon192.png" sizes="192x192">' .
328
			'<link rel="apple-touch-icon" sizes="180x180" href="' . $this->assetUrl() . 'favicon180.png">';
329
	}
330
331
	/**
332
	 * Add markup to a flash message.
333
	 *
334
	 * @param \stdClass $message
335
	 *
336
	 * @return string
337
	 */
338
	protected function flashMessageContainer(\stdClass $message) {
339
		return $this->htmlAlert($message->text, $message->status, true);
340
	}
341
342
	/**
343
	 * Create a container for messages that are "flashed" to the session
344
	 * on one request, and displayed on another. If there are many messages,
345
	 * the container may need a max-height and scroll-bar.
346
	 *
347
	 * @param \stdClass[] $messages
348
	 *
349
	 * @return string
350
	 */
351
	protected function flashMessagesContainer(array $messages) {
352
		$html = '';
353
		foreach ($messages as $message) {
354
			$html .= $this->flashMessageContainer($message);
355
		}
356
357
		if ($html) {
358
			return '<div class="flash-messages">' . $html . '</div>';
359
		} else {
360
			return '';
361
		}
362
	}
363
364
	/**
365
	 * Close the main content and create the <footer> tag.
366
	 *
367
	 * @return string
368
	 */
369
	public function footerContainer() {
370
		return '</main><footer>' . $this->footerContent() . '</footer>';
371
	}
372
373
	/**
374
	 * Close the main content.
375
	 * Note that popup windows are deprecated
376
	 *
377
	 * @return string
378
	 */
379
	public function footerContainerPopupWindow() {
380
		return '</main>';
381
	}
382
383
	/**
384
	 * Create the contents of the <footer> tag.
385
	 *
386
	 * @return string
387
	 */
388
	protected function footerContent() {
389
		return
390
			$this->formatContactLinks() .
391
			$this->logoPoweredBy() .
392
			$this->formatPageViews($this->page_views);
393
	}
394
395
	/**
396
	 * Format the contents of a variable-height home-page block.
397
	 *
398
	 * @param string $id
399
	 * @param string $title
400
	 * @param string $class
401
	 * @param string $content
402
	 *
403
	 * @return string
404
	 */
405 View Code Duplication
	public function formatBlock($id, $title, $class, $content) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
406
		return
407
			'<div id="' . $id . '" class="block" >' .
408
			'<div class="blockheader">' . $title . '</div>' .
409
			'<div class="blockcontent ' . $class . '">' . $content . '</div>' .
410
			'</div>';
411
	}
412
413
	/**
414
	 * Add markup to the contact links.
415
	 *
416
	 * @return string
417
	 */
418
	protected function formatContactLinks() {
419
		if ($this->tree) {
420
			return '<div class="contact-links">' . $this->contactLinks() . '</div>';
421
		} else {
422
			return '';
423
		}
424
	}
425
426
	/**
427
	 * Add markup to the hit counter.
428
	 *
429
	 * @param int $count
430
	 *
431
	 * @return string
432
	 */
433
	protected function formatPageViews($count) {
434
		if ($count > 0) {
435
			return
436
				'<div class="page-views">' .
437
				I18N::plural('This page has been viewed %s time.', 'This page has been viewed %s times.', $count,
438
				'<span class="odometer">' . I18N::digits($count) . '</span>') .
439
				'</div>';
440
		} else {
441
			return '';
442
		}
443
	}
444
445
	/**
446
	 * Create a pending changes link for the page footer.
447
	 *
448
	 * @return string
449
	 */
450
	protected function formatPendingChangesLink() {
451
		if ($this->pendingChangesExist()) {
452
			return '<div class="pending-changes-link">' . $this->pendingChangesLink() . '</div>';
453
		} else {
454
			return '';
455
		}
456
	}
457
458
	/**
459
	 * Create a quick search form for the header.
460
	 *
461
	 * @return string
462
	 */
463
	protected function formQuickSearch() {
464
		if ($this->tree) {
465
			return
466
				'<form action="search.php" class="header-search" role="search">' .
467
				'<input type="hidden" name="action" value="header">' .
468
				'<input type="hidden" name="ged" value="' . $this->tree->getNameHtml() . '">' .
469
				$this->formQuickSearchFields() .
470
				'</form>';
471
		} else {
472
			return '';
473
		}
474
	}
475
476
	/**
477
	 * Create a search field and submit button for the quick search form in the header.
478
	 *
479
	 * @return string
480
	 */
481
	protected function formQuickSearchFields() {
482
		return
483
			'<input type="search" name="query" size="15" placeholder="' . I18N::translate('Search') . '">' .
484
			'<input type="image" src="' . $this->assetUrl() . 'images/go.png" alt="' . I18N::translate('Search') . '">';
485
	}
486
487
	/**
488
	 * Add markup to the tree title.
489
	 *
490
	 * @return string
491
	 */
492
	protected function formatTreeTitle() {
493
		if ($this->tree) {
494
			return '<h1 class="header-title">' . $this->tree->getTitleHtml() . '</h1>';
495
		} else {
496
			return '';
497
		}
498
	}
499
500
	/**
501
	 * Add markup to the secondary menu.
502
	 *
503
	 * @return string
504
	 */
505
	protected function formatSecondaryMenu() {
506
		return
507
			'<ul class="secondary-menu">' .
508
			implode('', $this->secondaryMenu()) .
509
			'</ul>';
510
	}
511
512
	/**
513
	 * Add markup to an item in the secondary menu.
514
	 *
515
	 * @param Menu $menu
516
	 *
517
	 * @return string
518
	 */
519
	protected function formatSecondaryMenuItem(Menu $menu) {
520
		return $menu->getMenuAsList();
521
	}
522
523
	/**
524
	 * Create the <head> tag.
525
	 *
526
	 * @param PageController $controller The current controller
527
	 *
528
	 * @return string
529
	 */
530
	public function head(PageController $controller) {
531
		// Record this now. By the time we render the footer, $controller no longer exists.
532
		$this->page_views = $this->pageViews($controller);
533
534
		return
535
			'<head>' .
536
			$this->headContents($controller) .
537
			$this->hookHeaderExtraContent() .
538
			$this->analytics() .
539
			'</head>';
540
	}
541
542
	/**
543
	 * Create the contents of the <head> tag.
544
	 *
545
	 * @param PageController $controller The current controller
546
	 *
547
	 * @return string
548
	 */
549
	protected function headContents(PageController $controller) {
550
		// The title often includes the names of records, which may include HTML markup.
551
		$title = Filter::unescapeHtml($controller->getPageTitle());
552
553
		// If an extra (site) title is specified, append it.
554
		if ($this->tree && $this->tree->getPreference('META_TITLE')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->tree->getPreference('META_TITLE') of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
555
			$title .= ' - ' . Filter::escapeHtml($this->tree->getPreference('META_TITLE'));
556
		}
557
558
		$html =
559
			// modernizr.js and respond.js need to be loaded before the <body> to avoid FOUC
560
			'<!--[if IE 8]><script src="' . WT_MODERNIZR_JS_URL . '"></script><![endif]-->' .
561
			'<!--[if IE 8]><script src="' . WT_RESPOND_JS_URL . '"></script><![endif]-->' .
562
			$this->metaCharset() .
563
			$this->title($title) .
564
			$this->favicon() .
565
			$this->metaViewport() .
566
			$this->metaRobots($controller->getMetaRobots()) .
567
			$this->metaUaCompatible() .
568
			$this->metaGenerator(WT_WEBTREES . ' ' . WT_VERSION . ' - ' . WT_WEBTREES_URL);
569
570
		if ($this->tree) {
571
			$html .= $this->metaDescription($this->tree->getPreference('META_DESCRIPTION'));
572
		}
573
574
		// CSS files
575
		foreach ($this->stylesheets() as $css) {
576
			$html .= '<link rel="stylesheet" type="text/css" href="' . $css . '">';
577
		}
578
579
		return $html;
580
	}
581
582
	/**
583
	 * Create the contents of the <header> tag.
584
	 *
585
	 * @return string
586
	 */
587
	protected function headerContent() {
588
		return
589
			//$this->accessibilityLinks() .
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
590
			$this->logoHeader() .
591
			$this->secondaryMenuContainer($this->secondaryMenu()) .
592
			$this->formatTreeTitle() .
593
			$this->formQuickSearch();
594
	}
595
596
	/**
597
	 * Create the <header> tag for a popup window.
598
	 *
599
	 * @return string
600
	 */
601
	protected function headerSimple() {
602
		return
603
			$this->flashMessagesContainer(FlashMessages::getMessages()) .
0 ignored issues
show
Documentation introduced by
\Fisharebest\Webtrees\FlashMessages::getMessages() is of type array<integer,string>, but the function expects a array<integer,object<stdClass>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
604
			'<div id="content">';
605
	}
606
607
	/**
608
	 * Allow themes to do things after initialization (since they cannot use
609
	 * the constructor).
610
	 */
611
	public function hookAfterInit() {
612
	}
613
614
	/**
615
	 * Allow themes to add extra scripts to the page footer.
616
	 *
617
	 * @return string
618
	 */
619
	public function hookFooterExtraJavascript() {
620
		return '';
621
	}
622
623
	/**
624
	 * Allow themes to add extra content to the page header.
625
	 * Typically this will be additional CSS.
626
	 *
627
	 * @return string
628
	 */
629
	public function hookHeaderExtraContent() {
630
		return '';
631
	}
632
633
	/**
634
	 * Create the <html> tag.
635
	 *
636
	 * @return string
637
	 */
638
	public function html() {
639
		return '<html ' . I18N::htmlAttributes() . '>';
640
	}
641
642
	/**
643
	 * Add HTML markup to create an alert
644
	 *
645
	 * @param string $html        The content of the alert
646
	 * @param string $level       One of 'success', 'info', 'warning', 'danger'
647
	 * @param bool   $dismissible If true, add a close button.
648
	 *
649
	 * @return string
650
	 */
651
	public function htmlAlert($html, $level, $dismissible) {
652
		if ($dismissible) {
653
			return
654
				'<div class="alert alert-' . $level . ' alert-dismissible" role="alert">' .
655
				'<button type="button" class="close" data-dismiss="alert" aria-label="' . I18N::translate('close') . '">' .
656
				'<span aria-hidden="true">&times;</span>' .
657
				'</button>' .
658
				$html .
659
				'</div>';
660
		} else {
661
			return
662
				'<div class="alert alert-' . $level . '" role="alert">' .
663
				$html .
664
				'</div>';
665
		}
666
	}
667
668
	/**
669
	 * Display an icon for this fact.
670
	 *
671
	 * @param Fact $fact
672
	 *
673
	 * @return string
674
	 */
675
	public function icon(Fact $fact) {
676
		$icon = 'images/facts/' . $fact->getTag() . '.png';
677
		$dir  = substr($this->assetUrl(), strlen(WT_STATIC_URL));
678
		if (file_exists($dir . $icon)) {
679
			return '<img src="' . $this->assetUrl() . $icon . '" title="' . GedcomTag::getLabel($fact->getTag()) . '">';
680
		} elseif (file_exists($dir . 'images/facts/NULL.png')) {
681
			// Spacer image - for alignment - until we move to a sprite.
682
			return '<img src="' . Theme::theme()->assetUrl() . 'images/facts/NULL.png">';
683
		} else {
684
			return '';
685
		}
686
	}
687
688
	/**
689
	 * Display an individual in a box - for charts, etc.
690
	 *
691
	 * @param Individual $individual
692
	 *
693
	 * @return string
694
	 */
695
	public function individualBox(Individual $individual) {
696
		$personBoxClass = array_search($individual->getSex(), array('person_box' => 'M', 'person_boxF' => 'F', 'person_boxNN' => 'U'));
697 View Code Duplication
		if ($individual->canShow() && $individual->getTree()->getPreference('SHOW_HIGHLIGHT_IMAGES')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $individual->getTree()->...SHOW_HIGHLIGHT_IMAGES') of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
698
			$thumbnail = $individual->displayImage();
699
		} else {
700
			$thumbnail = '';
701
		}
702
703
		$content = '<span class="namedef name1">' . $individual->getFullName() . '</span>';
704
		$icons    = '';
705 View Code Duplication
		if ($individual->canShowName()) {
706
			$content =
707
				'<a href="' . $individual->getHtmlUrl() . '">' . $content . '</a>' .
708
				'<div class="namedef name1">' . $individual->getAddName() . '</div>';
709
			$icons =
710
				'<div class="noprint icons">' .
711
				'<span class="iconz icon-zoomin" title="' . I18N::translate('Zoom in/out on this box.') . '"></span>' .
712
				'<div class="itr"><i class="icon-pedigree"></i><div class="popup">' .
713
				'<ul class="' . $personBoxClass . '">' . implode('', $this->individualBoxMenu($individual)) . '</ul>' .
714
				'</div>' .
715
				'</div>' .
716
				'</div>';
717
		}
718
719
		return
720
			'<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">' .
721
			$icons .
722
			'<div class="chart_textbox" style="max-height:' . $this->parameter('chart-box-y') . 'px;">' .
723
			$thumbnail .
724
			$content .
725
			'<div class="inout2 details1">' . $this->individualBoxFacts($individual) . '</div>' .
726
			'</div>' .
727
			'<div class="inout"></div>' .
728
			'</div>';
729
	}
730
731
	/**
732
	 * Display an empty box - for a missing individual in a chart.
733
	 *
734
	 * @return string
735
	 */
736
	public function individualBoxEmpty() {
737
		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>';
738
	}
739
740
	/**
741
	 * Display an individual in a box - for charts, etc.
742
	 *
743
	 * @param Individual $individual
744
	 *
745
	 * @return string
746
	 */
747
	public function individualBoxLarge(Individual $individual) {
748
		$personBoxClass = array_search($individual->getSex(), array('person_box' => 'M', 'person_boxF' => 'F', 'person_boxNN' => 'U'));
749
		if ($individual->getTree()->getPreference('SHOW_HIGHLIGHT_IMAGES')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $individual->getTree()->...SHOW_HIGHLIGHT_IMAGES') of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
750
			$thumbnail = $individual->displayImage();
751
		} else {
752
			$thumbnail = '';
753
		}
754
755
		$content = '<span class="namedef name1">' . $individual->getFullName() . '</span>';
756
		$icons   = '';
757 View Code Duplication
		if ($individual->canShowName()) {
758
			$content =
759
				'<a href="' . $individual->getHtmlUrl() . '">' . $content . '</a>' .
760
				'<div class="namedef name2">' . $individual->getAddName() . '</div>';
761
			$icons =
762
				'<div class="noprint icons">' .
763
				'<span class="iconz icon-zoomin" title="' . I18N::translate('Zoom in/out on this box.') . '"></span>' .
764
				'<div class="itr"><i class="icon-pedigree"></i><div class="popup">' .
765
				'<ul class="' . $personBoxClass . '">' . implode('', $this->individualBoxMenu($individual)) . '</ul>' .
766
				'</div>' .
767
				'</div>' .
768
				'</div>';
769
		}
770
771
		return
772
			'<div data-pid="' . $individual->getXref() . '" class="person_box_template ' . $personBoxClass . ' box-style2">' .
773
			$icons .
774
			'<div class="chart_textbox" style="max-height:' . $this->parameter('chart-box-y') . 'px;">' .
775
			$thumbnail .
776
			$content .
777
			'<div class="inout2 details2">' . $this->individualBoxFacts($individual) . '</div>' .
778
			'</div>' .
779
			'<div class="inout"></div>' .
780
			'</div>';
781
	}
782
783
	/**
784
	 * Display an individual in a box - for charts, etc.
785
	 *
786
	 * @param Individual $individual
787
	 *
788
	 * @return string
789
	 */
790
	public function individualBoxSmall(Individual $individual) {
791
		$personBoxClass = array_search($individual->getSex(), array('person_box' => 'M', 'person_boxF' => 'F', 'person_boxNN' => 'U'));
792
		if ($individual->getTree()->getPreference('SHOW_HIGHLIGHT_IMAGES')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $individual->getTree()->...SHOW_HIGHLIGHT_IMAGES') of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
793
			$thumbnail = $individual->displayImage();
794
		} else {
795
			$thumbnail = '';
796
		}
797
798
		return
799
			'<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">' .
800
			'<div class="compact_view">' .
801
			$thumbnail .
802
			'<a href="' . $individual->getHtmlUrl() . '">' .
803
			'<span class="namedef name0">' . $individual->getFullName() . '</span>' .
804
			'</a>' .
805
			'<div class="inout2 details0">' . $individual->getLifeSpan() . '</div>' .
806
			'</div>' .
807
			'<div class="inout"></div>' .
808
			'</div>';
809
	}
810
811
	/**
812
	 * Display an individual in a box - for charts, etc.
813
	 *
814
	 * @return string
815
	 */
816
	public function individualBoxSmallEmpty() {
817
		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>';
818
	}
819
820
	/**
821
	 * Generate the facts, for display in charts.
822
	 *
823
	 * @param Individual $individual
824
	 *
825
	 * @return string
826
	 */
827
	protected function individualBoxFacts(Individual $individual) {
828
		$html = '';
829
830
		$opt_tags = preg_split('/\W/', $individual->getTree()->getPreference('CHART_BOX_TAGS'), 0, PREG_SPLIT_NO_EMPTY);
831
		// Show BIRT or equivalent event
832
		foreach (explode('|', WT_EVENTS_BIRT) as $birttag) {
833
			if (!in_array($birttag, $opt_tags)) {
834
				$event = $individual->getFirstFact($birttag);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $event is correct as $individual->getFirstFact($birttag) (which targets Fisharebest\Webtrees\GedcomRecord::getFirstFact()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
835
				if ($event) {
836
					$html .= $event->summary();
837
					break;
838
				}
839
			}
840
		}
841
		// Show optional events (before death)
842
		foreach ($opt_tags as $key => $tag) {
843
			if (!preg_match('/^(' . WT_EVENTS_DEAT . ')$/', $tag)) {
844
				$event = $individual->getFirstFact($tag);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $event is correct as $individual->getFirstFact($tag) (which targets Fisharebest\Webtrees\GedcomRecord::getFirstFact()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
845
				if (!is_null($event)) {
846
					$html .= $event->summary();
847
					unset($opt_tags[$key]);
848
				}
849
			}
850
		}
851
		// Show DEAT or equivalent event
852
		foreach (explode('|', WT_EVENTS_DEAT) as $deattag) {
853
			$event = $individual->getFirstFact($deattag);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $event is correct as $individual->getFirstFact($deattag) (which targets Fisharebest\Webtrees\GedcomRecord::getFirstFact()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
854
			if ($event) {
855
				$html .= $event->summary();
856
				if (in_array($deattag, $opt_tags)) {
857
					unset($opt_tags[array_search($deattag, $opt_tags)]);
858
				}
859
				break;
860
			}
861
		}
862
		// Show remaining optional events (after death)
863
		foreach ($opt_tags as $tag) {
864
			$event = $individual->getFirstFact($tag);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $event is correct as $individual->getFirstFact($tag) (which targets Fisharebest\Webtrees\GedcomRecord::getFirstFact()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
865
			if ($event) {
866
				$html .= $event->summary();
867
			}
868
		}
869
870
		return $html;
871
	}
872
873
	/**
874
	 * Generate the LDS summary, for display in charts.
875
	 *
876
	 * @param Individual $individual
877
	 *
878
	 * @return string
879
	 */
880
	protected function individualBoxLdsSummary(Individual $individual) {
881
		if ($individual->getTree()->getPreference('SHOW_LDS_AT_GLANCE')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $individual->getTree()->...e('SHOW_LDS_AT_GLANCE') of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
882
			$BAPL = $individual->getFacts('BAPL') ? 'B' : '_';
883
			$ENDL = $individual->getFacts('ENDL') ? 'E' : '_';
884
			$SLGC = $individual->getFacts('SLGC') ? 'C' : '_';
885
			$SLGS = '_';
886
887
			foreach ($individual->getSpouseFamilies() as $family) {
888
				if ($family->getFacts('SLGS')) {
889
					$SLGS = '';
890
				}
891
			}
892
893
			return $BAPL . $ENDL . $SLGS . $SLGC;
894
		} else {
895
			return '';
896
		}
897
	}
898
899
	/**
900
	 * Links, to show in chart boxes;
901
	 *
902
	 * @param Individual $individual
903
	 *
904
	 * @return Menu[]
905
	 */
906
	protected function individualBoxMenu(Individual $individual) {
907
		$menus = array_merge(
908
			$this->individualBoxMenuCharts($individual),
909
			$this->individualBoxMenuFamilyLinks($individual)
910
		);
911
912
		return $menus;
913
	}
914
915
	/**
916
	 * Chart links, to show in chart boxes;
917
	 *
918
	 * @param Individual $individual
919
	 *
920
	 * @return Menu[]
921
	 */
922
	protected function individualBoxMenuCharts(Individual $individual) {
923
		$menus = array_filter(array(
924
			$this->menuChartAncestors($individual),
925
			$this->menuChartCompact($individual),
926
			$this->menuChartDescendants($individual),
927
			$this->menuChartFanChart($individual),
928
			$this->menuChartHourglass($individual),
929
			$this->menuChartInteractiveTree($individual),
930
			$this->menuChartPedigree($individual),
931
			$this->menuChartPedigreeMap($individual),
932
			$this->menuChartRelationship($individual),
933
			$this->menuChartTimeline($individual),
934
		));
935
936
		usort($menus, function (Menu $x, Menu $y) {
937
			return I18N::strcasecmp($x->getLabel(), $y->getLabel());
938
		});
939
940
		return $menus;
941
	}
942
943
	/**
944
	 * Family links, to show in chart boxes.
945
	 *
946
	 * @param Individual $individual
947
	 *
948
	 * @return Menu[]
949
	 */
950
	protected function individualBoxMenuFamilyLinks(Individual $individual) {
951
		$menus = array();
952
953
		foreach ($individual->getSpouseFamilies() as $family) {
954
			$menus[] = new Menu('<strong>' . I18N::translate('Family with spouse') . '</strong>', $family->getHtmlUrl());
955
			$spouse  = $family->getSpouse($individual);
956
			if ($spouse && $spouse->canShowName()) {
957
				$menus[] = new Menu($spouse->getFullName(), $spouse->getHtmlUrl());
958
			}
959
			foreach ($family->getChildren() as $child) {
960
				if ($child->canShowName()) {
961
					$menus[] = new Menu($child->getFullName(), $child->getHtmlUrl());
962
				}
963
			}
964
		}
965
966
		return $menus;
967
	}
968
969
	/**
970
	 * Create part of an individual box
971
	 *
972
	 * @param Individual $individual
973
	 *
974
	 * @return string
975
	 */
976
	protected function individualBoxSexSymbol(Individual $individual) {
977
		if ($individual->getTree()->getPreference('PEDIGREE_SHOW_GENDER')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $individual->getTree()->...'PEDIGREE_SHOW_GENDER') of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
978
			return $individual->sexImage('large');
979
		} else {
980
			return '';
981
		}
982
	}
983
984
	/**
985
	 * Initialise the theme. We cannot pass these in a constructor, as the construction
986
	 * happens in a theme file, and we need to be able to change it.
987
	 *
988
	 * @param Tree|null $tree The current tree (if there is one).
989
	 */
990
	final public function init(Tree $tree = null) {
991
		$this->tree     = $tree;
992
		$this->tree_url = $tree ? 'ged=' . $tree->getNameUrl() : '';
993
994
		$this->hookAfterInit();
995
	}
996
997
	/**
998
	 * A large webtrees logo, for the header.
999
	 *
1000
	 * @return string
1001
	 */
1002
	protected function logoHeader() {
1003
		return '<div class="header-logo"></div>';
1004
	}
1005
1006
	/**
1007
	 * A small "powered by webtrees" logo for the footer.
1008
	 *
1009
	 * @return string
1010
	 */
1011
	protected function logoPoweredBy() {
1012
		return '<a href="' . WT_WEBTREES_URL . '" class="powered-by-webtrees" title="' . WT_WEBTREES_URL . '"></a>';
1013
	}
1014
1015
	/**
1016
	 * A menu for the day/month/year calendar views.
1017
	 *
1018
	 * @return Menu
1019
	 */
1020
	protected function menuCalendar() {
1021
		return new Menu(I18N::translate('Calendar'), '#', 'menu-calendar', array('rel' => 'nofollow'), array(
1022
			// Day view
1023
			new Menu(I18N::translate('Day'), 'calendar.php?' . $this->tree_url . '&amp;view=day', 'menu-calendar-day', array('rel' => 'nofollow')),
1024
			// Month view
1025
			new Menu(I18N::translate('Month'), 'calendar.php?' . $this->tree_url . '&amp;view=month', 'menu-calendar-month', array('rel' => 'nofollow')),
1026
			//Year view
1027
			new Menu(I18N::translate('Year'), 'calendar.php?' . $this->tree_url . '&amp;view=year', 'menu-calendar-year', array('rel' => 'nofollow')),
1028
		));
1029
	}
1030
1031
	/**
1032
	 * Generate a menu item to change the blocks on the current (index.php) page.
1033
	 *
1034
	 * @return Menu|null
1035
	 */
1036
	protected function menuChangeBlocks() {
1037
		if (WT_SCRIPT_NAME === 'index.php' && Auth::check() && Filter::get('ctype', 'gedcom|user', 'user') === 'user') {
1038
			return new Menu(I18N::translate('Customize this page'), 'index_edit.php?user_id=' . Auth::id(), 'menu-change-blocks');
1039
		} elseif (WT_SCRIPT_NAME === 'index.php' && Auth::isManager($this->tree)) {
1040
			return new Menu(I18N::translate('Customize this page'), 'index_edit.php?gedcom_id=' . $this->tree->getTreeId(), 'menu-change-blocks');
1041
		} else {
1042
			return null;
1043
		}
1044
	}
1045
1046
	/**
1047
	 * Generate a menu for each of the different charts.
1048
	 *
1049
	 * @param Individual $individual
1050
	 *
1051
	 * @return Menu
1052
	 */
1053
	protected function menuChart(Individual $individual) {
1054
		$submenus = array_filter(array(
1055
			$this->menuChartAncestors($individual),
1056
			$this->menuChartCompact($individual),
1057
			$this->menuChartDescendants($individual),
1058
			$this->menuChartFamilyBook($individual),
1059
			$this->menuChartFanChart($individual),
1060
			$this->menuChartHourglass($individual),
1061
			$this->menuChartInteractiveTree($individual),
1062
			$this->menuChartLifespan($individual),
1063
			$this->menuChartPedigree($individual),
1064
			$this->menuChartPedigreeMap($individual),
1065
			$this->menuChartRelationship($individual),
1066
			$this->menuChartStatistics(),
1067
			$this->menuChartTimeline($individual),
1068
		));
1069
1070
		usort($submenus, function (Menu $x, Menu $y) {
1071
			return I18N::strcasecmp($x->getLabel(), $y->getLabel());
1072
		});
1073
1074
		return new Menu(I18N::translate('Charts'), '#', 'menu-chart', array('rel' => 'nofollow'), $submenus);
1075
	}
1076
1077
	/**
1078
	 * Generate a menu item for the ancestors chart (ancestry.php).
1079
	 *
1080
	 * @param Individual $individual
1081
	 *
1082
	 * @return Menu
1083
	 */
1084
	protected function menuChartAncestors(Individual $individual) {
1085
		return new Menu(I18N::translate('Ancestors'), 'ancestry.php?rootid=' . $individual->getXref() . '&amp;' . $this->tree_url, 'menu-chart-ancestry', array('rel' => 'nofollow'));
1086
	}
1087
1088
	/**
1089
	 * Generate a menu item for the compact tree (compact.php).
1090
	 *
1091
	 * @param Individual $individual
1092
	 *
1093
	 * @return Menu
1094
	 */
1095
	protected function menuChartCompact(Individual $individual) {
1096
		return new Menu(I18N::translate('Compact tree'), 'compact.php?rootid=' . $individual->getXref() . '&amp;' . $this->tree_url, 'menu-chart-compact', array('rel' => 'nofollow'));
1097
	}
1098
1099
	/**
1100
	 * Generate a menu item for the descendants chart (descendancy.php).
1101
	 *
1102
	 * @param Individual $individual
1103
	 *
1104
	 * @return Menu
1105
	 */
1106
	protected function menuChartDescendants(Individual $individual) {
1107
		return new Menu(I18N::translate('Descendants'), 'descendancy.php?rootid=' . $individual->getXref() . '&amp;' . $this->tree_url, 'menu-chart-descendants', array('rel' => 'nofollow'));
1108
	}
1109
1110
	/**
1111
	 * Generate a menu item for the family-book chart (familybook.php).
1112
	 *
1113
	 * @param Individual $individual
1114
	 *
1115
	 * @return Menu
1116
	 */
1117
	protected function menuChartFamilyBook(Individual $individual) {
1118
		return new Menu(I18N::translate('Family book'), 'familybook.php?rootid=' . $individual->getXref() . '&amp;' . $this->tree_url, 'menu-chart-familybook', array('rel' => 'nofollow'));
1119
	}
1120
1121
	/**
1122
	 * Generate a menu item for the fan chart (fanchart.php).
1123
	 *
1124
	 * We can only do this if the GD2 library is installed with TrueType support.
1125
	 *
1126
	 * @param Individual $individual
1127
	 *
1128
	 * @return Menu|null
1129
	 */
1130 View Code Duplication
	protected function menuChartFanChart(Individual $individual) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1131
		if (function_exists('imagettftext')) {
1132
			return new Menu(I18N::translate('Fan chart'), 'fanchart.php?rootid=' . $individual->getXref() . '&amp;' . $this->tree_url, 'menu-chart-fanchart', array('rel' => 'nofollow'));
1133
		} else {
1134
			return null;
1135
		}
1136
	}
1137
1138
	/**
1139
	 * Generate a menu item for the interactive tree (tree module).
1140
	 *
1141
	 * @param Individual $individual
1142
	 *
1143
	 * @return Menu|null
1144
	 */
1145 View Code Duplication
	protected function menuChartInteractiveTree(Individual $individual) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1146
		if (Module::getModuleByName('tree')) {
1147
			return new Menu(I18N::translate('Interactive tree'), 'module.php?mod=tree&amp;mod_action=treeview&amp;' . $this->tree_url . '&amp;rootid=' . $individual->getXref(), 'menu-chart-tree', array('rel' => 'nofollow'));
1148
		} else {
1149
			return null;
1150
		}
1151
	}
1152
1153
	/**
1154
	 * Generate a menu item for the hourglass chart (hourglass.php).
1155
	 *
1156
	 * @param Individual $individual
1157
	 *
1158
	 * @return Menu
1159
	 */
1160
	protected function menuChartHourglass(Individual $individual) {
1161
		return new Menu(I18N::translate('Hourglass chart'), 'hourglass.php?rootid=' . $individual->getXref() . '&amp;' . $this->tree_url, 'menu-chart-hourglass', array('rel' => 'nofollow'));
1162
	}
1163
1164
	/**
1165
	 * Generate a menu item for the lifepsan chart (lifespan.php).
1166
	 *
1167
	 * @param Individual $individual
1168
	 *
1169
	 * @return Menu
1170
	 */
1171
	protected function menuChartLifespan(Individual $individual) {
0 ignored issues
show
Unused Code introduced by
The parameter $individual is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1172
		return new Menu(I18N::translate('Lifespans'), 'lifespan.php', 'menu-chart-lifespan', array('rel' => 'nofollow'));
1173
	}
1174
1175
	/**
1176
	 * Generate a menu item for the pedigree chart (pedigree.php).
1177
	 *
1178
	 * @param Individual $individual
1179
	 *
1180
	 * @return Menu
1181
	 */
1182
	protected function menuChartPedigree(Individual $individual) {
1183
		return new Menu(I18N::translate('Pedigree'), 'pedigree.php?rootid=' . $individual->getXref() . '&amp;' . $this->tree_url, 'menu-chart-pedigree', array('rel' => 'nofollow'));
1184
	}
1185
1186
	/**
1187
	 * Generate a menu item for the pedigree map (googlemap module).
1188
	 *
1189
	 * @param Individual $individual
1190
	 *
1191
	 * @return Menu|null
1192
	 */
1193 View Code Duplication
	protected function menuChartPedigreeMap(Individual $individual) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1194
		if (Module::getModuleByName('googlemap')) {
1195
			return new Menu(I18N::translate('Pedigree map'), 'module.php?' . $this->tree_url . '&amp;mod=googlemap&amp;mod_action=pedigree_map&amp;rootid=' . $individual->getXref(), 'menu-chart-pedigree_map', array('rel' => 'nofollow'));
1196
		} else {
1197
			return null;
1198
		}
1199
	}
1200
1201
	/**
1202
	 * Generate a menu item for the relationship chart (relationship.php).
1203
	 *
1204
	 * @param Individual $individual
1205
	 *
1206
	 * @return Menu
1207
	 */
1208
	protected function menuChartRelationship(Individual $individual) {
1209
		$gedcomid = $this->tree->getUserPreference(Auth::user(), 'gedcomid');
1210
1211
		if ($gedcomid && $individual->getXref()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $gedcomid of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1212
			return new Menu(I18N::translate('Relationship to me'), 'relationship.php?pid1=' . $gedcomid . '&amp;pid2=' . $individual->getXref() . '&amp;ged=' . $this->tree_url, 'menu-chart-relationship', array('rel' => 'nofollow'));
1213
		} else {
1214
			return new Menu(I18N::translate('Relationships'), 'relationship.php?pid1=' . $individual->getXref() . '&amp;ged=' . $this->tree_url, 'menu-chart-relationship', array('rel' => 'nofollow'));
1215
		}
1216
	}
1217
1218
	/**
1219
	 * Generate a menu item for the statistics charts (statistics.php).
1220
	 *
1221
	 * @return Menu
1222
	 */
1223
	protected function menuChartStatistics() {
1224
		return new Menu(I18N::translate('Statistics'), 'statistics.php?' . $this->tree_url, 'menu-chart-statistics', array('rel' => 'nofollow'));
1225
	}
1226
1227
	/**
1228
	 * Generate a menu item for the timeline chart (timeline.php).
1229
	 *
1230
	 * @param Individual $individual
1231
	 *
1232
	 * @return Menu
1233
	 */
1234
	protected function menuChartTimeline(Individual $individual) {
1235
		return new Menu(I18N::translate('Timeline'), 'timeline.php?pids%5B%5D=' . $individual->getXref() . '&amp;' . $this->tree_url, 'menu-chart-timeline', array('rel' => 'nofollow'));
1236
	}
1237
1238
	/**
1239
	 * Generate a menu item for the control panel (admin.php).
1240
	 *
1241
	 * @return Menu|null
1242
	 */
1243
	protected function menuControlPanel() {
1244
		if (Auth::isManager($this->tree)) {
1245
			return new Menu(I18N::translate('Control panel'), 'admin.php', 'menu-admin');
1246
		} else {
1247
			return null;
1248
		}
1249
	}
1250
1251
	/**
1252
	 * Favorites menu.
1253
	 *
1254
	 * @return Menu|null
1255
	 */
1256
	protected function menuFavorites() {
1257
		global $controller;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1258
1259
		$show_user_favorites = $this->tree && Module::getModuleByName('user_favorites') && Auth::check();
1260
		$show_tree_favorites = $this->tree && Module::getModuleByName('gedcom_favorites');
1261
1262
		if ($show_user_favorites && $show_tree_favorites) {
1263
			$favorites = array_merge(
1264
				FamilyTreeFavoritesModule::getFavorites($this->tree->getTreeId()),
1265
				UserFavoritesModule::getFavorites(Auth::id())
1266
			);
1267
		} elseif ($show_user_favorites) {
1268
			$favorites = UserFavoritesModule::getFavorites(Auth::id());
1269
		} elseif ($show_tree_favorites) {
1270
			$favorites = FamilyTreeFavoritesModule::getFavorites($this->tree->getTreeId());
1271
		} else {
1272
			return null;
1273
		}
1274
1275
		$menu = new Menu(I18N::translate('Favorites'), '#', 'menu-favorites');
1276
1277
		foreach ($favorites as $favorite) {
1278
			switch ($favorite['type']) {
1279
			case 'URL':
1280
				$submenu = new Menu($favorite['title'], $favorite['url']);
1281
				$menu->addSubmenu($submenu);
1282
				break;
1283
			case 'INDI':
1284
			case 'FAM':
1285
			case 'SOUR':
1286
			case 'OBJE':
1287
			case 'NOTE':
1288
				$obj = GedcomRecord::getInstance($favorite['gid'], $this->tree);
1289
				if ($obj && $obj->canShowName()) {
1290
					$menu->addSubmenu(new Menu($obj->getFullName(), $obj->getHtmlUrl()));
1291
				}
1292
				break;
1293
			}
1294
		}
1295
1296
		if ($show_user_favorites && isset($controller->record) && $controller->record instanceof GedcomRecord) {
1297
			$menu->addSubmenu(new Menu(I18N::translate('Add to favorites'), '#', '', array(
1298
				'onclick' => 'jQuery.post("module.php?mod=user_favorites&mod_action=menu-add-favorite", {xref:"' . $controller->record->getXref() . '"},function(){location.reload();})',
1299
			)));
1300
		}
1301
1302
		return $menu;
1303
	}
1304
1305
	/**
1306
	 * A menu for the home (family tree) pages.
1307
	 *
1308
	 * @return Menu
1309
	 */
1310
	protected function menuHomePage() {
1311
		if (count(Tree::getAll()) === 1 || Site::getPreference('ALLOW_CHANGE_GEDCOM') === '0') {
1312
			return new Menu(I18N::translate('Family tree'), 'index.php?ctype=gedcom&amp;' . $this->tree_url, 'menu-tree');
1313
		} else {
1314
			$submenus = array();
1315
			foreach (Tree::getAll() as $tree) {
1316
				if ($tree == $this->tree) {
1317
					$active = 'active ';
1318
				} else {
1319
					$active = '';
1320
				}
1321
				$submenus[] = new Menu($tree->getTitleHtml(), 'index.php?ctype=gedcom&amp;ged=' . $tree->getNameUrl(), $active . 'menu-tree-' . $tree->getTreeId());
1322
			}
1323
1324
			return new Menu(I18N::translate('Family trees'), '#', 'menu-tree', array(), $submenus);
1325
		}
1326
	}
1327
1328
	/**
1329
	 * A menu to show a list of available languages.
1330
	 *
1331
	 * @return Menu|null
1332
	 */
1333
	protected function menuLanguages() {
1334
		$menu = new Menu(I18N::translate('Language'), '#', 'menu-language');
1335
1336
		foreach (I18N::activeLocales() as $locale) {
1337
			$language_tag = $locale->languageTag();
1338
			$class        = 'menu-language-' . $language_tag . (WT_LOCALE === $language_tag ? ' active' : '');
1339
			$menu->addSubmenu(new Menu($locale->endonym(), '#', $class, array(
1340
				'onclick'       => 'return false;',
1341
				'data-language' => $language_tag,
1342
			)));
1343
		}
1344
1345
		if (count($menu->getSubmenus()) > 1) {
1346
			return $menu;
1347
		} else {
1348
			return null;
1349
		}
1350
	}
1351
1352
	/**
1353
	 * Create a menu to show lists of individuals, families, sources, etc.
1354
	 *
1355
	 * @param string $surname The significant surname on the page
1356
	 *
1357
	 * @return Menu
1358
	 */
1359
	protected function menuLists($surname) {
1360
		// Do not show empty lists
1361
		$row = Database::prepare(
1362
			"SELECT SQL_CACHE" .
1363
			" EXISTS(SELECT 1 FROM `##sources` WHERE s_file = ?) AS sour," .
1364
			" EXISTS(SELECT 1 FROM `##other` WHERE o_file = ? AND o_type='REPO') AS repo," .
1365
			" EXISTS(SELECT 1 FROM `##other` WHERE o_file = ? AND o_type='NOTE') AS note," .
1366
			" EXISTS(SELECT 1 FROM `##media` WHERE m_file = ?) AS obje"
1367
		)->execute(array(
1368
			$this->tree->getTreeId(),
1369
			$this->tree->getTreeId(),
1370
			$this->tree->getTreeId(),
1371
			$this->tree->getTreeId(),
1372
		))->fetchOneRow();
1373
1374
		$submenus = array(
1375
			$this->menuListsIndividuals($surname),
1376
			$this->menuListsFamilies($surname),
1377
			$this->menuListsBranches($surname),
1378
			$this->menuListsPlaces(),
1379
		);
1380
		if ($row->obje) {
1381
			$submenus[] = $this->menuListsMedia();
1382
		}
1383
		if ($row->repo) {
1384
			$submenus[] = $this->menuListsRepositories();
1385
		}
1386
		if ($row->sour) {
1387
			$submenus[] = $this->menuListsSources();
1388
		}
1389
		if ($row->note) {
1390
			$submenus[] = $this->menuListsNotes();
1391
		}
1392
1393
		uasort($submenus, function (Menu $x, Menu $y) {
1394
			return I18N::strcasecmp($x->getLabel(), $y->getLabel());
1395
		});
1396
1397
		return new Menu(I18N::translate('Lists'), '#', 'menu-list', array(), $submenus);
1398
	}
1399
1400
	/**
1401
	 * A menu for the list of branches
1402
	 *
1403
	 * @param string $surname The significant surname on the page
1404
	 *
1405
	 * @return Menu
1406
	 */
1407
	protected function menuListsBranches($surname) {
1408
		return new Menu(I18N::translate('Branches'), 'branches.php?ged=' . $this->tree->getNameUrl() . '&amp;surname=' . rawurlencode($surname), 'menu-branches', array('rel' => 'nofollow'));
1409
	}
1410
1411
	/**
1412
	 * A menu for the list of families
1413
	 *
1414
	 * @param string $surname The significant surname on the page
1415
	 *
1416
	 * @return Menu
1417
	 */
1418
	protected function menuListsFamilies($surname) {
1419
		return new Menu(I18N::translate('Families'), 'famlist.php?ged=' . $this->tree->getNameUrl() . '&amp;surname=' . rawurlencode($surname), 'menu-list-fam', array('rel' => 'nofollow'));
1420
	}
1421
1422
	/**
1423
	 * A menu for the list of individuals
1424
	 *
1425
	 * @param string $surname The significant surname on the page
1426
	 *
1427
	 * @return Menu
1428
	 */
1429
	protected function menuListsIndividuals($surname) {
1430
		return new Menu(I18N::translate('Individuals'), 'indilist.php?ged=' . $this->tree->getNameUrl() . '&amp;surname=' . rawurlencode($surname), 'menu-list-indi');
1431
	}
1432
1433
	/**
1434
	 * A menu for the list of media objects
1435
	 *
1436
	 * @return Menu
1437
	 */
1438
	protected function menuListsMedia() {
1439
		return new Menu(I18N::translate('Media objects'), 'medialist.php?' . $this->tree_url, 'menu-list-obje', array('rel' => 'nofollow'));
1440
	}
1441
1442
	/**
1443
	 * A menu for the list of notes
1444
	 *
1445
	 * @return Menu
1446
	 */
1447
	protected function menuListsNotes() {
1448
		return new Menu(I18N::translate('Shared notes'), 'notelist.php?' . $this->tree_url, 'menu-list-note', array('rel' => 'nofollow'));
1449
	}
1450
1451
	/**
1452
	 * A menu for the list of individuals
1453
	 *
1454
	 * @return Menu
1455
	 */
1456
	protected function menuListsPlaces() {
1457
		return new Menu(I18N::translate('Place hierarchy'), 'placelist.php?ged=' . $this->tree->getNameUrl(), 'menu-list-plac', array('rel' => 'nofollow'));
1458
	}
1459
1460
	/**
1461
	 * A menu for the list of repositories
1462
	 *
1463
	 * @return Menu
1464
	 */
1465
	protected function menuListsRepositories() {
1466
		return new Menu(I18N::translate('Repositories'), 'repolist.php?' . $this->tree_url, 'menu-list-repo', array('rel' => 'nofollow'));
1467
	}
1468
1469
	/**
1470
	 * A menu for the list of sources
1471
	 *
1472
	 * @return Menu
1473
	 */
1474
	protected function menuListsSources() {
1475
		return new Menu(I18N::translate('Sources'), 'sourcelist.php?' . $this->tree_url, 'menu-list-sour', array('rel' => 'nofollow'));
1476
	}
1477
1478
	/**
1479
	 * A login menu option (or null if we are already logged in).
1480
	 *
1481
	 * @return Menu|null
1482
	 */
1483
	protected function menuLogin() {
1484
		if (Auth::check() || WT_SCRIPT_NAME === 'login.php') {
1485
			return null;
1486
		} else {
1487
			return new Menu(I18N::translate('Login'), WT_LOGIN_URL . '?url=' . rawurlencode(Functions::getQueryUrl()), 'menu-login', array('rel' => 'nofollow'));
1488
		}
1489
	}
1490
1491
	/**
1492
	 * A logout menu option (or null if we are already logged out).
1493
	 *
1494
	 * @return Menu|null
1495
	 */
1496
	protected function menuLogout() {
1497
		if (Auth::check()) {
1498
			return new Menu(I18N::translate('Logout'), 'logout.php', 'menu-logout');
1499
		} else {
1500
			return null;
1501
		}
1502
	}
1503
1504
	/**
1505
	 * Get the additional menus created by each of the modules
1506
	 *
1507
	 * @return Menu[]
1508
	 */
1509
	protected function menuModules() {
1510
		$menus = array();
1511
		foreach (Module::getActiveMenus($this->tree) as $module) {
1512
			$menus[] = $module->getMenu();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Fisharebest\Webtrees\Module\AbstractModule as the method getMenu() does only exist in the following sub-classes of Fisharebest\Webtrees\Module\AbstractModule: Fisharebest\Webtrees\Module\ClippingsCartModule, Fisharebest\Webtrees\Mod...tlyAskedQuestionsModule, Fisharebest\Webtrees\Module\PageMenuModule, Fisharebest\Webtrees\Module\StoriesModule. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1513
		}
1514
1515
		return array_filter($menus);
1516
	}
1517
1518
	/**
1519
	 * A link to allow users to edit their account settings (edituser.php).
1520
	 *
1521
	 * @return Menu|null
1522
	 */
1523
	protected function menuMyAccount() {
1524
		if (Auth::check()) {
1525
			return new Menu(I18N::translate('My account'), 'edituser.php');
1526
		} else {
1527
			return null;
1528
		}
1529
	}
1530
1531
	/**
1532
	 * A link to the user's individual record (individual.php).
1533
	 *
1534
	 * @return Menu|null
1535
	 */
1536
	protected function menuMyIndividualRecord() {
1537
		$gedcomid = $this->tree->getUserPreference(Auth::user(), 'gedcomid');
1538
1539
		if ($gedcomid) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $gedcomid of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1540
			return new Menu(I18N::translate('My individual record'), 'individual.php?pid=' . $gedcomid . '&amp;' . $this->tree_url, 'menu-myrecord');
1541
		} else {
1542
			return null;
1543
		}
1544
	}
1545
1546
	/**
1547
	 * A link to the user's personal home page.
1548
	 *
1549
	 * @return Menu
1550
	 */
1551
	protected function menuMyPage() {
1552
		return new Menu(I18N::translate('My page'), 'index.php?ctype=user&amp;' . $this->tree_url, 'menu-mypage');
1553
	}
1554
1555
	/**
1556
	 * A menu for the user's personal pages.
1557
	 *
1558
	 * @return Menu|null
1559
	 */
1560
	protected function menuMyPages() {
1561
		if (Auth::id()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \Fisharebest\Webtrees\Auth::id() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1562
			return new Menu(I18N::translate('My pages'), '#', 'menu-mymenu', array(), array_filter(array(
1563
				$this->menuMyPage(),
1564
				$this->menuMyIndividualRecord(),
1565
				$this->menuMyPedigree(),
1566
				$this->menuMyAccount(),
1567
				$this->menuChangeBlocks(),
1568
				$this->menuControlPanel(),
1569
			)));
1570
		} else {
1571
			return null;
1572
		}
1573
	}
1574
1575
	/**
1576
	 * A link to the user's individual record (pedigree.php).
1577
	 *
1578
	 * @return Menu|null
1579
	 */
1580
	protected function menuMyPedigree() {
1581
		$gedcomid = $this->tree->getUserPreference(Auth::user(), 'gedcomid');
1582
1583
		if ($gedcomid) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $gedcomid of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1584
			$showFull   = $this->tree->getPreference('PEDIGREE_FULL_DETAILS') ? 1 : 0;
1585
			$showLayout = $this->tree->getPreference('PEDIGREE_LAYOUT') ? 1 : 0;
1586
1587
			return new Menu(
1588
				I18N::translate('My pedigree'),
1589
				'pedigree.php?' . $this->tree_url . '&amp;rootid=' . $gedcomid . '&amp;show_full=' . $showFull . '&amp;talloffset=' . $showLayout,
1590
				'menu-mypedigree'
1591
			);
1592
		} else {
1593
			return null;
1594
		}
1595
	}
1596
1597
	/**
1598
	 * Create a pending changes menu.
1599
	 *
1600
	 * @return Menu|null
1601
	 */
1602
	protected function menuPendingChanges() {
1603
		if ($this->pendingChangesExist()) {
1604
			$menu = new Menu(I18N::translate('Pending changes'), '#', 'menu-pending', array('onclick' => 'window.open("edit_changes.php", "_blank", chan_window_specs); return false;'));
1605
1606
			return $menu;
1607
		} else {
1608
			return null;
1609
		}
1610
	}
1611
1612
	/**
1613
	 * A menu with a list of reports.
1614
	 *
1615
	 * @return Menu|null
1616
	 */
1617
	protected function menuReports() {
1618
		$submenus = array();
1619
		foreach (Module::getActiveReports($this->tree) as $report) {
1620
			$submenus[] = $report->getReportMenu();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Fisharebest\Webtrees\Module\AbstractModule as the method getReportMenu() does only exist in the following sub-classes of Fisharebest\Webtrees\Module\AbstractModule: Fisharebest\Webtrees\Module\AhnentafelReportModule, Fisharebest\Webtrees\Mod...athMarriageReportModule, Fisharebest\Webtrees\Module\BirthReportModule, Fisharebest\Webtrees\Module\CemeteryReportModule, Fisharebest\Webtrees\Module\ChangeReportModule, Fisharebest\Webtrees\Module\DeathReportModule, Fisharebest\Webtrees\Mod...DescendancyReportModule, Fisharebest\Webtrees\Mod...FactSourcesReportModule, Fisharebest\Webtrees\Mod...FamilyGroupReportModule, Fisharebest\Webtrees\Mod...ualFamiliesReportModule, Fisharebest\Webtrees\Module\IndividualReportModule, Fisharebest\Webtrees\Module\MarriageReportModule, Fisharebest\Webtrees\Mod...issingFactsReportModule, Fisharebest\Webtrees\Module\OccupationReportModule, Fisharebest\Webtrees\Module\PedigreeReportModule, Fisharebest\Webtrees\Mod...IndividualsReportModule. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1621
		}
1622
1623
		if ($submenus) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $submenus of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1624
			return new Menu(I18N::translate('Reports'), '#', 'menu-report', array('rel' => 'nofollow'), $submenus);
1625
		} else {
1626
			return null;
1627
		}
1628
	}
1629
1630
	/**
1631
	 * Create the search menu
1632
	 *
1633
	 * @return Menu
1634
	 */
1635
	protected function menuSearch() {
1636
		//-- main search menu item
1637
		$menu = new Menu(I18N::translate('Search'), '#', 'menu-search', array('rel' => 'nofollow'));
1638
		//-- search_general sub menu
1639
		$menu->addSubmenu(new Menu(I18N::translate('General search'), 'search.php?' . $this->tree_url, 'menu-search-general', array('rel' => 'nofollow')));
1640
		//-- search_soundex sub menu
1641
		$menu->addSubmenu(new Menu(/* I18N: search using “sounds like”, rather than exact spelling */
1642
			I18N::translate('Phonetic search'), 'search.php?' . $this->tree_url . '&amp;action=soundex', 'menu-search-soundex', array('rel' => 'nofollow')));
1643
		//-- advanced search
1644
		$menu->addSubmenu(new Menu(I18N::translate('Advanced search'), 'search_advanced.php?' . $this->tree_url, 'menu-search-advanced', array('rel' => 'nofollow')));
1645
		//-- search_replace sub menu
1646
		if (Auth::isEditor($this->tree)) {
1647
			$menu->addSubmenu(new Menu(I18N::translate('Search and replace'), 'search.php?' . $this->tree_url . '&amp;action=replace', 'menu-search-replace'));
1648
		}
1649
1650
		return $menu;
1651
	}
1652
1653
	/**
1654
	 * Themes menu.
1655
	 *
1656
	 * @return Menu|null
1657
	 */
1658
	public function menuThemes() {
1659
		if ($this->tree && Site::getPreference('ALLOW_USER_THEMES') && $this->tree->getPreference('ALLOW_THEME_DROPDOWN')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \Fisharebest\Webtrees\Si...ce('ALLOW_USER_THEMES') of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug Best Practice introduced by
The expression $this->tree->getPreferen...'ALLOW_THEME_DROPDOWN') of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1660
			$submenus = array();
1661
			foreach (Theme::installedThemes() as $theme) {
1662
				$class      = 'menu-theme-' . $theme->themeId() . ($theme === $this ? ' active' : '');
1663
				$submenus[] = new Menu($theme->themeName(), '#', $class, array(
1664
					'onclick'    => 'return false;',
1665
					'data-theme' => $theme->themeId(),
1666
				));
1667
			}
1668
1669
			usort($submenus, function (Menu $x, Menu $y) {
1670
				return I18N::strcasecmp($x->getLabel(), $y->getLabel());
1671
			});
1672
1673
			$menu = new Menu(I18N::translate('Theme'), '#', 'menu-theme', array(), $submenus);
1674
1675
			return $menu;
1676
		} else {
1677
			return null;
1678
		}
1679
	}
1680
1681
	/**
1682
	 * Create the <meta charset=""> tag.
1683
	 *
1684
	 * @return string
1685
	 */
1686
	protected function metaCharset() {
1687
		return '<meta charset="UTF-8">';
1688
	}
1689
1690
	/**
1691
	 * Create the <meta name="description"> tag.
1692
	 *
1693
	 * @param string $description
1694
	 *
1695
	 * @return string
1696
	 */
1697
	protected function metaDescription($description) {
1698
		if ($description) {
1699
			return '<meta name="description" content="' . $description . '">';
1700
		} else {
1701
			return '';
1702
		}
1703
	}
1704
1705
	/**
1706
	 * Create the <meta name="generator"> tag.
1707
	 *
1708
	 * @param string $generator
1709
	 *
1710
	 * @return string
1711
	 */
1712
	protected function metaGenerator($generator) {
1713
		if ($generator) {
1714
			return '<meta name="generator" content="' . $generator . '">';
1715
		} else {
1716
			return '';
1717
		}
1718
	}
1719
1720
	/**
1721
	 * Create the <meta name="robots"> tag.
1722
	 *
1723
	 * @param string $robots
1724
	 *
1725
	 * @return string
1726
	 */
1727
	protected function metaRobots($robots) {
1728
		if ($robots) {
1729
			return '<meta name="robots" content="' . $robots . '">';
1730
		} else {
1731
			return '';
1732
		}
1733
	}
1734
1735
	/**
1736
	 * Create the <meta http-equiv="X-UA-Compatible"> tag.
1737
	 *
1738
	 * @return string
1739
	 */
1740
	protected function metaUaCompatible() {
1741
		return '<meta http-equiv="X-UA-Compatible" content="IE=edge">';
1742
	}
1743
1744
	/**
1745
	 * Create the <meta name="viewport" content="width=device-width, initial-scale=1"> tag.
1746
	 *
1747
	 * @return string
1748
	 */
1749
	protected function metaViewport() {
1750
		return '<meta name="viewport" content="width=device-width, initial-scale=1">';
1751
	}
1752
1753
	/**
1754
	 * How many times has the current page been shown?
1755
	 *
1756
	 * @param  PageController $controller
1757
	 *
1758
	 * @return int Number of views, or zero for pages that aren't logged.
1759
	 */
1760
	protected function pageViews(PageController $controller) {
1761
		if ($this->tree && $this->tree->getPreference('SHOW_COUNTER')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->tree->getPreference('SHOW_COUNTER') of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1762
			if (isset($controller->record) && $controller->record instanceof GedcomRecord) {
1763
				return HitCounter::countHit($this->tree, WT_SCRIPT_NAME, $controller->record->getXref());
0 ignored issues
show
Bug introduced by
The property record does not seem to exist in Fisharebest\Webtrees\Controller\PageController.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1764
			} elseif (isset($controller->root) && $controller->root instanceof GedcomRecord) {
0 ignored issues
show
Bug introduced by
The property root does not seem to exist in Fisharebest\Webtrees\Controller\PageController.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1765
				return HitCounter::countHit($this->tree, WT_SCRIPT_NAME, $controller->root->getXref());
1766
			} elseif (WT_SCRIPT_NAME === 'index.php') {
1767
				if (Auth::check() && Filter::get('ctype') !== 'gedcom') {
1768
					return HitCounter::countHit($this->tree, WT_SCRIPT_NAME, 'user:' . Auth::id());
1769
				} else {
1770
					return HitCounter::countHit($this->tree, WT_SCRIPT_NAME, 'gedcom:' . $this->tree->getTreeId());
1771
				}
1772
			}
1773
		}
1774
1775
		return 0;
1776
	}
1777
1778
	/**
1779
	 * Misecellaneous dimensions, fonts, styles, etc.
1780
	 *
1781
	 * @param string $parameter_name
1782
	 *
1783
	 * @return string|int|float
1784
	 */
1785
	public function parameter($parameter_name) {
1786
		$parameters = array(
1787
			'chart-background-f'             => 'dddddd',
1788
			'chart-background-m'             => 'cccccc',
1789
			'chart-background-u'             => 'eeeeee',
1790
			'chart-box-x'                    => 250,
1791
			'chart-box-y'                    => 80,
1792
			'chart-descendancy-indent'       => 15,
1793
			'chart-font-color'               => '000000',
1794
			'chart-font-name'                => WT_ROOT . 'packages/dejavu-fonts-ttf-2.35/ttf/DejaVuSans.ttf',
1795
			'chart-font-size'                => 7,
1796
			'chart-spacing-x'                => 5,
1797
			'chart-spacing-y'                => 10,
1798
			'compact-chart-box-x'            => 240,
1799
			'compact-chart-box-y'            => 50,
1800
			'distribution-chart-high-values' => '555555',
1801
			'distribution-chart-low-values'  => 'cccccc',
1802
			'distribution-chart-no-values'   => 'ffffff',
1803
			'distribution-chart-x'           => 440,
1804
			'distribution-chart-y'           => 220,
1805
			'line-width'                     => 1.5,
1806
			'shadow-blur'                    => 0,
1807
			'shadow-color'                   => '',
1808
			'shadow-offset-x'                => 0,
1809
			'shadow-offset-y'                => 0,
1810
			'stats-small-chart-x'            => 440,
1811
			'stats-small-chart-y'            => 125,
1812
			'stats-large-chart-x'            => 900,
1813
			'image-dline'                    => $this->assetUrl() . 'images/dline.png',
1814
			'image-dline2'                   => $this->assetUrl() . 'images/dline2.png',
1815
			'image-hline'                    => $this->assetUrl() . 'images/hline.png',
1816
			'image-spacer'                   => $this->assetUrl() . 'images/spacer.png',
1817
			'image-vline'                    => $this->assetUrl() . 'images/vline.png',
1818
			'image-minus'                    => $this->assetUrl() . 'images/minus.png',
1819
			'image-plus'                     => $this->assetUrl() . 'images/plus.png',
1820
		);
1821
1822
		if (array_key_exists($parameter_name, $parameters)) {
1823
			return $parameters[$parameter_name];
1824
		} else {
1825
			throw new \InvalidArgumentException($parameter_name);
1826
		}
1827
	}
1828
1829
	/**
1830
	 * Are there any pending changes for us to approve?
1831
	 *
1832
	 * @return bool
1833
	 */
1834
	protected function pendingChangesExist() {
1835
		return $this->tree && $this->tree->hasPendingEdit() && Auth::isManager($this->tree);
1836
	}
1837
1838
	/**
1839
	 * Create a pending changes link. Some themes prefer an alert/banner to a menu.
1840
	 *
1841
	 * @return string
1842
	 */
1843
	protected function pendingChangesLink() {
1844
		return
1845
			'<a href="#" onclick="window.open(\'edit_changes.php\', \'_blank\', chan_window_specs); return false;">' .
1846
			$this->pendingChangesLinkText() .
1847
			'</a>';
1848
	}
1849
1850
	/**
1851
	 * Text to use in the pending changes link.
1852
	 *
1853
	 * @return string
1854
	 */
1855
	protected function pendingChangesLinkText() {
1856
		return I18N::translate('There are pending changes for you to moderate.');
1857
	}
1858
1859
	/**
1860
	 * Generate a list of items for the main menu.
1861
	 *
1862
	 * @return Menu[]
1863
	 */
1864
	protected function primaryMenu() {
1865
		global $controller;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1866
1867
		if ($this->tree) {
1868
			$individual = $controller->getSignificantIndividual();
1869
1870
			return array_filter(array_merge(array(
1871
				$this->menuHomePage(),
1872
				$this->menuChart($individual),
1873
				$this->menuLists($controller->getSignificantSurname()),
1874
				$this->menuCalendar(),
1875
				$this->menuReports(),
1876
				$this->menuSearch(),
1877
			), $this->menuModules()));
1878
		} else {
1879
			// No public trees? No genealogy menu!
1880
			return array();
1881
		}
1882
	}
1883
1884
	/**
1885
	 * Add markup to the primary menu.
1886
	 *
1887
	 * @param Menu[] $menus
1888
	 *
1889
	 * @return string
1890
	 */
1891
	protected function primaryMenuContainer(array $menus) {
1892
		return '<nav><ul class="primary-menu">' . $this->primaryMenuContent($menus) . '</ul></nav>';
1893
	}
1894
1895
	/**
1896
	 * Create the primary menu.
1897
	 *
1898
	 * @param Menu[] $menus
1899
	 *
1900
	 * @return string
1901
	 */
1902
	protected function primaryMenuContent(array $menus) {
1903
		return implode('', array_map(function (Menu $menu) { return $menu->getMenuAsList(); }, $menus));
1904
	}
1905
1906
	/**
1907
	 * Generate a list of items for the user menu.
1908
	 *
1909
	 * @return Menu[]
1910
	 */
1911 View Code Duplication
	protected function secondaryMenu() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1912
		return array_filter(array(
1913
			$this->menuPendingChanges(),
1914
			$this->menuMyPages(),
1915
			$this->menuFavorites(),
1916
			$this->menuThemes(),
1917
			$this->menuLanguages(),
1918
			$this->menuLogin(),
1919
			$this->menuLogout(),
1920
		));
1921
	}
1922
1923
	/**
1924
	 * Add markup to the secondary menu.
1925
	 *
1926
	 * @param Menu[] $menus
1927
	 *
1928
	 * @return string
1929
	 */
1930
	protected function secondaryMenuContainer(array $menus) {
1931
		return '<ul class="nav nav-pills secondary-menu">' . $this->secondaryMenuContent($menus) . '</ul>';
1932
	}
1933
1934
	/**
1935
	 * Format the secondary menu.
1936
	 *
1937
	 * @param Menu[] $menus
1938
	 *
1939
	 * @return string
1940
	 */
1941
	protected function secondaryMenuContent(array $menus) {
1942
		return implode('', array_map(function (Menu $menu) { return $menu->getMenuAsList(); }, $menus));
1943
	}
1944
1945
	/**
1946
	 * Send any HTTP headers.
1947
	 */
1948
	public function sendHeaders() {
1949
		header('Content-Type: text/html; charset=UTF-8');
1950
	}
1951
1952
	/**
1953
	 * A list of CSS files to include for this page.
1954
	 *
1955
	 * @return string[]
1956
	 */
1957
	protected function stylesheets() {
1958
		$stylesheets = array(
1959
			WT_BOOTSTRAP_CSS_URL,
1960
			WT_FONT_AWESOME_CSS_URL,
1961
			WT_FONT_AWESOME_RTL_CSS_URL,
1962
		);
1963
1964
		if (I18N::direction() === 'rtl') {
1965
			$stylesheets[] = WT_BOOTSTRAP_RTL_CSS_URL;
1966
		}
1967
1968
		return $stylesheets;
1969
	}
1970
1971
	/**
1972
	 * Create the <title> tag.
1973
	 *
1974
	 * @param string $title
1975
	 *
1976
	 * @return string
1977
	 */
1978
	protected function title($title) {
1979
		return '<title>' . Filter::escapeHtml($title) . '</title>';
1980
	}
1981
}
1982