Passed
Push — master ( 64d12f...679203 )
by Greg
06:22
created

FunctionsExport::gedcomHeader()   B

Complexity

Conditions 6
Paths 32

Size

Total Lines 55
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 38
c 2
b 0
f 0
nc 32
nop 2
dl 0
loc 55
rs 8.6897

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2019 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 <http://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\Individual;
28
use Fisharebest\Webtrees\Media;
29
use Fisharebest\Webtrees\Source;
30
use Fisharebest\Webtrees\Tree;
31
use Fisharebest\Webtrees\Webtrees;
32
use Illuminate\Database\Capsule\Manager as DB;
33
use Illuminate\Support\Collection;
34
35
use function date;
36
use function explode;
37
use function fwrite;
38
use function pathinfo;
39
use function preg_match;
40
use function preg_replace;
41
use function preg_split;
42
use function str_replace;
43
use function strpos;
44
use function strtolower;
45
use function strtoupper;
46
47
use const PATHINFO_EXTENSION;
48
use const PREG_SPLIT_NO_EMPTY;
49
50
/**
51
 * Class FunctionsExport - common functions
52
 */
53
class FunctionsExport
54
{
55
    /**
56
     * Tidy up a gedcom record on export, for compatibility/portability.
57
     *
58
     * @param string $rec
59
     *
60
     * @return string
61
     */
62
    public static function reformatRecord($rec): string
63
    {
64
        $newrec = '';
65
        foreach (preg_split('/[\r\n]+/', $rec, -1, PREG_SPLIT_NO_EMPTY) as $line) {
66
            // Split long lines
67
            // The total length of a GEDCOM line, including level number, cross-reference number,
68
            // tag, value, delimiters, and terminator, must not exceed 255 (wide) characters.
69
            if (mb_strlen($line) > Gedcom::LINE_LENGTH) {
70
                [$level, $tag] = explode(' ', $line, 3);
71
                if ($tag !== 'CONT' && $tag !== 'CONC') {
72
                    $level++;
73
                }
74
                do {
75
                    // Split after $pos chars
76
                    $pos = Gedcom::LINE_LENGTH;
77
                    // Split on a non-space (standard gedcom behavior)
78
                    while (mb_substr($line, $pos - 1, 1) === ' ') {
79
                        --$pos;
80
                    }
81
                    if ($pos === strpos($line, ' ', 3)) {
82
                        // No non-spaces in the data! Can’t split it :-(
83
                        break;
84
                    }
85
                    $newrec .= mb_substr($line, 0, $pos) . Gedcom::EOL;
86
                    $line   = $level . ' CONC ' . mb_substr($line, $pos);
87
                } while (mb_strlen($line) > Gedcom::LINE_LENGTH);
88
            }
89
            $newrec .= $line . Gedcom::EOL;
90
        }
91
92
        return $newrec;
93
    }
94
95
    /**
96
     * Create a header for a (newly-created or already-imported) gedcom file.
97
     *
98
     * @param Tree   $tree
99
     * @param string $char "UTF-8" or "ANSI"
100
     *
101
     * @return string
102
     */
103
    public static function gedcomHeader(Tree $tree, string $char): string
104
    {
105
        // Force a ".ged" suffix
106
        $filename = $tree->name();
107
108
        if (strtolower(pathinfo($filename, PATHINFO_EXTENSION)) !== 'ged') {
109
            $filename .= '.ged';
110
        }
111
112
        // Default values for a new header
113
        $HEAD = '0 HEAD';
114
        $SOUR = "\n1 SOUR " . Webtrees::NAME . "\n2 NAME " . Webtrees::NAME . "\n2 VERS " . Webtrees::VERSION;
115
        $DEST = "\n1 DEST DISKETTE";
116
        $DATE = "\n1 DATE " . strtoupper(date('d M Y')) . "\n2 TIME " . date('H:i:s');
117
        $GEDC = "\n1 GEDC\n2 VERS 5.5.1\n2 FORM Lineage-Linked";
118
        $CHAR = "\n1 CHAR " . $char;
119
        $FILE = "\n1 FILE " . $filename;
120
        $COPR = '';
121
        $LANG = '';
122
123
        // Preserve some values from the original header
124
        $record = GedcomRecord::getInstance('HEAD', $tree) ?? new GedcomRecord('HEAD', '0 HEAD', null, $tree);
125
        $fact   = $record->facts(['COPR'])->first();
126
        if ($fact instanceof Fact) {
127
            $COPR = "\n1 COPR " . $fact->value();
128
        }
129
        $fact = $record->facts(['LANG'])->first();
130
        if ($fact instanceof Fact) {
131
            $LANG = "\n1 LANG " . $fact->value();
132
        }
133
        // Link to actual SUBM/SUBN records, if they exist
134
        $subn = DB::table('other')
135
            ->where('o_type', '=', 'SUBN')
136
            ->where('o_file', '=', $tree->id())
137
            ->value('o_id');
138
        if ($subn !== null) {
139
            $SUBN = "\n1 SUBN @{$subn}@";
140
        } else {
141
            $SUBN = '';
142
        }
143
144
        $subm = DB::table('other')
145
            ->where('o_type', '=', 'SUBM')
146
            ->where('o_file', '=', $tree->id())
147
            ->value('o_id');
148
        if ($subm !== null) {
149
            $SUBM          = "\n1 SUBM @{$subm}@";
150
            $new_submitter = '';
151
        } else {
152
            // The SUBM record is mandatory
153
            $SUBM          = "\n1 SUBM @SUBM@";
154
            $new_submitter = "\n0 @SUBM@ SUBM\n1 NAME " . Auth::user()->userName(); // The SUBM record is mandatory
155
        }
156
157
        return $HEAD . $SOUR . $DEST . $DATE . $SUBM . $SUBN . $FILE . $COPR . $GEDC . $CHAR . $LANG . $new_submitter . "\n";
158
    }
159
160
    /**
161
     * Prepend the GEDCOM_MEDIA_PATH to media filenames.
162
     *
163
     * @param string $rec
164
     * @param string $path
165
     *
166
     * @return string
167
     */
168
    private static function convertMediaPath($rec, $path): string
169
    {
170
        if ($path && preg_match('/\n1 FILE (.+)/', $rec, $match)) {
171
            $old_file_name = $match[1];
172
            // Don’t modify external links
173
            if (strpos($old_file_name, '://') === false) {
174
                // Adding a windows path? Convert the slashes.
175
                if (strpos($path, '\\') !== false) {
176
                    $new_file_name = preg_replace('~/+~', '\\', $old_file_name);
177
                } else {
178
                    $new_file_name = $old_file_name;
179
                }
180
                // Path not present - add it.
181
                if (strpos($new_file_name, $path) === false) {
182
                    $new_file_name = $path . $new_file_name;
183
                }
184
                $rec = str_replace("\n1 FILE " . $old_file_name, "\n1 FILE " . $new_file_name, $rec);
185
            }
186
        }
187
188
        return $rec;
189
    }
190
191
    /**
192
     * Export the database in GEDCOM format
193
     *
194
     * @param Tree     $tree         Which tree to export
195
     * @param resource $stream       Handle to a writable stream
196
     * @param int      $access_level Apply privacy filters
197
     * @param string   $media_path   Add this prefix to media file names
198
     * @param string   $encoding     UTF-8 or ANSI
199
     *
200
     * @return void
201
     */
202
    public static function exportGedcom(Tree $tree, $stream, int $access_level, string $media_path, string $encoding): void
203
    {
204
        $header = new Collection([self::gedcomHeader($tree, $encoding)]);
205
206
        // Generate the OBJE/SOUR/REPO/NOTE records first, as their privacy calcualations involve
207
        // database queries, and we wish to avoid large gaps between queries due to MySQL connection timeouts.
208
        $media = DB::table('media')
209
            ->where('m_file', '=', $tree->id())
210
            ->orderBy('m_id')
211
            ->get()
212
            ->map(Media::rowMapper($tree))
213
            ->map(static function (Media $record) use ($access_level): string {
214
                return $record->privatizeGedcom($access_level);
215
            })
216
            ->map(static function (string $gedcom) use ($media_path): string {
217
                return self::convertMediaPath($gedcom, $media_path);
218
            });
219
220
        $sources = DB::table('sources')
221
            ->where('s_file', '=', $tree->id())
222
            ->orderBy('s_id')
223
            ->get()
224
            ->map(Source::rowMapper($tree))
225
            ->map(static function (Source $record) use ($access_level): string {
226
                return $record->privatizeGedcom($access_level);
227
            });
228
229
        $other = DB::table('other')
230
            ->where('o_file', '=', $tree->id())
231
            ->whereNotIn('o_type', ['HEAD', 'TRLR'])
232
            ->orderBy('o_id')
233
            ->get()
234
            ->map(GedcomRecord::rowMapper($tree))
235
            ->map(static function (GedcomRecord $record) use ($access_level): string {
236
                return $record->privatizeGedcom($access_level);
237
            });
238
239
        $individuals = DB::table('individuals')
240
            ->where('i_file', '=', $tree->id())
241
            ->orderBy('i_id')
242
            ->get()
243
            ->map(Individual::rowMapper($tree))
244
            ->map(static function (Individual $record) use ($access_level): string {
245
                return $record->privatizeGedcom($access_level);
246
            });
247
248
        $families = DB::table('families')
249
            ->where('f_file', '=', $tree->id())
250
            ->orderBy('f_id')
251
            ->get()
252
            ->map(Family::rowMapper($tree))
253
            ->map(static function (Family $record) use ($access_level): string {
254
                return $record->privatizeGedcom($access_level);
255
            });
256
257
        $trailer = new Collection(['0 TRLR' . Gedcom::EOL]);
258
259
        $records = $header
260
            ->merge($media)
261
            ->merge($sources)
262
            ->merge($other)
263
            ->merge($individuals)
264
            ->merge($families)
265
            ->merge($trailer)
266
            ->map(static function (string $gedcom) use ($encoding): string {
267
                return $encoding === 'ANSI' ? utf8_decode($gedcom) : $gedcom;
268
            })
269
            ->map(static function (string $gedcom): string {
270
                return self::reformatRecord($gedcom);
271
            });
272
273
        fwrite($stream, $records->implode(''));
274
    }
275
}
276