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