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 /* I18N: Name of a module */ 69 return I18N::translate('Clippings cart'); 70 } 71 72 /** {@inheritdoc} */ 73 public function getDescription() 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() 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 * @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 private function addFamilyToCart(Family $family) 466 { 467 $this->addRecordToCart($family); 468 469 foreach ($family->getSpouses() as $spouse) { 470 $this->addRecordToCart($spouse); 471 } 472 } 473 474 /** 475 * @param Family $family 476 */ 477 private function addFamilyAndChildrenToCart(Family $family) 478 { 479 $this->addRecordToCart($family); 480 481 foreach ($family->getSpouses() as $spouse) { 482 $this->addRecordToCart($spouse); 483 } 484 foreach ($family->getChildren() as $child) { 485 $this->addRecordToCart($child); 486 } 487 } 488 489 /** 490 * @param Family $family 491 */ 492 private function addFamilyAndDescendantsToCart(Family $family) 493 { 494 $this->addRecordToCart($family); 495 496 foreach ($family->getSpouses() as $spouse) { 497 $this->addRecordToCart($spouse); 498 } 499 foreach ($family->getChildren() as $child) { 500 $this->addRecordToCart($child); 501 foreach ($child->getSpouseFamilies() as $child_family) { 502 $this->addFamilyAndDescendantsToCart($child_family); 503 } 504 } 505 } 506 507 /** 508 * @param Request $request 509 * @param Tree $tree 510 * 511 * @return Response 512 */ 513 public function getAddIndividualAction(Request $request, Tree $tree): Response 514 { 515 $xref = $request->get('xref'); 516 517 $individual = Individual::getInstance($xref, $tree); 518 519 if ($individual === null) { 520 throw new IndividualNotFoundException; 521 } 522 523 $options = $this->individualOptions($individual); 524 525 $title = I18N::translate('Add %s to the clippings cart', $individual->getFullName()); 526 527 return $this->viewResponse('modules/clippings/add-options', [ 528 'options' => $options, 529 'default' => key($options), 530 'record' => $individual, 531 'title' => $title, 532 'tree' => $tree, 533 ]); 534 } 535 536 /** 537 * @param Individual $individual 538 * 539 * @return string[] 540 */ 541 private function individualOptions(Individual $individual): array 542 { 543 $name = strip_tags($individual->getFullName()); 544 545 if ($individual->getSex() === 'F') { 546 return [ 547 'self' => $name, 548 'parents' => I18N::translate('%s, her parents and siblings', $name), 549 'spouses' => I18N::translate('%s, her spouses and children', $name), 550 'ancestors' => I18N::translate('%s and her ancestors', $name), 551 'ancestor_families' => I18N::translate('%s, her ancestors and their families', $name), 552 'descendants' => I18N::translate('%s, her spouses and descendants', $name), 553 ]; 554 } else { 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 /** 567 * @param Request $request 568 * @param Tree $tree 569 * 570 * @return RedirectResponse 571 */ 572 public function postAddIndividualAction(Request $request, Tree $tree): RedirectResponse 573 { 574 $xref = $request->get('xref'); 575 $option = $request->get('option'); 576 577 $individual = Individual::getInstance($xref, $tree); 578 579 if ($individual === null) { 580 throw new IndividualNotFoundException; 581 } 582 583 switch ($option) { 584 case 'self': 585 $this->addRecordToCart($individual); 586 break; 587 588 case 'parents': 589 foreach ($individual->getChildFamilies() as $family) { 590 $this->addFamilyAndChildrenToCart($family); 591 } 592 break; 593 594 case 'spouses': 595 foreach ($individual->getSpouseFamilies() as $family) { 596 $this->addFamilyAndChildrenToCart($family); 597 } 598 break; 599 600 case 'ancestors': 601 $this->addAncestorsToCart($individual); 602 break; 603 604 case 'ancestor_families': 605 $this->addAncestorFamiliesToCart($individual); 606 break; 607 608 case 'descendants': 609 foreach ($individual->getSpouseFamilies() as $family) { 610 $this->addFamilyAndDescendantsToCart($family); 611 } 612 break; 613 } 614 615 return new RedirectResponse($individual->url()); 616 } 617 618 /** 619 * @param Individual $individual 620 */ 621 private function addAncestorsToCart(Individual $individual) 622 { 623 $this->addRecordToCart($individual); 624 625 foreach ($individual->getChildFamilies() as $family) { 626 foreach ($family->getSpouses() as $parent) { 627 $this->addAncestorsToCart($parent); 628 } 629 } 630 } 631 632 /** 633 * @param Individual $individual 634 */ 635 private function addAncestorFamiliesToCart(Individual $individual) 636 { 637 foreach ($individual->getChildFamilies() as $family) { 638 $this->addFamilyAndChildrenToCart($family); 639 foreach ($family->getSpouses() as $parent) { 640 $this->addAncestorsToCart($parent); 641 } 642 } 643 } 644 645 /** 646 * @param Request $request 647 * @param Tree $tree 648 * 649 * @return Response 650 */ 651 public function getAddMediaAction(Request $request, Tree $tree): Response 652 { 653 $xref = $request->get('xref'); 654 655 $media = Media::getInstance($xref, $tree); 656 657 if ($media === null) { 658 throw new MediaNotFoundException; 659 } 660 661 $options = $this->mediaOptions($media); 662 663 $title = I18N::translate('Add %s to the clippings cart', $media->getFullName()); 664 665 return $this->viewResponse('modules/clippings/add-options', [ 666 'options' => $options, 667 'default' => key($options), 668 'record' => $media, 669 'title' => $title, 670 'tree' => $tree, 671 ]); 672 } 673 674 /** 675 * @param Media $media 676 * 677 * @return string[] 678 */ 679 private function mediaOptions(Media $media): array 680 { 681 $name = strip_tags($media->getFullName()); 682 683 return [ 684 'self' => $name, 685 ]; 686 } 687 688 /** 689 * @param Request $request 690 * @param Tree $tree 691 * 692 * @return RedirectResponse 693 */ 694 public function postAddMediaAction(Request $request, Tree $tree): RedirectResponse 695 { 696 $xref = $request->get('xref'); 697 698 $media = Media::getInstance($xref, $tree); 699 700 if ($media === null) { 701 throw new MediaNotFoundException; 702 } 703 704 $this->addRecordToCart($media); 705 706 return new RedirectResponse($media->url()); 707 } 708 709 /** 710 * @param Request $request 711 * @param Tree $tree 712 * 713 * @return Response 714 */ 715 public function getAddNoteAction(Request $request, Tree $tree): Response 716 { 717 $xref = $request->get('xref'); 718 719 $note = Note::getInstance($xref, $tree); 720 721 if ($note === null) { 722 throw new NoteNotFoundException; 723 } 724 725 $options = $this->noteOptions($note); 726 727 $title = I18N::translate('Add %s to the clippings cart', $note->getFullName()); 728 729 return $this->viewResponse('modules/clippings/add-options', [ 730 'options' => $options, 731 'default' => key($options), 732 'record' => $note, 733 'title' => $title, 734 'tree' => $tree, 735 ]); 736 } 737 738 /** 739 * @param Note $note 740 * 741 * @return string[] 742 */ 743 private function noteOptions(Note $note): array 744 { 745 $name = strip_tags($note->getFullName()); 746 747 return [ 748 'self' => $name, 749 ]; 750 } 751 752 /** 753 * @param Request $request 754 * @param Tree $tree 755 * 756 * @return RedirectResponse 757 */ 758 public function postAddNoteAction(Request $request, Tree $tree): RedirectResponse 759 { 760 $xref = $request->get('xref'); 761 762 $note = Note::getInstance($xref, $tree); 763 764 if ($note === null) { 765 throw new NoteNotFoundException; 766 } 767 768 $this->addRecordToCart($note); 769 770 return new RedirectResponse($note->url()); 771 } 772 773 /** 774 * @param Request $request 775 * @param Tree $tree 776 * 777 * @return Response 778 */ 779 public function getAddRepositoryAction(Request $request, Tree $tree): Response 780 { 781 $xref = $request->get('xref'); 782 783 $repository = Repository::getInstance($xref, $tree); 784 785 if ($repository === null) { 786 throw new RepositoryNotFoundException; 787 } 788 789 $options = $this->repositoryOptions($repository); 790 791 $title = I18N::translate('Add %s to the clippings cart', $repository->getFullName()); 792 793 return $this->viewResponse('modules/clippings/add-options', [ 794 'options' => $options, 795 'default' => key($options), 796 'record' => $repository, 797 'title' => $title, 798 'tree' => $tree, 799 ]); 800 } 801 802 /** 803 * @param Repository $repository 804 * 805 * @return string[] 806 */ 807 private function repositoryOptions(Repository $repository): array 808 { 809 $name = strip_tags($repository->getFullName()); 810 811 return [ 812 'self' => $name, 813 ]; 814 } 815 816 /** 817 * @param Request $request 818 * @param Tree $tree 819 * 820 * @return RedirectResponse 821 */ 822 public function postAddRepositoryAction(Request $request, Tree $tree): RedirectResponse 823 { 824 $xref = $request->get('xref'); 825 826 $repository = Repository::getInstance($xref, $tree); 827 828 if ($repository === null) { 829 throw new RepositoryNotFoundException; 830 } 831 832 $this->addRecordToCart($repository); 833 834 return new RedirectResponse($repository->url()); 835 } 836 837 /** 838 * @param Request $request 839 * @param Tree $tree 840 * 841 * @return Response 842 */ 843 public function getAddSourceAction(Request $request, Tree $tree): Response 844 { 845 $xref = $request->get('xref'); 846 847 $source = Source::getInstance($xref, $tree); 848 849 if ($source === null) { 850 throw new SourceNotFoundException; 851 } 852 853 $options = $this->sourceOptions($source); 854 855 $title = I18N::translate('Add %s to the clippings cart', $source->getFullName()); 856 857 return $this->viewResponse('modules/clippings/add-options', [ 858 'options' => $options, 859 'default' => key($options), 860 'record' => $source, 861 'title' => $title, 862 'tree' => $tree, 863 ]); 864 } 865 866 /** 867 * @param Source $source 868 * 869 * @return string[] 870 */ 871 private function sourceOptions(Source $source): array 872 { 873 $name = strip_tags($source->getFullName()); 874 875 return [ 876 'only' => strip_tags($source->getFullName()), 877 'linked' => I18N::translate('%s and the individuals that reference it.', $name), 878 ]; 879 } 880 881 /** 882 * @param Request $request 883 * @param Tree $tree 884 * 885 * @return RedirectResponse 886 */ 887 public function postAddSourceAction(Request $request, Tree $tree): RedirectResponse 888 { 889 $xref = $request->get('xref'); 890 $option = $request->get('option'); 891 892 $source = Source::getInstance($xref, $tree); 893 894 if ($source === null) { 895 throw new SourceNotFoundException; 896 } 897 898 $this->addRecordToCart($source); 899 900 if ($option === 'linked') { 901 foreach ($source->linkedIndividuals('SOUR') as $individual) { 902 $this->addRecordToCart($individual); 903 } 904 foreach ($source->linkedFamilies('SOUR') as $family) { 905 $this->addRecordToCart($family); 906 } 907 } 908 909 return new RedirectResponse($source->url()); 910 } 911 912 /** 913 * Get all the records in the cart. 914 * 915 * @param Tree $tree 916 * 917 * @return GedcomRecord[] 918 */ 919 private function allRecordsInCart(Tree $tree): array 920 { 921 $cart = Session::get('cart', []); 922 923 $xrefs = array_keys($cart[$tree->getName()] ?? []); 924 925 // Fetch all the records in the cart. 926 $records = array_map(function (string $xref) use ($tree) { 927 return GedcomRecord::getInstance($xref, $tree); 928 }, $xrefs); 929 930 // Some records may have been deleted after they were added to the cart. 931 $records = array_filter($records); 932 933 // Group and sort. 934 uasort($records, function (GedcomRecord $x, GedcomRecord $y) { 935 return $x::RECORD_TYPE <=> $y::RECORD_TYPE ?: GedcomRecord::compare($x, $y); 936 }); 937 938 return $records; 939 } 940 941 /** 942 * Add a record (and direclty linked sources, notes, etc. to the cart. 943 * 944 * @param GedcomRecord $record 945 */ 946 private function addRecordToCart(GedcomRecord $record) 947 { 948 $cart = Session::get('cart', []); 949 950 $tree_name = $record->getTree()->getName(); 951 952 // Add this record 953 $cart[$tree_name][$record->getXref()] = true; 954 955 // Add directly linked media, notes, repositories and sources. 956 preg_match_all('/\n\d (?:OBJE|NOTE|SOUR|REPO) @(' . WT_REGEX_XREF . ')@/', $record->getGedcom(), $matches); 957 958 foreach ($matches[1] as $match) { 959 $cart[$tree_name][$match] = true; 960 } 961 962 Session::put('cart', $cart); 963 } 964 965 /** 966 * @param Tree $tree 967 * 968 * @return bool 969 */ 970 private function isCartEmpty(Tree $tree): bool 971 { 972 $cart = Session::get('cart', []); 973 974 return empty($cart[$tree->getName()]); 975 } 976 977 /** 978 * Only allow access to the routes/functions if the menu is active 979 * 980 * @param Tree $tree 981 */ 982 private function checkModuleAccess(Tree $tree) 983 { 984 if (!array_key_exists($this->getName(), Module::getActiveMenus($tree))) { 985 throw new NotFoundHttpException; 986 } 987 } 988} 989