Passed
Push — master ( c46acb...852ede )
by Greg
05:30
created

IndividualPage::handle()   C

Complexity

Conditions 14
Paths 97

Size

Total Lines 77
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Importance

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