Completed
Push — openstreetmap ( 902efc...a5697c )
by Greg
20:13 queued 09:32
created

AbstractTheme::footerContainer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 0
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A AbstractTheme::hookFooterExtraJavascript() 0 2 1
1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2018 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\Html;
29
use Fisharebest\Webtrees\I18N;
30
use Fisharebest\Webtrees\Individual;
31
use Fisharebest\Webtrees\Menu;
32
use Fisharebest\Webtrees\Module;
33
use Fisharebest\Webtrees\Module\AncestorsChartModule;
34
use Fisharebest\Webtrees\Module\CompactTreeChartModule;
35
use Fisharebest\Webtrees\Module\DescendancyChartModule;
36
use Fisharebest\Webtrees\Module\FamilyBookChartModule;
37
use Fisharebest\Webtrees\Module\FamilyTreeFavoritesModule;
38
use Fisharebest\Webtrees\Module\FanChartModule;
39
use Fisharebest\Webtrees\Module\GoogleMapsModule;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Module\GoogleMapsModule was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
40
use Fisharebest\Webtrees\Module\HourglassChartModule;
41
use Fisharebest\Webtrees\Module\InteractiveTreeModule;
42
use Fisharebest\Webtrees\Module\LifespansChartModule;
43
use Fisharebest\Webtrees\Module\PedigreeChartModule;
44
use Fisharebest\Webtrees\Module\RelationshipsChartModule;
45
use Fisharebest\Webtrees\Module\StatisticsChartModule;
46
use Fisharebest\Webtrees\Module\TimelineChartModule;
47
use Fisharebest\Webtrees\Module\UserFavoritesModule;
48
use Fisharebest\Webtrees\Site;
49
use Fisharebest\Webtrees\Theme;
50
use Fisharebest\Webtrees\Tree;
51
use Fisharebest\Webtrees\User;
52
use Symfony\Component\HttpFoundation\Request;
53
use stdClass;
54
55
/**
56
 * Common functions for all themes.
57
 */
58
abstract class AbstractTheme {
59
	/**
60
	 * Where are our CSS, JS and other assets?
61
	 */
62
	const THEME_DIR  = '_common';
63
	const ASSET_DIR  = 'themes/' . self::THEME_DIR . '/css-2.0.0/';
64
	const STYLESHEET = self::ASSET_DIR . 'style.css';
65
66
	// Icons are created using <i class="..."></i>
67
	const ICONS = [
68
		// Icons for GEDCOM records
69
		'family'     => 'fas fa-users',
70
		'individual' => 'far fa-user',
71
		'media'      => 'far fa-file-image',
72
		'note'       => 'far fa-sticky-note',
73
		'repository' => 'fas fa-university',
74
		'source'     => 'far fa-file-alt',
75
		'submission' => 'fas fa-upload',
76
		'submitter'  => 'fas fa-user-plus',
77
78
		// Icons for sexes
79
		'F' => 'fas fa-venus',
80
		'M' => 'fas fa-mars',
81
		'U' => 'fas fa-genderless',
82
83
		// Icons for editing
84
		'add'    => 'fas fa-plus',
85
		'config' => 'far fa-cogs',
86
		'copy'   => 'far fa-copy',
87
		'create' => 'fas fa-plus',
88
		'delete' => 'fas fa-trash-alt',
89
		'edit'   => 'fas fa-pencil-alt',
90
		'link'   => 'fas fa-link',
91
		'unlink' => 'fas fa-unlink',
92
93
		// Icons for arrows
94
		'arrow-down'  => 'fas fa-arrow-down',
95
		'arrow-left'  => 'fas fa-arrow-left',
96
		'arrow-right' => 'fas fa-arrow-right',
97
		'arrow-up'    => 'fas fa-arrow-up',
98
99
		// Status icons
100
		'error'   => 'fas fa-exclamation-triangle',
101
		'info'    => 'fas fa-info-circle',
102
		'warning' => 'fas fa-exclamation-circle',
103
104
		// Icons for file types
105
		'mime-application-pdf' => '',
106
		'mime-text-html'       => '',
107
108
		// Other icons
109
		'mail'   => 'far fa-envelope',
110
		'help'   => 'fas fa-info-circle',
111
		'search' => 'fas fa-search',
112
	];
113
114
	/** @var  Request */
115
	protected $request;
116
117
	/** @var Tree */
118
	protected $tree;
119
120
	/** @var int The number of times this page has been shown */
121
	protected $page_views;
122
123
	/**
124
	 * Custom themes should place their initialization code in the function hookAfterInit(), not in
125
	 * the constructor, as all themes get constructed - whether they are used or not.
126
	 */
127
	final public function __construct() {
128
	}
129
130
	/**
131
	 * Create accessibility links for the header.
132
	 *
133
	 * "Skip to content" allows keyboard only users to navigate over the headers without
134
	 * pressing TAB many times.
135
	 *
136
	 * @return string
137
	 */
138
	public function accessibilityLinks() {
139
		return
140
			'<div class="wt-accessibility-links">' .
141
			'<a class="sr-only sr-only-focusable btn btn-info btn-sm" href="#content">' .
142
			/* I18N: Skip over the headers and menus, to the main content of the page */ I18N::translate('Skip to content') .
143
			'</a>' .
144
			'</div>';
145
	}
146
147
	/**
148
	 * Create scripts for analytics and tracking.
149
	 *
150
	 * @return string
151
	 */
152
	public function analytics() {
153
		if ($this->themeId() === '_administration' || !empty($_SERVER['HTTP_DNT'])) {
154
			return '';
155
		} else {
156
			return
157
				$this->analyticsBingWebmaster(
158
					Site::getPreference('BING_WEBMASTER_ID')
159
				) .
160
				$this->analyticsGoogleWebmaster(
161
					Site::getPreference('GOOGLE_WEBMASTER_ID')
162
				) .
163
				$this->analyticsGoogleTracker(
164
					Site::getPreference('GOOGLE_ANALYTICS_ID')
165
				) .
166
				$this->analyticsPiwikTracker(
167
					Site::getPreference('PIWIK_URL'),
168
					Site::getPreference('PIWIK_SITE_ID')
169
				) .
170
				$this->analyticsStatcounterTracker(
171
					Site::getPreference('STATCOUNTER_PROJECT_ID'),
172
					Site::getPreference('STATCOUNTER_SECURITY_ID')
173
				);
174
		}
175
	}
176
177
	/**
178
	 * Create the verification code for Google Webmaster Tools.
179
	 *
180
	 * @param string $verification_id
181
	 *
182
	 * @return string
183
	 */
184
	public function analyticsBingWebmaster($verification_id) {
185
		// Only need to add this to the home page.
186
		if (WT_SCRIPT_NAME === 'index.php' && $verification_id) {
187
			return '<meta name="msvalidate.01" content="' . $verification_id . '">';
188
		} else {
189
			return '';
190
		}
191
	}
192
193
	/**
194
	 * Create the verification code for Google Webmaster Tools.
195
	 *
196
	 * @param string $verification_id
197
	 *
198
	 * @return string
199
	 */
200
	public function analyticsGoogleWebmaster($verification_id) {
201
		// Only need to add this to the home page.
202
		if (WT_SCRIPT_NAME === 'index.php' && $verification_id) {
203
			return '<meta name="google-site-verification" content="' . $verification_id . '">';
204
		} else {
205
			return '';
206
		}
207
	}
208
209
	/**
210
	 * Create the tracking code for Google Analytics.
211
	 *
212
	 * See https://developers.google.com/analytics/devguides/collection/analyticsjs/advanced
213
	 *
214
	 * @param string $analytics_id
215
	 *
216
	 * @return string
217
	 */
218
	public function analyticsGoogleTracker($analytics_id) {
219
		if ($analytics_id) {
220
			// Add extra dimensions (i.e. filtering categories)
221
			$dimensions = (object) [
222
				'dimension1' => $this->tree ? $this->tree->getName() : '-',
223
				'dimension2' => $this->tree ? Auth::accessLevel($this->tree) : '-',
224
			];
225
226
			return
227
				'<script async src="https://www.google-analytics.com/analytics.js"></script>' .
228
				'<script>' .
229
				'window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;' .
230
				'ga("create","' . $analytics_id . '","auto");' .
231
				'ga("send", "pageview", ' . json_encode($dimensions) . ');' .
232
				'</script>';
233
		} else {
234
			return '';
235
		}
236
	}
237
238
	/**
239
	 * Create the tracking code for Piwik Analytics.
240
	 *
241
	 * @param string $url     - The domain/path to Piwik
242
	 * @param string $site_id - The Piwik site identifier
243
	 *
244
	 * @return string
245
	 */
246
	public function analyticsPiwikTracker($url, $site_id) {
247
		$url = preg_replace(['/^https?:\/\//', '/\/$/'], '', $url);
248
249
		if ($url && $site_id) {
250
			return
251
				'<script>' .
252
				'var _paq=_paq||[];' .
253
				'(function(){var u=(("https:"==document.location.protocol)?"https://' . $url . '/":"http://' . $url . '/");' .
254
				'_paq.push(["setSiteId",' . $site_id . ']);' .
255
				'_paq.push(["setTrackerUrl",u+"piwik.php"]);' .
256
				'_paq.push(["trackPageView"]);' .
257
				'_paq.push(["enableLinkTracking"]);' .
258
				'var d=document,g=d.createElement("script"),s=d.getElementsByTagName("script")[0];g.defer=true;g.async=true;g.src=u+"piwik.js";' .
259
				's.parentNode.insertBefore(g,s);})();' .
260
				'</script>';
261
		} else {
262
			return '';
263
		}
264
	}
265
266
	/**
267
	 * Create the tracking code for Statcounter.
268
	 *
269
	 * @param string $project_id  - The statcounter project ID
270
	 * @param string $security_id - The statcounter security ID
271
	 *
272
	 * @return string
273
	 */
274
	public function analyticsStatcounterTracker($project_id, $security_id) {
275
		if ($project_id && $security_id) {
276
			return
277
				'<script>' .
278
				'var sc_project=' . (int) $project_id . ',sc_invisible=1,sc_security="' . $security_id .
279
				'",scJsHost = (("https:"===document.location.protocol)?"https://secure.":"http://www.");' .
280
				'document.write("<sc"+"ript src=\'"+scJsHost+"statcounter.com/counter/counter.js\'></"+"script>");' .
281
				'</script>';
282
		} else {
283
			return '';
284
		}
285
	}
286
287
	/**
288
	 * Where are our CSS, JS and other assets?
289
	 *
290
	 * @deprecated - use the constant directly
291
	 *
292
	 * @return string A relative path, such as "themes/foo/"
293
	 */
294
	public function assetUrl() {
295
		return self::ASSET_DIR;
296
	}
297
298
	/**
299
	 * Create a contact link for a user.
300
	 *
301
	 * @param User $user
302
	 *
303
	 * @return string
304
	 */
305
	public function contactLink(User $user) {
306
		$method = $user->getPreference('contactmethod');
307
308
		switch ($method) {
309
			case 'none':
310
				return '';
311
			case 'mailto':
312
				return '<a href="mailto:' . e($user->getEmail()) . '">' . e($user->getRealName()) . '</a>';
313
			default:
314
				$url = route(Auth::check() ? 'message' : 'contact', [
315
					'ged' => $this->tree->getName(),
316
					'to'  => $user->getUserName(),
317
					'url' => $this->request->getRequestUri(),
318
				]);
319
320
				return '<a href="' . e($url) . '">' . e($user->getRealName()) . '</a>';
321
		}
322
	}
323
324
	/**
325
	 * Create contact link for both technical and genealogy support.
326
	 *
327
	 * @param User $user
328
	 *
329
	 * @return string
330
	 */
331
	public function contactLinkEverything(User $user) {
332
		return I18N::translate('For technical support or genealogy questions contact %s.', $this->contactLink($user));
333
	}
334
335
	/**
336
	 * Create contact link for genealogy support.
337
	 *
338
	 * @param User $user
339
	 *
340
	 * @return string
341
	 */
342
	public function contactLinkGenealogy(User $user) {
343
		return I18N::translate('For help with genealogy questions contact %s.', $this->contactLink($user));
344
	}
345
346
	/**
347
	 * Create contact link for technical support.
348
	 *
349
	 * @param User $user
350
	 *
351
	 * @return string
352
	 */
353
	public function contactLinkTechnical(User $user) {
354
		return I18N::translate('For technical support and information contact %s.', $this->contactLink($user));
355
	}
356
357
	/**
358
	 * Create contact links for the page footer.
359
	 *
360
	 * @return string
361
	 */
362
	public function contactLinks() {
363
		$contact_user   = User::find($this->tree->getPreference('CONTACT_USER_ID'));
0 ignored issues
show
Bug introduced by
$this->tree->getPreference('CONTACT_USER_ID') of type string is incompatible with the type null|integer expected by parameter $user_id of Fisharebest\Webtrees\User::find(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

363
		$contact_user   = User::find(/** @scrutinizer ignore-type */ $this->tree->getPreference('CONTACT_USER_ID'));
Loading history...
364
		$webmaster_user = User::find($this->tree->getPreference('WEBMASTER_USER_ID'));
365
366
		if ($contact_user && $contact_user === $webmaster_user) {
367
			return $this->contactLinkEverything($contact_user);
368
		} elseif ($contact_user && $webmaster_user) {
0 ignored issues
show
introduced by
$webmaster_user is of type Fisharebest\Webtrees\User, thus it always evaluated to true.
Loading history...
369
			return $this->contactLinkGenealogy($contact_user) . '<br>' . $this->contactLinkTechnical($webmaster_user);
370
		} elseif ($contact_user) {
371
			return $this->contactLinkGenealogy($contact_user);
372
		} elseif ($webmaster_user) {
373
			return $this->contactLinkTechnical($webmaster_user);
374
		} else {
375
			return '';
376
		}
377
	}
378
379
	/**
380
	 * Create a cookie warning.
381
	 *
382
	 * @return string
383
	 */
384
	public function cookieWarning() {
385
		if (
386
			empty($_SERVER['HTTP_DNT']) &&
387
			empty($_COOKIE['cookie']) &&
388
			(Site::getPreference('GOOGLE_ANALYTICS_ID') === '1' || Site::getPreference('PIWIK_SITE_ID') === '1' || Site::getPreference('STATCOUNTER_PROJECT_ID') === '1')
389
		) {
390
			return
391
				'<div class="wt-cookie-warning">' .
392
				I18N::translate('Cookies') . ' - ' .
393
				I18N::translate('This website uses cookies to learn about visitor behaviour.') . ' ' .
394
				'<button onclick="document.cookie=\'cookie=1\'; this.parentNode.classList.add(\'hidden\');">' . I18N::translate('continue') . '</button>' .
395
				'</div>';
396
		} else {
397
			return '';
398
		}
399
	}
400
401
	/**
402
	 * Add markup to the contact links.
403
	 *
404
	 * @return string
405
	 */
406
	public function formatContactLinks() {
407
		if ($this->tree) {
408
			return '<div class="wt-contact-links">' . $this->contactLinks() . '</div>';
409
		} else {
410
			return '';
411
		}
412
	}
413
414
	/**
415
	 * Add markup to the hit counter.
416
	 *
417
	 * @param int $count
418
	 *
419
	 * @return string
420
	 */
421
	public function formatPageViews($count) {
422
		if ($count > 0) {
423
			return
424
				'<div class="wt-page-views">' .
425
				I18N::plural('This page has been viewed %s time.', 'This page has been viewed %s times.', $count,
426
					'<span class="odometer">' . I18N::digits($count) . '</span>') .
427
				'</div>';
428
		} else {
429
			return '';
430
		}
431
	}
432
433
	/**
434
	 * Create a pending changes link for the page footer.
435
	 *
436
	 * @return string
437
	 */
438
	public function formatPendingChangesLink() {
439
		if ($this->pendingChangesExist()) {
440
			return '<div class="pending-changes-link">' . $this->pendingChangesLink() . '</div>';
441
		} else {
442
			return '';
443
		}
444
	}
445
446
	/**
447
	 * Add markup to the secondary menu.
448
	 *
449
	 * @return string
450
	 */
451
	public function formatSecondaryMenu() {
452
		return
453
			'<ul class="nav wt-secondary-menu">' .
454
			implode('', array_map(function (Menu $menu) {
455
				return $this->formatSecondaryMenuItem($menu);
456
			}, $this->secondaryMenu())) .
457
			'</ul>';
458
	}
459
460
	/**
461
	 * Add markup to an item in the secondary menu.
462
	 *
463
	 * @param Menu $menu
464
	 *
465
	 * @return string
466
	 */
467
	public function formatSecondaryMenuItem(Menu $menu) {
468
		return $menu->bootstrap4();
469
	}
470
471
	/**
472
	 * Create resources for the colors theme.
473
	 */
474
	public function hookAfterInit() {
475
	}
476
477
	/**
478
	 * Allow themes to add extra scripts to the page footer.
479
	 *
480
	 * @return string
481
	 */
482
	public function hookFooterExtraJavascript() {
483
		return '';
484
	}
485
486
	/**
487
	 * Display an icon for this fact.
488
	 *
489
	 * @param Fact $fact
490
	 *
491
	 * @return string
492
	 */
493
	public function icon(Fact $fact) {
494
		$icon = 'images/facts/' . $fact->getTag() . '.png';
495
		if (file_exists(self::ASSET_DIR . $icon)) {
496
			return '<img src="' . self::ASSET_DIR . $icon . '" title="' . GedcomTag::getLabel($fact->getTag()) . '">';
497
		} elseif (file_exists(self::ASSET_DIR . 'images/facts/NULL.png')) {
498
			// Spacer image - for alignment - until we move to a sprite.
499
			return '<img src="' . Theme::theme()->assetUrl() . 'images/facts/NULL.png">';
0 ignored issues
show
Bug introduced by
The method assetUrl() does not exist on Fisharebest\Webtrees\Theme\ThemeInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Fisharebest\Webtrees\Theme\ThemeInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

499
			return '<img src="' . Theme::theme()->/** @scrutinizer ignore-call */ assetUrl() . 'images/facts/NULL.png">';
Loading history...
500
		} else {
501
			return '';
502
		}
503
	}
504
505
	/**
506
	 * Decorative icons are used in addition to text.
507
	 * They need additional markup to hide them from assistive technologies.
508
	 *
509
	 * Semantic icons are used in place of text.
510
	 * They need additional markup to convey their meaning to assistive technologies.
511
	 *
512
	 * @link http://fontawesome.io/accessibility
513
	 *
514
	 * @param string $icon
515
	 * @param string $text
516
	 *
517
	 * @return string
518
	 */
519
	public function replacementIconFunction($icon, $text = '') {
520
		if (array_key_exists($icon, self::ICONS)) {
521
			if ($text === '') {
522
				// Decorative icon.  Hiden from assistive technology.
523
				return '<i class="' . self::ICONS[$icon] . '" aria-hidden="true"></i>';
524
			} else {
525
				// Semantic icon.  Label for assistive technology.
526
				return
527
					'<i class="' . self::ICONS[$icon] . '" title="' . $text . '"></i>' .
528
					'<span class="sr-only">' . $text . '</span>';
529
			}
530
		} else {
531
			return $text;
532
		}
533
	}
534
535
536
	/**
537
	 * Display an individual in a box - for charts, etc.
538
	 *
539
	 * @param Individual $individual
540
	 *
541
	 * @return string
542
	 */
543
	public function individualBox(Individual $individual) {
544
		$personBoxClass = array_search($individual->getSex(), ['person_box' => 'M', 'person_boxF' => 'F', 'person_boxNN' => 'U']);
545
		if ($individual->canShow() && $individual->getTree()->getPreference('SHOW_HIGHLIGHT_IMAGES')) {
546
			$thumbnail = $individual->displayImage(40, 50, 'crop', []);
547
		} else {
548
			$thumbnail = '';
549
		}
550
551
		$content = '<span class="namedef name1">' . $individual->getFullName() . '</span>';
552
		$icons   = '';
553
		if ($individual->canShow()) {
554
			$content = '<a href="' . e($individual->url()) . '">' . $content . '</a>' .
555
				'<div class="namedef name1">' . $individual->getAddName() . '</div>';
556
			$icons = '<div class="icons">' .
557
				'<span class="iconz icon-zoomin" title="' . I18N::translate('Zoom in/out on this box.') . '"></span>' .
558
				'<div class="itr"><i class="icon-pedigree"></i><div class="popup">' .
559
				'<ul class="' . $personBoxClass . '">' . implode('', array_map(function (Menu $menu) {
0 ignored issues
show
Bug introduced by
Are you sure $personBoxClass of type integer|string|false can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

559
				'<ul class="' . /** @scrutinizer ignore-type */ $personBoxClass . '">' . implode('', array_map(function (Menu $menu) {
Loading history...
560
					return $menu->bootstrap4();
561
				}, $this->individualBoxMenu($individual))) . '</ul>' .
562
				'</div>' .
563
				'</div>' .
564
				'</div>';
565
		}
566
567
		return
568
			'<div data-xref="' . e($individual->getXref()) . '" data-tree="' . e($individual->getTree()->getName()) . '" class="person_box_template ' . $personBoxClass . ' box-style1" style="width: ' . $this->parameter('chart-box-x') . 'px; height: ' . $this->parameter('chart-box-y') . 'px">' .
569
			$icons .
570
			'<div class="chart_textbox" style="max-height:' . $this->parameter('chart-box-y') . 'px;">' .
571
			$thumbnail .
572
			$content .
573
			'<div class="inout2 details1">' . $this->individualBoxFacts($individual) . '</div>' .
574
			'</div>' .
575
			'<div class="inout"></div>' .
576
			'</div>';
577
	}
578
579
	/**
580
	 * Display an empty box - for a missing individual in a chart.
581
	 *
582
	 * @return string
583
	 */
584
	public function individualBoxEmpty() {
585
		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>';
586
	}
587
588
	/**
589
	 * Display an individual in a box - for charts, etc.
590
	 *
591
	 * @param Individual $individual
592
	 *
593
	 * @return string
594
	 */
595
	public function individualBoxLarge(Individual $individual) {
596
		$personBoxClass = array_search($individual->getSex(), ['person_box' => 'M', 'person_boxF' => 'F', 'person_boxNN' => 'U']);
597
		if ($individual->getTree()->getPreference('SHOW_HIGHLIGHT_IMAGES')) {
598
			$thumbnail = $individual->displayImage(40, 50, 'crop', []);
599
		} else {
600
			$thumbnail = '';
601
		}
602
603
		$content = '<span class="namedef name1">' . $individual->getFullName() . '</span>';
604
		$icons   = '';
605
		if ($individual->canShow()) {
606
			$content = '<a href="' . e($individual->url()) . '">' . $content . '</a>' .
607
				'<div class="namedef name2">' . $individual->getAddName() . '</div>';
608
			$icons = '<div class="icons">' .
609
				'<span class="iconz icon-zoomin" title="' . I18N::translate('Zoom in/out on this box.') . '"></span>' .
610
				'<div class="itr"><i class="icon-pedigree"></i><div class="popup">' .
611
				'<ul class="' . $personBoxClass . '">' . implode('', array_map(function (Menu $menu) {
0 ignored issues
show
Bug introduced by
Are you sure $personBoxClass of type integer|string|false can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

611
				'<ul class="' . /** @scrutinizer ignore-type */ $personBoxClass . '">' . implode('', array_map(function (Menu $menu) {
Loading history...
612
					return $menu->bootstrap4();
613
				}, $this->individualBoxMenu($individual))) . '</ul>' .
614
				'</div>' .
615
				'</div>' .
616
				'</div>';
617
		}
618
619
		return
620
			'<div data-xref="' . e($individual->getXref()) . '" data-tree="' . e($individual->getTree()->getName()) . '" class="person_box_template ' . $personBoxClass . ' box-style2">' .
621
			$icons .
622
			'<div class="chart_textbox" style="max-height:' . $this->parameter('chart-box-y') . 'px;">' .
623
			$thumbnail .
624
			$content .
625
			'<div class="inout2 details2">' . $this->individualBoxFacts($individual) . '</div>' .
626
			'</div>' .
627
			'<div class="inout"></div>' .
628
			'</div>';
629
	}
630
631
	/**
632
	 * Display an individual in a box - for charts, etc.
633
	 *
634
	 * @param Individual $individual
635
	 *
636
	 * @return string
637
	 */
638
	public function individualBoxSmall(Individual $individual) {
639
		$personBoxClass = array_search($individual->getSex(), ['person_box' => 'M', 'person_boxF' => 'F', 'person_boxNN' => 'U']);
640
		if ($individual->getTree()->getPreference('SHOW_HIGHLIGHT_IMAGES')) {
641
			$thumbnail = $individual->displayImage(40, 50, 'crop', []);
642
		} else {
643
			$thumbnail = '';
644
		}
645
646
		return
647
			'<div data-xref="' . $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">' .
0 ignored issues
show
Bug introduced by
Are you sure $personBoxClass of type integer|string|false can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

647
			'<div data-xref="' . $individual->getXref() . '" class="person_box_template ' . /** @scrutinizer ignore-type */ $personBoxClass . ' iconz box-style0" style="width: ' . $this->parameter('compact-chart-box-x') . 'px; min-height: ' . $this->parameter('compact-chart-box-y') . 'px">' .
Loading history...
648
			'<div class="compact_view">' .
649
			$thumbnail .
650
			'<a href="' . e($individual->url()) . '">' .
651
			'<span class="namedef name0">' . $individual->getFullName() . '</span>' .
652
			'</a>' .
653
			'<div class="inout2 details0">' . $individual->getLifeSpan() . '</div>' .
654
			'</div>' .
655
			'<div class="inout"></div>' .
656
			'</div>';
657
	}
658
659
	/**
660
	 * Display an individual in a box - for charts, etc.
661
	 *
662
	 * @return string
663
	 */
664
	public function individualBoxSmallEmpty() {
665
		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>';
666
	}
667
668
	/**
669
	 * Generate the facts, for display in charts.
670
	 *
671
	 * @param Individual $individual
672
	 *
673
	 * @return string
674
	 */
675
	public function individualBoxFacts(Individual $individual) {
676
		$html = '';
677
678
		$opt_tags = preg_split('/\W/', $individual->getTree()->getPreference('CHART_BOX_TAGS'), 0, PREG_SPLIT_NO_EMPTY);
679
		// Show BIRT or equivalent event
680
		foreach (explode('|', WT_EVENTS_BIRT) as $birttag) {
681
			if (!in_array($birttag, $opt_tags)) {
0 ignored issues
show
Bug introduced by
It seems like $opt_tags can also be of type false; however, parameter $haystack of in_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

681
			if (!in_array($birttag, /** @scrutinizer ignore-type */ $opt_tags)) {
Loading history...
682
				$event = $individual->getFirstFact($birttag);
683
				if ($event) {
684
					$html .= $event->summary();
685
					break;
686
				}
687
			}
688
		}
689
		// Show optional events (before death)
690
		foreach ($opt_tags as $key => $tag) {
691
			if (!preg_match('/^(' . WT_EVENTS_DEAT . ')$/', $tag)) {
692
				$event = $individual->getFirstFact($tag);
693
				if (!is_null($event)) {
694
					$html .= $event->summary();
695
					unset($opt_tags[$key]);
696
				}
697
			}
698
		}
699
		// Show DEAT or equivalent event
700
		foreach (explode('|', WT_EVENTS_DEAT) as $deattag) {
701
			$event = $individual->getFirstFact($deattag);
702
			if ($event) {
703
				$html .= $event->summary();
704
				if (in_array($deattag, $opt_tags)) {
705
					unset($opt_tags[array_search($deattag, $opt_tags)]);
0 ignored issues
show
Bug introduced by
It seems like $opt_tags can also be of type false; however, parameter $haystack of array_search() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

705
					unset($opt_tags[array_search($deattag, /** @scrutinizer ignore-type */ $opt_tags)]);
Loading history...
706
				}
707
				break;
708
			}
709
		}
710
		// Show remaining optional events (after death)
711
		foreach ($opt_tags as $tag) {
712
			$event = $individual->getFirstFact($tag);
713
			if ($event) {
714
				$html .= $event->summary();
715
			}
716
		}
717
718
		return $html;
719
	}
720
721
	/**
722
	 * Generate the LDS summary, for display in charts.
723
	 *
724
	 * @param Individual $individual
725
	 *
726
	 * @return string
727
	 */
728
	public function individualBoxLdsSummary(Individual $individual) {
729
		if ($individual->getTree()->getPreference('SHOW_LDS_AT_GLANCE')) {
730
			$BAPL = $individual->getFacts('BAPL') ? 'B' : '_';
731
			$ENDL = $individual->getFacts('ENDL') ? 'E' : '_';
732
			$SLGC = $individual->getFacts('SLGC') ? 'C' : '_';
733
			$SLGS = '_';
734
735
			foreach ($individual->getSpouseFamilies() as $family) {
736
				if ($family->getFacts('SLGS')) {
737
					$SLGS = '';
738
				}
739
			}
740
741
			return $BAPL . $ENDL . $SLGS . $SLGC;
742
		} else {
743
			return '';
744
		}
745
	}
746
747
	/**
748
	 * Links, to show in chart boxes;
749
	 *
750
	 * @param Individual $individual
751
	 *
752
	 * @return Menu[]
753
	 */
754
	public function individualBoxMenu(Individual $individual) {
755
		$menus = array_merge(
756
			$this->individualBoxMenuCharts($individual),
757
			$this->individualBoxMenuFamilyLinks($individual)
758
		);
759
760
		return $menus;
761
	}
762
763
	/**
764
	 * Chart links, to show in chart boxes;
765
	 *
766
	 * @param Individual $individual
767
	 *
768
	 * @return Menu[]
769
	 */
770
	public function individualBoxMenuCharts(Individual $individual) {
771
		$menus = [];
772
		foreach (Module::getActiveCharts($this->tree) as $chart) {
773
			$menu = $chart->getBoxChartMenu($individual);
774
			if ($menu) {
775
				$menus[] = $menu;
776
			}
777
		}
778
779
		usort($menus, function (Menu $x, Menu $y) {
780
			return I18N::strcasecmp($x->getLabel(), $y->getLabel());
781
		});
782
783
		return $menus;
784
	}
785
786
	/**
787
	 * Family links, to show in chart boxes.
788
	 *
789
	 * @param Individual $individual
790
	 *
791
	 * @return Menu[]
792
	 */
793
	public function individualBoxMenuFamilyLinks(Individual $individual) {
794
		$menus = [];
795
796
		foreach ($individual->getSpouseFamilies() as $family) {
797
			$menus[] = new Menu('<strong>' . I18N::translate('Family with spouse') . '</strong>', e($family->url()));
798
			$spouse  = $family->getSpouse($individual);
799
			if ($spouse && $spouse->canShowName()) {
800
				$menus[] = new Menu($spouse->getFullName(), e($spouse->url()));
801
			}
802
			foreach ($family->getChildren() as $child) {
803
				if ($child->canShowName()) {
804
					$menus[] = new Menu($child->getFullName(), e($child->url()));
805
				}
806
			}
807
		}
808
809
		return $menus;
810
	}
811
812
	/**
813
	 * Create part of an individual box
814
	 *
815
	 * @param Individual $individual
816
	 *
817
	 * @return string
818
	 */
819
	public function individualBoxSexSymbol(Individual $individual) {
820
		if ($individual->getTree()->getPreference('PEDIGREE_SHOW_GENDER')) {
821
			return $individual->sexImage('large');
822
		} else {
823
			return '';
824
		}
825
	}
826
827
	/**
828
	 * Initialise the theme. We cannot pass these in a constructor, as the construction
829
	 * happens in a theme file, and we need to be able to change it.
830
	 *
831
	 * @param Tree|null $tree The current tree (if there is one).
832
	 */
833
	final public function init(Tree $tree = null) {
834
		$this->request  = Request::createFromGlobals();
835
		$this->tree     = $tree;
836
837
		$this->hookAfterInit();
838
	}
839
840
	/**
841
	 * A small "powered by webtrees" logo for the footer.
842
	 *
843
	 * @return string
844
	 */
845
	public function logoPoweredBy() {
846
		return '<a href="' . WT_WEBTREES_URL . '" class="wt-powered-by-webtrees" title="' . WT_WEBTREES_URL . '" dir="ltr">' . WT_WEBTREES_URL . '</a>';
847
	}
848
849
	/**
850
	 * A menu for the day/month/year calendar views.
851
	 *
852
	 * @return Menu
853
	 */
854
	public function menuCalendar() {
855
		return new Menu(I18N::translate('Calendar'), '#', 'menu-calendar', ['rel' => 'nofollow'], [
856
			// Day view
857
			new Menu(I18N::translate('Day'), e(route('calendar', ['view' => 'day', 'ged' => $this->tree->getName()])), 'menu-calendar-day', ['rel' => 'nofollow']),
858
			// Month view
859
			new Menu(I18N::translate('Month'), e(route('calendar', ['view' => 'month', 'ged' => $this->tree->getName()])), 'menu-calendar-month', ['rel' => 'nofollow']),
860
			//Year view
861
			new Menu(I18N::translate('Year'), e(route('calendar', ['view' => 'year', 'ged' => $this->tree->getName()])), 'menu-calendar-year', ['rel' => 'nofollow']),
862
		]);
863
	}
864
865
	/**
866
	 * Generate a menu item to change the blocks on the current (index.php) page.
867
	 *
868
	 * @return Menu|null
869
	 */
870
	public function menuChangeBlocks() {
871
		if (WT_SCRIPT_NAME === 'index.php' && Auth::check() && Filter::get('route') === 'user-page') {
872
			return new Menu(I18N::translate('Customize this page'), route('user-page-edit', ['ged' => $this->tree->getName()]), 'menu-change-blocks');
873
		} elseif (WT_SCRIPT_NAME === 'index.php' && Auth::isManager($this->tree) && Filter::get('route') === 'tree-page') {
874
			return new Menu(I18N::translate('Customize this page'), route('tree-page-edit', ['ged' => $this->tree->getName()]), 'menu-change-blocks');
875
		} else {
876
			return null;
877
		}
878
	}
879
880
	/**
881
	 * Generate a menu for each of the different charts.
882
	 *
883
	 * @param Individual $individual
884
	 *
885
	 * @return Menu|null
886
	 */
887
	public function menuChart(Individual $individual) {
888
		$submenus = [];
889
		foreach (Module::getActiveCharts($this->tree) as $chart) {
890
			$menu = $chart->getChartMenu($individual);
891
			if ($menu) {
892
				$submenus[] = $menu;
893
			}
894
		}
895
896
		if (empty($submenus)) {
897
			return null;
898
		} else {
899
			usort($submenus, function (Menu $x, Menu $y) {
900
				return I18N::strcasecmp($x->getLabel(), $y->getLabel());
901
			});
902
903
			return new Menu(I18N::translate('Charts'), '#', 'menu-chart', ['rel' => 'nofollow'], $submenus);
904
		}
905
	}
906
907
	/**
908
	 * Generate a menu item for the ancestors chart.
909
	 *
910
	 * @param Individual $individual
911
	 *
912
	 * @return Menu|null
913
	 *
914
	 * @deprecated
915
	 */
916
	public function menuChartAncestors(Individual $individual) {
917
		$chart = new AncestorsChartModule(WT_ROOT . WT_MODULES_DIR . 'ancestors_chart');
918
919
		return $chart->getChartMenu($individual);
920
	}
921
922
	/**
923
	 * Generate a menu item for the compact tree.
924
	 *
925
	 * @param Individual $individual
926
	 *
927
	 * @return Menu|null
928
	 *
929
	 * @deprecated
930
	 */
931
	public function menuChartCompact(Individual $individual) {
932
		$chart = new CompactTreeChartModule(WT_ROOT . WT_MODULES_DIR . 'compact_tree_chart');
933
934
		return $chart->getChartMenu($individual);
935
	}
936
937
	/**
938
	 * Generate a menu item for the descendants chart.
939
	 *
940
	 * @param Individual $individual
941
	 *
942
	 * @return Menu|null
943
	 *
944
	 * @deprecated
945
	 */
946
	public function menuChartDescendants(Individual $individual) {
947
		$chart = new DescendancyChartModule(WT_ROOT . WT_MODULES_DIR . 'descendancy_chart');
948
949
		return $chart->getChartMenu($individual);
950
	}
951
952
	/**
953
	 * Generate a menu item for the family-book chart.
954
	 *
955
	 * @param Individual $individual
956
	 *
957
	 * @return Menu|null
958
	 *
959
	 * @deprecated
960
	 */
961
	public function menuChartFamilyBook(Individual $individual) {
962
		$chart = new FamilyBookChartModule(WT_ROOT . WT_MODULES_DIR . 'family_book_chart');
963
964
		return $chart->getChartMenu($individual);
965
	}
966
967
	/**
968
	 * Generate a menu item for the fan chart.
969
	 *
970
	 * We can only do this if the GD2 library is installed with TrueType support.
971
	 *
972
	 * @param Individual $individual
973
	 *
974
	 * @return Menu|null
975
	 *
976
	 * @deprecated
977
	 */
978
	public function menuChartFanChart(Individual $individual) {
979
		$chart = new FanChartModule(WT_ROOT . WT_MODULES_DIR . 'fan_chart');
980
981
		return $chart->getChartMenu($individual);
982
	}
983
984
	/**
985
	 * Generate a menu item for the interactive tree.
986
	 *
987
	 * @param Individual $individual
988
	 *
989
	 * @return Menu|null
990
	 *
991
	 * @deprecated
992
	 */
993
	public function menuChartInteractiveTree(Individual $individual) {
994
		$chart = new InteractiveTreeModule(WT_ROOT . WT_MODULES_DIR . 'tree');
995
996
		return $chart->getChartMenu($individual);
997
	}
998
999
	/**
1000
	 * Generate a menu item for the hourglass chart.
1001
	 *
1002
	 * @param Individual $individual
1003
	 *
1004
	 * @return Menu|null
1005
	 *
1006
	 * @deprecated
1007
	 */
1008
	public function menuChartHourglass(Individual $individual) {
1009
		$chart = new HourglassChartModule(WT_ROOT . WT_MODULES_DIR . 'hourglass_chart');
1010
1011
		return $chart->getChartMenu($individual);
1012
	}
1013
1014
	/**
1015
	 * Generate a menu item for the lifepsan chart.
1016
	 *
1017
	 * @param Individual $individual
1018
	 *
1019
	 * @return Menu|null
1020
	 *
1021
	 * @deprecated
1022
	 */
1023
	public function menuChartLifespan(Individual $individual) {
1024
		$chart = new LifespansChartModule(WT_ROOT . WT_MODULES_DIR . 'lifespans_chart');
1025
1026
		return $chart->getChartMenu($individual);
1027
	}
1028
1029
	/**
1030
	 * Generate a menu item for the pedigree chart.
1031
	 *
1032
	 * @param Individual $individual
1033
	 *
1034
	 * @return Menu|null
1035
	 *
1036
	 * @deprecated
1037
	 */
1038
	public function menuChartPedigree(Individual $individual) {
1039
		$chart = new PedigreeChartModule(WT_ROOT . WT_MODULES_DIR . 'pedigree_chart');
1040
1041
		return $chart->getChartMenu($individual);
1042
	}
1043
1044
	/**
1045
	 * Generate a menu item for the pedigree map.
1046
	 *
1047
	 * @param Individual $individual
1048
	 *
1049
	 * @return Menu|null
1050
	 *
1051
	 * @deprecated
1052
	 */
1053
	public function menuChartPedigreeMap(Individual $individual) {
1054
		$chart = new GoogleMapsModule(WT_ROOT . WT_MODULES_DIR . 'googlemap');
1055
1056
		return $chart->getChartMenu($individual);
1057
	}
1058
1059
	/**
1060
	 * Generate a menu item for the relationship chart.
1061
	 *
1062
	 * @param Individual $individual
1063
	 *
1064
	 * @return Menu|null
1065
	 *
1066
	 * @deprecated
1067
	 */
1068
	public function menuChartRelationship(Individual $individual) {
1069
		$chart = new RelationshipsChartModule(WT_ROOT . WT_MODULES_DIR . 'relationships_chart');
1070
1071
		return $chart->getChartMenu($individual);
1072
	}
1073
1074
	/**
1075
	 * Generate a menu item for the statistics charts.
1076
	 *
1077
	 * @return Menu|null
1078
	 *
1079
	 * @deprecated
1080
	 */
1081
	public function menuChartStatistics() {
1082
		$chart = new StatisticsChartModule(WT_ROOT . WT_MODULES_DIR . 'statistics_chart');
1083
1084
		return $chart->getChartMenu(null);
0 ignored issues
show
Bug introduced by
null of type null is incompatible with the type Fisharebest\Webtrees\Individual expected by parameter $individual of Fisharebest\Webtrees\Mod...tModule::getChartMenu(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1084
		return $chart->getChartMenu(/** @scrutinizer ignore-type */ null);
Loading history...
1085
	}
1086
1087
	/**
1088
	 * Generate a menu item for the timeline chart.
1089
	 *
1090
	 * @param Individual $individual
1091
	 *
1092
	 * @return Menu|null
1093
	 *
1094
	 * @deprecated
1095
	 */
1096
	public function menuChartTimeline(Individual $individual) {
1097
		$chart = new TimelineChartModule(WT_ROOT . WT_MODULES_DIR . 'timeline_chart');
1098
1099
		return $chart->getChartMenu($individual);
1100
	}
1101
1102
	/**
1103
	 * Generate a menu item for the control panel.
1104
	 *
1105
	 * @return Menu|null
1106
	 */
1107
	public function menuControlPanel() {
1108
		if (Auth::isAdmin()) {
1109
			return new Menu(I18N::translate('Control panel'), route('admin-control-panel'), 'menu-admin');
1110
		} elseif (Auth::isManager($this->tree)) {
1111
			return new Menu(I18N::translate('Control panel'), route('admin-control-panel-manager'), 'menu-admin');
1112
		} else {
1113
			return null;
1114
		}
1115
	}
1116
1117
	/**
1118
	 * Favorites menu.
1119
	 *
1120
	 * @return Menu|null
1121
	 */
1122
	public function menuFavorites() {
1123
		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...
1124
1125
		$show_user_favorites = $this->tree && Module::getModuleByName('user_favorites') && Auth::check();
1126
		$show_tree_favorites = $this->tree && Module::getModuleByName('gedcom_favorites');
1127
1128
		if ($show_user_favorites && $show_tree_favorites) {
1129
			$favorites = array_merge(
1130
				FamilyTreeFavoritesModule::getFavorites($this->tree, Auth::user()),
1131
				UserFavoritesModule::getFavorites($this->tree, Auth::user())
1132
			);
1133
		} elseif ($show_user_favorites) {
1134
			$favorites = UserFavoritesModule::getFavorites($this->tree, Auth::user());
1135
		} elseif ($show_tree_favorites) {
1136
			$favorites = FamilyTreeFavoritesModule::getFavorites($this->tree, Auth::user());
1137
		} else {
1138
			$favorites = [];
1139
		}
1140
1141
		$submenus = [];
1142
		$records  = [];
1143
		foreach ($favorites as $favorite) {
1144
			switch ($favorite->favorite_type) {
1145
				case 'URL':
1146
					$submenus[] = new Menu(e($favorite->title), e($favorite->url));
1147
					break;
1148
				default:
1149
					$record = GedcomRecord::getInstance($favorite->xref, $this->tree);
1150
					if ($record && $record->canShowName()) {
1151
						$submenus[] = new Menu($record->getFullName(), e($record->url()));
1152
						$records[]  = $record;
1153
					}
1154
					break;
1155
			}
1156
		}
1157
1158
		// @TODO we no longer have a global $controller
1159
		if ($show_user_favorites && isset($controller->record) && $controller->record instanceof GedcomRecord && !in_array($controller->record, $records)) {
1160
			$url = route('module', [
1161
				'module' => 'user_favorites',
1162
				'action' => 'AddFavorite',
1163
				'ged'    => $this->tree->getName(),
1164
				'xref'   => $controller->record->getXref(),
1165
			]);
1166
1167
			$submenus[] = new Menu(I18N::translate('Add to favorites'), '#', '', [
1168
				'data-url' => $url,
1169
				'onclick'  => 'jQuery.post(this.dataset.url,function() {location.reload();})',
1170
			]);
1171
		}
1172
1173
		if (empty($submenus)) {
1174
			return null;
1175
		} else {
1176
			return new Menu(I18N::translate('Favorites'), '#', 'menu-favorites', [], $submenus);
1177
		}
1178
	}
1179
1180
	/**
1181
	 * A menu for the home (family tree) pages.
1182
	 *
1183
	 * @return Menu
1184
	 */
1185
	public function menuHomePage() {
1186
		if (count(Tree::getAll()) === 1 || Site::getPreference('ALLOW_CHANGE_GEDCOM') !== '1') {
1187
			return new Menu(I18N::translate('Family tree'), route('tree-page', ['ged' => $this->tree->getName()]), 'menu-tree');
1188
		} else {
1189
			$submenus = [];
1190
			foreach (Tree::getAll() as $tree) {
1191
				if ($tree == $this->tree) {
1192
					$active = 'active ';
1193
				} else {
1194
					$active = '';
1195
				}
1196
				$submenus[] = new Menu(e($tree->getTitle()), route('tree-page', ['ged' => $tree->getName()]), $active . 'menu-tree-' . $tree->getTreeId());
1197
			}
1198
1199
			return new Menu(I18N::translate('Family trees'), '#', 'menu-tree', [], $submenus);
1200
		}
1201
	}
1202
1203
	/**
1204
	 * A menu to show a list of available languages.
1205
	 *
1206
	 * @return Menu|null
1207
	 */
1208
	public function menuLanguages() {
1209
		$menu = new Menu(I18N::translate('Language'), '#', 'menu-language');
1210
1211
		foreach (I18N::activeLocales() as $locale) {
1212
			$language_tag = $locale->languageTag();
1213
			$class        = 'menu-language-' . $language_tag . (WT_LOCALE === $language_tag ? ' active' : '');
1214
			$menu->addSubmenu(new Menu($locale->endonym(), '#', $class, [
1215
				'onclick'       => 'return false;',
1216
				'data-language' => $language_tag,
1217
			]));
1218
		}
1219
1220
		if (count($menu->getSubmenus()) > 1) {
1221
			return $menu;
1222
		} else {
1223
			return null;
1224
		}
1225
	}
1226
1227
	/**
1228
	 * Create a menu to show lists of individuals, families, sources, etc.
1229
	 *
1230
	 * @param string $surname The significant surname on the page
1231
	 *
1232
	 * @return Menu
1233
	 */
1234
	public function menuLists($surname) {
1235
		// Do not show empty lists
1236
		$row = Database::prepare(
1237
			"SELECT SQL_CACHE" .
1238
			" EXISTS(SELECT 1 FROM `##sources` WHERE s_file = ?) AS sour," .
1239
			" EXISTS(SELECT 1 FROM `##other` WHERE o_file = ? AND o_type='REPO') AS repo," .
1240
			" EXISTS(SELECT 1 FROM `##other` WHERE o_file = ? AND o_type='NOTE') AS note," .
1241
			" EXISTS(SELECT 1 FROM `##media` WHERE m_file = ?) AS obje"
1242
		)->execute([
1243
			$this->tree->getTreeId(),
1244
			$this->tree->getTreeId(),
1245
			$this->tree->getTreeId(),
1246
			$this->tree->getTreeId(),
1247
		])->fetchOneRow();
1248
1249
		$submenus = [
1250
			$this->menuListsIndividuals($surname),
1251
			$this->menuListsFamilies($surname),
1252
			$this->menuListsBranches($surname),
1253
			$this->menuListsPlaces(),
1254
		];
1255
		if ($row->obje) {
1256
			$submenus[] = $this->menuListsMedia();
1257
		}
1258
		if ($row->repo) {
1259
			$submenus[] = $this->menuListsRepositories();
1260
		}
1261
		if ($row->sour) {
1262
			$submenus[] = $this->menuListsSources();
1263
		}
1264
		if ($row->note) {
1265
			$submenus[] = $this->menuListsNotes();
1266
		}
1267
1268
		uasort($submenus, function (Menu $x, Menu $y) {
1269
			return I18N::strcasecmp($x->getLabel(), $y->getLabel());
1270
		});
1271
1272
		return new Menu(I18N::translate('Lists'), '#', 'menu-list', [], $submenus);
1273
	}
1274
1275
	/**
1276
	 * A menu for the list of branches
1277
	 *
1278
	 * @param string $surname The significant surname on the page
1279
	 *
1280
	 * @return Menu
1281
	 */
1282
	public function menuListsBranches($surname) {
1283
		return new Menu(I18N::translate('Branches'), e(route('branches', ['ged' => $this->tree->getName(), 'surname' => $surname])), 'menu-branches', ['rel' => 'nofollow']);
1284
	}
1285
1286
	/**
1287
	 * A menu for the list of families
1288
	 *
1289
	 * @param string $surname The significant surname on the page
1290
	 *
1291
	 * @return Menu
1292
	 */
1293
	public function menuListsFamilies($surname) {
1294
		return new Menu(I18N::translate('Families'), e(route('family-list', ['ged' => $this->tree->getName(), 'surname' => $surname])), 'menu-list-indi');
1295
	}
1296
1297
	/**
1298
	 * A menu for the list of individuals
1299
	 *
1300
	 * @param string $surname The significant surname on the page
1301
	 *
1302
	 * @return Menu
1303
	 */
1304
	public function menuListsIndividuals($surname) {
1305
		return new Menu(I18N::translate('Individuals'), e(route('individual-list', ['ged' => $this->tree->getName(), 'surname' => $surname])), 'menu-list-indi');
1306
	}
1307
1308
	/**
1309
	 * A menu for the list of media objects
1310
	 *
1311
	 * @return Menu
1312
	 */
1313
	public function menuListsMedia() {
1314
		return new Menu(I18N::translate('Media objects'), e(route('media-list', ['ged' => $this->tree->getName()])), 'menu-list-obje', ['rel' => 'nofollow']);
1315
	}
1316
1317
	/**
1318
	 * A menu for the list of notes
1319
	 *
1320
	 * @return Menu
1321
	 */
1322
	public function menuListsNotes() {
1323
		return new Menu(I18N::translate('Shared notes'), e(route('note-list', ['ged' => $this->tree->getName()])), 'menu-list-note', ['rel' => 'nofollow']);
1324
	}
1325
1326
	/**
1327
	 * A menu for the list of individuals
1328
	 *
1329
	 * @return Menu
1330
	 */
1331
	protected function menuListsPlaces() {
1332
		return new Menu(I18N::translate('Place hierarchy'), e(route('place-hierarchy', ['ged' => $this->tree->getName()])), 'menu-list-plac', ['rel' => 'nofollow']);
1333
	}
1334
1335
	/**
1336
	 * A menu for the list of repositories
1337
	 *
1338
	 * @return Menu
1339
	 */
1340
	public function menuListsRepositories() {
1341
		return new Menu(I18N::translate('Repositories'), e(route('repository-list', ['ged' => $this->tree->getName()])), 'menu-list-repo', ['rel' => 'nofollow']);
1342
	}
1343
1344
	/**
1345
	 * A menu for the list of sources
1346
	 *
1347
	 * @return Menu
1348
	 */
1349
	public function menuListsSources() {
1350
		return new Menu(
1351
			I18N::translate('Sources'), e(route('source-list', ['ged' => $this->tree->getName()])), 'menu-list-sour', ['rel' => 'nofollow']);
1352
	}
1353
1354
	/**
1355
	 * A login menu option (or null if we are already logged in).
1356
	 *
1357
	 * @return Menu|null
1358
	 */
1359
	public function menuLogin() {
1360
		if (Auth::check()) {
1361
			return null;
1362
		} else {
1363
			// Return to this page after login...
1364
			$url = Functions::getQueryUrl();
1365
			// ...but switch from the tree-page to the user-page
1366
			$url = str_replace('route=tree-page', 'route=user-page', $url);
1367
1368
			return new Menu(I18N::translate('Sign in'), e(route('login', ['url' => $url])), 'menu-login', ['rel' => 'nofollow']);
1369
		}
1370
	}
1371
1372
	/**
1373
	 * A logout menu option (or null if we are already logged out).
1374
	 *
1375
	 * @return Menu|null
1376
	 */
1377
	public function menuLogout() {
1378
		if (Auth::check()) {
1379
			return new Menu(I18N::translate('Sign out'), e(route('logout')), 'menu-logout');
1380
		} else {
1381
			return null;
1382
		}
1383
	}
1384
1385
	/**
1386
	 * Get the additional menus created by each of the modules
1387
	 *
1388
	 * @return Menu[]
1389
	 */
1390
	public function menuModules() {
1391
		$menus = [];
1392
		foreach (Module::getActiveMenus($this->tree) as $module) {
1393
			$menus[] = $module->getMenu();
1394
		}
1395
1396
		return array_filter($menus);
1397
	}
1398
1399
	/**
1400
	 * A link to allow users to edit their account settings.
1401
	 *
1402
	 * @return Menu|null
1403
	 */
1404
	public function menuMyAccount() {
1405
		if (Auth::check()) {
1406
			return new Menu(I18N::translate('My account'), e(route('my-account', [])));
1407
		} else {
1408
			return null;
1409
		}
1410
	}
1411
1412
	/**
1413
	 * A link to the user's individual record (individual.php).
1414
	 *
1415
	 * @return Menu|null
1416
	 */
1417
	public function menuMyIndividualRecord() {
1418
		$record = Individual::getInstance($this->tree->getUserPreference(Auth::user(), 'gedcomid'), $this->tree);
1419
1420
		if ($record) {
1421
			return new Menu(I18N::translate('My individual record'), e($record->url()), 'menu-myrecord');
1422
		} else {
1423
			return null;
1424
		}
1425
	}
1426
1427
	/**
1428
	 * A link to the user's personal home page.
1429
	 *
1430
	 * @return Menu
1431
	 */
1432
	public function menuMyPage() {
1433
		return new Menu(I18N::translate('My page'), route('user-page'), 'menu-mypage');
1434
	}
1435
1436
	/**
1437
	 * A menu for the user's personal pages.
1438
	 *
1439
	 * @return Menu|null
1440
	 */
1441
	public function menuMyPages() {
1442
		if (Auth::id() && $this->tree !== null) {
1443
			return new Menu(I18N::translate('My pages'), '#', 'menu-mymenu', [], array_filter([
1444
				$this->menuMyPage(),
1445
				$this->menuMyIndividualRecord(),
1446
				$this->menuMyPedigree(),
1447
				$this->menuMyAccount(),
1448
				$this->menuControlPanel(),
1449
				$this->menuChangeBlocks(),
1450
			]));
1451
		} else {
1452
			return null;
1453
		}
1454
	}
1455
1456
	/**
1457
	 * A link to the user's individual record.
1458
	 *
1459
	 * @return Menu|null
1460
	 */
1461
	public function menuMyPedigree() {
1462
		$gedcomid = $this->tree->getUserPreference(Auth::user(), 'gedcomid');
1463
1464
		if ($gedcomid && Module::isActiveChart($this->tree, 'pedigree_chart')) {
1465
			return new Menu(
1466
				I18N::translate('My pedigree'),
1467
				e(route('pedigree', ['xref' => $gedcomid, 'ged' => $this->tree->getName()])),
1468
				'menu-mypedigree'
1469
			);
1470
		} else {
1471
			return null;
1472
		}
1473
	}
1474
1475
	/**
1476
	 * Create a pending changes menu.
1477
	 *
1478
	 * @return Menu|null
1479
	 */
1480
	public function menuPendingChanges() {
1481
		if ($this->pendingChangesExist()) {
1482
			$url = route('show-pending', [
1483
				'ged' => $this->tree ? $this->tree->getName() : '',
1484
				'url' => $this->request->getRequestUri()
1485
			]);
1486
1487
			return new Menu(I18N::translate('Pending changes'), e($url), 'menu-pending');
1488
		} else {
1489
			return null;
1490
		}
1491
	}
1492
1493
	/**
1494
	 * A menu with a list of reports.
1495
	 *
1496
	 * @return Menu|null
1497
	 */
1498
	public function menuReports() {
1499
		$submenus = [];
1500
		foreach (Module::getActiveReports($this->tree) as $report) {
1501
			$submenus[] = $report->getReportMenu($this->tree);
1502
		}
1503
1504
		if (empty($submenus)) {
1505
			return null;
1506
		} else {
1507
			return new Menu(I18N::translate('Reports'), '#', 'menu-report', ['rel' => 'nofollow'], $submenus);
1508
		}
1509
	}
1510
1511
	/**
1512
	 * Create the search menu.
1513
	 *
1514
	 * @return Menu
1515
	 */
1516
	public function menuSearch() {
1517
		return new Menu(I18N::translate('Search'), '#', 'menu-search', ['rel' => 'nofollow'], array_filter([
1518
			$this->menuSearchGeneral(),
1519
			$this->menuSearchPhonetic(),
1520
			$this->menuSearchAdvanced(),
1521
			$this->menuSearchAndReplace(),
1522
		]));
1523
	}
1524
1525
	/**
1526
	 * Create the general search sub-menu.
1527
	 *
1528
	 * @return Menu
1529
	 */
1530
	public function menuSearchGeneral() {
1531
		return new Menu(I18N::translate('General search'), e(route('search-general', ['ged' => $this->tree->getName()])), 'menu-search-general', ['rel' => 'nofollow']);
1532
	}
1533
1534
	/**
1535
	 * Create the phonetic search sub-menu.
1536
	 *
1537
	 * @return Menu
1538
	 */
1539
	public function menuSearchPhonetic() {
1540
		return new Menu(/* I18N: search using “sounds like”, rather than exact spelling */ I18N::translate('Phonetic search'), e(route('search-phonetic', ['ged' => $this->tree->getName(), 'action' => 'soundex'])), 'menu-search-soundex', ['rel' => 'nofollow']);
1541
	}
1542
1543
	/**
1544
	 * Create the advanced search sub-menu.
1545
	 *
1546
	 * @return Menu
1547
	 */
1548
	public function menuSearchAdvanced() {
1549
		return new Menu(I18N::translate('Advanced search'), e(route('search-advanced', ['ged' => $this->tree->getName()])), 'menu-search-advanced', ['rel' => 'nofollow']);
1550
	}
1551
1552
	/**
1553
	 * Create the advanced search sub-menu.
1554
	 *
1555
	 * @return Menu
1556
	 */
1557
	public function menuSearchAndReplace() {
1558
		if (Auth::isEditor($this->tree)) {
1559
			return new Menu(I18N::translate('Search and replace'), e(route('search-replace', ['ged' => $this->tree->getName(), 'action' => 'replace'])), 'menu-search-replace');
1560
		} else {
1561
			return null;
1562
		}
1563
	}
1564
1565
	/**
1566
	 * Themes menu.
1567
	 *
1568
	 * @return Menu|null
1569
	 */
1570
	public function menuThemes() {
1571
		if ($this->tree !== null && Site::getPreference('ALLOW_USER_THEMES') === '1' && $this->tree->getPreference('ALLOW_THEME_DROPDOWN') === '1') {
1572
			$submenus = [];
1573
			foreach (Theme::installedThemes() as $theme) {
1574
				$class      = 'menu-theme-' . $theme->themeId() . ($theme === $this ? ' active' : '');
1575
				$submenus[] = new Menu($theme->themeName(), '#', $class, [
1576
					'onclick'    => 'return false;',
1577
					'data-theme' => $theme->themeId(),
1578
				]);
1579
			}
1580
1581
			usort($submenus, function (Menu $x, Menu $y) {
1582
				return I18N::strcasecmp($x->getLabel(), $y->getLabel());
1583
			});
1584
1585
			$menu = new Menu(I18N::translate('Theme'), '#', 'menu-theme', [], $submenus);
1586
1587
			return $menu;
1588
		} else {
1589
			return null;
1590
		}
1591
	}
1592
1593
	/**
1594
	 * How many times has the current page been shown?
1595
	 *
1596
	 * @param  PageController $controller
1597
	 *
1598
	 * @return int Number of views, or zero for pages that aren't logged.
1599
	 */
1600
	public function pageViews(PageController $controller) {
1601
		if ($this->tree && $this->tree->getPreference('SHOW_COUNTER')) {
1602
			if (isset($controller->record) && $controller->record instanceof GedcomRecord) {
1603
				return HitCounter::countHit($this->tree, WT_SCRIPT_NAME, $controller->record->getXref());
1604
			} elseif (isset($controller->root) && $controller->root instanceof GedcomRecord) {
1605
				return HitCounter::countHit($this->tree, WT_SCRIPT_NAME, $controller->root->getXref());
1606
			} elseif (WT_SCRIPT_NAME === 'index.php') {
1607
				if (Auth::check() && Filter::get('ctype') !== 'gedcom') {
1608
					return HitCounter::countHit($this->tree, WT_SCRIPT_NAME, 'user:' . Auth::id());
1609
				} else {
1610
					return HitCounter::countHit($this->tree, WT_SCRIPT_NAME, 'gedcom:' . $this->tree->getTreeId());
1611
				}
1612
			}
1613
		}
1614
1615
		return 0;
1616
	}
1617
1618
	/**
1619
	 * Misecellaneous dimensions, fonts, styles, etc.
1620
	 *
1621
	 * @param string $parameter_name
1622
	 *
1623
	 * @return string|int|float
1624
	 */
1625
	public function parameter($parameter_name) {
1626
		$parameters = [
1627
			'chart-background-f'             => 'dddddd',
1628
			'chart-background-m'             => 'cccccc',
1629
			'chart-background-u'             => 'eeeeee',
1630
			'chart-box-x'                    => 250,
1631
			'chart-box-y'                    => 80,
1632
			'chart-font-color'               => '000000',
1633
			'chart-spacing-x'                => 5,
1634
			'chart-spacing-y'                => 10,
1635
			'compact-chart-box-x'            => 240,
1636
			'compact-chart-box-y'            => 50,
1637
			'distribution-chart-high-values' => '555555',
1638
			'distribution-chart-low-values'  => 'cccccc',
1639
			'distribution-chart-no-values'   => 'ffffff',
1640
			'distribution-chart-x'           => 440,
1641
			'distribution-chart-y'           => 220,
1642
			'line-width'                     => 1.5,
1643
			'shadow-blur'                    => 0,
1644
			'shadow-color'                   => '',
1645
			'shadow-offset-x'                => 0,
1646
			'shadow-offset-y'                => 0,
1647
			'stats-small-chart-x'            => 440,
1648
			'stats-small-chart-y'            => 125,
1649
			'stats-large-chart-x'            => 900,
1650
			'image-dline'                    => static::ASSET_DIR . 'images/dline.png',
1651
			'image-dline2'                   => static::ASSET_DIR . 'images/dline2.png',
1652
			'image-hline'                    => static::ASSET_DIR . 'images/hline.png',
1653
			'image-spacer'                   => static::ASSET_DIR . 'images/spacer.png',
1654
			'image-vline'                    => static::ASSET_DIR . 'images/vline.png',
1655
			'image-minus'                    => static::ASSET_DIR . 'images/minus.png',
1656
			'image-plus'                     => static::ASSET_DIR . 'images/plus.png',
1657
		];
1658
1659
		if (array_key_exists($parameter_name, $parameters)) {
1660
			return $parameters[$parameter_name];
1661
		} else {
1662
			throw new \InvalidArgumentException($parameter_name);
1663
		}
1664
	}
1665
1666
	/**
1667
	 * Are there any pending changes for us to approve?
1668
	 *
1669
	 * @return bool
1670
	 */
1671
	public function pendingChangesExist() {
1672
		return $this->tree && $this->tree->hasPendingEdit() && Auth::isModerator($this->tree);
1673
	}
1674
1675
	/**
1676
	 * Create a pending changes link. Some themes prefer an alert/banner to a menu.
1677
	 *
1678
	 * @return string
1679
	 */
1680
	public function pendingChangesLink() {
1681
		return '<a href="' . e(route('show-pending', ['ged' => $this->tree->getName()])) . '">' . $this->pendingChangesLinkText() . '</a>';
1682
	}
1683
1684
	/**
1685
	 * Text to use in the pending changes link.
1686
	 *
1687
	 * @return string
1688
	 */
1689
	public function pendingChangesLinkText() {
1690
		return I18N::translate('There are pending changes for you to moderate.');
1691
	}
1692
1693
	/**
1694
	 * Generate a list of items for the main menu.
1695
	 *
1696
	 * @return Menu[]
1697
	 */
1698
	public function primaryMenu() {
1699
		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...
1700
1701
		if ($this->tree) {
1702
			$individual = $controller->getSignificantIndividual();
1703
1704
			return array_filter(array_merge([
1705
				$this->menuHomePage(),
1706
				$this->menuChart($individual),
1707
				$this->menuLists($controller->getSignificantSurname()),
1708
				$this->menuCalendar(),
1709
				$this->menuReports(),
1710
				$this->menuSearch(),
1711
			], $this->menuModules()));
1712
		} else {
1713
			// No public trees? No genealogy menu!
1714
			return [];
1715
		}
1716
	}
1717
1718
	/**
1719
	 * Create the primary menu.
1720
	 *
1721
	 * @param Menu[] $menus
1722
	 *
1723
	 * @return string
1724
	 */
1725
	public function primaryMenuContent(array $menus) {
1726
		return implode('', array_map(function (Menu $menu) {
1727
			return $menu->bootstrap4();
1728
		}, $menus));
1729
	}
1730
1731
	/**
1732
	 * Generate a list of items for the user menu.
1733
	 *
1734
	 * @return Menu[]
1735
	 */
1736
	public function secondaryMenu() {
1737
		return array_filter([
1738
			$this->menuPendingChanges(),
1739
			$this->menuMyPages(),
1740
			$this->menuFavorites(),
1741
			$this->menuThemes(),
1742
			$this->menuLanguages(),
1743
			$this->menuLogin(),
1744
			$this->menuLogout(),
1745
		]);
1746
	}
1747
1748
	/**
1749
	 * Format the secondary menu.
1750
	 *
1751
	 * @param Menu[] $menus
1752
	 *
1753
	 * @return string
1754
	 */
1755
	public function secondaryMenuContent(array $menus) {
1756
		return implode('', array_map(function (Menu $menu) {
1757
			return $menu->bootstrap4();
1758
		}, $menus));
1759
	}
1760
1761
	/**
1762
	 * A list of CSS files to include for this page.
1763
	 *
1764
	 * @return string[]
1765
	 */
1766
	public function stylesheets() {
1767
1768
		if (I18N::direction() === 'rtl') {
1769
			$stylesheets = [
1770
				WT_ASSETS_URL . 'css/vendor-rtl.css',
1771
				self::STYLESHEET,
1772
			];
1773
		} else {
1774
			$stylesheets = [
1775
				WT_ASSETS_URL . 'css/vendor.css',
1776
				self::STYLESHEET,
1777
			];
1778
		}
1779
1780
		return $stylesheets;
1781
	}
1782
1783
	/**
1784
	 * A fixed string to identify this theme, in settings, etc.
1785
	 *
1786
	 * @return string
1787
	 */
1788
	public function themeId() {
1789
		return static::THEME_DIR;
1790
	}
1791
1792
	/**
1793
	 * What is this theme called?
1794
	 *
1795
	 * @return string
1796
	 */
1797
	abstract public function themeName();
1798
1799
}
1800