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