Passed
Push — master ( d2ebda...80489a )
by Greg
05:54
created

IndividualPage::handle()   B

Complexity

Conditions 11
Paths 65

Size

Total Lines 74
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 47
nc 65
nop 1
dl 0
loc 74
rs 7.3166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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