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