169c05a6eSGreg Roach<?php 269c05a6eSGreg Roach 369c05a6eSGreg Roach/** 469c05a6eSGreg Roach * webtrees: online genealogy 5d11be702SGreg Roach * Copyright (C) 2023 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; 236f4ec3caSGreg Roachuse Fisharebest\Webtrees\DB; 241c6adce8SGreg Roachuse Fisharebest\Webtrees\Encodings\UTF16BE; 251c6adce8SGreg Roachuse Fisharebest\Webtrees\Encodings\UTF16LE; 261c6adce8SGreg Roachuse Fisharebest\Webtrees\Encodings\UTF8; 271c6adce8SGreg Roachuse Fisharebest\Webtrees\Encodings\Windows1252; 2869c05a6eSGreg Roachuse Fisharebest\Webtrees\Factories\AbstractGedcomRecordFactory; 2969c05a6eSGreg Roachuse Fisharebest\Webtrees\Gedcom; 301c6adce8SGreg Roachuse Fisharebest\Webtrees\GedcomFilters\GedcomEncodingFilter; 3169c05a6eSGreg Roachuse Fisharebest\Webtrees\GedcomRecord; 3269c05a6eSGreg Roachuse Fisharebest\Webtrees\Header; 331c6adce8SGreg Roachuse Fisharebest\Webtrees\Registry; 3469c05a6eSGreg Roachuse Fisharebest\Webtrees\Tree; 3569c05a6eSGreg Roachuse Fisharebest\Webtrees\Webtrees; 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 public function __construct(ResponseFactoryInterface $response_factory, StreamFactoryInterface $stream_factory) 8716ecfcafSGreg Roach { 8816ecfcafSGreg Roach $this->response_factory = $response_factory; 8916ecfcafSGreg Roach $this->stream_factory = $stream_factory; 9016ecfcafSGreg Roach } 9116ecfcafSGreg Roach 9216ecfcafSGreg Roach /** 93e5766395SGreg Roach * @param Tree $tree Export data from this tree 94e5766395SGreg Roach * @param bool $sort_by_xref Write GEDCOM records in XREF order 95e5766395SGreg Roach * @param string $encoding Convert from UTF-8 to other encoding 96e5766395SGreg Roach * @param string $privacy Filter records by role 97*6830c5c1SGreg Roach * @param string $line_endings CRLF or LF 98e5766395SGreg Roach * @param string $filename Name of download file, without an extension 99e5766395SGreg Roach * @param string $format One of: gedcom, zip, zipmedia, gedzip 100e5766395SGreg Roach * @param Collection<int,string|object|GedcomRecord>|null $records 10116ecfcafSGreg Roach */ 10216ecfcafSGreg Roach public function downloadResponse( 10316ecfcafSGreg Roach Tree $tree, 10416ecfcafSGreg Roach bool $sort_by_xref, 10516ecfcafSGreg Roach string $encoding, 10616ecfcafSGreg Roach string $privacy, 10716ecfcafSGreg Roach string $line_endings, 10816ecfcafSGreg Roach string $filename, 10916ecfcafSGreg Roach string $format, 11016ecfcafSGreg Roach Collection $records = null 11116ecfcafSGreg Roach ): ResponseInterface { 11216ecfcafSGreg Roach $access_level = self::ACCESS_LEVELS[$privacy]; 11316ecfcafSGreg Roach 11416ecfcafSGreg Roach if ($format === 'gedcom') { 11516ecfcafSGreg Roach $resource = $this->export($tree, $sort_by_xref, $encoding, $access_level, $line_endings, $records); 11616ecfcafSGreg Roach $stream = $this->stream_factory->createStreamFromResource($resource); 11716ecfcafSGreg Roach 11816ecfcafSGreg Roach return $this->response_factory->createResponse() 11916ecfcafSGreg Roach ->withBody($stream) 12016ecfcafSGreg Roach ->withHeader('content-type', 'text/x-gedcom; charset=' . UTF8::NAME) 12116ecfcafSGreg Roach ->withHeader('content-disposition', 'attachment; filename="' . addcslashes($filename, '"') . '.ged"'); 12216ecfcafSGreg Roach } 12316ecfcafSGreg Roach 12416ecfcafSGreg Roach // Create a new/empty .ZIP file 12516ecfcafSGreg Roach $temp_zip_file = stream_get_meta_data(tmpfile())['uri']; 12616ecfcafSGreg Roach $zip_provider = new FilesystemZipArchiveProvider($temp_zip_file, 0755); 12716ecfcafSGreg Roach $zip_adapter = new ZipArchiveAdapter($zip_provider); 12816ecfcafSGreg Roach $zip_filesystem = new Filesystem($zip_adapter); 12916ecfcafSGreg Roach 13016ecfcafSGreg Roach if ($format === 'zipmedia') { 13116ecfcafSGreg Roach $media_path = $tree->getPreference('MEDIA_DIRECTORY'); 13216ecfcafSGreg Roach } elseif ($format === 'gedzip') { 13316ecfcafSGreg Roach $media_path = ''; 13416ecfcafSGreg Roach } else { 13516ecfcafSGreg Roach // Don't add media 13616ecfcafSGreg Roach $media_path = null; 13716ecfcafSGreg Roach } 13816ecfcafSGreg Roach 13916ecfcafSGreg Roach $resource = $this->export($tree, $sort_by_xref, $encoding, $access_level, $line_endings, $records, $zip_filesystem, $media_path); 14016ecfcafSGreg Roach 14116ecfcafSGreg Roach if ($format === 'gedzip') { 14216ecfcafSGreg Roach $zip_filesystem->writeStream('gedcom.ged', $resource); 14316ecfcafSGreg Roach $extension = '.gdz'; 14416ecfcafSGreg Roach } else { 14516ecfcafSGreg Roach $zip_filesystem->writeStream($filename . '.ged', $resource); 14616ecfcafSGreg Roach $extension = '.zip'; 14716ecfcafSGreg Roach } 14816ecfcafSGreg Roach 14916ecfcafSGreg Roach fclose($resource); 15016ecfcafSGreg Roach 15116ecfcafSGreg Roach $stream = $this->stream_factory->createStreamFromFile($temp_zip_file); 15216ecfcafSGreg Roach 15316ecfcafSGreg Roach return $this->response_factory->createResponse() 15416ecfcafSGreg Roach ->withBody($stream) 15516ecfcafSGreg Roach ->withHeader('content-type', 'application/zip') 15616ecfcafSGreg Roach ->withHeader('content-disposition', 'attachment; filename="' . addcslashes($filename, '"') . $extension . '"'); 15716ecfcafSGreg Roach } 15816ecfcafSGreg Roach 15969c05a6eSGreg Roach /** 16069c05a6eSGreg Roach * Write GEDCOM data to a stream. 16169c05a6eSGreg Roach * 162e5766395SGreg Roach * @param Tree $tree Export data from this tree 163e5766395SGreg Roach * @param bool $sort_by_xref Write GEDCOM records in XREF order 164e5766395SGreg Roach * @param string $encoding Convert from UTF-8 to other encoding 165e5766395SGreg Roach * @param int $access_level Apply privacy filtering 166e5766395SGreg Roach * @param string $line_endings CRLF or LF 167e5766395SGreg Roach * @param Collection<int,string|object|GedcomRecord>|null $records Just export these records 168e5766395SGreg Roach * @param FilesystemOperator|null $zip_filesystem Write media files to this filesystem 169e5766395SGreg Roach * @param string|null $media_path Location within the zip filesystem 170ea517a3bSGreg Roach * 171ea517a3bSGreg Roach * @return resource 17269c05a6eSGreg Roach */ 17369c05a6eSGreg Roach public function export( 17469c05a6eSGreg Roach Tree $tree, 17569c05a6eSGreg Roach bool $sort_by_xref = false, 1761c6adce8SGreg Roach string $encoding = UTF8::NAME, 17769c05a6eSGreg Roach int $access_level = Auth::PRIV_HIDE, 1781c6adce8SGreg Roach string $line_endings = 'CRLF', 1792c6f1bd5SGreg Roach Collection|null $records = null, 1802c6f1bd5SGreg Roach FilesystemOperator|null $zip_filesystem = null, 18116ecfcafSGreg Roach string $media_path = null 182ea517a3bSGreg Roach ) { 183ea517a3bSGreg Roach $stream = fopen('php://memory', 'wb+'); 184ea517a3bSGreg Roach 185ea517a3bSGreg Roach if ($stream === false) { 186ea517a3bSGreg Roach throw new RuntimeException('Failed to create temporary stream'); 187ea517a3bSGreg Roach } 188ea517a3bSGreg Roach 1891c6adce8SGreg Roach stream_filter_append($stream, GedcomEncodingFilter::class, STREAM_FILTER_WRITE, ['src_encoding' => UTF8::NAME, 'dst_encoding' => $encoding]); 1901c6adce8SGreg Roach 19169c05a6eSGreg Roach if ($records instanceof Collection) { 19269c05a6eSGreg Roach // Export just these records - e.g. from clippings cart. 19369c05a6eSGreg Roach $data = [ 19469c05a6eSGreg Roach new Collection([$this->createHeader($tree, $encoding, false)]), 19569c05a6eSGreg Roach $records, 19669c05a6eSGreg Roach new Collection(['0 TRLR']), 19769c05a6eSGreg Roach ]; 19869c05a6eSGreg Roach } elseif ($access_level === Auth::PRIV_HIDE) { 19969c05a6eSGreg Roach // If we will be applying privacy filters, then we will need the GEDCOM record objects. 20069c05a6eSGreg Roach $data = [ 20169c05a6eSGreg Roach new Collection([$this->createHeader($tree, $encoding, true)]), 20269c05a6eSGreg Roach $this->individualQuery($tree, $sort_by_xref)->cursor(), 20369c05a6eSGreg Roach $this->familyQuery($tree, $sort_by_xref)->cursor(), 20469c05a6eSGreg Roach $this->sourceQuery($tree, $sort_by_xref)->cursor(), 20569c05a6eSGreg Roach $this->otherQuery($tree, $sort_by_xref)->cursor(), 20669c05a6eSGreg Roach $this->mediaQuery($tree, $sort_by_xref)->cursor(), 20769c05a6eSGreg Roach new Collection(['0 TRLR']), 20869c05a6eSGreg Roach ]; 20969c05a6eSGreg Roach } else { 21069c05a6eSGreg Roach // Disable the pending changes before creating GEDCOM records. 211f25fc0f9SGreg Roach Registry::cache()->array()->remember(AbstractGedcomRecordFactory::class . $tree->id(), static fn (): Collection => new Collection()); 21269c05a6eSGreg Roach 21369c05a6eSGreg Roach $data = [ 21469c05a6eSGreg Roach new Collection([$this->createHeader($tree, $encoding, true)]), 2156b9cb339SGreg Roach $this->individualQuery($tree, $sort_by_xref)->get()->map(Registry::individualFactory()->mapper($tree)), 2166b9cb339SGreg Roach $this->familyQuery($tree, $sort_by_xref)->get()->map(Registry::familyFactory()->mapper($tree)), 2176b9cb339SGreg Roach $this->sourceQuery($tree, $sort_by_xref)->get()->map(Registry::sourceFactory()->mapper($tree)), 2186b9cb339SGreg Roach $this->otherQuery($tree, $sort_by_xref)->get()->map(Registry::gedcomRecordFactory()->mapper($tree)), 2196b9cb339SGreg Roach $this->mediaQuery($tree, $sort_by_xref)->get()->map(Registry::mediaFactory()->mapper($tree)), 22069c05a6eSGreg Roach new Collection(['0 TRLR']), 22169c05a6eSGreg Roach ]; 22269c05a6eSGreg Roach } 22369c05a6eSGreg Roach 2249458f20aSGreg Roach $media_filesystem = $tree->mediaFilesystem(); 22516ecfcafSGreg Roach 22669c05a6eSGreg Roach foreach ($data as $rows) { 22769c05a6eSGreg Roach foreach ($rows as $datum) { 22869c05a6eSGreg Roach if (is_string($datum)) { 22969c05a6eSGreg Roach $gedcom = $datum; 23069c05a6eSGreg Roach } elseif ($datum instanceof GedcomRecord) { 23169c05a6eSGreg Roach $gedcom = $datum->privatizeGedcom($access_level); 232*6830c5c1SGreg Roach 233*6830c5c1SGreg Roach if ($gedcom === '') { 234*6830c5c1SGreg Roach continue; 235*6830c5c1SGreg Roach } 23669c05a6eSGreg Roach } else { 237813bb733SGreg Roach $gedcom = 238813bb733SGreg Roach $datum->i_gedcom ?? 239813bb733SGreg Roach $datum->f_gedcom ?? 240813bb733SGreg Roach $datum->s_gedcom ?? 241813bb733SGreg Roach $datum->m_gedcom ?? 242813bb733SGreg Roach $datum->o_gedcom; 24369c05a6eSGreg Roach } 24469c05a6eSGreg Roach 24516ecfcafSGreg Roach if ($media_path !== null && $zip_filesystem !== null && preg_match('/0 @' . Gedcom::REGEX_XREF . '@ OBJE/', $gedcom) === 1) { 24616ecfcafSGreg Roach preg_match_all('/\n1 FILE (.+)/', $gedcom, $matches, PREG_SET_ORDER); 24716ecfcafSGreg Roach 24816ecfcafSGreg Roach foreach ($matches as $match) { 24916ecfcafSGreg Roach $media_file = $match[1]; 25016ecfcafSGreg Roach 25116ecfcafSGreg Roach if ($media_filesystem->fileExists($media_file)) { 25216ecfcafSGreg Roach $zip_filesystem->writeStream($media_path . $media_file, $media_filesystem->readStream($media_file)); 25316ecfcafSGreg Roach } 25416ecfcafSGreg Roach } 25569c05a6eSGreg Roach } 25669c05a6eSGreg Roach 2571c6adce8SGreg Roach $gedcom = $this->wrapLongLines($gedcom, Gedcom::LINE_LENGTH) . "\n"; 2581c6adce8SGreg Roach 2591c6adce8SGreg Roach if ($line_endings === 'CRLF') { 2601c6adce8SGreg Roach $gedcom = strtr($gedcom, ["\n" => "\r\n"]); 2611c6adce8SGreg Roach } 26269c05a6eSGreg Roach 263783b32e3SGreg Roach $bytes_written = fwrite($stream, $gedcom); 264783b32e3SGreg Roach 265783b32e3SGreg Roach if ($bytes_written !== strlen($gedcom)) { 266783b32e3SGreg Roach throw new RuntimeException('Unable to write to stream. Perhaps the disk is full?'); 267783b32e3SGreg Roach } 26869c05a6eSGreg Roach } 26969c05a6eSGreg Roach } 270ea517a3bSGreg Roach 271ea517a3bSGreg Roach if (rewind($stream) === false) { 272ea517a3bSGreg Roach throw new RuntimeException('Cannot rewind temporary stream'); 273ea517a3bSGreg Roach } 274ea517a3bSGreg Roach 275ea517a3bSGreg Roach return $stream; 27669c05a6eSGreg Roach } 27769c05a6eSGreg Roach 27869c05a6eSGreg Roach public function createHeader(Tree $tree, string $encoding, bool $include_sub): string 27969c05a6eSGreg Roach { 28069c05a6eSGreg Roach // Force a ".ged" suffix 28169c05a6eSGreg Roach $filename = $tree->name(); 28269c05a6eSGreg Roach 28369c05a6eSGreg Roach if (strtolower(pathinfo($filename, PATHINFO_EXTENSION)) !== 'ged') { 28469c05a6eSGreg Roach $filename .= '.ged'; 28569c05a6eSGreg Roach } 28669c05a6eSGreg Roach 2871c6adce8SGreg Roach $gedcom_encodings = [ 2881c6adce8SGreg Roach UTF16BE::NAME => 'UNICODE', 2891c6adce8SGreg Roach UTF16LE::NAME => 'UNICODE', 2901c6adce8SGreg Roach Windows1252::NAME => 'ANSI', 2911c6adce8SGreg Roach ]; 2921c6adce8SGreg Roach 2931c6adce8SGreg Roach $encoding = $gedcom_encodings[$encoding] ?? $encoding; 2941c6adce8SGreg Roach 29569c05a6eSGreg Roach // Build a new header record 29669c05a6eSGreg Roach $gedcom = '0 HEAD'; 29769c05a6eSGreg Roach $gedcom .= "\n1 SOUR " . Webtrees::NAME; 29869c05a6eSGreg Roach $gedcom .= "\n2 NAME " . Webtrees::NAME; 29969c05a6eSGreg Roach $gedcom .= "\n2 VERS " . Webtrees::VERSION; 30069c05a6eSGreg Roach $gedcom .= "\n1 DEST DISKETTE"; 30169c05a6eSGreg Roach $gedcom .= "\n1 DATE " . strtoupper(date('d M Y')); 30269c05a6eSGreg Roach $gedcom .= "\n2 TIME " . date('H:i:s'); 30388a91440SGreg Roach $gedcom .= "\n1 GEDC\n2 VERS 5.5.1\n2 FORM LINEAGE-LINKED"; 30469c05a6eSGreg Roach $gedcom .= "\n1 CHAR " . $encoding; 30569c05a6eSGreg Roach $gedcom .= "\n1 FILE " . $filename; 30669c05a6eSGreg Roach 30769c05a6eSGreg Roach // Preserve some values from the original header 3086b9cb339SGreg Roach $header = Registry::headerFactory()->make('HEAD', $tree) ?? Registry::headerFactory()->new('HEAD', '0 HEAD', null, $tree); 30969c05a6eSGreg Roach 310771780e6SGreg Roach // There should always be a header record. 311771780e6SGreg Roach if ($header instanceof Header) { 31269c05a6eSGreg Roach foreach ($header->facts(['COPR', 'LANG', 'PLAC', 'NOTE']) as $fact) { 31369c05a6eSGreg Roach $gedcom .= "\n" . $fact->gedcom(); 31469c05a6eSGreg Roach } 31569c05a6eSGreg Roach 31669c05a6eSGreg Roach if ($include_sub) { 31769c05a6eSGreg Roach foreach ($header->facts(['SUBM', 'SUBN']) as $fact) { 31869c05a6eSGreg Roach $gedcom .= "\n" . $fact->gedcom(); 31969c05a6eSGreg Roach } 32069c05a6eSGreg Roach } 321771780e6SGreg Roach } 32269c05a6eSGreg Roach 32369c05a6eSGreg Roach return $gedcom; 32469c05a6eSGreg Roach } 32569c05a6eSGreg Roach 32669c05a6eSGreg Roach public function wrapLongLines(string $gedcom, int $max_line_length): string 32769c05a6eSGreg Roach { 32869c05a6eSGreg Roach $lines = []; 32969c05a6eSGreg Roach 33069c05a6eSGreg Roach foreach (explode("\n", $gedcom) as $line) { 33169c05a6eSGreg Roach // Split long lines 33269c05a6eSGreg Roach // The total length of a GEDCOM line, including level number, cross-reference number, 33369c05a6eSGreg Roach // tag, value, delimiters, and terminator, must not exceed 255 (wide) characters. 33469c05a6eSGreg Roach if (mb_strlen($line) > $max_line_length) { 33569c05a6eSGreg Roach [$level, $tag] = explode(' ', $line, 3); 33669c05a6eSGreg Roach if ($tag !== 'CONT') { 33769c05a6eSGreg Roach $level++; 33869c05a6eSGreg Roach } 33969c05a6eSGreg Roach do { 34069c05a6eSGreg Roach // Split after $pos chars 34169c05a6eSGreg Roach $pos = $max_line_length; 34269c05a6eSGreg Roach // Split on a non-space (standard gedcom behavior) 34369c05a6eSGreg Roach while (mb_substr($line, $pos - 1, 1) === ' ') { 34469c05a6eSGreg Roach --$pos; 34569c05a6eSGreg Roach } 34669c05a6eSGreg Roach if ($pos === strpos($line, ' ', 3)) { 34769c05a6eSGreg Roach // No non-spaces in the data! Can’t split it :-( 34869c05a6eSGreg Roach break; 34969c05a6eSGreg Roach } 35069c05a6eSGreg Roach $lines[] = mb_substr($line, 0, $pos); 35169c05a6eSGreg Roach $line = $level . ' CONC ' . mb_substr($line, $pos); 35269c05a6eSGreg Roach } while (mb_strlen($line) > $max_line_length); 35369c05a6eSGreg Roach } 35469c05a6eSGreg Roach $lines[] = $line; 35569c05a6eSGreg Roach } 35669c05a6eSGreg Roach 3571c6adce8SGreg Roach return implode("\n", $lines); 35869c05a6eSGreg Roach } 35969c05a6eSGreg Roach 36069c05a6eSGreg Roach private function familyQuery(Tree $tree, bool $sort_by_xref): Builder 36169c05a6eSGreg Roach { 36269c05a6eSGreg Roach $query = DB::table('families') 36369c05a6eSGreg Roach ->where('f_file', '=', $tree->id()) 364813bb733SGreg Roach ->select(['f_gedcom', 'f_id']); 36569c05a6eSGreg Roach 36669c05a6eSGreg Roach if ($sort_by_xref) { 36769c05a6eSGreg Roach $query 36869c05a6eSGreg Roach ->orderBy(new Expression('LENGTH(f_id)')) 36969c05a6eSGreg Roach ->orderBy('f_id'); 37069c05a6eSGreg Roach } 37169c05a6eSGreg Roach 37269c05a6eSGreg Roach return $query; 37369c05a6eSGreg Roach } 37469c05a6eSGreg Roach 37569c05a6eSGreg Roach private function individualQuery(Tree $tree, bool $sort_by_xref): Builder 37669c05a6eSGreg Roach { 37769c05a6eSGreg Roach $query = DB::table('individuals') 37869c05a6eSGreg Roach ->where('i_file', '=', $tree->id()) 379813bb733SGreg Roach ->select(['i_gedcom', 'i_id']); 38069c05a6eSGreg Roach 38169c05a6eSGreg Roach if ($sort_by_xref) { 38269c05a6eSGreg Roach $query 38369c05a6eSGreg Roach ->orderBy(new Expression('LENGTH(i_id)')) 38469c05a6eSGreg Roach ->orderBy('i_id'); 38569c05a6eSGreg Roach } 38669c05a6eSGreg Roach 38769c05a6eSGreg Roach return $query; 38869c05a6eSGreg Roach } 38969c05a6eSGreg Roach 39069c05a6eSGreg Roach private function sourceQuery(Tree $tree, bool $sort_by_xref): Builder 39169c05a6eSGreg Roach { 39269c05a6eSGreg Roach $query = DB::table('sources') 39369c05a6eSGreg Roach ->where('s_file', '=', $tree->id()) 394813bb733SGreg Roach ->select(['s_gedcom', 's_id']); 39569c05a6eSGreg Roach 39669c05a6eSGreg Roach if ($sort_by_xref) { 39769c05a6eSGreg Roach $query 39869c05a6eSGreg Roach ->orderBy(new Expression('LENGTH(s_id)')) 39969c05a6eSGreg Roach ->orderBy('s_id'); 40069c05a6eSGreg Roach } 40169c05a6eSGreg Roach 40269c05a6eSGreg Roach return $query; 40369c05a6eSGreg Roach } 40469c05a6eSGreg Roach 40569c05a6eSGreg Roach private function mediaQuery(Tree $tree, bool $sort_by_xref): Builder 40669c05a6eSGreg Roach { 40769c05a6eSGreg Roach $query = DB::table('media') 40869c05a6eSGreg Roach ->where('m_file', '=', $tree->id()) 409813bb733SGreg Roach ->select(['m_gedcom', 'm_id']); 41069c05a6eSGreg Roach 41169c05a6eSGreg Roach if ($sort_by_xref) { 41269c05a6eSGreg Roach $query 41369c05a6eSGreg Roach ->orderBy(new Expression('LENGTH(m_id)')) 41469c05a6eSGreg Roach ->orderBy('m_id'); 41569c05a6eSGreg Roach } 41669c05a6eSGreg Roach 41769c05a6eSGreg Roach return $query; 41869c05a6eSGreg Roach } 41969c05a6eSGreg Roach 42069c05a6eSGreg Roach private function otherQuery(Tree $tree, bool $sort_by_xref): Builder 42169c05a6eSGreg Roach { 42269c05a6eSGreg Roach $query = DB::table('other') 42369c05a6eSGreg Roach ->where('o_file', '=', $tree->id()) 42469c05a6eSGreg Roach ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR']) 425813bb733SGreg Roach ->select(['o_gedcom', 'o_id']); 42669c05a6eSGreg Roach 42769c05a6eSGreg Roach if ($sort_by_xref) { 42869c05a6eSGreg Roach $query 42969c05a6eSGreg Roach ->orderBy('o_type') 43069c05a6eSGreg Roach ->orderBy(new Expression('LENGTH(o_id)')) 43169c05a6eSGreg Roach ->orderBy('o_id'); 43269c05a6eSGreg Roach } 43369c05a6eSGreg Roach 43469c05a6eSGreg Roach return $query; 43569c05a6eSGreg Roach } 43669c05a6eSGreg Roach} 437