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