Issues (2558)

app/Services/AdminService.php (12 issues)

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2025 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\DB;
0 ignored issues
show
The type Fisharebest\Webtrees\DB was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
23
use Fisharebest\Webtrees\Encodings\UTF8;
0 ignored issues
show
The type Fisharebest\Webtrees\Encodings\UTF8 was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
24
use Fisharebest\Webtrees\Family;
0 ignored issues
show
The type Fisharebest\Webtrees\Family was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
25
use Fisharebest\Webtrees\GedcomRecord;
0 ignored issues
show
The type Fisharebest\Webtrees\GedcomRecord was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
26
use Fisharebest\Webtrees\Header;
0 ignored issues
show
The type Fisharebest\Webtrees\Header was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
27
use Fisharebest\Webtrees\I18N;
0 ignored issues
show
The type Fisharebest\Webtrees\I18N was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
28
use Fisharebest\Webtrees\Individual;
0 ignored issues
show
The type Fisharebest\Webtrees\Individual was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
29
use Fisharebest\Webtrees\Media;
0 ignored issues
show
The type Fisharebest\Webtrees\Media was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
30
use Fisharebest\Webtrees\Registry;
31
use Fisharebest\Webtrees\Site;
32
use Fisharebest\Webtrees\Source;
0 ignored issues
show
The type Fisharebest\Webtrees\Source was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
33
use Fisharebest\Webtrees\Tree;
34
use Illuminate\Database\Query\Expression;
35
use Illuminate\Database\Query\JoinClause;
36
use Illuminate\Support\Collection;
37
use League\Flysystem\FilesystemException;
38
use League\Flysystem\FilesystemOperator;
39
use League\Flysystem\StorageAttributes;
40
41
use function array_map;
42
use function array_unique;
43
use function explode;
44
use function fclose;
45
use function fread;
46
use function implode;
47
use function preg_match;
48
use function sort;
49
50
/**
51
 * Utilities for the control panel.
52
 */
53
class AdminService
54
{
55
    /**
56
     * Count of XREFs used by two trees at the same time.
57
     *
58
     * @param Tree $tree1
59
     * @param Tree $tree2
60
     *
61
     * @return int
62
     */
63
    public function countCommonXrefs(Tree $tree1, Tree $tree2): int
64
    {
65
        $subquery1 = DB::table('change')
66
            ->where('gedcom_id', '=', $tree1->id())
67
            ->select(['xref AS xref1'])
68
            ->union(DB::table('individuals')
69
                ->where('i_file', '=', $tree1->id())
70
                ->select(['i_id AS xref']))
71
            ->union(DB::table('families')
72
                ->where('f_file', '=', $tree1->id())
73
                ->select(['f_id AS xref']))
74
            ->union(DB::table('sources')
75
                ->where('s_file', '=', $tree1->id())
76
                ->select(['s_id AS xref']))
77
            ->union(DB::table('media')
78
                ->where('m_file', '=', $tree1->id())
79
                ->select(['m_id AS xref']))
80
            ->union(DB::table('other')
81
                ->where('o_file', '=', $tree1->id())
82
                ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR'])
83
                ->select(['o_id AS xref']));
84
85
        $subquery2 = DB::table('change')
86
            ->where('gedcom_id', '=', $tree2->id())
87
            ->select(['xref AS xref2'])
88
            ->union(DB::table('individuals')
89
                ->where('i_file', '=', $tree2->id())
90
                ->select(['i_id AS xref']))
91
            ->union(DB::table('families')
92
                ->where('f_file', '=', $tree2->id())
93
                ->select(['f_id AS xref']))
94
            ->union(DB::table('sources')
95
                ->where('s_file', '=', $tree2->id())
96
                ->select(['s_id AS xref']))
97
            ->union(DB::table('media')
98
                ->where('m_file', '=', $tree2->id())
99
                ->select(['m_id AS xref']))
100
            ->union(DB::table('other')
101
                ->where('o_file', '=', $tree2->id())
102
                ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR'])
103
                ->select(['o_id AS xref']));
104
105
        return DB::query()
106
            ->fromSub($subquery1, 'sub1')
107
            ->joinSub($subquery2, 'sub2', 'xref1', '=', 'xref2')
108
            ->count();
109
    }
110
111
    /**
112
     * @param Tree $tree
113
     *
114
     * @return array<string,array<int,array<int,GedcomRecord>>>
115
     */
116
    public function duplicateRecords(Tree $tree): array
117
    {
118
        // We can't do any reasonable checks using MySQL.
119
        // Will need to wait for a "repositories" table.
120
        $repositories = [];
121
122
        $sources = DB::table('sources')
123
            ->where('s_file', '=', $tree->id())
124
            ->groupBy(['s_name'])
125
            ->having(new Expression('COUNT(s_id)'), '>', '1')
0 ignored issues
show
'COUNT(s_id)' of type string is incompatible with the type Illuminate\Database\Query\TValue expected by parameter $value of Illuminate\Database\Quer...pression::__construct(). ( Ignorable by Annotation )

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

125
            ->having(new Expression(/** @scrutinizer ignore-type */ 'COUNT(s_id)'), '>', '1')
Loading history...
126
            ->select([new Expression(DB::groupConcat('s_id') . ' AS xrefs')])
127
            ->orderBy('xrefs')
128
            ->pluck('xrefs')
129
            ->map(static fn (string $xrefs): array => array_map(static fn (string $xref): Source => Registry::sourceFactory()->make($xref, $tree), explode(',', $xrefs)))
130
            ->all();
131
132
        // Database agnostic way to do GROUP_CONCAT(DISTINCT x ORDER BY x)
133
        $distinct_order_by = static function (string $xrefs): string {
134
            $array = explode(',', $xrefs);
135
            sort($array);
136
137
            return implode(',', array_unique($array));
138
        };
139
140
        $individuals = DB::table('dates')
141
            ->join('name', static function (JoinClause $join): void {
142
                $join
143
                    ->on('d_file', '=', 'n_file')
144
                    ->on('d_gid', '=', 'n_id');
145
            })
146
            ->where('d_file', '=', $tree->id())
147
            ->whereIn('d_fact', ['BIRT', 'CHR', 'BAPM', 'DEAT', 'BURI'])
148
            ->groupBy(['d_year', 'd_month', 'd_day', 'd_type', 'd_fact', 'n_type', 'n_full'])
149
            ->having(new Expression('COUNT(DISTINCT d_gid)'), '>', '1')
150
            ->select([new Expression(DB::groupConcat('d_gid') . ' AS xrefs')])
151
            ->orderBy('xrefs')
152
            ->pluck('xrefs')
153
            ->map($distinct_order_by)
154
            ->unique()
155
            ->map(static fn (string $xrefs): array => array_map(static fn (string $xref): Individual => Registry::individualFactory()->make($xref, $tree), explode(',', $xrefs)))
156
            ->all();
157
158
        $families = DB::table('families')
159
            ->where('f_file', '=', $tree->id())
160
            ->groupBy([new Expression('LEAST(f_husb, f_wife)')])
161
            ->groupBy([new Expression('GREATEST(f_husb, f_wife)')])
162
            ->having(new Expression('COUNT(f_id)'), '>', '1')
163
            ->select([new Expression(DB::groupConcat('f_id') . ' AS xrefs')])
164
            ->orderBy('xrefs')
165
            ->pluck('xrefs')
166
            ->map(static fn (string $xrefs): array => array_map(static fn (string $xref): Family => Registry::familyFactory()->make($xref, $tree), explode(',', $xrefs)))
167
            ->all();
168
169
        $media = DB::table('media_file')
170
            ->where('m_file', '=', $tree->id())
171
            ->where('descriptive_title', '<>', '')
172
            ->groupBy(['descriptive_title'])
173
            ->having(new Expression('COUNT(DISTINCT m_id)'), '>', '1')
174
            ->select([new Expression(DB::groupConcat('m_id') . ' AS xrefs')])
175
            ->orderBy('xrefs')
176
            ->pluck('xrefs')
177
            ->map(static fn (string $xrefs): array => array_map(static fn (string $xref): Media => Registry::mediaFactory()->make($xref, $tree), explode(',', $xrefs)))
178
            ->all();
179
180
        return [
181
            I18N::translate('Repositories')  => $repositories,
182
            I18N::translate('Sources')       => $sources,
183
            I18N::translate('Individuals')   => $individuals,
184
            I18N::translate('Families')      => $families,
185
            I18N::translate('Media objects') => $media,
186
        ];
187
    }
188
189
    /**
190
     * Every XREF used by this tree and also used by some other tree
191
     *
192
     * @param Tree $tree
193
     *
194
     * @return array<string>
195
     */
196
    public function duplicateXrefs(Tree $tree): array
197
    {
198
        $subquery1 = DB::table('individuals')
199
            ->where('i_file', '=', $tree->id())
200
            ->select(['i_id AS xref', new Expression("'INDI' AS type")])
0 ignored issues
show
''INDI' AS type' of type string is incompatible with the type Illuminate\Database\Query\TValue expected by parameter $value of Illuminate\Database\Quer...pression::__construct(). ( Ignorable by Annotation )

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

200
            ->select(['i_id AS xref', new Expression(/** @scrutinizer ignore-type */ "'INDI' AS type")])
Loading history...
201
            ->union(DB::table('families')
202
                ->where('f_file', '=', $tree->id())
203
                ->select(['f_id AS xref', new Expression("'FAM' AS type")]))
204
            ->union(DB::table('sources')
205
                ->where('s_file', '=', $tree->id())
206
                ->select(['s_id AS xref', new Expression("'SOUR' AS type")]))
207
            ->union(DB::table('media')
208
                ->where('m_file', '=', $tree->id())
209
                ->select(['m_id AS xref', new Expression("'OBJE' AS type")]))
210
            ->union(DB::table('other')
211
                ->where('o_file', '=', $tree->id())
212
                ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR'])
213
                ->select(['o_id AS xref', 'o_type AS type']));
214
215
        $subquery2 = DB::table('change')
216
            ->where('gedcom_id', '<>', $tree->id())
217
            ->select(['xref AS other_xref'])
218
            ->union(DB::table('individuals')
219
                ->where('i_file', '<>', $tree->id())
220
                ->select(['i_id AS xref']))
221
            ->union(DB::table('families')
222
                ->where('f_file', '<>', $tree->id())
223
                ->select(['f_id AS xref']))
224
            ->union(DB::table('sources')
225
                ->where('s_file', '<>', $tree->id())
226
                ->select(['s_id AS xref']))
227
            ->union(DB::table('media')
228
                ->where('m_file', '<>', $tree->id())
229
                ->select(['m_id AS xref']))
230
            ->union(DB::table('other')
231
                ->where('o_file', '<>', $tree->id())
232
                ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR'])
233
                ->select(['o_id AS xref']));
234
235
        return DB::query()
236
            ->fromSub($subquery1, 'sub1')
237
            ->joinSub($subquery2, 'sub2', 'other_xref', '=', 'xref')
238
            ->pluck('type', 'xref')
239
            ->all();
240
    }
241
242
    /**
243
     * A list of GEDCOM files in the data folder.
244
     *
245
     * @param FilesystemOperator $filesystem
246
     *
247
     * @return Collection<int,string>
248
     */
249
    public function gedcomFiles(FilesystemOperator $filesystem): Collection
250
    {
251
        try {
252
            $files = $filesystem->listContents('')
253
                ->filter(static function (StorageAttributes $attributes) use ($filesystem) {
254
                    if (!$attributes->isFile()) {
255
                        return false;
256
                    }
257
258
                    $stream = $filesystem->readStream($attributes->path());
259
260
                    $header = fread($stream, 10);
261
                    fclose($stream);
262
263
                    return preg_match('/^(' . UTF8::BYTE_ORDER_MARK . ')?0 HEAD/', $header) > 0;
264
                })
265
                ->map(fn (StorageAttributes $attributes) => $attributes->path())
266
                ->toArray();
267
        } catch (FilesystemException) {
268
            $files = [];
269
        }
270
271
        return Collection::make($files)->sort();
0 ignored issues
show
$files of type array is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $items of Illuminate\Support\Collection::make(). ( Ignorable by Annotation )

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

271
        return Collection::make(/** @scrutinizer ignore-type */ $files)->sort();
Loading history...
272
    }
273
274
    /**
275
     * Change the behaviour a little, when there are a lot of trees.
276
     *
277
     * @return int
278
     */
279
    public function multipleTreeThreshold(): int
280
    {
281
        return (int) Site::getPreference('MULTIPLE_TREE_THRESHOLD');
282
    }
283
}
284