169c05a6eSGreg Roach<?php 269c05a6eSGreg Roach 369c05a6eSGreg Roach/** 469c05a6eSGreg Roach * webtrees: online genealogy 55bfc6897SGreg Roach * Copyright (C) 2022 webtrees development team 669c05a6eSGreg Roach * This program is free software: you can redistribute it and/or modify 769c05a6eSGreg Roach * it under the terms of the GNU General Public License as published by 869c05a6eSGreg Roach * the Free Software Foundation, either version 3 of the License, or 969c05a6eSGreg Roach * (at your option) any later version. 1069c05a6eSGreg Roach * This program is distributed in the hope that it will be useful, 1169c05a6eSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 1269c05a6eSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 1369c05a6eSGreg Roach * GNU General Public License for more details. 1469c05a6eSGreg Roach * You should have received a copy of the GNU General Public License 1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>. 1669c05a6eSGreg Roach */ 1769c05a6eSGreg Roach 1869c05a6eSGreg Roachdeclare(strict_types=1); 1969c05a6eSGreg Roach 2069c05a6eSGreg Roachnamespace Fisharebest\Webtrees\Services; 2169c05a6eSGreg Roach 2269c05a6eSGreg Roachuse Fisharebest\Webtrees\Auth; 231c6adce8SGreg Roachuse Fisharebest\Webtrees\Encodings\UTF16BE; 241c6adce8SGreg Roachuse Fisharebest\Webtrees\Encodings\UTF16LE; 251c6adce8SGreg Roachuse Fisharebest\Webtrees\Encodings\UTF8; 261c6adce8SGreg Roachuse Fisharebest\Webtrees\Encodings\Windows1252; 2769c05a6eSGreg Roachuse Fisharebest\Webtrees\Factories\AbstractGedcomRecordFactory; 2869c05a6eSGreg Roachuse Fisharebest\Webtrees\Gedcom; 291c6adce8SGreg Roachuse Fisharebest\Webtrees\GedcomFilters\GedcomEncodingFilter; 3069c05a6eSGreg Roachuse Fisharebest\Webtrees\GedcomRecord; 3169c05a6eSGreg Roachuse Fisharebest\Webtrees\Header; 321c6adce8SGreg Roachuse Fisharebest\Webtrees\Registry; 3369c05a6eSGreg Roachuse Fisharebest\Webtrees\Tree; 3469c05a6eSGreg Roachuse Fisharebest\Webtrees\Webtrees; 3569c05a6eSGreg Roachuse Illuminate\Database\Capsule\Manager as DB; 3669c05a6eSGreg Roachuse Illuminate\Database\Query\Builder; 3769c05a6eSGreg Roachuse Illuminate\Database\Query\Expression; 3869c05a6eSGreg Roachuse Illuminate\Support\Collection; 3916ecfcafSGreg Roachuse League\Flysystem\Filesystem; 4016ecfcafSGreg Roachuse League\Flysystem\FilesystemOperator; 4116ecfcafSGreg Roachuse League\Flysystem\ZipArchive\FilesystemZipArchiveProvider; 4216ecfcafSGreg Roachuse League\Flysystem\ZipArchive\ZipArchiveAdapter; 4316ecfcafSGreg Roachuse Psr\Http\Message\ResponseFactoryInterface; 4416ecfcafSGreg Roachuse Psr\Http\Message\ResponseInterface; 4516ecfcafSGreg Roachuse Psr\Http\Message\StreamFactoryInterface; 46783b32e3SGreg Roachuse RuntimeException; 4769c05a6eSGreg Roach 4816ecfcafSGreg Roachuse function addcslashes; 4969c05a6eSGreg Roachuse function date; 5069c05a6eSGreg Roachuse function explode; 5116ecfcafSGreg Roachuse function fclose; 52ea517a3bSGreg Roachuse function fopen; 5369c05a6eSGreg Roachuse function fwrite; 5410e06497SGreg Roachuse function is_string; 5569c05a6eSGreg Roachuse function pathinfo; 5616ecfcafSGreg Roachuse function preg_match_all; 57ea517a3bSGreg Roachuse function rewind; 581c6adce8SGreg Roachuse function stream_filter_append; 5916ecfcafSGreg Roachuse function stream_get_meta_data; 60783b32e3SGreg Roachuse function strlen; 6169c05a6eSGreg Roachuse function strpos; 6269c05a6eSGreg Roachuse function strtolower; 6369c05a6eSGreg Roachuse function strtoupper; 6416ecfcafSGreg Roachuse function tmpfile; 6569c05a6eSGreg Roach 6669c05a6eSGreg Roachuse const PATHINFO_EXTENSION; 6716ecfcafSGreg Roachuse const PREG_SET_ORDER; 681c6adce8SGreg Roachuse const STREAM_FILTER_WRITE; 6969c05a6eSGreg Roach 7069c05a6eSGreg Roach/** 7169c05a6eSGreg Roach * Export data in GEDCOM format 7269c05a6eSGreg Roach */ 7369c05a6eSGreg Roachclass GedcomExportService 7469c05a6eSGreg Roach{ 7516ecfcafSGreg Roach private const ACCESS_LEVELS = [ 7616ecfcafSGreg Roach 'gedadmin' => Auth::PRIV_NONE, 7716ecfcafSGreg Roach 'user' => Auth::PRIV_USER, 7816ecfcafSGreg Roach 'visitor' => Auth::PRIV_PRIVATE, 7916ecfcafSGreg Roach 'none' => Auth::PRIV_HIDE, 8016ecfcafSGreg Roach ]; 8116ecfcafSGreg Roach 8216ecfcafSGreg Roach private ResponseFactoryInterface $response_factory; 8316ecfcafSGreg Roach 8416ecfcafSGreg Roach private StreamFactoryInterface $stream_factory; 8516ecfcafSGreg Roach 8616ecfcafSGreg Roach /** 8716ecfcafSGreg Roach * @param ResponseFactoryInterface $response_factory 8816ecfcafSGreg Roach * @param StreamFactoryInterface $stream_factory 8916ecfcafSGreg Roach */ 9016ecfcafSGreg Roach public function __construct(ResponseFactoryInterface $response_factory, StreamFactoryInterface $stream_factory) 9116ecfcafSGreg Roach { 9216ecfcafSGreg Roach $this->response_factory = $response_factory; 9316ecfcafSGreg Roach $this->stream_factory = $stream_factory; 9416ecfcafSGreg Roach } 9516ecfcafSGreg Roach 9616ecfcafSGreg Roach /** 9716ecfcafSGreg Roach * @param Tree $tree - Export data from this tree 9816ecfcafSGreg Roach * @param bool $sort_by_xref - Write GEDCOM records in XREF order 9916ecfcafSGreg Roach * @param string $encoding - Convert from UTF-8 to other encoding 10016ecfcafSGreg Roach * @param string $privacy - Filter records by role 10161351a03SGreg Roach * @param string $line_endings 10216ecfcafSGreg Roach * @param string $filename - Name of download file, without an extension 10316ecfcafSGreg Roach * @param string $format - One of: gedcom, zip, zipmedia, gedzip 10461351a03SGreg Roach * @param Collection|null $records 10516ecfcafSGreg Roach * 10616ecfcafSGreg Roach * @return ResponseInterface 10716ecfcafSGreg Roach */ 10816ecfcafSGreg Roach public function downloadResponse( 10916ecfcafSGreg Roach Tree $tree, 11016ecfcafSGreg Roach bool $sort_by_xref, 11116ecfcafSGreg Roach string $encoding, 11216ecfcafSGreg Roach string $privacy, 11316ecfcafSGreg Roach string $line_endings, 11416ecfcafSGreg Roach string $filename, 11516ecfcafSGreg Roach string $format, 11616ecfcafSGreg Roach Collection $records = null 11716ecfcafSGreg Roach ): ResponseInterface { 11816ecfcafSGreg Roach $access_level = self::ACCESS_LEVELS[$privacy]; 11916ecfcafSGreg Roach 12016ecfcafSGreg Roach if ($format === 'gedcom') { 12116ecfcafSGreg Roach $resource = $this->export($tree, $sort_by_xref, $encoding, $access_level, $line_endings, $records); 12216ecfcafSGreg Roach $stream = $this->stream_factory->createStreamFromResource($resource); 12316ecfcafSGreg Roach 12416ecfcafSGreg Roach return $this->response_factory->createResponse() 12516ecfcafSGreg Roach ->withBody($stream) 12616ecfcafSGreg Roach ->withHeader('content-type', 'text/x-gedcom; charset=' . UTF8::NAME) 12716ecfcafSGreg Roach ->withHeader('content-disposition', 'attachment; filename="' . addcslashes($filename, '"') . '.ged"'); 12816ecfcafSGreg Roach } 12916ecfcafSGreg Roach 13016ecfcafSGreg Roach // Create a new/empty .ZIP file 13116ecfcafSGreg Roach $temp_zip_file = stream_get_meta_data(tmpfile())['uri']; 13216ecfcafSGreg Roach $zip_provider = new FilesystemZipArchiveProvider($temp_zip_file, 0755); 13316ecfcafSGreg Roach $zip_adapter = new ZipArchiveAdapter($zip_provider); 13416ecfcafSGreg Roach $zip_filesystem = new Filesystem($zip_adapter); 13516ecfcafSGreg Roach 13616ecfcafSGreg Roach if ($format === 'zipmedia') { 13716ecfcafSGreg Roach $media_path = $tree->getPreference('MEDIA_DIRECTORY'); 13816ecfcafSGreg Roach } elseif ($format === 'gedzip') { 13916ecfcafSGreg Roach $media_path = ''; 14016ecfcafSGreg Roach } else { 14116ecfcafSGreg Roach // Don't add media 14216ecfcafSGreg Roach $media_path = null; 14316ecfcafSGreg Roach } 14416ecfcafSGreg Roach 14516ecfcafSGreg Roach $resource = $this->export($tree, $sort_by_xref, $encoding, $access_level, $line_endings, $records, $zip_filesystem, $media_path); 14616ecfcafSGreg Roach 14716ecfcafSGreg Roach if ($format === 'gedzip') { 14816ecfcafSGreg Roach $zip_filesystem->writeStream('gedcom.ged', $resource); 14916ecfcafSGreg Roach $extension = '.gdz'; 15016ecfcafSGreg Roach } else { 15116ecfcafSGreg Roach $zip_filesystem->writeStream($filename . '.ged', $resource); 15216ecfcafSGreg Roach $extension = '.zip'; 15316ecfcafSGreg Roach } 15416ecfcafSGreg Roach 15516ecfcafSGreg Roach fclose($resource); 15616ecfcafSGreg Roach 15716ecfcafSGreg Roach $stream = $this->stream_factory->createStreamFromFile($temp_zip_file); 15816ecfcafSGreg Roach 15916ecfcafSGreg Roach return $this->response_factory->createResponse() 16016ecfcafSGreg Roach ->withBody($stream) 16116ecfcafSGreg Roach ->withHeader('content-type', 'application/zip') 16216ecfcafSGreg Roach ->withHeader('content-disposition', 'attachment; filename="' . addcslashes($filename, '"') . $extension . '"'); 16316ecfcafSGreg Roach } 16416ecfcafSGreg Roach 16569c05a6eSGreg Roach /** 16669c05a6eSGreg Roach * Write GEDCOM data to a stream. 16769c05a6eSGreg Roach * 16869c05a6eSGreg Roach * @param Tree $tree - Export data from this tree 16969c05a6eSGreg Roach * @param bool $sort_by_xref - Write GEDCOM records in XREF order 17069c05a6eSGreg Roach * @param string $encoding - Convert from UTF-8 to other encoding 17169c05a6eSGreg Roach * @param int $access_level - Apply privacy filtering 1721c6adce8SGreg Roach * @param string $line_endings - CRLF or LF 17336779af1SGreg Roach * @param Collection<int,string>|null $records - Just export these records 17416ecfcafSGreg Roach * @param FilesystemOperator|null $zip_filesystem - Write media files to this filesystem 17516ecfcafSGreg Roach * @param string|null $media_path - Location within the zip filesystem 176ea517a3bSGreg Roach * 177ea517a3bSGreg Roach * @return resource 17869c05a6eSGreg Roach */ 17969c05a6eSGreg Roach public function export( 18069c05a6eSGreg Roach Tree $tree, 18169c05a6eSGreg Roach bool $sort_by_xref = false, 1821c6adce8SGreg Roach string $encoding = UTF8::NAME, 18369c05a6eSGreg Roach int $access_level = Auth::PRIV_HIDE, 1841c6adce8SGreg Roach string $line_endings = 'CRLF', 18516ecfcafSGreg Roach Collection $records = null, 18616ecfcafSGreg Roach FilesystemOperator $zip_filesystem = null, 18716ecfcafSGreg Roach string $media_path = null 188ea517a3bSGreg Roach ) { 189ea517a3bSGreg Roach $stream = fopen('php://memory', 'wb+'); 190ea517a3bSGreg Roach 191ea517a3bSGreg Roach if ($stream === false) { 192ea517a3bSGreg Roach throw new RuntimeException('Failed to create temporary stream'); 193ea517a3bSGreg Roach } 194ea517a3bSGreg Roach 1951c6adce8SGreg Roach stream_filter_append($stream, GedcomEncodingFilter::class, STREAM_FILTER_WRITE, ['src_encoding' => UTF8::NAME, 'dst_encoding' => $encoding]); 1961c6adce8SGreg Roach 19769c05a6eSGreg Roach if ($records instanceof Collection) { 19869c05a6eSGreg Roach // Export just these records - e.g. from clippings cart. 19969c05a6eSGreg Roach $data = [ 20069c05a6eSGreg Roach new Collection([$this->createHeader($tree, $encoding, false)]), 20169c05a6eSGreg Roach $records, 20269c05a6eSGreg Roach new Collection(['0 TRLR']), 20369c05a6eSGreg Roach ]; 20469c05a6eSGreg Roach } elseif ($access_level === Auth::PRIV_HIDE) { 20569c05a6eSGreg Roach // If we will be applying privacy filters, then we will need the GEDCOM record objects. 20669c05a6eSGreg Roach $data = [ 20769c05a6eSGreg Roach new Collection([$this->createHeader($tree, $encoding, true)]), 20869c05a6eSGreg Roach $this->individualQuery($tree, $sort_by_xref)->cursor(), 20969c05a6eSGreg Roach $this->familyQuery($tree, $sort_by_xref)->cursor(), 21069c05a6eSGreg Roach $this->sourceQuery($tree, $sort_by_xref)->cursor(), 21169c05a6eSGreg Roach $this->otherQuery($tree, $sort_by_xref)->cursor(), 21269c05a6eSGreg Roach $this->mediaQuery($tree, $sort_by_xref)->cursor(), 21369c05a6eSGreg Roach new Collection(['0 TRLR']), 21469c05a6eSGreg Roach ]; 21569c05a6eSGreg Roach } else { 21669c05a6eSGreg Roach // Disable the pending changes before creating GEDCOM records. 2176b9cb339SGreg Roach Registry::cache()->array()->remember(AbstractGedcomRecordFactory::class . $tree->id(), static function (): Collection { 21869c05a6eSGreg Roach return new Collection(); 21969c05a6eSGreg Roach }); 22069c05a6eSGreg Roach 22169c05a6eSGreg Roach $data = [ 22269c05a6eSGreg Roach new Collection([$this->createHeader($tree, $encoding, true)]), 2236b9cb339SGreg Roach $this->individualQuery($tree, $sort_by_xref)->get()->map(Registry::individualFactory()->mapper($tree)), 2246b9cb339SGreg Roach $this->familyQuery($tree, $sort_by_xref)->get()->map(Registry::familyFactory()->mapper($tree)), 2256b9cb339SGreg Roach $this->sourceQuery($tree, $sort_by_xref)->get()->map(Registry::sourceFactory()->mapper($tree)), 2266b9cb339SGreg Roach $this->otherQuery($tree, $sort_by_xref)->get()->map(Registry::gedcomRecordFactory()->mapper($tree)), 2276b9cb339SGreg Roach $this->mediaQuery($tree, $sort_by_xref)->get()->map(Registry::mediaFactory()->mapper($tree)), 22869c05a6eSGreg Roach new Collection(['0 TRLR']), 22969c05a6eSGreg Roach ]; 23069c05a6eSGreg Roach } 23169c05a6eSGreg Roach 2329458f20aSGreg Roach $media_filesystem = $tree->mediaFilesystem(); 23316ecfcafSGreg Roach 23469c05a6eSGreg Roach foreach ($data as $rows) { 23569c05a6eSGreg Roach foreach ($rows as $datum) { 23669c05a6eSGreg Roach if (is_string($datum)) { 23769c05a6eSGreg Roach $gedcom = $datum; 23869c05a6eSGreg Roach } elseif ($datum instanceof GedcomRecord) { 23969c05a6eSGreg Roach $gedcom = $datum->privatizeGedcom($access_level); 24069c05a6eSGreg Roach } else { 241813bb733SGreg Roach $gedcom = 242813bb733SGreg Roach $datum->i_gedcom ?? 243813bb733SGreg Roach $datum->f_gedcom ?? 244813bb733SGreg Roach $datum->s_gedcom ?? 245813bb733SGreg Roach $datum->m_gedcom ?? 246813bb733SGreg Roach $datum->o_gedcom; 24769c05a6eSGreg Roach } 24869c05a6eSGreg Roach 24916ecfcafSGreg Roach if ($media_path !== null && $zip_filesystem !== null && preg_match('/0 @' . Gedcom::REGEX_XREF . '@ OBJE/', $gedcom) === 1) { 25016ecfcafSGreg Roach preg_match_all('/\n1 FILE (.+)/', $gedcom, $matches, PREG_SET_ORDER); 25116ecfcafSGreg Roach 25216ecfcafSGreg Roach foreach ($matches as $match) { 25316ecfcafSGreg Roach $media_file = $match[1]; 25416ecfcafSGreg Roach 25516ecfcafSGreg Roach if ($media_filesystem->fileExists($media_file)) { 25616ecfcafSGreg Roach $zip_filesystem->writeStream($media_path . $media_file, $media_filesystem->readStream($media_file)); 25716ecfcafSGreg Roach } 25816ecfcafSGreg Roach } 25969c05a6eSGreg Roach } 26069c05a6eSGreg Roach 2611c6adce8SGreg Roach $gedcom = $this->wrapLongLines($gedcom, Gedcom::LINE_LENGTH) . "\n"; 2621c6adce8SGreg Roach 2631c6adce8SGreg Roach if ($line_endings === 'CRLF') { 2641c6adce8SGreg Roach $gedcom = strtr($gedcom, ["\n" => "\r\n"]); 2651c6adce8SGreg Roach } 26669c05a6eSGreg Roach 267783b32e3SGreg Roach $bytes_written = fwrite($stream, $gedcom); 268783b32e3SGreg Roach 269783b32e3SGreg Roach if ($bytes_written !== strlen($gedcom)) { 270783b32e3SGreg Roach throw new RuntimeException('Unable to write to stream. Perhaps the disk is full?'); 271783b32e3SGreg Roach } 27269c05a6eSGreg Roach } 27369c05a6eSGreg Roach } 274ea517a3bSGreg Roach 275ea517a3bSGreg Roach if (rewind($stream) === false) { 276ea517a3bSGreg Roach throw new RuntimeException('Cannot rewind temporary stream'); 277ea517a3bSGreg Roach } 278ea517a3bSGreg Roach 279ea517a3bSGreg Roach return $stream; 28069c05a6eSGreg Roach } 28169c05a6eSGreg Roach 28269c05a6eSGreg Roach /** 28369c05a6eSGreg Roach * Create a header record for a gedcom file. 28469c05a6eSGreg Roach * 28569c05a6eSGreg Roach * @param Tree $tree 28669c05a6eSGreg Roach * @param string $encoding 28769c05a6eSGreg Roach * @param bool $include_sub 28869c05a6eSGreg Roach * 28969c05a6eSGreg Roach * @return string 29069c05a6eSGreg Roach */ 29169c05a6eSGreg Roach public function createHeader(Tree $tree, string $encoding, bool $include_sub): string 29269c05a6eSGreg Roach { 29369c05a6eSGreg Roach // Force a ".ged" suffix 29469c05a6eSGreg Roach $filename = $tree->name(); 29569c05a6eSGreg Roach 29669c05a6eSGreg Roach if (strtolower(pathinfo($filename, PATHINFO_EXTENSION)) !== 'ged') { 29769c05a6eSGreg Roach $filename .= '.ged'; 29869c05a6eSGreg Roach } 29969c05a6eSGreg Roach 3001c6adce8SGreg Roach $gedcom_encodings = [ 3011c6adce8SGreg Roach UTF16BE::NAME => 'UNICODE', 3021c6adce8SGreg Roach UTF16LE::NAME => 'UNICODE', 3031c6adce8SGreg Roach Windows1252::NAME => 'ANSI', 3041c6adce8SGreg Roach ]; 3051c6adce8SGreg Roach 3061c6adce8SGreg Roach $encoding = $gedcom_encodings[$encoding] ?? $encoding; 3071c6adce8SGreg Roach 30869c05a6eSGreg Roach // Build a new header record 30969c05a6eSGreg Roach $gedcom = '0 HEAD'; 31069c05a6eSGreg Roach $gedcom .= "\n1 SOUR " . Webtrees::NAME; 31169c05a6eSGreg Roach $gedcom .= "\n2 NAME " . Webtrees::NAME; 31269c05a6eSGreg Roach $gedcom .= "\n2 VERS " . Webtrees::VERSION; 31369c05a6eSGreg Roach $gedcom .= "\n1 DEST DISKETTE"; 31469c05a6eSGreg Roach $gedcom .= "\n1 DATE " . strtoupper(date('d M Y')); 31569c05a6eSGreg Roach $gedcom .= "\n2 TIME " . date('H:i:s'); 31688a91440SGreg Roach $gedcom .= "\n1 GEDC\n2 VERS 5.5.1\n2 FORM LINEAGE-LINKED"; 31769c05a6eSGreg Roach $gedcom .= "\n1 CHAR " . $encoding; 31869c05a6eSGreg Roach $gedcom .= "\n1 FILE " . $filename; 31969c05a6eSGreg Roach 32069c05a6eSGreg Roach // Preserve some values from the original header 3216b9cb339SGreg Roach $header = Registry::headerFactory()->make('HEAD', $tree) ?? Registry::headerFactory()->new('HEAD', '0 HEAD', null, $tree); 32269c05a6eSGreg Roach 323*771780e6SGreg Roach // There should always be a header record. 324*771780e6SGreg Roach if ($header instanceof Header) { 32569c05a6eSGreg Roach foreach ($header->facts(['COPR', 'LANG', 'PLAC', 'NOTE']) as $fact) { 32669c05a6eSGreg Roach $gedcom .= "\n" . $fact->gedcom(); 32769c05a6eSGreg Roach } 32869c05a6eSGreg Roach 32969c05a6eSGreg Roach if ($include_sub) { 33069c05a6eSGreg Roach foreach ($header->facts(['SUBM', 'SUBN']) as $fact) { 33169c05a6eSGreg Roach $gedcom .= "\n" . $fact->gedcom(); 33269c05a6eSGreg Roach } 33369c05a6eSGreg Roach } 334*771780e6SGreg Roach } 33569c05a6eSGreg Roach 33669c05a6eSGreg Roach return $gedcom; 33769c05a6eSGreg Roach } 33869c05a6eSGreg Roach 33969c05a6eSGreg Roach /** 34069c05a6eSGreg Roach * Wrap long lines using concatenation records. 34169c05a6eSGreg Roach * 34269c05a6eSGreg Roach * @param string $gedcom 34369c05a6eSGreg Roach * @param int $max_line_length 34469c05a6eSGreg Roach * 34569c05a6eSGreg Roach * @return string 34669c05a6eSGreg Roach */ 34769c05a6eSGreg Roach public function wrapLongLines(string $gedcom, int $max_line_length): string 34869c05a6eSGreg Roach { 34969c05a6eSGreg Roach $lines = []; 35069c05a6eSGreg Roach 35169c05a6eSGreg Roach foreach (explode("\n", $gedcom) as $line) { 35269c05a6eSGreg Roach // Split long lines 35369c05a6eSGreg Roach // The total length of a GEDCOM line, including level number, cross-reference number, 35469c05a6eSGreg Roach // tag, value, delimiters, and terminator, must not exceed 255 (wide) characters. 35569c05a6eSGreg Roach if (mb_strlen($line) > $max_line_length) { 35669c05a6eSGreg Roach [$level, $tag] = explode(' ', $line, 3); 35769c05a6eSGreg Roach if ($tag !== 'CONT') { 35869c05a6eSGreg Roach $level++; 35969c05a6eSGreg Roach } 36069c05a6eSGreg Roach do { 36169c05a6eSGreg Roach // Split after $pos chars 36269c05a6eSGreg Roach $pos = $max_line_length; 36369c05a6eSGreg Roach // Split on a non-space (standard gedcom behavior) 36469c05a6eSGreg Roach while (mb_substr($line, $pos - 1, 1) === ' ') { 36569c05a6eSGreg Roach --$pos; 36669c05a6eSGreg Roach } 36769c05a6eSGreg Roach if ($pos === strpos($line, ' ', 3)) { 36869c05a6eSGreg Roach // No non-spaces in the data! Can’t split it :-( 36969c05a6eSGreg Roach break; 37069c05a6eSGreg Roach } 37169c05a6eSGreg Roach $lines[] = mb_substr($line, 0, $pos); 37269c05a6eSGreg Roach $line = $level . ' CONC ' . mb_substr($line, $pos); 37369c05a6eSGreg Roach } while (mb_strlen($line) > $max_line_length); 37469c05a6eSGreg Roach } 37569c05a6eSGreg Roach $lines[] = $line; 37669c05a6eSGreg Roach } 37769c05a6eSGreg Roach 3781c6adce8SGreg Roach return implode("\n", $lines); 37969c05a6eSGreg Roach } 38069c05a6eSGreg Roach 38169c05a6eSGreg Roach /** 38269c05a6eSGreg Roach * @param Tree $tree 38369c05a6eSGreg Roach * @param bool $sort_by_xref 38469c05a6eSGreg Roach * 38569c05a6eSGreg Roach * @return Builder 38669c05a6eSGreg Roach */ 38769c05a6eSGreg Roach private function familyQuery(Tree $tree, bool $sort_by_xref): Builder 38869c05a6eSGreg Roach { 38969c05a6eSGreg Roach $query = DB::table('families') 39069c05a6eSGreg Roach ->where('f_file', '=', $tree->id()) 391813bb733SGreg Roach ->select(['f_gedcom', 'f_id']); 39269c05a6eSGreg Roach 39369c05a6eSGreg Roach 39469c05a6eSGreg Roach if ($sort_by_xref) { 39569c05a6eSGreg Roach $query 39669c05a6eSGreg Roach ->orderBy(new Expression('LENGTH(f_id)')) 39769c05a6eSGreg Roach ->orderBy('f_id'); 39869c05a6eSGreg Roach } 39969c05a6eSGreg Roach 40069c05a6eSGreg Roach return $query; 40169c05a6eSGreg Roach } 40269c05a6eSGreg Roach 40369c05a6eSGreg Roach /** 40469c05a6eSGreg Roach * @param Tree $tree 40569c05a6eSGreg Roach * @param bool $sort_by_xref 40669c05a6eSGreg Roach * 40769c05a6eSGreg Roach * @return Builder 40869c05a6eSGreg Roach */ 40969c05a6eSGreg Roach private function individualQuery(Tree $tree, bool $sort_by_xref): Builder 41069c05a6eSGreg Roach { 41169c05a6eSGreg Roach $query = DB::table('individuals') 41269c05a6eSGreg Roach ->where('i_file', '=', $tree->id()) 413813bb733SGreg Roach ->select(['i_gedcom', 'i_id']); 41469c05a6eSGreg Roach 41569c05a6eSGreg Roach if ($sort_by_xref) { 41669c05a6eSGreg Roach $query 41769c05a6eSGreg Roach ->orderBy(new Expression('LENGTH(i_id)')) 41869c05a6eSGreg Roach ->orderBy('i_id'); 41969c05a6eSGreg Roach } 42069c05a6eSGreg Roach 42169c05a6eSGreg Roach return $query; 42269c05a6eSGreg Roach } 42369c05a6eSGreg Roach 42469c05a6eSGreg Roach /** 42569c05a6eSGreg Roach * @param Tree $tree 42669c05a6eSGreg Roach * @param bool $sort_by_xref 42769c05a6eSGreg Roach * 42869c05a6eSGreg Roach * @return Builder 42969c05a6eSGreg Roach */ 43069c05a6eSGreg Roach private function sourceQuery(Tree $tree, bool $sort_by_xref): Builder 43169c05a6eSGreg Roach { 43269c05a6eSGreg Roach $query = DB::table('sources') 43369c05a6eSGreg Roach ->where('s_file', '=', $tree->id()) 434813bb733SGreg Roach ->select(['s_gedcom', 's_id']); 43569c05a6eSGreg Roach 43669c05a6eSGreg Roach if ($sort_by_xref) { 43769c05a6eSGreg Roach $query 43869c05a6eSGreg Roach ->orderBy(new Expression('LENGTH(s_id)')) 43969c05a6eSGreg Roach ->orderBy('s_id'); 44069c05a6eSGreg Roach } 44169c05a6eSGreg Roach 44269c05a6eSGreg Roach return $query; 44369c05a6eSGreg Roach } 44469c05a6eSGreg Roach 44569c05a6eSGreg Roach /** 44669c05a6eSGreg Roach * @param Tree $tree 44769c05a6eSGreg Roach * @param bool $sort_by_xref 44869c05a6eSGreg Roach * 44969c05a6eSGreg Roach * @return Builder 45069c05a6eSGreg Roach */ 45169c05a6eSGreg Roach private function mediaQuery(Tree $tree, bool $sort_by_xref): Builder 45269c05a6eSGreg Roach { 45369c05a6eSGreg Roach $query = DB::table('media') 45469c05a6eSGreg Roach ->where('m_file', '=', $tree->id()) 455813bb733SGreg Roach ->select(['m_gedcom', 'm_id']); 45669c05a6eSGreg Roach 45769c05a6eSGreg Roach if ($sort_by_xref) { 45869c05a6eSGreg Roach $query 45969c05a6eSGreg Roach ->orderBy(new Expression('LENGTH(m_id)')) 46069c05a6eSGreg Roach ->orderBy('m_id'); 46169c05a6eSGreg Roach } 46269c05a6eSGreg Roach 46369c05a6eSGreg Roach return $query; 46469c05a6eSGreg Roach } 46569c05a6eSGreg Roach 46669c05a6eSGreg Roach /** 46769c05a6eSGreg Roach * @param Tree $tree 46869c05a6eSGreg Roach * @param bool $sort_by_xref 46969c05a6eSGreg Roach * 47069c05a6eSGreg Roach * @return Builder 47169c05a6eSGreg Roach */ 47269c05a6eSGreg Roach private function otherQuery(Tree $tree, bool $sort_by_xref): Builder 47369c05a6eSGreg Roach { 47469c05a6eSGreg Roach $query = DB::table('other') 47569c05a6eSGreg Roach ->where('o_file', '=', $tree->id()) 47669c05a6eSGreg Roach ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR']) 477813bb733SGreg Roach ->select(['o_gedcom', 'o_id']); 47869c05a6eSGreg Roach 47969c05a6eSGreg Roach if ($sort_by_xref) { 48069c05a6eSGreg Roach $query 48169c05a6eSGreg Roach ->orderBy('o_type') 48269c05a6eSGreg Roach ->orderBy(new Expression('LENGTH(o_id)')) 48369c05a6eSGreg Roach ->orderBy('o_id'); 48469c05a6eSGreg Roach } 48569c05a6eSGreg Roach 48669c05a6eSGreg Roach return $query; 48769c05a6eSGreg Roach } 48869c05a6eSGreg Roach} 489