1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2021 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Module; 21 22use Aura\Router\Route; 23use Fisharebest\Webtrees\Auth; 24use Fisharebest\Webtrees\Family; 25use Fisharebest\Webtrees\Gedcom; 26use Fisharebest\Webtrees\GedcomRecord; 27use Fisharebest\Webtrees\Http\RequestHandlers\FamilyPage; 28use Fisharebest\Webtrees\Http\RequestHandlers\IndividualPage; 29use Fisharebest\Webtrees\Http\RequestHandlers\LocationPage; 30use Fisharebest\Webtrees\Http\RequestHandlers\MediaPage; 31use Fisharebest\Webtrees\Http\RequestHandlers\NotePage; 32use Fisharebest\Webtrees\Http\RequestHandlers\RepositoryPage; 33use Fisharebest\Webtrees\Http\RequestHandlers\SourcePage; 34use Fisharebest\Webtrees\Http\RequestHandlers\SubmitterPage; 35use Fisharebest\Webtrees\I18N; 36use Fisharebest\Webtrees\Individual; 37use Fisharebest\Webtrees\Location; 38use Fisharebest\Webtrees\Media; 39use Fisharebest\Webtrees\Menu; 40use Fisharebest\Webtrees\Note; 41use Fisharebest\Webtrees\Registry; 42use Fisharebest\Webtrees\Repository; 43use Fisharebest\Webtrees\Services\GedcomExportService; 44use Fisharebest\Webtrees\Services\UserService; 45use Fisharebest\Webtrees\Session; 46use Fisharebest\Webtrees\Source; 47use Fisharebest\Webtrees\Submitter; 48use Fisharebest\Webtrees\Tree; 49use Illuminate\Support\Collection; 50use League\Flysystem\Filesystem; 51use League\Flysystem\FilesystemException; 52use League\Flysystem\ZipArchive\FilesystemZipArchiveProvider; 53use League\Flysystem\ZipArchive\ZipArchiveAdapter; 54use Psr\Http\Message\ResponseFactoryInterface; 55use Psr\Http\Message\ResponseInterface; 56use Psr\Http\Message\ServerRequestInterface; 57use Psr\Http\Message\StreamFactoryInterface; 58use RuntimeException; 59 60use function app; 61use function array_filter; 62use function array_keys; 63use function array_map; 64use function array_search; 65use function assert; 66use function fclose; 67use function in_array; 68use function is_string; 69use function preg_match_all; 70use function redirect; 71use function route; 72use function str_replace; 73use function stream_get_meta_data; 74use function tmpfile; 75use function uasort; 76use function view; 77 78use const PREG_SET_ORDER; 79 80/** 81 * Class ClippingsCartModule 82 */ 83class ClippingsCartModule extends AbstractModule implements ModuleMenuInterface 84{ 85 use ModuleMenuTrait; 86 87 // What to add to the cart? 88 private const ADD_RECORD_ONLY = 'record'; 89 private const ADD_CHILDREN = 'children'; 90 private const ADD_DESCENDANTS = 'descendants'; 91 private const ADD_PARENT_FAMILIES = 'parents'; 92 private const ADD_SPOUSE_FAMILIES = 'spouses'; 93 private const ADD_ANCESTORS = 'ancestors'; 94 private const ADD_ANCESTOR_FAMILIES = 'families'; 95 private const ADD_LINKED_INDIVIDUALS = 'linked'; 96 97 // Routes that have a record which can be added to the clipboard 98 private const ROUTES_WITH_RECORDS = [ 99 'Family' => FamilyPage::class, 100 'Individual' => IndividualPage::class, 101 'Media' => MediaPage::class, 102 'Location' => LocationPage::class, 103 'Note' => NotePage::class, 104 'Repository' => RepositoryPage::class, 105 'Source' => SourcePage::class, 106 'Submitter' => SubmitterPage::class, 107 ]; 108 109 /** @var int The default access level for this module. It can be changed in the control panel. */ 110 protected $access_level = Auth::PRIV_USER; 111 112 private GedcomExportService $gedcom_export_service; 113 114 private UserService $user_service; 115 116 private ResponseFactoryInterface $response_factory; 117 118 private StreamFactoryInterface $stream_factory; 119 120 /** 121 * ClippingsCartModule constructor. 122 * 123 * @param GedcomExportService $gedcom_export_service 124 * @param ResponseFactoryInterface $response_factory 125 * @param StreamFactoryInterface $stream_factory 126 * @param UserService $user_service 127 */ 128 public function __construct( 129 GedcomExportService $gedcom_export_service, 130 ResponseFactoryInterface $response_factory, 131 StreamFactoryInterface $stream_factory, 132 UserService $user_service 133 ) { 134 $this->gedcom_export_service = $gedcom_export_service; 135 $this->response_factory = $response_factory; 136 $this->stream_factory = $stream_factory; 137 $this->user_service = $user_service; 138 } 139 140 /** 141 * A sentence describing what this module does. 142 * 143 * @return string 144 */ 145 public function description(): string 146 { 147 /* I18N: Description of the “Clippings cart” module */ 148 return I18N::translate('Select records from your family tree and save them as a GEDCOM file.'); 149 } 150 151 /** 152 * The default position for this menu. It can be changed in the control panel. 153 * 154 * @return int 155 */ 156 public function defaultMenuOrder(): int 157 { 158 return 6; 159 } 160 161 /** 162 * A menu, to be added to the main application menu. 163 * 164 * @param Tree $tree 165 * 166 * @return Menu|null 167 */ 168 public function getMenu(Tree $tree): ?Menu 169 { 170 /** @var ServerRequestInterface $request */ 171 $request = app(ServerRequestInterface::class); 172 173 $route = $request->getAttribute('route'); 174 assert($route instanceof Route); 175 176 $cart = Session::get('cart', []); 177 $count = count($cart[$tree->name()] ?? []); 178 $badge = view('components/badge', ['count' => $count]); 179 180 $submenus = [ 181 new Menu($this->title() . ' ' . $badge, route('module', [ 182 'module' => $this->name(), 183 'action' => 'Show', 184 'tree' => $tree->name(), 185 ]), 'menu-clippings-cart', ['rel' => 'nofollow']), 186 ]; 187 188 $action = array_search($route->name, self::ROUTES_WITH_RECORDS, true); 189 if ($action !== false) { 190 $xref = $route->attributes['xref']; 191 assert(is_string($xref)); 192 193 $add_route = route('module', [ 194 'module' => $this->name(), 195 'action' => 'Add' . $action, 196 'xref' => $xref, 197 'tree' => $tree->name(), 198 ]); 199 200 $submenus[] = new Menu(I18N::translate('Add to the clippings cart'), $add_route, 'menu-clippings-add', ['rel' => 'nofollow']); 201 } 202 203 if (!$this->isCartEmpty($tree)) { 204 $submenus[] = new Menu(I18N::translate('Empty the clippings cart'), route('module', [ 205 'module' => $this->name(), 206 'action' => 'Empty', 207 'tree' => $tree->name(), 208 ]), 'menu-clippings-empty', ['rel' => 'nofollow']); 209 210 $submenus[] = new Menu(I18N::translate('Download'), route('module', [ 211 'module' => $this->name(), 212 'action' => 'DownloadForm', 213 'tree' => $tree->name(), 214 ]), 'menu-clippings-download', ['rel' => 'nofollow']); 215 } 216 217 return new Menu($this->title(), '#', 'menu-clippings', ['rel' => 'nofollow'], $submenus); 218 } 219 220 /** 221 * How should this module be identified in the control panel, etc.? 222 * 223 * @return string 224 */ 225 public function title(): string 226 { 227 /* I18N: Name of a module */ 228 return I18N::translate('Clippings cart'); 229 } 230 231 /** 232 * @param Tree $tree 233 * 234 * @return bool 235 */ 236 private function isCartEmpty(Tree $tree): bool 237 { 238 $cart = Session::get('cart', []); 239 $contents = $cart[$tree->name()] ?? []; 240 241 return $contents === []; 242 } 243 244 /** 245 * @param ServerRequestInterface $request 246 * 247 * @return ResponseInterface 248 */ 249 public function getDownloadFormAction(ServerRequestInterface $request): ResponseInterface 250 { 251 $tree = $request->getAttribute('tree'); 252 assert($tree instanceof Tree); 253 254 $user = $request->getAttribute('user'); 255 $title = I18N::translate('Family tree clippings cart') . ' — ' . I18N::translate('Download'); 256 257 return $this->viewResponse('modules/clippings/download', [ 258 'is_manager' => Auth::isManager($tree, $user), 259 'is_member' => Auth::isMember($tree, $user), 260 'module' => $this->name(), 261 'title' => $title, 262 'tree' => $tree, 263 ]); 264 } 265 266 /** 267 * @param ServerRequestInterface $request 268 * 269 * @return ResponseInterface 270 * @throws FilesystemException 271 */ 272 public function postDownloadAction(ServerRequestInterface $request): ResponseInterface 273 { 274 $tree = $request->getAttribute('tree'); 275 assert($tree instanceof Tree); 276 277 $data_filesystem = Registry::filesystem()->data(); 278 279 $params = (array) $request->getParsedBody(); 280 281 $privatize_export = $params['privatize_export'] ?? 'none'; 282 283 if ($privatize_export === 'none' && !Auth::isManager($tree)) { 284 $privatize_export = 'member'; 285 } 286 287 if ($privatize_export === 'gedadmin' && !Auth::isManager($tree)) { 288 $privatize_export = 'member'; 289 } 290 291 if ($privatize_export === 'user' && !Auth::isMember($tree)) { 292 $privatize_export = 'visitor'; 293 } 294 295 $convert = (bool) ($params['convert'] ?? false); 296 297 $cart = Session::get('cart', []); 298 299 $xrefs = array_keys($cart[$tree->name()] ?? []); 300 $xrefs = array_map('strval', $xrefs); // PHP converts numeric keys to integers. 301 302 // Create a new/empty .ZIP file 303 $temp_zip_file = stream_get_meta_data(tmpfile())['uri']; 304 $zip_provider = new FilesystemZipArchiveProvider($temp_zip_file, 0755); 305 $zip_adapter = new ZipArchiveAdapter($zip_provider); 306 $zip_filesystem = new Filesystem($zip_adapter); 307 308 $media_filesystem = $tree->mediaFilesystem($data_filesystem); 309 310 // Media file prefix 311 $path = $tree->getPreference('MEDIA_DIRECTORY'); 312 313 $encoding = $convert ? 'ANSI' : 'UTF-8'; 314 315 $records = new Collection(); 316 317 switch ($privatize_export) { 318 case 'gedadmin': 319 $access_level = Auth::PRIV_NONE; 320 break; 321 case 'user': 322 $access_level = Auth::PRIV_USER; 323 break; 324 case 'visitor': 325 $access_level = Auth::PRIV_PRIVATE; 326 break; 327 case 'none': 328 default: 329 $access_level = Auth::PRIV_HIDE; 330 break; 331 } 332 333 foreach ($xrefs as $xref) { 334 $object = Registry::gedcomRecordFactory()->make($xref, $tree); 335 // The object may have been deleted since we added it to the cart.... 336 if ($object instanceof GedcomRecord) { 337 $record = $object->privatizeGedcom($access_level); 338 // Remove links to objects that aren't in the cart 339 preg_match_all('/\n1 ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@(\n[2-9].*)*/', $record, $matches, PREG_SET_ORDER); 340 foreach ($matches as $match) { 341 if (!in_array($match[1], $xrefs, true)) { 342 $record = str_replace($match[0], '', $record); 343 } 344 } 345 preg_match_all('/\n2 ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@(\n[3-9].*)*/', $record, $matches, PREG_SET_ORDER); 346 foreach ($matches as $match) { 347 if (!in_array($match[1], $xrefs, true)) { 348 $record = str_replace($match[0], '', $record); 349 } 350 } 351 preg_match_all('/\n3 ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@(\n[4-9].*)*/', $record, $matches, PREG_SET_ORDER); 352 foreach ($matches as $match) { 353 if (!in_array($match[1], $xrefs, true)) { 354 $record = str_replace($match[0], '', $record); 355 } 356 } 357 358 $records->add($record); 359 360 if ($object instanceof Media) { 361 // Add the media files to the archive 362 foreach ($object->mediaFiles() as $media_file) { 363 $from = $media_file->filename(); 364 $to = $path . $media_file->filename(); 365 if (!$media_file->isExternal() && $media_filesystem->fileExists($from)) { 366 $zip_filesystem->writeStream($to, $media_filesystem->readStream($from)); 367 } 368 } 369 } 370 } 371 } 372 373 // We have already applied privacy filtering, so do not do it again. 374 $resource = $this->gedcom_export_service->export($tree, false, $encoding, Auth::PRIV_HIDE, $path, $records); 375 376 // Finally add the GEDCOM file to the .ZIP file. 377 $zip_filesystem->writeStream('clippings.ged', $resource); 378 fclose($resource); 379 380 // Use a stream, so that we do not have to load the entire file into memory. 381 $resource = $this->stream_factory->createStreamFromFile($temp_zip_file); 382 383 return $this->response_factory->createResponse() 384 ->withBody($resource) 385 ->withHeader('Content-Type', 'application/zip') 386 ->withHeader('Content-Disposition', 'attachment; filename="clippings.zip'); 387 } 388 389 /** 390 * @param ServerRequestInterface $request 391 * 392 * @return ResponseInterface 393 */ 394 public function getEmptyAction(ServerRequestInterface $request): ResponseInterface 395 { 396 $tree = $request->getAttribute('tree'); 397 assert($tree instanceof Tree); 398 399 $cart = Session::get('cart', []); 400 $cart[$tree->name()] = []; 401 Session::put('cart', $cart); 402 403 $url = route('module', [ 404 'module' => $this->name(), 405 'action' => 'Show', 406 'tree' => $tree->name(), 407 ]); 408 409 return redirect($url); 410 } 411 412 /** 413 * @param ServerRequestInterface $request 414 * 415 * @return ResponseInterface 416 */ 417 public function postRemoveAction(ServerRequestInterface $request): ResponseInterface 418 { 419 $tree = $request->getAttribute('tree'); 420 assert($tree instanceof Tree); 421 422 $xref = $request->getQueryParams()['xref'] ?? ''; 423 424 $cart = Session::get('cart', []); 425 unset($cart[$tree->name()][$xref]); 426 Session::put('cart', $cart); 427 428 $url = route('module', [ 429 'module' => $this->name(), 430 'action' => 'Show', 431 'tree' => $tree->name(), 432 ]); 433 434 return redirect($url); 435 } 436 437 /** 438 * @param ServerRequestInterface $request 439 * 440 * @return ResponseInterface 441 */ 442 public function getShowAction(ServerRequestInterface $request): ResponseInterface 443 { 444 $tree = $request->getAttribute('tree'); 445 assert($tree instanceof Tree); 446 447 return $this->viewResponse('modules/clippings/show', [ 448 'module' => $this->name(), 449 'records' => $this->allRecordsInCart($tree), 450 'title' => I18N::translate('Family tree clippings cart'), 451 'tree' => $tree, 452 ]); 453 } 454 455 /** 456 * Get all the records in the cart. 457 * 458 * @param Tree $tree 459 * 460 * @return GedcomRecord[] 461 */ 462 private function allRecordsInCart(Tree $tree): array 463 { 464 $cart = Session::get('cart', []); 465 466 $xrefs = array_keys($cart[$tree->name()] ?? []); 467 $xrefs = array_map('strval', $xrefs); // PHP converts numeric keys to integers. 468 469 // Fetch all the records in the cart. 470 $records = array_map(static function (string $xref) use ($tree): ?GedcomRecord { 471 return Registry::gedcomRecordFactory()->make($xref, $tree); 472 }, $xrefs); 473 474 // Some records may have been deleted after they were added to the cart. 475 $records = array_filter($records); 476 477 // Group and sort. 478 uasort($records, static function (GedcomRecord $x, GedcomRecord $y): int { 479 return $x->tag() <=> $y->tag() ?: GedcomRecord::nameComparator()($x, $y); 480 }); 481 482 return $records; 483 } 484 485 /** 486 * @param ServerRequestInterface $request 487 * 488 * @return ResponseInterface 489 */ 490 public function getAddFamilyAction(ServerRequestInterface $request): ResponseInterface 491 { 492 $tree = $request->getAttribute('tree'); 493 assert($tree instanceof Tree); 494 495 $xref = $request->getQueryParams()['xref'] ?? ''; 496 497 $family = Registry::familyFactory()->make($xref, $tree); 498 $family = Auth::checkFamilyAccess($family); 499 $name = $family->fullName(); 500 501 $options = [ 502 self::ADD_RECORD_ONLY => $name, 503 /* I18N: %s is a family (husband + wife) */ 504 self::ADD_CHILDREN => I18N::translate('%s and their children', $name), 505 /* I18N: %s is a family (husband + wife) */ 506 self::ADD_DESCENDANTS => I18N::translate('%s and their descendants', $name), 507 ]; 508 509 $title = I18N::translate('Add %s to the clippings cart', $name); 510 511 return $this->viewResponse('modules/clippings/add-options', [ 512 'options' => $options, 513 'record' => $family, 514 'title' => $title, 515 'tree' => $tree, 516 ]); 517 } 518 519 /** 520 * @param ServerRequestInterface $request 521 * 522 * @return ResponseInterface 523 */ 524 public function postAddFamilyAction(ServerRequestInterface $request): ResponseInterface 525 { 526 $tree = $request->getAttribute('tree'); 527 assert($tree instanceof Tree); 528 529 $params = (array) $request->getParsedBody(); 530 531 $xref = $params['xref'] ?? ''; 532 $option = $params['option'] ?? ''; 533 534 $family = Registry::familyFactory()->make($xref, $tree); 535 $family = Auth::checkFamilyAccess($family); 536 537 switch ($option) { 538 case self::ADD_RECORD_ONLY: 539 $this->addFamilyToCart($family); 540 break; 541 542 case self::ADD_CHILDREN: 543 $this->addFamilyAndChildrenToCart($family); 544 break; 545 546 case self::ADD_DESCENDANTS: 547 $this->addFamilyAndDescendantsToCart($family); 548 break; 549 } 550 551 return redirect($family->url()); 552 } 553 554 555 /** 556 * @param Family $family 557 * 558 * @return void 559 */ 560 protected function addFamilyAndChildrenToCart(Family $family): void 561 { 562 $this->addFamilyToCart($family); 563 564 foreach ($family->children() as $child) { 565 $this->addIndividualToCart($child); 566 } 567 } 568 569 /** 570 * @param Family $family 571 * 572 * @return void 573 */ 574 protected function addFamilyAndDescendantsToCart(Family $family): void 575 { 576 $this->addFamilyAndChildrenToCart($family); 577 578 foreach ($family->children() as $child) { 579 foreach ($child->spouseFamilies() as $child_family) { 580 $this->addFamilyAndDescendantsToCart($child_family); 581 } 582 } 583 } 584 585 /** 586 * @param ServerRequestInterface $request 587 * 588 * @return ResponseInterface 589 */ 590 public function getAddIndividualAction(ServerRequestInterface $request): ResponseInterface 591 { 592 $tree = $request->getAttribute('tree'); 593 assert($tree instanceof Tree); 594 595 $xref = $request->getQueryParams()['xref'] ?? ''; 596 597 $individual = Registry::individualFactory()->make($xref, $tree); 598 $individual = Auth::checkIndividualAccess($individual); 599 $name = $individual->fullName(); 600 601 if ($individual->sex() === 'F') { 602 $options = [ 603 self::ADD_RECORD_ONLY => $name, 604 self::ADD_PARENT_FAMILIES => I18N::translate('%s, her parents and siblings', $name), 605 self::ADD_SPOUSE_FAMILIES => I18N::translate('%s, her spouses and children', $name), 606 self::ADD_ANCESTORS => I18N::translate('%s and her ancestors', $name), 607 self::ADD_ANCESTOR_FAMILIES => I18N::translate('%s, her ancestors and their families', $name), 608 self::ADD_DESCENDANTS => I18N::translate('%s, her spouses and descendants', $name), 609 ]; 610 } else { 611 $options = [ 612 self::ADD_RECORD_ONLY => $name, 613 self::ADD_PARENT_FAMILIES => I18N::translate('%s, his parents and siblings', $name), 614 self::ADD_SPOUSE_FAMILIES => I18N::translate('%s, his spouses and children', $name), 615 self::ADD_ANCESTORS => I18N::translate('%s and his ancestors', $name), 616 self::ADD_ANCESTOR_FAMILIES => I18N::translate('%s, his ancestors and their families', $name), 617 self::ADD_DESCENDANTS => I18N::translate('%s, his spouses and descendants', $name), 618 ]; 619 } 620 621 $title = I18N::translate('Add %s to the clippings cart', $name); 622 623 return $this->viewResponse('modules/clippings/add-options', [ 624 'options' => $options, 625 'record' => $individual, 626 'title' => $title, 627 'tree' => $tree, 628 ]); 629 } 630 631 /** 632 * @param ServerRequestInterface $request 633 * 634 * @return ResponseInterface 635 */ 636 public function postAddIndividualAction(ServerRequestInterface $request): ResponseInterface 637 { 638 $tree = $request->getAttribute('tree'); 639 assert($tree instanceof Tree); 640 641 $params = (array) $request->getParsedBody(); 642 643 $xref = $params['xref'] ?? ''; 644 $option = $params['option'] ?? ''; 645 646 $individual = Registry::individualFactory()->make($xref, $tree); 647 $individual = Auth::checkIndividualAccess($individual); 648 649 switch ($option) { 650 case self::ADD_RECORD_ONLY: 651 $this->addIndividualToCart($individual); 652 break; 653 654 case self::ADD_PARENT_FAMILIES: 655 foreach ($individual->childFamilies() as $family) { 656 $this->addFamilyAndChildrenToCart($family); 657 } 658 break; 659 660 case self::ADD_SPOUSE_FAMILIES: 661 foreach ($individual->spouseFamilies() as $family) { 662 $this->addFamilyAndChildrenToCart($family); 663 } 664 break; 665 666 case self::ADD_ANCESTORS: 667 $this->addAncestorsToCart($individual); 668 break; 669 670 case self::ADD_ANCESTOR_FAMILIES: 671 $this->addAncestorFamiliesToCart($individual); 672 break; 673 674 case self::ADD_DESCENDANTS: 675 foreach ($individual->spouseFamilies() as $family) { 676 $this->addFamilyAndDescendantsToCart($family); 677 } 678 break; 679 } 680 681 return redirect($individual->url()); 682 } 683 684 /** 685 * @param Individual $individual 686 * 687 * @return void 688 */ 689 protected function addAncestorsToCart(Individual $individual): void 690 { 691 $this->addIndividualToCart($individual); 692 693 foreach ($individual->childFamilies() as $family) { 694 $this->addFamilyToCart($family); 695 696 foreach ($family->spouses() as $parent) { 697 $this->addAncestorsToCart($parent); 698 } 699 } 700 } 701 702 /** 703 * @param Individual $individual 704 * 705 * @return void 706 */ 707 protected function addAncestorFamiliesToCart(Individual $individual): void 708 { 709 foreach ($individual->childFamilies() as $family) { 710 $this->addFamilyAndChildrenToCart($family); 711 712 foreach ($family->spouses() as $parent) { 713 $this->addAncestorFamiliesToCart($parent); 714 } 715 } 716 } 717 718 /** 719 * @param ServerRequestInterface $request 720 * 721 * @return ResponseInterface 722 */ 723 public function getAddLocationAction(ServerRequestInterface $request): ResponseInterface 724 { 725 $tree = $request->getAttribute('tree'); 726 assert($tree instanceof Tree); 727 728 $xref = $request->getQueryParams()['xref'] ?? ''; 729 730 $location = Registry::locationFactory()->make($xref, $tree); 731 $location = Auth::checkLocationAccess($location); 732 $name = $location->fullName(); 733 734 $options = [ 735 self::ADD_RECORD_ONLY => $name, 736 ]; 737 738 $title = I18N::translate('Add %s to the clippings cart', $name); 739 740 return $this->viewResponse('modules/clippings/add-options', [ 741 'options' => $options, 742 'record' => $location, 743 'title' => $title, 744 'tree' => $tree, 745 ]); 746 } 747 748 /** 749 * @param ServerRequestInterface $request 750 * 751 * @return ResponseInterface 752 */ 753 public function postAddLocationAction(ServerRequestInterface $request): ResponseInterface 754 { 755 $tree = $request->getAttribute('tree'); 756 assert($tree instanceof Tree); 757 758 $xref = $request->getQueryParams()['xref'] ?? ''; 759 760 $location = Registry::locationFactory()->make($xref, $tree); 761 $location = Auth::checkLocationAccess($location); 762 763 $this->addLocationToCart($location); 764 765 return redirect($location->url()); 766 } 767 768 /** 769 * @param ServerRequestInterface $request 770 * 771 * @return ResponseInterface 772 */ 773 public function getAddMediaAction(ServerRequestInterface $request): ResponseInterface 774 { 775 $tree = $request->getAttribute('tree'); 776 assert($tree instanceof Tree); 777 778 $xref = $request->getQueryParams()['xref'] ?? ''; 779 780 $media = Registry::mediaFactory()->make($xref, $tree); 781 $media = Auth::checkMediaAccess($media); 782 $name = $media->fullName(); 783 784 $options = [ 785 self::ADD_RECORD_ONLY => $name, 786 ]; 787 788 $title = I18N::translate('Add %s to the clippings cart', $name); 789 790 return $this->viewResponse('modules/clippings/add-options', [ 791 'options' => $options, 792 'record' => $media, 793 'title' => $title, 794 'tree' => $tree, 795 ]); 796 } 797 798 /** 799 * @param ServerRequestInterface $request 800 * 801 * @return ResponseInterface 802 */ 803 public function postAddMediaAction(ServerRequestInterface $request): ResponseInterface 804 { 805 $tree = $request->getAttribute('tree'); 806 assert($tree instanceof Tree); 807 808 $xref = $request->getQueryParams()['xref'] ?? ''; 809 810 $media = Registry::mediaFactory()->make($xref, $tree); 811 $media = Auth::checkMediaAccess($media); 812 813 $this->addMediaToCart($media); 814 815 return redirect($media->url()); 816 } 817 818 /** 819 * @param ServerRequestInterface $request 820 * 821 * @return ResponseInterface 822 */ 823 public function getAddNoteAction(ServerRequestInterface $request): ResponseInterface 824 { 825 $tree = $request->getAttribute('tree'); 826 assert($tree instanceof Tree); 827 828 $xref = $request->getQueryParams()['xref'] ?? ''; 829 830 $note = Registry::noteFactory()->make($xref, $tree); 831 $note = Auth::checkNoteAccess($note); 832 $name = $note->fullName(); 833 834 $options = [ 835 self::ADD_RECORD_ONLY => $name, 836 ]; 837 838 $title = I18N::translate('Add %s to the clippings cart', $name); 839 840 return $this->viewResponse('modules/clippings/add-options', [ 841 'options' => $options, 842 'record' => $note, 843 'title' => $title, 844 'tree' => $tree, 845 ]); 846 } 847 848 /** 849 * @param ServerRequestInterface $request 850 * 851 * @return ResponseInterface 852 */ 853 public function postAddNoteAction(ServerRequestInterface $request): ResponseInterface 854 { 855 $tree = $request->getAttribute('tree'); 856 assert($tree instanceof Tree); 857 858 $xref = $request->getQueryParams()['xref'] ?? ''; 859 860 $note = Registry::noteFactory()->make($xref, $tree); 861 $note = Auth::checkNoteAccess($note); 862 863 $this->addNoteToCart($note); 864 865 return redirect($note->url()); 866 } 867 868 /** 869 * @param ServerRequestInterface $request 870 * 871 * @return ResponseInterface 872 */ 873 public function getAddRepositoryAction(ServerRequestInterface $request): ResponseInterface 874 { 875 $tree = $request->getAttribute('tree'); 876 assert($tree instanceof Tree); 877 878 $xref = $request->getQueryParams()['xref'] ?? ''; 879 880 $repository = Registry::repositoryFactory()->make($xref, $tree); 881 $repository = Auth::checkRepositoryAccess($repository); 882 $name = $repository->fullName(); 883 884 $options = [ 885 self::ADD_RECORD_ONLY => $name, 886 ]; 887 888 $title = I18N::translate('Add %s to the clippings cart', $name); 889 890 return $this->viewResponse('modules/clippings/add-options', [ 891 'options' => $options, 892 'record' => $repository, 893 'title' => $title, 894 'tree' => $tree, 895 ]); 896 } 897 898 /** 899 * @param ServerRequestInterface $request 900 * 901 * @return ResponseInterface 902 */ 903 public function postAddRepositoryAction(ServerRequestInterface $request): ResponseInterface 904 { 905 $tree = $request->getAttribute('tree'); 906 assert($tree instanceof Tree); 907 908 $xref = $request->getQueryParams()['xref'] ?? ''; 909 910 $repository = Registry::repositoryFactory()->make($xref, $tree); 911 $repository = Auth::checkRepositoryAccess($repository); 912 913 $this->addRepositoryToCart($repository); 914 915 foreach ($repository->linkedSources('REPO') as $source) { 916 $this->addSourceToCart($source); 917 } 918 919 return redirect($repository->url()); 920 } 921 922 /** 923 * @param ServerRequestInterface $request 924 * 925 * @return ResponseInterface 926 */ 927 public function getAddSourceAction(ServerRequestInterface $request): ResponseInterface 928 { 929 $tree = $request->getAttribute('tree'); 930 assert($tree instanceof Tree); 931 932 $xref = $request->getQueryParams()['xref'] ?? ''; 933 934 $source = Registry::sourceFactory()->make($xref, $tree); 935 $source = Auth::checkSourceAccess($source); 936 $name = $source->fullName(); 937 938 $options = [ 939 self::ADD_RECORD_ONLY => $name, 940 self::ADD_LINKED_INDIVIDUALS => I18N::translate('%s and the individuals that reference it.', $name), 941 ]; 942 943 $title = I18N::translate('Add %s to the clippings cart', $name); 944 945 return $this->viewResponse('modules/clippings/add-options', [ 946 'options' => $options, 947 'record' => $source, 948 'title' => $title, 949 'tree' => $tree, 950 ]); 951 } 952 953 /** 954 * @param ServerRequestInterface $request 955 * 956 * @return ResponseInterface 957 */ 958 public function postAddSourceAction(ServerRequestInterface $request): ResponseInterface 959 { 960 $tree = $request->getAttribute('tree'); 961 assert($tree instanceof Tree); 962 963 $params = (array) $request->getParsedBody(); 964 965 $xref = $params['xref'] ?? ''; 966 $option = $params['option'] ?? ''; 967 968 $source = Registry::sourceFactory()->make($xref, $tree); 969 $source = Auth::checkSourceAccess($source); 970 971 $this->addSourceToCart($source); 972 973 if ($option === self::ADD_LINKED_INDIVIDUALS) { 974 foreach ($source->linkedIndividuals('SOUR') as $individual) { 975 $this->addIndividualToCart($individual); 976 } 977 foreach ($source->linkedFamilies('SOUR') as $family) { 978 $this->addFamilyToCart($family); 979 } 980 } 981 982 return redirect($source->url()); 983 } 984 985 /** 986 * @param ServerRequestInterface $request 987 * 988 * @return ResponseInterface 989 */ 990 public function getAddSubmitterAction(ServerRequestInterface $request): ResponseInterface 991 { 992 $tree = $request->getAttribute('tree'); 993 assert($tree instanceof Tree); 994 995 $xref = $request->getQueryParams()['xref'] ?? ''; 996 997 $submitter = Registry::submitterFactory()->make($xref, $tree); 998 $submitter = Auth::checkSubmitterAccess($submitter); 999 $name = $submitter->fullName(); 1000 1001 $options = [ 1002 self::ADD_RECORD_ONLY => $name, 1003 ]; 1004 1005 $title = I18N::translate('Add %s to the clippings cart', $name); 1006 1007 return $this->viewResponse('modules/clippings/add-options', [ 1008 'options' => $options, 1009 'record' => $submitter, 1010 'title' => $title, 1011 'tree' => $tree, 1012 ]); 1013 } 1014 1015 /** 1016 * @param ServerRequestInterface $request 1017 * 1018 * @return ResponseInterface 1019 */ 1020 public function postAddSubmitterAction(ServerRequestInterface $request): ResponseInterface 1021 { 1022 $tree = $request->getAttribute('tree'); 1023 assert($tree instanceof Tree); 1024 1025 $xref = $request->getQueryParams()['xref'] ?? ''; 1026 1027 $submitter = Registry::submitterFactory()->make($xref, $tree); 1028 $submitter = Auth::checkSubmitterAccess($submitter); 1029 1030 $this->addSubmitterToCart($submitter); 1031 1032 return redirect($submitter->url()); 1033 } 1034 1035 /** 1036 * @param Family $family 1037 */ 1038 protected function addFamilyToCart(Family $family): void 1039 { 1040 $cart = Session::get('cart', []); 1041 $tree = $family->tree()->name(); 1042 $xref = $family->xref(); 1043 1044 if (($cart[$tree][$xref] ?? false) === false) { 1045 $cart[$tree][$xref] = true; 1046 1047 Session::put('cart', $cart); 1048 1049 foreach ($family->spouses() as $spouse) { 1050 $this->addIndividualToCart($spouse); 1051 } 1052 1053 $this->addLocationLinksToCart($family); 1054 $this->addMediaLinksToCart($family); 1055 $this->addNoteLinksToCart($family); 1056 $this->addSourceLinksToCart($family); 1057 $this->addSubmitterLinksToCart($family); 1058 } 1059 } 1060 1061 /** 1062 * @param Individual $individual 1063 */ 1064 protected function addIndividualToCart(Individual $individual): void 1065 { 1066 $cart = Session::get('cart', []); 1067 $tree = $individual->tree()->name(); 1068 $xref = $individual->xref(); 1069 1070 if (($cart[$tree][$xref] ?? false) === false) { 1071 $cart[$tree][$xref] = true; 1072 1073 Session::put('cart', $cart); 1074 1075 $this->addLocationLinksToCart($individual); 1076 $this->addMediaLinksToCart($individual); 1077 $this->addNoteLinksToCart($individual); 1078 $this->addSourceLinksToCart($individual); 1079 } 1080 } 1081 1082 /** 1083 * @param Location $location 1084 */ 1085 protected function addLocationToCart(Location $location): void 1086 { 1087 $cart = Session::get('cart', []); 1088 $tree = $location->tree()->name(); 1089 $xref = $location->xref(); 1090 1091 if (($cart[$tree][$xref] ?? false) === false) { 1092 $cart[$tree][$xref] = true; 1093 1094 Session::put('cart', $cart); 1095 1096 $this->addLocationLinksToCart($location); 1097 $this->addMediaLinksToCart($location); 1098 $this->addNoteLinksToCart($location); 1099 $this->addSourceLinksToCart($location); 1100 } 1101 } 1102 1103 /** 1104 * @param GedcomRecord $record 1105 */ 1106 protected function addLocationLinksToCart(GedcomRecord $record): void 1107 { 1108 preg_match_all('/\n\d _LOC @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches); 1109 1110 foreach ($matches[1] as $xref) { 1111 $location = Registry::locationFactory()->make($xref, $record->tree()); 1112 1113 if ($location instanceof Location && $location->canShow()) { 1114 $this->addLocationToCart($location); 1115 } 1116 } 1117 } 1118 1119 /** 1120 * @param Media $media 1121 */ 1122 protected function addMediaToCart(Media $media): void 1123 { 1124 $cart = Session::get('cart', []); 1125 $tree = $media->tree()->name(); 1126 $xref = $media->xref(); 1127 1128 if (($cart[$tree][$xref] ?? false) === false) { 1129 $cart[$tree][$xref] = true; 1130 1131 Session::put('cart', $cart); 1132 1133 $this->addNoteLinksToCart($media); 1134 } 1135 } 1136 1137 /** 1138 * @param GedcomRecord $record 1139 */ 1140 protected function addMediaLinksToCart(GedcomRecord $record): void 1141 { 1142 preg_match_all('/\n\d OBJE @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches); 1143 1144 foreach ($matches[1] as $xref) { 1145 $media = Registry::mediaFactory()->make($xref, $record->tree()); 1146 1147 if ($media instanceof Media && $media->canShow()) { 1148 $this->addMediaToCart($media); 1149 } 1150 } 1151 } 1152 1153 /** 1154 * @param Note $note 1155 */ 1156 protected function addNoteToCart(Note $note): void 1157 { 1158 $cart = Session::get('cart', []); 1159 $tree = $note->tree()->name(); 1160 $xref = $note->xref(); 1161 1162 if (($cart[$tree][$xref] ?? false) === false) { 1163 $cart[$tree][$xref] = true; 1164 1165 Session::put('cart', $cart); 1166 } 1167 } 1168 1169 /** 1170 * @param GedcomRecord $record 1171 */ 1172 protected function addNoteLinksToCart(GedcomRecord $record): void 1173 { 1174 preg_match_all('/\n\d NOTE @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches); 1175 1176 foreach ($matches[1] as $xref) { 1177 $note = Registry::noteFactory()->make($xref, $record->tree()); 1178 1179 if ($note instanceof Note && $note->canShow()) { 1180 $this->addNoteToCart($note); 1181 } 1182 } 1183 } 1184 1185 /** 1186 * @param Source $source 1187 */ 1188 protected function addSourceToCart(Source $source): void 1189 { 1190 $cart = Session::get('cart', []); 1191 $tree = $source->tree()->name(); 1192 $xref = $source->xref(); 1193 1194 if (($cart[$tree][$xref] ?? false) === false) { 1195 $cart[$tree][$xref] = true; 1196 1197 Session::put('cart', $cart); 1198 1199 $this->addNoteLinksToCart($source); 1200 $this->addRepositoryLinksToCart($source); 1201 } 1202 } 1203 1204 /** 1205 * @param GedcomRecord $record 1206 */ 1207 protected function addSourceLinksToCart(GedcomRecord $record): void 1208 { 1209 preg_match_all('/\n\d SOUR @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches); 1210 1211 foreach ($matches[1] as $xref) { 1212 $source = Registry::sourceFactory()->make($xref, $record->tree()); 1213 1214 if ($source instanceof Source && $source->canShow()) { 1215 $this->addSourceToCart($source); 1216 } 1217 } 1218 } 1219 1220 /** 1221 * @param Repository $repository 1222 */ 1223 protected function addRepositoryToCart(Repository $repository): void 1224 { 1225 $cart = Session::get('cart', []); 1226 $tree = $repository->tree()->name(); 1227 $xref = $repository->xref(); 1228 1229 if (($cart[$tree][$xref] ?? false) === false) { 1230 $cart[$tree][$xref] = true; 1231 1232 Session::put('cart', $cart); 1233 1234 $this->addNoteLinksToCart($repository); 1235 } 1236 } 1237 1238 /** 1239 * @param GedcomRecord $record 1240 */ 1241 protected function addRepositoryLinksToCart(GedcomRecord $record): void 1242 { 1243 preg_match_all('/\n\d REPO @(' . Gedcom::REGEX_XREF . '@)/', $record->gedcom(), $matches); 1244 1245 foreach ($matches[1] as $xref) { 1246 $repository = Registry::repositoryFactory()->make($xref, $record->tree()); 1247 1248 if ($repository instanceof Repository && $repository->canShow()) { 1249 $this->addRepositoryToCart($repository); 1250 } 1251 } 1252 } 1253 1254 /** 1255 * @param Submitter $submitter 1256 */ 1257 protected function addSubmitterToCart(Submitter $submitter): void 1258 { 1259 $cart = Session::get('cart', []); 1260 $tree = $submitter->tree()->name(); 1261 $xref = $submitter->xref(); 1262 1263 if (($cart[$tree][$xref] ?? false) === false) { 1264 $cart[$tree][$xref] = true; 1265 1266 Session::put('cart', $cart); 1267 1268 $this->addNoteLinksToCart($submitter); 1269 } 1270 } 1271 1272 /** 1273 * @param GedcomRecord $record 1274 */ 1275 protected function addSubmitterLinksToCart(GedcomRecord $record): void 1276 { 1277 preg_match_all('/\n\d SUBM @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches); 1278 1279 foreach ($matches[1] as $xref) { 1280 $submitter = Registry::submitterFactory()->make($xref, $record->tree()); 1281 1282 if ($submitter instanceof Submitter && $submitter->canShow()) { 1283 $this->addSubmitterToCart($submitter); 1284 } 1285 } 1286 } 1287} 1288