xref: /webtrees/app/Services/GedcomExportService.php (revision 771780e647d145a0deff020f601c955b6f89cd2a)
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