Completed
Push — master ( 4a8100...5cd281 )
by Greg
07:25
created

TreeService   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 295
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 142
dl 0
loc 295
rs 10
c 0
b 0
f 0
wmc 18

8 Methods

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