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