Completed
Push — master ( 56eca4...2839b8 )
by Greg
06:20
created

ClippingsCartModule::getAddNoteAction()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 14
nc 2
nop 1
dl 0
loc 23
rs 9.7998
c 0
b 0
f 0
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\Module;
21
22
use Fisharebest\Webtrees\Auth;
23
use Fisharebest\Webtrees\Exceptions\FamilyNotFoundException;
24
use Fisharebest\Webtrees\Exceptions\IndividualNotFoundException;
25
use Fisharebest\Webtrees\Exceptions\MediaNotFoundException;
26
use Fisharebest\Webtrees\Exceptions\NoteNotFoundException;
27
use Fisharebest\Webtrees\Exceptions\RepositoryNotFoundException;
28
use Fisharebest\Webtrees\Exceptions\SourceNotFoundException;
29
use Fisharebest\Webtrees\Family;
30
use Fisharebest\Webtrees\Functions\FunctionsExport;
31
use Fisharebest\Webtrees\Gedcom;
32
use Fisharebest\Webtrees\GedcomRecord;
33
use Fisharebest\Webtrees\Http\RequestHandlers\FamilyPage;
34
use Fisharebest\Webtrees\Http\RequestHandlers\IndividualPage;
35
use Fisharebest\Webtrees\Http\RequestHandlers\MediaPage;
36
use Fisharebest\Webtrees\Http\RequestHandlers\NotePage;
37
use Fisharebest\Webtrees\Http\RequestHandlers\RepositoryPage;
38
use Fisharebest\Webtrees\Http\RequestHandlers\SourcePage;
39
use Fisharebest\Webtrees\I18N;
40
use Fisharebest\Webtrees\Individual;
41
use Fisharebest\Webtrees\Media;
42
use Fisharebest\Webtrees\Menu;
43
use Fisharebest\Webtrees\Note;
44
use Fisharebest\Webtrees\Repository;
45
use Fisharebest\Webtrees\Services\UserService;
46
use Fisharebest\Webtrees\Session;
47
use Fisharebest\Webtrees\Source;
48
use Fisharebest\Webtrees\Tree;
49
use League\Flysystem\Filesystem;
50
use League\Flysystem\MountManager;
51
use League\Flysystem\ZipArchive\ZipArchiveAdapter;
52
use Psr\Http\Message\ResponseFactoryInterface;
53
use Psr\Http\Message\ResponseInterface;
54
use Psr\Http\Message\ServerRequestInterface;
55
use Psr\Http\Message\StreamFactoryInterface;
56
57
use function app;
58
use function array_filter;
59
use function array_keys;
60
use function array_map;
61
use function assert;
62
use function in_array;
63
use function is_string;
64
use function key;
65
use function preg_match_all;
66
use function redirect;
67
use function route;
68
use function str_replace;
69
use function strip_tags;
70
use function sys_get_temp_dir;
71
use function tempnam;
72
use function ucfirst;
73
use function utf8_decode;
74
75
/**
76
 * Class ClippingsCartModule
77
 */
78
class ClippingsCartModule extends AbstractModule implements ModuleMenuInterface
79
{
80
    use ModuleMenuTrait;
81
82
    // Routes that have a record which can be added to the clipboard
83
    private const ROUTES_WITH_RECORDS = [
84
        'Family' => FamilyPage::class,
85
        'Individual' => IndividualPage::class,
86
        'Media' => MediaPage::class,
87
        'Note' => NotePage::class,
88
        'Repository' => RepositoryPage::class,
89
        'Source' => SourcePage::class,
90
    ];
91
92
    /** @var int The default access level for this module.  It can be changed in the control panel. */
93
    protected $access_level = Auth::PRIV_USER;
94
95
    /**
96
     * @var UserService
97
     */
98
    private $user_service;
99
100
    /**
101
     * ClippingsCartModule constructor.
102
     *
103
     * @param UserService $user_service
104
     */
105
    public function __construct(UserService $user_service)
106
    {
107
        $this->user_service = $user_service;
108
    }
109
110
    /**
111
     * How should this module be identified in the control panel, etc.?
112
     *
113
     * @return string
114
     */
115
    public function title(): string
116
    {
117
        /* I18N: Name of a module */
118
        return I18N::translate('Clippings cart');
119
    }
120
121
    /**
122
     * A sentence describing what this module does.
123
     *
124
     * @return string
125
     */
126
    public function description(): string
127
    {
128
        /* I18N: Description of the “Clippings cart” module */
129
        return I18N::translate('Select records from your family tree and save them as a GEDCOM file.');
130
    }
131
132
    /**
133
     * The default position for this menu.  It can be changed in the control panel.
134
     *
135
     * @return int
136
     */
137
    public function defaultMenuOrder(): int
138
    {
139
        return 6;
140
    }
141
142
    /**
143
     * A menu, to be added to the main application menu.
144
     *
145
     * @param Tree $tree
146
     *
147
     * @return Menu|null
148
     */
149
    public function getMenu(Tree $tree): ?Menu
150
    {
151
        /** @var ServerRequestInterface $request */
152
        $request = app(ServerRequestInterface::class);
153
154
        $route = $request->getAttribute('route');
155
156
        $submenus = [
157
            new Menu($this->title(), route('module', [
158
                'module' => $this->name(),
159
                'action' => 'Show',
160
                'tree'    => $tree->name(),
161
            ]), 'menu-clippings-cart', ['rel' => 'nofollow']),
162
        ];
163
164
        $action = array_search($route, self::ROUTES_WITH_RECORDS);
165
        if ($action !== false) {
166
            $xref = $request->getAttribute('xref');
167
            assert(is_string($xref));
168
169
            $add_route = route('module', [
170
                'module' => $this->name(),
171
                'action' => 'Add' . $action,
172
                'xref'   => $xref,
173
                'tree'    => $tree->name(),
174
            ]);
175
176
            $submenus[] = new Menu(I18N::translate('Add to the clippings cart'), $add_route, 'menu-clippings-add', ['rel' => 'nofollow']);
177
        }
178
179
        if (!$this->isCartEmpty($tree)) {
180
            $submenus[] = new Menu(I18N::translate('Empty the clippings cart'), route('module', [
181
                'module' => $this->name(),
182
                'action' => 'Empty',
183
                'tree'    => $tree->name(),
184
            ]), 'menu-clippings-empty', ['rel' => 'nofollow']);
185
186
            $submenus[] = new Menu(I18N::translate('Download'), route('module', [
187
                'module' => $this->name(),
188
                'action' => 'DownloadForm',
189
                'tree'    => $tree->name(),
190
            ]), 'menu-clippings-download', ['rel' => 'nofollow']);
191
        }
192
193
        return new Menu($this->title(), '#', 'menu-clippings', ['rel' => 'nofollow'], $submenus);
194
    }
195
196
    /**
197
     * @param ServerRequestInterface $request
198
     *
199
     * @return ResponseInterface
200
     */
201
    public function postDownloadAction(ServerRequestInterface $request): ResponseInterface
202
    {
203
        $tree = $request->getAttribute('tree');
204
        assert($tree instanceof Tree);
205
206
        $privatize_export = $request->getParsedBody()['privatize_export'];
207
        $convert          = (bool) ($request->getParsedBody()['convert'] ?? false);
208
209
        $cart = Session::get('cart', []);
210
211
        $xrefs = array_keys($cart[$tree->name()] ?? []);
212
213
        // Create a new/empty .ZIP file
214
        $temp_zip_file  = tempnam(sys_get_temp_dir(), 'webtrees-zip-');
215
        $zip_adapter    = new ZipArchiveAdapter($temp_zip_file);
216
        $zip_filesystem = new Filesystem($zip_adapter);
217
218
        $manager = new MountManager([
219
            'media' => $tree->mediaFilesystem(),
220
            'zip'   => $zip_filesystem,
221
        ]);
222
223
        // Media file prefix
224
        $path = $tree->getPreference('MEDIA_DIRECTORY');
225
226
        // GEDCOM file header
227
        $filetext = FunctionsExport::gedcomHeader($tree, $convert ? 'ANSI' : 'UTF-8');
228
229
        switch ($privatize_export) {
230
            case 'gedadmin':
231
                $access_level = Auth::PRIV_NONE;
232
                break;
233
            case 'user':
234
                $access_level = Auth::PRIV_USER;
235
                break;
236
            case 'visitor':
237
                $access_level = Auth::PRIV_PRIVATE;
238
                break;
239
            case 'none':
240
            default:
241
                $access_level = Auth::PRIV_HIDE;
242
                break;
243
        }
244
245
        foreach ($xrefs as $xref) {
246
            $object = GedcomRecord::getInstance($xref, $tree);
247
            // The object may have been deleted since we added it to the cart....
248
            if ($object instanceof  GedcomRecord) {
249
                $record = $object->privatizeGedcom($access_level);
250
                // Remove links to objects that aren't in the cart
251
                preg_match_all('/\n1 ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@(\n[2-9].*)*/', $record, $matches, PREG_SET_ORDER);
252
                foreach ($matches as $match) {
253
                    if (!in_array($match[1], $xrefs, true)) {
254
                        $record = str_replace($match[0], '', $record);
255
                    }
256
                }
257
                preg_match_all('/\n2 ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@(\n[3-9].*)*/', $record, $matches, PREG_SET_ORDER);
258
                foreach ($matches as $match) {
259
                    if (!in_array($match[1], $xrefs, true)) {
260
                        $record = str_replace($match[0], '', $record);
261
                    }
262
                }
263
                preg_match_all('/\n3 ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@(\n[4-9].*)*/', $record, $matches, PREG_SET_ORDER);
264
                foreach ($matches as $match) {
265
                    if (!in_array($match[1], $xrefs, true)) {
266
                        $record = str_replace($match[0], '', $record);
267
                    }
268
                }
269
270
                if ($object instanceof Individual || $object instanceof Family) {
271
                    $filetext .= $record . "\n";
272
                    $filetext .= "1 SOUR @WEBTREES@\n";
273
                    $filetext .= '2 PAGE ' . $object->url() . "\n";
274
                } elseif ($object instanceof Source) {
275
                    $filetext .= $record . "\n";
276
                    $filetext .= '1 NOTE ' . $object->url() . "\n";
277
                } elseif ($object instanceof Media) {
278
                    // Add the media files to the archive
279
                    foreach ($object->mediaFiles() as $media_file) {
280
                        $from = 'media://' . $media_file->filename();
281
                        $to   = 'zip://' . $path . $media_file->filename();
282
                        if (!$media_file->isExternal() && $manager->has($from)) {
283
                            $manager->copy($from, $to);
284
                        }
285
                    }
286
                    $filetext .= $record . "\n";
287
                } else {
288
                    $filetext .= $record . "\n";
289
                }
290
            }
291
        }
292
293
        $base_url = $request->getAttribute('base_url');
294
295
        // Create a source, to indicate the source of the data.
296
        $filetext .= "0 @WEBTREES@ SOUR\n1 TITL " . $base_url . "\n";
297
        $author   = $this->user_service->find((int) $tree->getPreference('CONTACT_USER_ID'));
298
        if ($author !== null) {
299
            $filetext .= '1 AUTH ' . $author->realName() . "\n";
300
        }
301
        $filetext .= "0 TRLR\n";
302
303
        // Make sure the preferred line endings are used
304
        $filetext = str_replace('\n', Gedcom::EOL, $filetext);
305
306
        if ($convert) {
307
            $filetext = utf8_decode($filetext);
308
        }
309
310
        // Finally add the GEDCOM file to the .ZIP file.
311
        $zip_filesystem->write('clippings.ged', $filetext);
312
313
        // Need to force-close ZipArchive filesystems.
314
        $zip_adapter->getArchive()->close();
315
316
        // Use a stream, so that we do not have to load the entire file into memory.
317
        $stream = app(StreamFactoryInterface::class)->createStreamFromFile($temp_zip_file);
318
319
        /** @var ResponseFactoryInterface $response_factory */
320
        $response_factory = app(ResponseFactoryInterface::class);
321
322
        return $response_factory->createResponse()
323
            ->withBody($stream)
324
            ->withHeader('Content-Type', 'application/zip')
325
            ->withHeader('Content-Disposition', 'attachment; filename="clippings.zip');
326
    }
327
328
    /**
329
     * @param ServerRequestInterface $request
330
     *
331
     * @return ResponseInterface
332
     */
333
    public function getDownloadFormAction(ServerRequestInterface $request): ResponseInterface
334
    {
335
        $tree = $request->getAttribute('tree');
336
        assert($tree instanceof Tree);
337
338
        $user  = $request->getAttribute('user');
339
        $title = I18N::translate('Family tree clippings cart') . ' — ' . I18N::translate('Download');
340
341
        return $this->viewResponse('modules/clippings/download', [
342
            'is_manager' => Auth::isManager($tree, $user),
343
            'is_member'  => Auth::isMember($tree, $user),
344
            'module'     => $this->name(),
345
            'title'      => $title,
346
            'tree'       => $tree,
347
        ]);
348
    }
349
350
    /**
351
     * @param ServerRequestInterface $request
352
     *
353
     * @return ResponseInterface
354
     */
355
    public function getEmptyAction(ServerRequestInterface $request): ResponseInterface
356
    {
357
        $tree = $request->getAttribute('tree');
358
        assert($tree instanceof Tree);
359
360
        $cart                = Session::get('cart', []);
361
        $cart[$tree->name()] = [];
362
        Session::put('cart', $cart);
363
364
        $url = route('module', [
365
            'module' => $this->name(),
366
            'action' => 'Show',
367
            'tree'    => $tree->name(),
368
        ]);
369
370
        return redirect($url);
371
    }
372
373
    /**
374
     * @param ServerRequestInterface $request
375
     *
376
     * @return ResponseInterface
377
     */
378
    public function postRemoveAction(ServerRequestInterface $request): ResponseInterface
379
    {
380
        $tree = $request->getAttribute('tree');
381
        assert($tree instanceof Tree);
382
383
        $xref = $request->getQueryParams()['xref'];
384
385
        $cart = Session::get('cart', []);
386
        unset($cart[$tree->name()][$xref]);
387
        Session::put('cart', $cart);
388
389
        $url = route('module', [
390
            'module' => $this->name(),
391
            'action' => 'Show',
392
            'tree'    => $tree->name(),
393
        ]);
394
395
        return redirect($url);
396
    }
397
398
    /**
399
     * @param ServerRequestInterface $request
400
     *
401
     * @return ResponseInterface
402
     */
403
    public function getShowAction(ServerRequestInterface $request): ResponseInterface
404
    {
405
        $tree = $request->getAttribute('tree');
406
        assert($tree instanceof Tree);
407
408
        return $this->viewResponse('modules/clippings/show', [
409
            'records' => $this->allRecordsInCart($tree),
410
            'title'   => I18N::translate('Family tree clippings cart'),
411
            'tree'    => $tree,
412
        ]);
413
    }
414
415
    /**
416
     * @param ServerRequestInterface $request
417
     *
418
     * @return ResponseInterface
419
     */
420
    public function getAddFamilyAction(ServerRequestInterface $request): ResponseInterface
421
    {
422
        $tree = $request->getAttribute('tree');
423
        assert($tree instanceof Tree);
424
425
        $xref = $request->getQueryParams()['xref'];
426
427
        $family = Family::getInstance($xref, $tree);
428
429
        if ($family === null) {
430
            throw new FamilyNotFoundException();
431
        }
432
433
        $options = $this->familyOptions($family);
434
435
        $title = I18N::translate('Add %s to the clippings cart', $family->fullName());
436
437
        return $this->viewResponse('modules/clippings/add-options', [
438
            'options' => $options,
439
            'default' => key($options),
440
            'record'  => $family,
441
            'title'   => $title,
442
            'tree'    => $tree,
443
        ]);
444
    }
445
446
    /**
447
     * @param Family $family
448
     *
449
     * @return string[]
450
     */
451
    private function familyOptions(Family $family): array
452
    {
453
        $name = strip_tags($family->fullName());
454
455
        return [
456
            'parents'     => $name,
457
            /* I18N: %s is a family (husband + wife) */
458
            'members'     => I18N::translate('%s and their children', $name),
459
            /* I18N: %s is a family (husband + wife) */
460
            'descendants' => I18N::translate('%s and their descendants', $name),
461
        ];
462
    }
463
464
    /**
465
     * @param ServerRequestInterface $request
466
     *
467
     * @return ResponseInterface
468
     */
469
    public function postAddFamilyAction(ServerRequestInterface $request): ResponseInterface
470
    {
471
        $tree = $request->getAttribute('tree');
472
        assert($tree instanceof Tree);
473
474
        $xref   = $request->getQueryParams()['xref'];
475
        $option = $request->getParsedBody()['option'];
476
477
        $family = Family::getInstance($xref, $tree);
478
479
        if ($family === null) {
480
            throw new FamilyNotFoundException();
481
        }
482
483
        switch ($option) {
484
            case 'parents':
485
                $this->addFamilyToCart($family);
486
                break;
487
488
            case 'members':
489
                $this->addFamilyAndChildrenToCart($family);
490
                break;
491
492
            case 'descendants':
493
                $this->addFamilyAndDescendantsToCart($family);
494
                break;
495
        }
496
497
        return redirect($family->url());
498
    }
499
500
    /**
501
     * @param Family $family
502
     *
503
     * @return void
504
     */
505
    private function addFamilyToCart(Family $family): void
506
    {
507
        $this->addRecordToCart($family);
508
509
        foreach ($family->spouses() as $spouse) {
510
            $this->addRecordToCart($spouse);
511
        }
512
    }
513
514
    /**
515
     * @param Family $family
516
     *
517
     * @return void
518
     */
519
    private function addFamilyAndChildrenToCart(Family $family): void
520
    {
521
        $this->addRecordToCart($family);
522
523
        foreach ($family->spouses() as $spouse) {
524
            $this->addRecordToCart($spouse);
525
        }
526
        foreach ($family->children() as $child) {
527
            $this->addRecordToCart($child);
528
        }
529
    }
530
531
    /**
532
     * @param Family $family
533
     *
534
     * @return void
535
     */
536
    private function addFamilyAndDescendantsToCart(Family $family): void
537
    {
538
        $this->addRecordToCart($family);
539
540
        foreach ($family->spouses() as $spouse) {
541
            $this->addRecordToCart($spouse);
542
        }
543
        foreach ($family->children() as $child) {
544
            $this->addRecordToCart($child);
545
            foreach ($child->spouseFamilies() as $child_family) {
546
                $this->addFamilyAndDescendantsToCart($child_family);
547
            }
548
        }
549
    }
550
551
    /**
552
     * @param ServerRequestInterface $request
553
     *
554
     * @return ResponseInterface
555
     */
556
    public function getAddIndividualAction(ServerRequestInterface $request): ResponseInterface
557
    {
558
        $tree = $request->getAttribute('tree');
559
        assert($tree instanceof Tree);
560
561
        $xref = $request->getQueryParams()['xref'];
562
563
        $individual = Individual::getInstance($xref, $tree);
564
565
        if ($individual === null) {
566
            throw new IndividualNotFoundException();
567
        }
568
569
        $options = $this->individualOptions($individual);
570
571
        $title = I18N::translate('Add %s to the clippings cart', $individual->fullName());
572
573
        return $this->viewResponse('modules/clippings/add-options', [
574
            'options' => $options,
575
            'default' => key($options),
576
            'record'  => $individual,
577
            'title'   => $title,
578
            'tree'    => $tree,
579
        ]);
580
    }
581
582
    /**
583
     * @param Individual $individual
584
     *
585
     * @return string[]
586
     */
587
    private function individualOptions(Individual $individual): array
588
    {
589
        $name = strip_tags($individual->fullName());
590
591
        if ($individual->sex() === 'F') {
592
            return [
593
                'self'              => $name,
594
                'parents'           => I18N::translate('%s, her parents and siblings', $name),
595
                'spouses'           => I18N::translate('%s, her spouses and children', $name),
596
                'ancestors'         => I18N::translate('%s and her ancestors', $name),
597
                'ancestor_families' => I18N::translate('%s, her ancestors and their families', $name),
598
                'descendants'       => I18N::translate('%s, her spouses and descendants', $name),
599
            ];
600
        }
601
602
        return [
603
            'self'              => $name,
604
            'parents'           => I18N::translate('%s, his parents and siblings', $name),
605
            'spouses'           => I18N::translate('%s, his spouses and children', $name),
606
            'ancestors'         => I18N::translate('%s and his ancestors', $name),
607
            'ancestor_families' => I18N::translate('%s, his ancestors and their families', $name),
608
            'descendants'       => I18N::translate('%s, his spouses and descendants', $name),
609
        ];
610
    }
611
612
    /**
613
     * @param ServerRequestInterface $request
614
     *
615
     * @return ResponseInterface
616
     */
617
    public function postAddIndividualAction(ServerRequestInterface $request): ResponseInterface
618
    {
619
        $tree = $request->getAttribute('tree');
620
        assert($tree instanceof Tree);
621
622
        $xref   = $request->getQueryParams()['xref'];
623
        $option = $request->getParsedBody()['option'];
624
625
        $individual = Individual::getInstance($xref, $tree);
626
627
        if ($individual === null) {
628
            throw new IndividualNotFoundException();
629
        }
630
631
        switch ($option) {
632
            case 'self':
633
                $this->addRecordToCart($individual);
634
                break;
635
636
            case 'parents':
637
                foreach ($individual->childFamilies() as $family) {
638
                    $this->addFamilyAndChildrenToCart($family);
639
                }
640
                break;
641
642
            case 'spouses':
643
                foreach ($individual->spouseFamilies() as $family) {
644
                    $this->addFamilyAndChildrenToCart($family);
645
                }
646
                break;
647
648
            case 'ancestors':
649
                $this->addAncestorsToCart($individual);
650
                break;
651
652
            case 'ancestor_families':
653
                $this->addAncestorFamiliesToCart($individual);
654
                break;
655
656
            case 'descendants':
657
                foreach ($individual->spouseFamilies() as $family) {
658
                    $this->addFamilyAndDescendantsToCart($family);
659
                }
660
                break;
661
        }
662
663
        return redirect($individual->url());
664
    }
665
666
    /**
667
     * @param Individual $individual
668
     *
669
     * @return void
670
     */
671
    private function addAncestorsToCart(Individual $individual): void
672
    {
673
        $this->addRecordToCart($individual);
674
675
        foreach ($individual->childFamilies() as $family) {
676
            foreach ($family->spouses() as $parent) {
677
                $this->addAncestorsToCart($parent);
678
            }
679
        }
680
    }
681
682
    /**
683
     * @param Individual $individual
684
     *
685
     * @return void
686
     */
687
    private function addAncestorFamiliesToCart(Individual $individual): void
688
    {
689
        foreach ($individual->childFamilies() as $family) {
690
            $this->addFamilyAndChildrenToCart($family);
691
            foreach ($family->spouses() as $parent) {
692
                $this->addAncestorsToCart($parent);
693
            }
694
        }
695
    }
696
697
    /**
698
     * @param ServerRequestInterface $request
699
     *
700
     * @return ResponseInterface
701
     */
702
    public function getAddMediaAction(ServerRequestInterface $request): ResponseInterface
703
    {
704
        $tree = $request->getAttribute('tree');
705
        assert($tree instanceof Tree);
706
707
        $xref = $request->getQueryParams()['xref'];
708
709
        $media = Media::getInstance($xref, $tree);
710
711
        if ($media === null) {
712
            throw new MediaNotFoundException();
713
        }
714
715
        $options = $this->mediaOptions($media);
716
717
        $title = I18N::translate('Add %s to the clippings cart', $media->fullName());
718
719
        return $this->viewResponse('modules/clippings/add-options', [
720
            'options' => $options,
721
            'default' => key($options),
722
            'record'  => $media,
723
            'title'   => $title,
724
            'tree'    => $tree,
725
        ]);
726
    }
727
728
    /**
729
     * @param Media $media
730
     *
731
     * @return string[]
732
     */
733
    private function mediaOptions(Media $media): array
734
    {
735
        $name = strip_tags($media->fullName());
736
737
        return [
738
            'self' => $name,
739
        ];
740
    }
741
742
    /**
743
     * @param ServerRequestInterface $request
744
     *
745
     * @return ResponseInterface
746
     */
747
    public function postAddMediaAction(ServerRequestInterface $request): ResponseInterface
748
    {
749
        $tree = $request->getAttribute('tree');
750
        assert($tree instanceof Tree);
751
752
        $xref = $request->getQueryParams()['xref'];
753
754
        $media = Media::getInstance($xref, $tree);
755
756
        if ($media === null) {
757
            throw new MediaNotFoundException();
758
        }
759
760
        $this->addRecordToCart($media);
761
762
        return redirect($media->url());
763
    }
764
765
    /**
766
     * @param ServerRequestInterface $request
767
     *
768
     * @return ResponseInterface
769
     */
770
    public function getAddNoteAction(ServerRequestInterface $request): ResponseInterface
771
    {
772
        $tree = $request->getAttribute('tree');
773
        assert($tree instanceof Tree);
774
775
        $xref = $request->getQueryParams()['xref'];
776
777
        $note = Note::getInstance($xref, $tree);
778
779
        if ($note === null) {
780
            throw new NoteNotFoundException();
781
        }
782
783
        $options = $this->noteOptions($note);
784
785
        $title = I18N::translate('Add %s to the clippings cart', $note->fullName());
786
787
        return $this->viewResponse('modules/clippings/add-options', [
788
            'options' => $options,
789
            'default' => key($options),
790
            'record'  => $note,
791
            'title'   => $title,
792
            'tree'    => $tree,
793
        ]);
794
    }
795
796
    /**
797
     * @param Note $note
798
     *
799
     * @return string[]
800
     */
801
    private function noteOptions(Note $note): array
802
    {
803
        $name = strip_tags($note->fullName());
804
805
        return [
806
            'self' => $name,
807
        ];
808
    }
809
810
    /**
811
     * @param ServerRequestInterface $request
812
     *
813
     * @return ResponseInterface
814
     */
815
    public function postAddNoteAction(ServerRequestInterface $request): ResponseInterface
816
    {
817
        $tree = $request->getAttribute('tree');
818
        assert($tree instanceof Tree);
819
820
        $xref = $request->getQueryParams()['xref'];
821
822
        $note = Note::getInstance($xref, $tree);
823
824
        if ($note === null) {
825
            throw new NoteNotFoundException();
826
        }
827
828
        $this->addRecordToCart($note);
829
830
        return redirect($note->url());
831
    }
832
833
    /**
834
     * @param ServerRequestInterface $request
835
     *
836
     * @return ResponseInterface
837
     */
838
    public function getAddRepositoryAction(ServerRequestInterface $request): ResponseInterface
839
    {
840
        $tree = $request->getAttribute('tree');
841
        assert($tree instanceof Tree);
842
843
        $xref = $request->getQueryParams()['xref'];
844
845
        $repository = Repository::getInstance($xref, $tree);
846
847
        if ($repository === null) {
848
            throw new RepositoryNotFoundException();
849
        }
850
851
        $options = $this->repositoryOptions($repository);
852
853
        $title = I18N::translate('Add %s to the clippings cart', $repository->fullName());
854
855
        return $this->viewResponse('modules/clippings/add-options', [
856
            'options' => $options,
857
            'default' => key($options),
858
            'record'  => $repository,
859
            'title'   => $title,
860
            'tree'    => $tree,
861
        ]);
862
    }
863
864
    /**
865
     * @param Repository $repository
866
     *
867
     * @return string[]
868
     */
869
    private function repositoryOptions(Repository $repository): array
870
    {
871
        $name = strip_tags($repository->fullName());
872
873
        return [
874
            'self' => $name,
875
        ];
876
    }
877
878
    /**
879
     * @param ServerRequestInterface $request
880
     *
881
     * @return ResponseInterface
882
     */
883
    public function postAddRepositoryAction(ServerRequestInterface $request): ResponseInterface
884
    {
885
        $tree = $request->getAttribute('tree');
886
        assert($tree instanceof Tree);
887
888
        $xref = $request->getQueryParams()['xref'];
889
890
        $repository = Repository::getInstance($xref, $tree);
891
892
        if ($repository === null) {
893
            throw new RepositoryNotFoundException();
894
        }
895
896
        $this->addRecordToCart($repository);
897
898
        return redirect($repository->url());
899
    }
900
901
    /**
902
     * @param ServerRequestInterface $request
903
     *
904
     * @return ResponseInterface
905
     */
906
    public function getAddSourceAction(ServerRequestInterface $request): ResponseInterface
907
    {
908
        $tree = $request->getAttribute('tree');
909
        assert($tree instanceof Tree);
910
911
        $xref = $request->getQueryParams()['xref'];
912
913
        $source = Source::getInstance($xref, $tree);
914
915
        if ($source === null) {
916
            throw new SourceNotFoundException();
917
        }
918
919
        $options = $this->sourceOptions($source);
920
921
        $title = I18N::translate('Add %s to the clippings cart', $source->fullName());
922
923
        return $this->viewResponse('modules/clippings/add-options', [
924
            'options' => $options,
925
            'default' => key($options),
926
            'record'  => $source,
927
            'title'   => $title,
928
            'tree'    => $tree,
929
        ]);
930
    }
931
932
    /**
933
     * @param Source $source
934
     *
935
     * @return string[]
936
     */
937
    private function sourceOptions(Source $source): array
938
    {
939
        $name = strip_tags($source->fullName());
940
941
        return [
942
            'only'   => strip_tags($source->fullName()),
943
            'linked' => I18N::translate('%s and the individuals that reference it.', $name),
944
        ];
945
    }
946
947
    /**
948
     * @param ServerRequestInterface $request
949
     *
950
     * @return ResponseInterface
951
     */
952
    public function postAddSourceAction(ServerRequestInterface $request): ResponseInterface
953
    {
954
        $tree = $request->getAttribute('tree');
955
        assert($tree instanceof Tree);
956
957
        $xref   = $request->getQueryParams()['xref'];
958
        $option = $request->getParsedBody()['option'];
959
960
        $source = Source::getInstance($xref, $tree);
961
962
        if ($source === null) {
963
            throw new SourceNotFoundException();
964
        }
965
966
        $this->addRecordToCart($source);
967
968
        if ($option === 'linked') {
969
            foreach ($source->linkedIndividuals('SOUR') as $individual) {
970
                $this->addRecordToCart($individual);
971
            }
972
            foreach ($source->linkedFamilies('SOUR') as $family) {
973
                $this->addRecordToCart($family);
974
            }
975
        }
976
977
        return redirect($source->url());
978
    }
979
980
    /**
981
     * Get all the records in the cart.
982
     *
983
     * @param Tree $tree
984
     *
985
     * @return GedcomRecord[]
986
     */
987
    private function allRecordsInCart(Tree $tree): array
988
    {
989
        $cart = Session::get('cart', []);
990
991
        $xrefs = array_keys($cart[$tree->name()] ?? []);
992
993
        // Fetch all the records in the cart.
994
        $records = array_map(static function (string $xref) use ($tree): ?GedcomRecord {
995
            return GedcomRecord::getInstance($xref, $tree);
996
        }, $xrefs);
997
998
        // Some records may have been deleted after they were added to the cart.
999
        $records = array_filter($records);
1000
1001
        // Group and sort.
1002
        uasort($records, static function (GedcomRecord $x, GedcomRecord $y): int {
1003
            return $x::RECORD_TYPE <=> $y::RECORD_TYPE ?: GedcomRecord::nameComparator()($x, $y);
1004
        });
1005
1006
        return $records;
1007
    }
1008
1009
    /**
1010
     * Add a record (and direclty linked sources, notes, etc. to the cart.
1011
     *
1012
     * @param GedcomRecord $record
1013
     *
1014
     * @return void
1015
     */
1016
    private function addRecordToCart(GedcomRecord $record): void
1017
    {
1018
        $cart = Session::get('cart', []);
1019
1020
        $tree_name = $record->tree()->name();
1021
1022
        // Add this record
1023
        $cart[$tree_name][$record->xref()] = true;
1024
1025
        // Add directly linked media, notes, repositories and sources.
1026
        preg_match_all('/\n\d (?:OBJE|NOTE|SOUR|REPO) @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
1027
1028
        foreach ($matches[1] as $match) {
1029
            $cart[$tree_name][$match] = true;
1030
        }
1031
1032
        Session::put('cart', $cart);
1033
    }
1034
1035
    /**
1036
     * @param Tree $tree
1037
     *
1038
     * @return bool
1039
     */
1040
    private function isCartEmpty(Tree $tree): bool
1041
    {
1042
        $cart     = Session::get('cart', []);
1043
        $contents = $cart[$tree->name()] ?? [];
1044
1045
        return $contents === [];
1046
    }
1047
}
1048