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