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