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