Issues (120)

app/Functions/FunctionsExport.php (1 issue)

Labels
Severity
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2022 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
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Functions;
21
22
use Fisharebest\Webtrees\Auth;
23
use Fisharebest\Webtrees\Fact;
24
use Fisharebest\Webtrees\Family;
25
use Fisharebest\Webtrees\Gedcom;
26
use Fisharebest\Webtrees\GedcomRecord;
27
use Fisharebest\Webtrees\Header;
28
use Fisharebest\Webtrees\Individual;
29
use Fisharebest\Webtrees\Media;
30
use Fisharebest\Webtrees\Registry;
31
use Fisharebest\Webtrees\Source;
32
use Fisharebest\Webtrees\Tree;
33
use Fisharebest\Webtrees\Webtrees;
34
use Illuminate\Database\Capsule\Manager as DB;
35
use Illuminate\Support\Collection;
36
37
use function date;
38
use function explode;
39
use function fwrite;
40
use function pathinfo;
41
use function preg_match;
42
use function preg_replace;
43
use function preg_split;
44
use function str_contains;
45
use function str_replace;
46
use function strpos;
47
use function strtolower;
48
use function strtoupper;
49
50
use const PATHINFO_EXTENSION;
51
use const PREG_SPLIT_NO_EMPTY;
52
53
/**
54
 * Class FunctionsExport - common functions
55
 *
56
 * @deprecated since 2.0.5.  Will be removed in 2.1.0
57
 */
58
class FunctionsExport
59
{
60
    /**
61
     * Tidy up a gedcom record on export, for compatibility/portability.
62
     *
63
     * @param string $rec
64
     *
65
     * @return string
66
     */
67
    public static function reformatRecord(string $rec): string
68
    {
69
        $newrec = '';
70
        foreach (preg_split('/[\r\n]+/', $rec, -1, PREG_SPLIT_NO_EMPTY) as $line) {
71
            // Split long lines
72
            // The total length of a GEDCOM line, including level number, cross-reference number,
73
            // tag, value, delimiters, and terminator, must not exceed 255 (wide) characters.
74
            if (mb_strlen($line) > Gedcom::LINE_LENGTH) {
75
                [$level, $tag] = explode(' ', $line, 3);
76
                if ($tag !== 'CONT' && $tag !== 'CONC') {
77
                    $level++;
78
                }
79
                do {
80
                    // Split after $pos chars
81
                    $pos = Gedcom::LINE_LENGTH;
82
                    // Split on a non-space (standard gedcom behavior)
83
                    while (mb_substr($line, $pos - 1, 1) === ' ') {
84
                        --$pos;
85
                    }
86
                    if ($pos === strpos($line, ' ', 3)) {
87
                        // No non-spaces in the data! Can’t split it :-(
88
                        break;
89
                    }
90
                    $newrec .= mb_substr($line, 0, $pos) . Gedcom::EOL;
91
                    $line   = $level . ' CONC ' . mb_substr($line, $pos);
92
                } while (mb_strlen($line) > Gedcom::LINE_LENGTH);
93
            }
94
            $newrec .= $line . Gedcom::EOL;
95
        }
96
97
        return $newrec;
98
    }
99
100
    /**
101
     * Create a header for a (newly-created or already-imported) gedcom file.
102
     *
103
     * @param Tree   $tree
104
     * @param string $char "UTF-8" or "ANSI"
105
     *
106
     * @return string
107
     */
108
    public static function gedcomHeader(Tree $tree, string $char): string
109
    {
110
        // Force a ".ged" suffix
111
        $filename = $tree->name();
112
113
        if (strtolower(pathinfo($filename, PATHINFO_EXTENSION)) !== 'ged') {
0 ignored issues
show
It seems like pathinfo($filename, PATHINFO_EXTENSION) can also be of type array; however, parameter $string of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

113
        if (strtolower(/** @scrutinizer ignore-type */ pathinfo($filename, PATHINFO_EXTENSION)) !== 'ged') {
Loading history...
114
            $filename .= '.ged';
115
        }
116
117
        $today = strtoupper(date('d M Y'));
118
        $now   = date('H:i:s');
119
120
        // Default values for a new header
121
        $HEAD = '0 HEAD';
122
        $SOUR = "\n1 SOUR " . Webtrees::NAME . "\n2 NAME " . Webtrees::NAME . "\n2 VERS " . Webtrees::VERSION;
123
        $DEST = "\n1 DEST DISKETTE";
124
        $DATE = "\n1 DATE " . $today . "\n2 TIME " . $now;
125
        $GEDC = "\n1 GEDC\n2 VERS 5.5.1\n2 FORM Lineage-Linked";
126
        $CHAR = "\n1 CHAR " . $char;
127
        $FILE = "\n1 FILE " . $filename;
128
        $COPR = '';
129
        $LANG = '';
130
131
        // Preserve some values from the original header
132
        $header = Registry::headerFactory()->make('HEAD', $tree) ?? Registry::headerFactory()->new('HEAD', '0 HEAD', null, $tree);
133
134
        $fact   = $header->facts(['COPR'])->first();
135
136
        if ($fact instanceof Fact) {
137
            $COPR = "\n1 COPR " . $fact->value();
138
        }
139
140
        $fact = $header->facts(['LANG'])->first();
141
142
        if ($fact instanceof Fact) {
143
            $LANG = "\n1 LANG " . $fact->value();
144
        }
145
146
        // Link to actual SUBM/SUBN records, if they exist
147
        $subn = DB::table('other')
148
            ->where('o_type', '=', 'SUBN')
149
            ->where('o_file', '=', $tree->id())
150
            ->value('o_id');
151
        if ($subn !== null) {
152
            $SUBN = "\n1 SUBN @{$subn}@";
153
        } else {
154
            $SUBN = '';
155
        }
156
157
        $subm = DB::table('other')
158
            ->where('o_type', '=', 'SUBM')
159
            ->where('o_file', '=', $tree->id())
160
            ->value('o_id');
161
        if ($subm !== null) {
162
            $SUBM          = "\n1 SUBM @{$subm}@";
163
            $new_submitter = '';
164
        } else {
165
            // The SUBM record is mandatory
166
            $SUBM          = "\n1 SUBM @SUBM@";
167
            $new_submitter = "\n0 @SUBM@ SUBM\n1 NAME " . Auth::user()->userName(); // The SUBM record is mandatory
168
        }
169
170
        return $HEAD . $SOUR . $DEST . $DATE . $SUBM . $SUBN . $FILE . $COPR . $GEDC . $CHAR . $LANG . $new_submitter . "\n";
171
    }
172
173
    /**
174
     * Prepend the GEDCOM_MEDIA_PATH to media filenames.
175
     *
176
     * @param string $rec
177
     * @param string $path
178
     *
179
     * @return string
180
     */
181
    private static function convertMediaPath(string $rec, string $path): string
182
    {
183
        if ($path && preg_match('/\n1 FILE (.+)/', $rec, $match)) {
184
            $old_file_name = $match[1];
185
            // Don’t modify external links
186
            if (!str_contains($old_file_name, '://')) {
187
                // Adding a windows path? Convert the slashes.
188
                if (str_contains($path, '\\')) {
189
                    $new_file_name = preg_replace('~/+~', '\\', $old_file_name);
190
                } else {
191
                    $new_file_name = $old_file_name;
192
                }
193
                // Path not present - add it.
194
                if (!str_contains($new_file_name, $path)) {
195
                    $new_file_name = $path . $new_file_name;
196
                }
197
                $rec = str_replace("\n1 FILE " . $old_file_name, "\n1 FILE " . $new_file_name, $rec);
198
            }
199
        }
200
201
        return $rec;
202
    }
203
204
    /**
205
     * Export the database in GEDCOM format
206
     *
207
     * @param Tree     $tree         Which tree to export
208
     * @param resource $stream       Handle to a writable stream
209
     * @param int      $access_level Apply privacy filters
210
     * @param string   $media_path   Add this prefix to media file names
211
     * @param string   $encoding     UTF-8 or ANSI
212
     *
213
     * @return void
214
     */
215
    public static function exportGedcom(Tree $tree, $stream, int $access_level, string $media_path, string $encoding): void
216
    {
217
        $header = new Collection([self::gedcomHeader($tree, $encoding)]);
218
219
        // Generate the OBJE/SOUR/REPO/NOTE records first, as their privacy calculations involve
220
        // database queries, and we wish to avoid large gaps between queries due to MySQL connection timeouts.
221
        $media = DB::table('media')
222
            ->where('m_file', '=', $tree->id())
223
            ->orderBy('m_id')
224
            ->get()
225
            ->map(Registry::mediaFactory()->mapper($tree))
226
            ->map(static function (Media $record) use ($access_level): string {
227
                return $record->privatizeGedcom($access_level);
228
            })
229
            ->map(static function (string $gedcom) use ($media_path): string {
230
                return self::convertMediaPath($gedcom, $media_path);
231
            });
232
233
        $sources = DB::table('sources')
234
            ->where('s_file', '=', $tree->id())
235
            ->orderBy('s_id')
236
            ->get()
237
            ->map(Registry::sourceFactory()->mapper($tree))
238
            ->map(static function (Source $record) use ($access_level): string {
239
                return $record->privatizeGedcom($access_level);
240
            });
241
242
        $other = DB::table('other')
243
            ->where('o_file', '=', $tree->id())
244
            ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR'])
245
            ->orderBy('o_id')
246
            ->get()
247
            ->map(Registry::gedcomRecordFactory()->mapper($tree))
248
            ->map(static function (GedcomRecord $record) use ($access_level): string {
249
                return $record->privatizeGedcom($access_level);
250
            });
251
252
        $individuals = DB::table('individuals')
253
            ->where('i_file', '=', $tree->id())
254
            ->orderBy('i_id')
255
            ->get()
256
            ->map(Registry::individualFactory()->mapper($tree))
257
            ->map(static function (Individual $record) use ($access_level): string {
258
                return $record->privatizeGedcom($access_level);
259
            });
260
261
        $families = DB::table('families')
262
            ->where('f_file', '=', $tree->id())
263
            ->orderBy('f_id')
264
            ->get()
265
            ->map(Registry::familyFactory()->mapper($tree))
266
            ->map(static function (Family $record) use ($access_level): string {
267
                return $record->privatizeGedcom($access_level);
268
            });
269
270
        $trailer = new Collection(['0 TRLR' . Gedcom::EOL]);
271
272
        $records = $header
273
            ->merge($media)
274
            ->merge($sources)
275
            ->merge($other)
276
            ->merge($individuals)
277
            ->merge($families)
278
            ->merge($trailer)
279
            ->map(static function (string $gedcom) use ($encoding): string {
280
                return $encoding === 'ANSI' ? utf8_decode($gedcom) : $gedcom;
281
            })
282
            ->map(static function (string $gedcom): string {
283
                return self::reformatRecord($gedcom);
284
            });
285
286
        fwrite($stream, $records->implode(''));
287
    }
288
}
289