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