Passed
Push — dev ( 824cd4...d410ef )
by Greg
12:51
created

TreeService   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 311
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 148
dl 0
loc 311
rs 10
c 0
b 0
f 0
wmc 15

9 Methods

Rating   Name   Duplication   Size   Complexity  
A titles() 0 5 1
A uniqueTreeName() 0 10 2
A create() 0 52 1
A all() 0 61 2
A find() 0 9 1
A importGedcomFile() 0 39 3
A deleteGenealogyData() 0 20 2
A delete() 0 24 2
A __construct() 0 3 1
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2021 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\Services;
21
22
use Fisharebest\Webtrees\Auth;
23
use Fisharebest\Webtrees\Contracts\UserInterface;
24
use Fisharebest\Webtrees\GedcomFilters\GedcomEncodingFilter;
25
use Fisharebest\Webtrees\I18N;
26
use Fisharebest\Webtrees\Registry;
27
use Fisharebest\Webtrees\Site;
28
use Fisharebest\Webtrees\Tree;
29
use Illuminate\Database\Capsule\Manager as DB;
30
use Illuminate\Database\Query\Builder;
31
use Illuminate\Database\Query\Expression;
32
use Illuminate\Database\Query\JoinClause;
33
use Illuminate\Support\Collection;
34
use Psr\Http\Message\StreamInterface;
35
use RuntimeException;
36
37
use function assert;
38
use function fclose;
39
use function feof;
40
use function fread;
41
use function max;
42
use function stream_filter_append;
43
use function strrpos;
44
use function substr;
45
46
use const STREAM_FILTER_READ;
47
48
/**
49
 * Tree management and queries.
50
 */
51
class TreeService
52
{
53
    // The most likely surname tradition for a given language.
54
    private const DEFAULT_SURNAME_TRADITIONS = [
55
        'es'    => 'spanish',
56
        'is'    => 'icelandic',
57
        'lt'    => 'lithuanian',
58
        'pl'    => 'polish',
59
        'pt'    => 'portuguese',
60
        'pt-BR' => 'portuguese',
61
    ];
62
63
    private GedcomImportService $gedcom_import_service;
64
65
    /**
66
     * @param GedcomImportService $gedcom_import_service
67
     */
68
    public function __construct(GedcomImportService $gedcom_import_service)
69
    {
70
        $this->gedcom_import_service = $gedcom_import_service;
71
    }
72
73
    /**
74
     * All the trees that the current user has permission to access.
75
     *
76
     * @return Collection<array-key,Tree>
77
     */
78
    public function all(): Collection
79
    {
80
        return Registry::cache()->array()->remember('all-trees', static function (): Collection {
81
            // All trees
82
            $query = DB::table('gedcom')
83
                ->leftJoin('gedcom_setting', static function (JoinClause $join): void {
84
                    $join->on('gedcom_setting.gedcom_id', '=', 'gedcom.gedcom_id')
85
                        ->where('gedcom_setting.setting_name', '=', 'title');
86
                })
87
                ->where('gedcom.gedcom_id', '>', 0)
88
                ->select([
89
                    'gedcom.gedcom_id AS tree_id',
90
                    'gedcom.gedcom_name AS tree_name',
91
                    'gedcom_setting.setting_value AS tree_title',
92
                ])
93
                ->orderBy('gedcom.sort_order')
94
                ->orderBy('gedcom_setting.setting_value');
95
96
            // Non-admins may not see all trees
97
            if (!Auth::isAdmin()) {
98
                $query
99
                    ->join('gedcom_setting AS gs2', static function (JoinClause $join): void {
100
                        $join
101
                            ->on('gs2.gedcom_id', '=', 'gedcom.gedcom_id')
102
                            ->where('gs2.setting_name', '=', 'imported');
103
                    })
104
                    ->join('gedcom_setting AS gs3', static function (JoinClause $join): void {
105
                        $join
106
                            ->on('gs3.gedcom_id', '=', 'gedcom.gedcom_id')
107
                            ->where('gs3.setting_name', '=', 'REQUIRE_AUTHENTICATION');
108
                    })
109
                    ->leftJoin('user_gedcom_setting', static function (JoinClause $join): void {
110
                        $join
111
                            ->on('user_gedcom_setting.gedcom_id', '=', 'gedcom.gedcom_id')
112
                            ->where('user_gedcom_setting.user_id', '=', Auth::id())
113
                            ->where('user_gedcom_setting.setting_name', '=', UserInterface::PREF_TREE_ROLE);
114
                    })
115
                    ->where(static function (Builder $query): void {
116
                        $query
117
                            // Managers
118
                            ->where('user_gedcom_setting.setting_value', '=', UserInterface::ROLE_MANAGER)
119
                            // Members
120
                            ->orWhere(static function (Builder $query): void {
121
                                $query
122
                                    ->where('gs2.setting_value', '=', '1')
123
                                    ->where('gs3.setting_value', '=', '1')
124
                                    ->where('user_gedcom_setting.setting_value', '<>', UserInterface::ROLE_VISITOR);
125
                            })
126
                            // Public trees
127
                            ->orWhere(static function (Builder $query): void {
128
                                $query
129
                                    ->where('gs2.setting_value', '=', '1')
130
                                    ->where('gs3.setting_value', '<>', '1');
131
                            });
132
                    });
133
            }
134
135
            return $query
136
                ->get()
137
                ->mapWithKeys(static function (object $row): array {
138
                    return [$row->tree_name => Tree::rowMapper()($row)];
139
                });
140
        });
141
    }
142
143
    /**
144
     * Find a tree by its ID.
145
     *
146
     * @param int $id
147
     *
148
     * @return Tree
149
     */
150
    public function find(int $id): Tree
151
    {
152
        $tree = $this->all()->first(static function (Tree $tree) use ($id): bool {
153
            return $tree->id() === $id;
154
        });
155
156
        assert($tree instanceof Tree, new RuntimeException());
157
158
        return $tree;
159
    }
160
161
    /**
162
     * All trees, name => title
163
     *
164
     * @return array<string>
165
     */
166
    public function titles(): array
167
    {
168
        return $this->all()->map(static function (Tree $tree): string {
169
            return $tree->title();
170
        })->all();
171
    }
172
173
    /**
174
     * @param string $name
175
     * @param string $title
176
     *
177
     * @return Tree
178
     */
179
    public function create(string $name, string $title): Tree
180
    {
181
        DB::table('gedcom')->insert([
182
            'gedcom_name' => $name,
183
        ]);
184
185
        $tree_id = (int) DB::connection()->getPdo()->lastInsertId();
186
187
        $tree = new Tree($tree_id, $name, $title);
188
189
        $tree->setPreference('imported', '1');
190
        $tree->setPreference('title', $title);
191
192
        // Set preferences from default tree
193
        (new Builder(DB::connection()))->from('gedcom_setting')->insertUsing(
194
            ['gedcom_id', 'setting_name', 'setting_value'],
195
            static function (Builder $query) use ($tree_id): void {
196
                $query
197
                    ->select([new Expression($tree_id), 'setting_name', 'setting_value'])
198
                    ->from('gedcom_setting')
199
                    ->where('gedcom_id', '=', -1);
200
            }
201
        );
202
203
        (new Builder(DB::connection()))->from('default_resn')->insertUsing(
204
            ['gedcom_id', 'tag_type', 'resn'],
205
            static function (Builder $query) use ($tree_id): void {
206
                $query
207
                    ->select([new Expression($tree_id), 'tag_type', 'resn'])
208
                    ->from('default_resn')
209
                    ->where('gedcom_id', '=', -1);
210
            }
211
        );
212
213
        // Gedcom and privacy settings
214
        $tree->setPreference('REQUIRE_AUTHENTICATION', '');
215
        $tree->setPreference('CONTACT_USER_ID', (string) Auth::id());
216
        $tree->setPreference('WEBMASTER_USER_ID', (string) Auth::id());
217
        $tree->setPreference('LANGUAGE', I18N::languageTag()); // Default to the current admin’s language
218
        $tree->setPreference('SURNAME_TRADITION', self::DEFAULT_SURNAME_TRADITIONS[I18N::languageTag()] ?? 'paternal');
219
220
        // A tree needs at least one record.
221
        $head = "0 HEAD\n1 SOUR webtrees\n2 DEST webtrees\n1 GEDC\n2 VERS 5.5.1\n2 FORM LINEAGE-LINKED\n1 CHAR UTF-8";
222
        $this->gedcom_import_service->importRecord($head, $tree, true);
223
224
        // I18N: This should be a common/default/placeholder name of an individual. Put slashes around the surname.
225
        $name = I18N::translate('John /DOE/');
226
        $note = I18N::translate('Edit this individual and replace their details with your own.');
227
        $indi = "0 @X1@ INDI\n1 NAME " . $name . "\n1 SEX M\n1 BIRT\n2 DATE 01 JAN 1850\n2 NOTE " . $note;
228
        $this->gedcom_import_service->importRecord($indi, $tree, true);
229
230
        return $tree;
231
    }
232
233
    /**
234
     * Import data from a gedcom file into this tree.
235
     *
236
     * @param Tree            $tree
237
     * @param StreamInterface $stream   The GEDCOM file.
238
     * @param string          $filename The preferred filename, for export/download.
239
     * @param string          $encoding Override the encoding specified in the header.
240
     *
241
     * @return void
242
     */
243
    public function importGedcomFile(Tree $tree, StreamInterface $stream, string $filename, string $encoding): void
244
    {
245
        // Read the file in blocks of roughly 64K. Ensure that each block
246
        // contains complete gedcom records. This will ensure we don’t split
247
        // multi-byte characters, as well as simplifying the code to import
248
        // each block.
249
250
        $file_data = '';
251
252
        $tree->setPreference('gedcom_filename', $filename);
253
        $tree->setPreference('imported', '0');
254
255
        DB::table('gedcom_chunk')->where('gedcom_id', '=', $tree->id())->delete();
256
257
        $stream = $stream->detach();
258
259
        // Convert to UTF-8.
260
        stream_filter_append($stream, GedcomEncodingFilter::class, STREAM_FILTER_READ, ['src_encoding' => $encoding]);
261
262
        while (!feof($stream)) {
263
            $file_data .= fread($stream, 65536);
264
            $eol_pos = max((int) strrpos($file_data, "\r0"), (int) strrpos($file_data, "\n0"));
265
266
            if ($eol_pos > 0) {
267
                DB::table('gedcom_chunk')->insert([
268
                    'gedcom_id'  => $tree->id(),
269
                    'chunk_data' => substr($file_data, 0, $eol_pos + 1),
270
                ]);
271
272
                $file_data = substr($file_data, $eol_pos + 1);
273
            }
274
        }
275
276
        DB::table('gedcom_chunk')->insert([
277
            'gedcom_id'  => $tree->id(),
278
            'chunk_data' => $file_data,
279
        ]);
280
281
        fclose($stream);
282
    }
283
284
    /**
285
     * @param Tree $tree
286
     */
287
    public function delete(Tree $tree): void
288
    {
289
        // If this is the default tree, then unset it
290
        if (Site::getPreference('DEFAULT_GEDCOM') === $tree->name()) {
291
            Site::setPreference('DEFAULT_GEDCOM', '');
292
        }
293
294
        DB::table('gedcom_chunk')->where('gedcom_id', '=', $tree->id())->delete();
295
296
        $this->deleteGenealogyData($tree, false);
297
298
        DB::table('block_setting')
299
            ->join('block', 'block.block_id', '=', 'block_setting.block_id')
300
            ->where('gedcom_id', '=', $tree->id())
301
            ->delete();
302
        DB::table('block')->where('gedcom_id', '=', $tree->id())->delete();
303
        DB::table('user_gedcom_setting')->where('gedcom_id', '=', $tree->id())->delete();
304
        DB::table('gedcom_setting')->where('gedcom_id', '=', $tree->id())->delete();
305
        DB::table('module_privacy')->where('gedcom_id', '=', $tree->id())->delete();
306
        DB::table('hit_counter')->where('gedcom_id', '=', $tree->id())->delete();
307
        DB::table('default_resn')->where('gedcom_id', '=', $tree->id())->delete();
308
        DB::table('gedcom_chunk')->where('gedcom_id', '=', $tree->id())->delete();
309
        DB::table('log')->where('gedcom_id', '=', $tree->id())->delete();
310
        DB::table('gedcom')->where('gedcom_id', '=', $tree->id())->delete();
311
    }
312
313
    /**
314
     * Delete all the genealogy data from a tree - in preparation for importing
315
     * new data. Optionally retain the media data, for when the user has been
316
     * editing their data offline using an application which deletes (or does not
317
     * support) media data.
318
     *
319
     * @param Tree $tree
320
     * @param bool $keep_media
321
     *
322
     * @return void
323
     */
324
    public function deleteGenealogyData(Tree $tree, bool $keep_media): void
325
    {
326
        DB::table('individuals')->where('i_file', '=', $tree->id())->delete();
327
        DB::table('families')->where('f_file', '=', $tree->id())->delete();
328
        DB::table('sources')->where('s_file', '=', $tree->id())->delete();
329
        DB::table('other')->where('o_file', '=', $tree->id())->delete();
330
        DB::table('places')->where('p_file', '=', $tree->id())->delete();
331
        DB::table('placelinks')->where('pl_file', '=', $tree->id())->delete();
332
        DB::table('name')->where('n_file', '=', $tree->id())->delete();
333
        DB::table('dates')->where('d_file', '=', $tree->id())->delete();
334
        DB::table('change')->where('gedcom_id', '=', $tree->id())->delete();
335
336
        if ($keep_media) {
337
            DB::table('link')->where('l_file', '=', $tree->id())
338
                ->where('l_type', '<>', 'OBJE')
339
                ->delete();
340
        } else {
341
            DB::table('link')->where('l_file', '=', $tree->id())->delete();
342
            DB::table('media_file')->where('m_file', '=', $tree->id())->delete();
343
            DB::table('media')->where('m_file', '=', $tree->id())->delete();
344
        }
345
    }
346
347
    /**
348
     * Generate a unique name for a new tree.
349
     *
350
     * @return string
351
     */
352
    public function uniqueTreeName(): string
353
    {
354
        $name   = 'tree';
355
        $number = 1;
356
357
        while ($this->all()->get($name . $number) instanceof Tree) {
358
            $number++;
359
        }
360
361
        return $name . $number;
362
    }
363
}
364