Passed
Push — master ( 8d1dab...502cab )
by Greg
06:00
created

IndividualController::getSidebars()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2019 webtrees development team
5
 * This program is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
 * GNU General Public License for more details.
13
 * You should have received a copy of the GNU General Public License
14
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15
 */
16
declare(strict_types=1);
17
18
namespace Fisharebest\Webtrees\Http\Controllers;
19
20
use Fisharebest\Webtrees\Auth;
21
use Fisharebest\Webtrees\Contracts\UserInterface;
22
use Fisharebest\Webtrees\Date;
23
use Fisharebest\Webtrees\Fact;
24
use Fisharebest\Webtrees\Functions\FunctionsDate;
25
use Fisharebest\Webtrees\Functions\FunctionsPrint;
26
use Fisharebest\Webtrees\Functions\FunctionsPrintFacts;
27
use Fisharebest\Webtrees\GedcomCode\GedcomCodeName;
28
use Fisharebest\Webtrees\GedcomTag;
29
use Fisharebest\Webtrees\I18N;
30
use Fisharebest\Webtrees\Individual;
31
use Fisharebest\Webtrees\Media;
32
use Fisharebest\Webtrees\MediaFile;
33
use Fisharebest\Webtrees\Module\ModuleSidebarInterface;
34
use Fisharebest\Webtrees\Module\ModuleTabInterface;
35
use Fisharebest\Webtrees\Services\ClipboardService;
36
use Fisharebest\Webtrees\Services\ModuleService;
37
use Fisharebest\Webtrees\Services\UserService;
38
use Fisharebest\Webtrees\Tree;
39
use Illuminate\Support\Collection;
40
use Psr\Http\Message\ResponseInterface;
41
use Psr\Http\Message\ServerRequestInterface;
42
use stdClass;
43
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
44
use function explode;
45
use function ob_get_clean;
46
use function ob_start;
47
use function preg_match_all;
48
use function preg_replace;
49
use function str_replace;
50
use function strpos;
51
52
/**
53
 * Controller for the individual page.
54
 */
55
class IndividualController extends AbstractBaseController
56
{
57
    /** @var ModuleService */
58
    private $module_service;
59
60
    /** @var UserService */
61
    private $user_service;
62
63
    /**
64
     * IndividualController constructor.
65
     *
66
     * @param ModuleService $module_service
67
     * @param UserService   $user_service
68
     */
69
    public function __construct(ModuleService $module_service, UserService $user_service)
70
    {
71
        $this->module_service = $module_service;
72
        $this->user_service   = $user_service;
73
    }
74
75
    /**
76
     * Show a individual's page.
77
     *
78
     * @param ServerRequestInterface $request
79
     * @param Tree                   $tree
80
     * @param ClipboardService       $clipboard_service
81
     *
82
     * @return ResponseInterface
83
     */
84
    public function show(ServerRequestInterface $request, Tree $tree, ClipboardService $clipboard_service): ResponseInterface
85
    {
86
        $xref       = $request->getQueryParams()['xref'];
87
        $individual = Individual::getInstance($xref, $tree);
88
89
        Auth::checkIndividualAccess($individual);
90
91
        // What is (was) the age of the individual
92
        $bdate = $individual->getBirthDate();
93
        $ddate = $individual->getDeathDate();
94
        if ($bdate->isOK() && !$individual->isDead()) {
95
            // If living display age
96
            $age = ' (' . I18N::translate('age') . ' ' . FunctionsDate::getAgeAtEvent(Date::getAgeGedcom($bdate, new Date(strtoupper(date('d M Y'))))) . ')';
97
        } elseif ($bdate->isOK() && $ddate->isOK()) {
98
            // If dead, show age at death
99
            $age = ' (' . I18N::translate('age') . ' ' . FunctionsDate::getAgeAtEvent(Date::getAgeGedcom($bdate, $ddate)) . ')';
100
        } else {
101
            $age = '';
102
        }
103
104
        // What images are linked to this individual
105
        $individual_media = [];
106
        foreach ($individual->facts(['OBJE']) as $fact) {
107
            $media_object = $fact->target();
108
            if ($media_object instanceof Media) {
109
                $media_file = $media_object->firstImageFile();
110
                if ($media_file instanceof MediaFile) {
111
                    $individual_media[] = $media_file;
112
                }
113
            }
114
        }
115
116
        $name_records = [];
117
        foreach ($individual->facts(['NAME']) as $n => $name_fact) {
118
            $name_records[] = $this->formatNameRecord($tree, $n, $name_fact);
119
        }
120
121
        $sex_records = [];
122
        foreach ($individual->facts(['SEX']) as $n => $sex_fact) {
123
            $sex_records[] = $this->formatSexRecord($sex_fact);
124
        }
125
126
        // If this individual is linked to a user account, show the link
127
        $user_link = '';
128
        if (Auth::isAdmin()) {
129
            $users = $this->user_service->findByIndividual($individual);
130
            foreach ($users as $user) {
131
                $user_link = ' —  <a href="' . e(route('admin-users', ['filter' => $user->email()])) . '">' . e($user->userName()) . '</a>';
132
            }
133
        }
134
135
        return $this->viewResponse('individual-page', [
136
            'age'              => $age,
137
            'clipboard_facts'  => $clipboard_service->pastableFacts($individual, new Collection()),
138
            'count_media'      => $this->countFacts($individual, ['OBJE']),
139
            'count_names'      => $this->countFacts($individual, ['NAME']),
140
            'count_sex'        => $this->countFacts($individual, ['SEX']),
141
            'individual'       => $individual,
142
            'individual_media' => $individual_media,
143
            'meta_robots'      => 'index,follow',
144
            'name_records'     => $name_records,
145
            'sex_records'      => $sex_records,
146
            'sidebars'         => $this->getSidebars($individual),
147
            'tabs'             => $this->getTabs($individual),
148
            'significant'      => $this->significant($individual),
149
            'title'            => $individual->fullName() . ' ' . $individual->getLifeSpan(),
150
            'user_link'        => $user_link,
151
        ]);
152
    }
153
154
    /**
155
     * @param ServerRequestInterface $request
156
     * @param Tree                   $tree
157
     * @param UserInterface          $user
158
     *
159
     * @return ResponseInterface
160
     */
161
    public function tab(ServerRequestInterface $request, Tree $tree, UserInterface $user): ResponseInterface
162
    {
163
        $xref        = $request->getQueryParams()['xref'];
164
        $record      = Individual::getInstance($xref, $tree);
165
        $module_name = $request->getQueryParams()['module'];
166
        $module      = $this->module_service->findByName($module_name);
167
168
        if (!$module instanceof ModuleTabInterface) {
169
            throw new NotFoundHttpException('No such tab: ' . $module_name);
170
        }
171
172
        Auth::checkIndividualAccess($record);
173
        Auth::checkComponentAccess($module, 'tab', $tree, $user);
174
175
        $layout = view('layouts/ajax', [
176
            'content' => $module->getTabContent($record),
177
        ]);
178
179
        return response($layout);
180
    }
181
182
    /**
183
     * Count the (non-pending-delete) name records for an individual.
184
     *
185
     * @param Individual $individual
186
     * @param string[]   $tags
187
     *
188
     * @return int
189
     */
190
    private function countFacts(Individual $individual, array $tags): int
191
    {
192
        $count = 0;
193
194
        foreach ($individual->facts($tags) as $fact) {
195
            if (!$fact->isPendingDeletion()) {
196
                $count++;
197
            }
198
        }
199
200
        return $count;
201
    }
202
203
    /**
204
     * Format a name record
205
     *
206
     * @param Tree $tree
207
     * @param int  $n
208
     * @param Fact $fact
209
     *
210
     * @return string
211
     */
212
    private function formatNameRecord(Tree $tree, $n, Fact $fact): string
213
    {
214
        $individual = $fact->record();
215
216
        // Create a dummy record, so we can extract the formatted NAME value from it.
217
        $dummy = new Individual(
218
            'xref',
219
            "0 @xref@ INDI\n1 DEAT Y\n" . $fact->gedcom(),
220
            null,
221
            $individual->tree()
222
        );
223
        $dummy->setPrimaryName(0); // Make sure we use the name from "1 NAME"
224
225
        $container_class = 'card';
226
        $content_class   = 'collapse';
227
        $aria            = 'false';
228
229
        if ($n === 0) {
230
            $content_class = 'collapse show';
231
            $aria          = 'true';
232
        }
233
        if ($fact->isPendingDeletion()) {
234
            $container_class .= ' wt-old';
235
        } elseif ($fact->isPendingAddition()) {
236
            $container_class .= ' wt-new';
237
        }
238
239
        ob_start();
240
        echo '<dl><dt class="label">', I18N::translate('Name'), '</dt>';
241
        echo '<dd class="field">', $dummy->fullName(), '</dd>';
242
        $ct = preg_match_all('/\n2 (\w+) (.*)/', $fact->gedcom(), $nmatch, PREG_SET_ORDER);
243
        for ($i = 0; $i < $ct; $i++) {
244
            $tag = $nmatch[$i][1];
245
            if ($tag !== 'SOUR' && $tag !== 'NOTE' && $tag !== 'SPFX') {
246
                echo '<dt class="label">', GedcomTag::getLabel($tag, $individual), '</dt>';
247
                echo '<dd class="field">'; // Before using dir="auto" on this field, note that Gecko treats this as an inline element but WebKit treats it as a block element
248
                if (isset($nmatch[$i][2])) {
249
                    $name = e($nmatch[$i][2]);
250
                    $name = str_replace('/', '', $name);
251
                    $name = preg_replace('/(\S*)\*/', '<span class="starredname">\\1</span>', $name);
252
                    switch ($tag) {
253
                        case 'TYPE':
254
                            echo GedcomCodeName::getValue($name, $individual);
255
                            break;
256
                        case 'SURN':
257
                            // The SURN field is not necessarily the surname.
258
                            // Where it is not a substring of the real surname, show it after the real surname.
259
                            $surname = e($dummy->getAllNames()[0]['surname']);
260
                            $surns   = preg_replace('/, */', ' ', $nmatch[$i][2]);
261
                            if (strpos($dummy->getAllNames()[0]['surname'], $surns) !== false) {
262
                                echo '<span dir="auto">' . $surname . '</span>';
263
                            } else {
264
                                echo I18N::translate('%1$s (%2$s)', '<span dir="auto">' . $surname . '</span>', '<span dir="auto">' . $name . '</span>');
265
                            }
266
                            break;
267
                        default:
268
                            echo '<span dir="auto">' . $name . '</span>';
269
                            break;
270
                    }
271
                }
272
                echo '</dd>';
273
            }
274
        }
275
        echo '</dl>';
276
        if (strpos($fact->gedcom(), "\n2 SOUR") !== false) {
277
            echo '<div id="indi_sour" class="clearfix">', FunctionsPrintFacts::printFactSources($tree, $fact->gedcom(), 2), '</div>';
278
        }
279
        if (strpos($fact->gedcom(), "\n2 NOTE") !== false) {
280
            echo '<div id="indi_note" class="clearfix">', FunctionsPrint::printFactNotes($tree, $fact->gedcom(), 2), '</div>';
281
        }
282
        $content = ob_get_clean();
283
284
        if ($fact->canEdit()) {
285
            $edit_links =
286
                '<a class="btn btn-link" href="#" data-confirm="' . I18N::translate('Are you sure you want to delete this fact?') . '" onclick="return delete_fact(this.dataset.confirm, \'' . e($individual->tree()->name()) . '\', \'' . e($individual->xref()) . '\', \'' . $fact->id() . '\');" title="' . I18N::translate('Delete this name') . '">' . view('icons/delete') . '<span class="sr-only">' . I18N::translate('Delete this name') . '</span></a>' .
287
                '<a class="btn btn-link" href="' . e(route('edit-name', ['xref' => $individual->xref(), 'fact_id' => $fact->id(), 'ged' => $individual->tree()->name()])) . '" title="' . I18N::translate('Edit the name') . '">' . view('icons/edit') . '<span class="sr-only">' . I18N::translate('Edit the name') . '</span></a>';
288
        } else {
289
            $edit_links = '';
290
        }
291
292
        return '
293
			<div class="' . $container_class . '">
294
        <div class="card-header" role="tab" id="name-header-' . $n . '">
295
		        <a data-toggle="collapse" data-parent="#individual-names" href="#name-content-' . $n . '" aria-expanded="' . $aria . '" aria-controls="name-content-' . $n . '">' . $dummy->fullName() . '</a>
296
		      ' . $edit_links . '
297
        </div>
298
		    <div id="name-content-' . $n . '" class="' . $content_class . '" role="tabpanel" aria-labelledby="name-header-' . $n . '">
299
		      <div class="card-body">' . $content . '</div>
300
        </div>
301
      </div>';
302
    }
303
304
    /**
305
     * print information for a sex record
306
     *
307
     * @param Fact $fact
308
     *
309
     * @return string
310
     */
311
    private function formatSexRecord(Fact $fact): string
312
    {
313
        $individual = $fact->record();
314
315
        switch ($fact->value()) {
316
            case 'M':
317
                $sex = I18N::translate('Male');
318
                break;
319
            case 'F':
320
                $sex = I18N::translate('Female');
321
                break;
322
            default:
323
                $sex = I18N::translateContext('unknown gender', 'Unknown');
324
                break;
325
        }
326
327
        $container_class = 'card';
328
        if ($fact->isPendingDeletion()) {
329
            $container_class .= ' wt-old';
330
        } elseif ($fact->isPendingAddition()) {
331
            $container_class .= ' wt-new';
332
        }
333
334
        if ($individual->canEdit()) {
335
            $edit_links = '<a class="btn btn-link" href="' . e(route('edit-fact', ['xref' => $individual->xref(), 'fact_id' => $fact->id(), 'ged' => $individual->tree()->name()])) . '" title="' . I18N::translate('Edit the gender') . '">' . view('icons/edit') . '<span class="sr-only">' . I18N::translate('Edit the gender') . '</span></a>';
336
        } else {
337
            $edit_links = '';
338
        }
339
340
        return '
341
		<div class="' . $container_class . '">
342
			<div class="card-header" role="tab" id="name-header-add">
343
				<div class="card-title mb-0">
344
					<b>' . I18N::translate('Gender') . '</b> ' . $sex . $edit_links . '
345
				</div>
346
			</div>
347
		</div>';
348
    }
349
350
    /**
351
     * Which tabs should we show on this individual's page.
352
     * We don't show empty tabs.
353
     *
354
     * @param Individual $individual
355
     *
356
     * @return Collection
357
     */
358
    public function getSidebars(Individual $individual): Collection
359
    {
360
        return $this->module_service->findByComponent(ModuleSidebarInterface::class, $individual->tree(), Auth::user())
361
            ->filter(static function (ModuleSidebarInterface $sidebar) use ($individual): bool {
362
                return $sidebar->hasSidebarContent($individual);
363
            });
364
    }
365
366
    /**
367
     * Which tabs should we show on this individual's page.
368
     * We don't show empty tabs.
369
     *
370
     * @param Individual $individual
371
     *
372
     * @return Collection
373
     */
374
    public function getTabs(Individual $individual): Collection
375
    {
376
        return $this->module_service->findByComponent(ModuleTabInterface::class, $individual->tree(), Auth::user())
377
            ->filter(static function (ModuleTabInterface $tab) use ($individual): bool {
378
                return $tab->hasTabContent($individual);
379
            });
380
    }
381
382
    /**
383
     * What are the significant elements of this page?
384
     * The layout will need them to generate URLs for charts and reports.
385
     *
386
     * @param Individual $individual
387
     *
388
     * @return stdClass
389
     */
390
    private function significant(Individual $individual): stdClass
391
    {
392
        [$surname] = explode(',', $individual->sortName());
393
394
        $family = $individual->childFamilies()->merge($individual->spouseFamilies())->first();
395
396
        return (object) [
397
            'family'     => $family,
398
            'individual' => $individual,
399
            'surname'    => $surname,
400
        ];
401
    }
402
}
403