Completed
Push — master ( 1e9c29...6fd018 )
by Greg
14:03 queued 07:52
created

AdminService::multipleTreeThreshold()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 1
b 0
f 0
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2020 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\Services;
21
22
use Fisharebest\Webtrees\Factory;
23
use Fisharebest\Webtrees\Family;
24
use Fisharebest\Webtrees\Gedcom;
25
use Fisharebest\Webtrees\GedcomRecord;
26
use Fisharebest\Webtrees\Header;
27
use Fisharebest\Webtrees\I18N;
28
use Fisharebest\Webtrees\Individual;
29
use Fisharebest\Webtrees\Media;
30
use Fisharebest\Webtrees\Site;
31
use Fisharebest\Webtrees\Source;
32
use Fisharebest\Webtrees\Tree;
33
use Illuminate\Database\Capsule\Manager as DB;
34
use Illuminate\Database\Query\Expression;
35
use Illuminate\Database\Query\JoinClause;
36
use Illuminate\Support\Collection;
37
use League\Flysystem\FilesystemInterface;
38
39
use function array_map;
40
use function explode;
41
use function fclose;
42
use function fread;
43
use function preg_match;
44
45
/**
46
 * Utilities for the control panel.
47
 */
48
class AdminService
49
{
50
    // Show a reduced page when there are more than a certain number of trees
51
    private const MULTIPLE_TREE_THRESHOLD = '500';
52
53
    /**
54
     * Count of XREFs used by two trees at the same time.
55
     *
56
     * @param Tree $tree1
57
     * @param Tree $tree2
58
     *
59
     * @return int
60
     */
61
    public function countCommonXrefs(Tree $tree1, Tree $tree2): int
62
    {
63
        $subquery1 = DB::table('individuals')
64
            ->where('i_file', '=', $tree1->id())
65
            ->select(['i_id AS xref'])
66
            ->union(DB::table('families')
67
                ->where('f_file', '=', $tree1->id())
68
                ->select(['f_id AS xref']))
69
            ->union(DB::table('sources')
70
                ->where('s_file', '=', $tree1->id())
71
                ->select(['s_id AS xref']))
72
            ->union(DB::table('media')
73
                ->where('m_file', '=', $tree1->id())
74
                ->select(['m_id AS xref']))
75
            ->union(DB::table('other')
76
                ->where('o_file', '=', $tree1->id())
77
                ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR'])
78
                ->select(['o_id AS xref']));
79
80
        $subquery2 = DB::table('change')
81
            ->where('gedcom_id', '=', $tree2->id())
82
            ->select(['xref AS other_xref'])
83
            ->union(DB::table('individuals')
84
                ->where('i_file', '=', $tree2->id())
85
                ->select(['i_id AS xref']))
86
            ->union(DB::table('families')
87
                ->where('f_file', '=', $tree2->id())
88
                ->select(['f_id AS xref']))
89
            ->union(DB::table('sources')
90
                ->where('s_file', '=', $tree2->id())
91
                ->select(['s_id AS xref']))
92
            ->union(DB::table('media')
93
                ->where('m_file', '=', $tree2->id())
94
                ->select(['m_id AS xref']))
95
            ->union(DB::table('other')
96
                ->where('o_file', '=', $tree2->id())
97
                ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR'])
98
                ->select(['o_id AS xref']));
99
100
        return DB::table(new Expression('(' . $subquery1->toSql() . ') AS sub1'))
101
            ->mergeBindings($subquery1)
102
            ->joinSub($subquery2, 'sub2', 'other_xref', '=', 'xref')
103
            ->count();
104
    }
105
106
    /**
107
     * @param Tree $tree
108
     *
109
     * @return array<string,array<GedcomRecord>>
110
     */
111
    public function duplicateRecords(Tree $tree): array
112
    {
113
        // We can't do any reasonable checks using MySQL.
114
        // Will need to wait for a "repositories" table.
115
        $repositories = [];
116
117
        $sources = DB::table('sources')
118
            ->where('s_file', '=', $tree->id())
119
            ->groupBy(['s_name'])
120
            ->having(new Expression('COUNT(s_id)'), '>', '1')
121
            ->select([new Expression('GROUP_CONCAT(s_id) AS xrefs')])
122
            ->pluck('xrefs')
123
            ->map(static function (string $xrefs) use ($tree): array {
124
                return array_map(static function (string $xref) use ($tree): Source {
125
                    return Factory::source()->make($xref, $tree);
126
                }, explode(',', $xrefs));
127
            })
128
            ->all();
129
130
        $individuals = DB::table('dates')
131
            ->join('name', static function (JoinClause $join): void {
132
                $join
133
                    ->on('d_file', '=', 'n_file')
134
                    ->on('d_gid', '=', 'n_id');
135
            })
136
            ->where('d_file', '=', $tree->id())
137
            ->whereIn('d_fact', ['BIRT', 'CHR', 'BAPM', 'DEAT', 'BURI'])
138
            ->groupBy(['d_year', 'd_month', 'd_day', 'd_type', 'd_fact', 'n_type', 'n_full'])
139
            ->having(new Expression('COUNT(DISTINCT d_gid)'), '>', '1')
140
            ->select([new Expression('GROUP_CONCAT(DISTINCT d_gid ORDER BY d_gid) AS xrefs')])
141
            ->distinct()
142
            ->pluck('xrefs')
143
            ->map(static function (string $xrefs) use ($tree): array {
144
                return array_map(static function (string $xref) use ($tree): Individual {
145
                    return Factory::individual()->make($xref, $tree);
146
                }, explode(',', $xrefs));
147
            })
148
            ->all();
149
150
        $families = DB::table('families')
151
            ->where('f_file', '=', $tree->id())
152
            ->groupBy([new Expression('LEAST(f_husb, f_wife)')])
153
            ->groupBy([new Expression('GREATEST(f_husb, f_wife)')])
154
            ->having(new Expression('COUNT(f_id)'), '>', '1')
155
            ->select([new Expression('GROUP_CONCAT(f_id) AS xrefs')])
156
            ->pluck('xrefs')
157
            ->map(static function (string $xrefs) use ($tree): array {
158
                return array_map(static function (string $xref) use ($tree): Family {
159
                    return Factory::family()->make($xref, $tree);
160
                }, explode(',', $xrefs));
161
            })
162
            ->all();
163
164
        $media = DB::table('media_file')
165
            ->where('m_file', '=', $tree->id())
166
            ->where('descriptive_title', '<>', '')
167
            ->groupBy(['descriptive_title'])
168
            ->having(new Expression('COUNT(m_id)'), '>', '1')
169
            ->select([new Expression('GROUP_CONCAT(m_id) AS xrefs')])
170
            ->pluck('xrefs')
171
            ->map(static function (string $xrefs) use ($tree): array {
172
                return array_map(static function (string $xref) use ($tree): Media {
173
                    return Factory::media()->make($xref, $tree);
174
                }, explode(',', $xrefs));
175
            })
176
            ->all();
177
178
        return [
179
            I18N::translate('Repositories')  => $repositories,
180
            I18N::translate('Sources')       => $sources,
181
            I18N::translate('Individuals')   => $individuals,
182
            I18N::translate('Families')      => $families,
183
            I18N::translate('Media objects') => $media,
184
        ];
185
    }
186
187
    /**
188
     * Every XREF used by this tree and also used by some other tree
189
     *
190
     * @param Tree $tree
191
     *
192
     * @return string[]
193
     */
194
    public function duplicateXrefs(Tree $tree): array
195
    {
196
        $subquery1 = DB::table('individuals')
197
            ->where('i_file', '=', $tree->id())
198
            ->select(['i_id AS xref', new Expression("'INDI' AS type")])
199
            ->union(DB::table('families')
200
                ->where('f_file', '=', $tree->id())
201
                ->select(['f_id AS xref', new Expression("'FAM' AS type")]))
202
            ->union(DB::table('sources')
203
                ->where('s_file', '=', $tree->id())
204
                ->select(['s_id AS xref', new Expression("'SOUR' AS type")]))
205
            ->union(DB::table('media')
206
                ->where('m_file', '=', $tree->id())
207
                ->select(['m_id AS xref', new Expression("'OBJE' AS type")]))
208
            ->union(DB::table('other')
209
                ->where('o_file', '=', $tree->id())
210
                ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR'])
211
                ->select(['o_id AS xref', 'o_type AS type']));
212
213
        $subquery2 = DB::table('change')
214
            ->where('gedcom_id', '<>', $tree->id())
215
            ->select(['xref AS other_xref'])
216
            ->union(DB::table('individuals')
217
                ->where('i_file', '<>', $tree->id())
218
                ->select(['i_id AS xref']))
219
            ->union(DB::table('families')
220
                ->where('f_file', '<>', $tree->id())
221
                ->select(['f_id AS xref']))
222
            ->union(DB::table('sources')
223
                ->where('s_file', '<>', $tree->id())
224
                ->select(['s_id AS xref']))
225
            ->union(DB::table('media')
226
                ->where('m_file', '<>', $tree->id())
227
                ->select(['m_id AS xref']))
228
            ->union(DB::table('other')
229
                ->where('o_file', '<>', $tree->id())
230
                ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR'])
231
                ->select(['o_id AS xref']));
232
233
        return DB::table(new Expression('(' . $subquery1->toSql() . ') AS sub1'))
234
            ->mergeBindings($subquery1)
235
            ->joinSub($subquery2, 'sub2', 'other_xref', '=', 'xref')
236
            ->pluck('type', 'xref')
237
            ->all();
238
    }
239
240
    /**
241
     * A list of GEDCOM files in the data folder.
242
     *
243
     * @param FilesystemInterface $filesystem
244
     *
245
     * @return Collection<string>
246
     */
247
    public function gedcomFiles(FilesystemInterface $filesystem): Collection
248
    {
249
        return Collection::make($filesystem->listContents())
250
            ->filter(static function (array $path) use ($filesystem): bool {
251
                if ($path['type'] !== 'file') {
252
                    return false;
253
                }
254
255
                $stream = $filesystem->readStream($path['path']);
256
257
                $header = fread($stream, 10);
0 ignored issues
show
Bug introduced by
It seems like $stream can also be of type false; however, parameter $handle of fread() does only seem to accept resource, 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

257
                $header = fread(/** @scrutinizer ignore-type */ $stream, 10);
Loading history...
258
                fclose($stream);
0 ignored issues
show
Bug introduced by
It seems like $stream can also be of type false; however, parameter $handle of fclose() does only seem to accept resource, 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

258
                fclose(/** @scrutinizer ignore-type */ $stream);
Loading history...
259
260
                return preg_match('/^(' . Gedcom::UTF8_BOM . ')?0 HEAD/', $header) > 0;
261
            })
262
            ->map(static function (array $path): string {
263
                return $path['path'];
264
            })
265
            ->sort();
266
    }
267
268
    /**
269
     * Change the behaviour a little, when there are a lot of trees.
270
     *
271
     * @return int
272
     */
273
    public function multipleTreeThreshold(): int
274
    {
275
        return (int) Site::getPreference('MULTIPLE_TREE_THRESHOLD', self::MULTIPLE_TREE_THRESHOLD);
276
    }
277
}
278