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