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