xref: /webtrees/app/Services/AdminService.php (revision 6fd01894a78d321fac365dd0291a2fc52129fa03)
1*6fd01894SGreg Roach<?php
2*6fd01894SGreg Roach
3*6fd01894SGreg Roach/**
4*6fd01894SGreg Roach * webtrees: online genealogy
5*6fd01894SGreg Roach * Copyright (C) 2020 webtrees development team
6*6fd01894SGreg Roach * This program is free software: you can redistribute it and/or modify
7*6fd01894SGreg Roach * it under the terms of the GNU General Public License as published by
8*6fd01894SGreg Roach * the Free Software Foundation, either version 3 of the License, or
9*6fd01894SGreg Roach * (at your option) any later version.
10*6fd01894SGreg Roach * This program is distributed in the hope that it will be useful,
11*6fd01894SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
12*6fd01894SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13*6fd01894SGreg Roach * GNU General Public License for more details.
14*6fd01894SGreg Roach * You should have received a copy of the GNU General Public License
15*6fd01894SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>.
16*6fd01894SGreg Roach */
17*6fd01894SGreg Roach
18*6fd01894SGreg Roachdeclare(strict_types=1);
19*6fd01894SGreg Roach
20*6fd01894SGreg Roachnamespace Fisharebest\Webtrees\Services;
21*6fd01894SGreg Roach
22*6fd01894SGreg Roachuse Fisharebest\Webtrees\Factory;
23*6fd01894SGreg Roachuse Fisharebest\Webtrees\Family;
24*6fd01894SGreg Roachuse Fisharebest\Webtrees\Gedcom;
25*6fd01894SGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
26*6fd01894SGreg Roachuse Fisharebest\Webtrees\Header;
27*6fd01894SGreg Roachuse Fisharebest\Webtrees\I18N;
28*6fd01894SGreg Roachuse Fisharebest\Webtrees\Individual;
29*6fd01894SGreg Roachuse Fisharebest\Webtrees\Media;
30*6fd01894SGreg Roachuse Fisharebest\Webtrees\Site;
31*6fd01894SGreg Roachuse Fisharebest\Webtrees\Source;
32*6fd01894SGreg Roachuse Fisharebest\Webtrees\Tree;
33*6fd01894SGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
34*6fd01894SGreg Roachuse Illuminate\Database\Query\Expression;
35*6fd01894SGreg Roachuse Illuminate\Database\Query\JoinClause;
36*6fd01894SGreg Roachuse Illuminate\Support\Collection;
37*6fd01894SGreg Roachuse League\Flysystem\FilesystemInterface;
38*6fd01894SGreg Roach
39*6fd01894SGreg Roachuse function array_map;
40*6fd01894SGreg Roachuse function explode;
41*6fd01894SGreg Roachuse function fclose;
42*6fd01894SGreg Roachuse function fread;
43*6fd01894SGreg Roachuse function preg_match;
44*6fd01894SGreg Roach
45*6fd01894SGreg Roach/**
46*6fd01894SGreg Roach * Utilities for the control panel.
47*6fd01894SGreg Roach */
48*6fd01894SGreg Roachclass AdminService
49*6fd01894SGreg Roach{
50*6fd01894SGreg Roach    // Show a reduced page when there are more than a certain number of trees
51*6fd01894SGreg Roach    private const MULTIPLE_TREE_THRESHOLD = '500';
52*6fd01894SGreg Roach
53*6fd01894SGreg Roach    /**
54*6fd01894SGreg Roach     * Count of XREFs used by two trees at the same time.
55*6fd01894SGreg Roach     *
56*6fd01894SGreg Roach     * @param Tree $tree1
57*6fd01894SGreg Roach     * @param Tree $tree2
58*6fd01894SGreg Roach     *
59*6fd01894SGreg Roach     * @return int
60*6fd01894SGreg Roach     */
61*6fd01894SGreg Roach    public function countCommonXrefs(Tree $tree1, Tree $tree2): int
62*6fd01894SGreg Roach    {
63*6fd01894SGreg Roach        $subquery1 = DB::table('individuals')
64*6fd01894SGreg Roach            ->where('i_file', '=', $tree1->id())
65*6fd01894SGreg Roach            ->select(['i_id AS xref'])
66*6fd01894SGreg Roach            ->union(DB::table('families')
67*6fd01894SGreg Roach                ->where('f_file', '=', $tree1->id())
68*6fd01894SGreg Roach                ->select(['f_id AS xref']))
69*6fd01894SGreg Roach            ->union(DB::table('sources')
70*6fd01894SGreg Roach                ->where('s_file', '=', $tree1->id())
71*6fd01894SGreg Roach                ->select(['s_id AS xref']))
72*6fd01894SGreg Roach            ->union(DB::table('media')
73*6fd01894SGreg Roach                ->where('m_file', '=', $tree1->id())
74*6fd01894SGreg Roach                ->select(['m_id AS xref']))
75*6fd01894SGreg Roach            ->union(DB::table('other')
76*6fd01894SGreg Roach                ->where('o_file', '=', $tree1->id())
77*6fd01894SGreg Roach                ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR'])
78*6fd01894SGreg Roach                ->select(['o_id AS xref']));
79*6fd01894SGreg Roach
80*6fd01894SGreg Roach        $subquery2 = DB::table('change')
81*6fd01894SGreg Roach            ->where('gedcom_id', '=', $tree2->id())
82*6fd01894SGreg Roach            ->select(['xref AS other_xref'])
83*6fd01894SGreg Roach            ->union(DB::table('individuals')
84*6fd01894SGreg Roach                ->where('i_file', '=', $tree2->id())
85*6fd01894SGreg Roach                ->select(['i_id AS xref']))
86*6fd01894SGreg Roach            ->union(DB::table('families')
87*6fd01894SGreg Roach                ->where('f_file', '=', $tree2->id())
88*6fd01894SGreg Roach                ->select(['f_id AS xref']))
89*6fd01894SGreg Roach            ->union(DB::table('sources')
90*6fd01894SGreg Roach                ->where('s_file', '=', $tree2->id())
91*6fd01894SGreg Roach                ->select(['s_id AS xref']))
92*6fd01894SGreg Roach            ->union(DB::table('media')
93*6fd01894SGreg Roach                ->where('m_file', '=', $tree2->id())
94*6fd01894SGreg Roach                ->select(['m_id AS xref']))
95*6fd01894SGreg Roach            ->union(DB::table('other')
96*6fd01894SGreg Roach                ->where('o_file', '=', $tree2->id())
97*6fd01894SGreg Roach                ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR'])
98*6fd01894SGreg Roach                ->select(['o_id AS xref']));
99*6fd01894SGreg Roach
100*6fd01894SGreg Roach        return DB::table(new Expression('(' . $subquery1->toSql() . ') AS sub1'))
101*6fd01894SGreg Roach            ->mergeBindings($subquery1)
102*6fd01894SGreg Roach            ->joinSub($subquery2, 'sub2', 'other_xref', '=', 'xref')
103*6fd01894SGreg Roach            ->count();
104*6fd01894SGreg Roach    }
105*6fd01894SGreg Roach
106*6fd01894SGreg Roach    /**
107*6fd01894SGreg Roach     * @param Tree $tree
108*6fd01894SGreg Roach     *
109*6fd01894SGreg Roach     * @return array<string,array<GedcomRecord>>
110*6fd01894SGreg Roach     */
111*6fd01894SGreg Roach    public function duplicateRecords(Tree $tree): array
112*6fd01894SGreg Roach    {
113*6fd01894SGreg Roach        // We can't do any reasonable checks using MySQL.
114*6fd01894SGreg Roach        // Will need to wait for a "repositories" table.
115*6fd01894SGreg Roach        $repositories = [];
116*6fd01894SGreg Roach
117*6fd01894SGreg Roach        $sources = DB::table('sources')
118*6fd01894SGreg Roach            ->where('s_file', '=', $tree->id())
119*6fd01894SGreg Roach            ->groupBy(['s_name'])
120*6fd01894SGreg Roach            ->having(new Expression('COUNT(s_id)'), '>', '1')
121*6fd01894SGreg Roach            ->select([new Expression('GROUP_CONCAT(s_id) AS xrefs')])
122*6fd01894SGreg Roach            ->pluck('xrefs')
123*6fd01894SGreg Roach            ->map(static function (string $xrefs) use ($tree): array {
124*6fd01894SGreg Roach                return array_map(static function (string $xref) use ($tree): Source {
125*6fd01894SGreg Roach                    return Factory::source()->make($xref, $tree);
126*6fd01894SGreg Roach                }, explode(',', $xrefs));
127*6fd01894SGreg Roach            })
128*6fd01894SGreg Roach            ->all();
129*6fd01894SGreg Roach
130*6fd01894SGreg Roach        $individuals = DB::table('dates')
131*6fd01894SGreg Roach            ->join('name', static function (JoinClause $join): void {
132*6fd01894SGreg Roach                $join
133*6fd01894SGreg Roach                    ->on('d_file', '=', 'n_file')
134*6fd01894SGreg Roach                    ->on('d_gid', '=', 'n_id');
135*6fd01894SGreg Roach            })
136*6fd01894SGreg Roach            ->where('d_file', '=', $tree->id())
137*6fd01894SGreg Roach            ->whereIn('d_fact', ['BIRT', 'CHR', 'BAPM', 'DEAT', 'BURI'])
138*6fd01894SGreg Roach            ->groupBy(['d_year', 'd_month', 'd_day', 'd_type', 'd_fact', 'n_type', 'n_full'])
139*6fd01894SGreg Roach            ->having(new Expression('COUNT(DISTINCT d_gid)'), '>', '1')
140*6fd01894SGreg Roach            ->select([new Expression('GROUP_CONCAT(DISTINCT d_gid ORDER BY d_gid) AS xrefs')])
141*6fd01894SGreg Roach            ->distinct()
142*6fd01894SGreg Roach            ->pluck('xrefs')
143*6fd01894SGreg Roach            ->map(static function (string $xrefs) use ($tree): array {
144*6fd01894SGreg Roach                return array_map(static function (string $xref) use ($tree): Individual {
145*6fd01894SGreg Roach                    return Factory::individual()->make($xref, $tree);
146*6fd01894SGreg Roach                }, explode(',', $xrefs));
147*6fd01894SGreg Roach            })
148*6fd01894SGreg Roach            ->all();
149*6fd01894SGreg Roach
150*6fd01894SGreg Roach        $families = DB::table('families')
151*6fd01894SGreg Roach            ->where('f_file', '=', $tree->id())
152*6fd01894SGreg Roach            ->groupBy([new Expression('LEAST(f_husb, f_wife)')])
153*6fd01894SGreg Roach            ->groupBy([new Expression('GREATEST(f_husb, f_wife)')])
154*6fd01894SGreg Roach            ->having(new Expression('COUNT(f_id)'), '>', '1')
155*6fd01894SGreg Roach            ->select([new Expression('GROUP_CONCAT(f_id) AS xrefs')])
156*6fd01894SGreg Roach            ->pluck('xrefs')
157*6fd01894SGreg Roach            ->map(static function (string $xrefs) use ($tree): array {
158*6fd01894SGreg Roach                return array_map(static function (string $xref) use ($tree): Family {
159*6fd01894SGreg Roach                    return Factory::family()->make($xref, $tree);
160*6fd01894SGreg Roach                }, explode(',', $xrefs));
161*6fd01894SGreg Roach            })
162*6fd01894SGreg Roach            ->all();
163*6fd01894SGreg Roach
164*6fd01894SGreg Roach        $media = DB::table('media_file')
165*6fd01894SGreg Roach            ->where('m_file', '=', $tree->id())
166*6fd01894SGreg Roach            ->where('descriptive_title', '<>', '')
167*6fd01894SGreg Roach            ->groupBy(['descriptive_title'])
168*6fd01894SGreg Roach            ->having(new Expression('COUNT(m_id)'), '>', '1')
169*6fd01894SGreg Roach            ->select([new Expression('GROUP_CONCAT(m_id) AS xrefs')])
170*6fd01894SGreg Roach            ->pluck('xrefs')
171*6fd01894SGreg Roach            ->map(static function (string $xrefs) use ($tree): array {
172*6fd01894SGreg Roach                return array_map(static function (string $xref) use ($tree): Media {
173*6fd01894SGreg Roach                    return Factory::media()->make($xref, $tree);
174*6fd01894SGreg Roach                }, explode(',', $xrefs));
175*6fd01894SGreg Roach            })
176*6fd01894SGreg Roach            ->all();
177*6fd01894SGreg Roach
178*6fd01894SGreg Roach        return [
179*6fd01894SGreg Roach            I18N::translate('Repositories')  => $repositories,
180*6fd01894SGreg Roach            I18N::translate('Sources')       => $sources,
181*6fd01894SGreg Roach            I18N::translate('Individuals')   => $individuals,
182*6fd01894SGreg Roach            I18N::translate('Families')      => $families,
183*6fd01894SGreg Roach            I18N::translate('Media objects') => $media,
184*6fd01894SGreg Roach        ];
185*6fd01894SGreg Roach    }
186*6fd01894SGreg Roach
187*6fd01894SGreg Roach    /**
188*6fd01894SGreg Roach     * Every XREF used by this tree and also used by some other tree
189*6fd01894SGreg Roach     *
190*6fd01894SGreg Roach     * @param Tree $tree
191*6fd01894SGreg Roach     *
192*6fd01894SGreg Roach     * @return string[]
193*6fd01894SGreg Roach     */
194*6fd01894SGreg Roach    public function duplicateXrefs(Tree $tree): array
195*6fd01894SGreg Roach    {
196*6fd01894SGreg Roach        $subquery1 = DB::table('individuals')
197*6fd01894SGreg Roach            ->where('i_file', '=', $tree->id())
198*6fd01894SGreg Roach            ->select(['i_id AS xref', new Expression("'INDI' AS type")])
199*6fd01894SGreg Roach            ->union(DB::table('families')
200*6fd01894SGreg Roach                ->where('f_file', '=', $tree->id())
201*6fd01894SGreg Roach                ->select(['f_id AS xref', new Expression("'FAM' AS type")]))
202*6fd01894SGreg Roach            ->union(DB::table('sources')
203*6fd01894SGreg Roach                ->where('s_file', '=', $tree->id())
204*6fd01894SGreg Roach                ->select(['s_id AS xref', new Expression("'SOUR' AS type")]))
205*6fd01894SGreg Roach            ->union(DB::table('media')
206*6fd01894SGreg Roach                ->where('m_file', '=', $tree->id())
207*6fd01894SGreg Roach                ->select(['m_id AS xref', new Expression("'OBJE' AS type")]))
208*6fd01894SGreg Roach            ->union(DB::table('other')
209*6fd01894SGreg Roach                ->where('o_file', '=', $tree->id())
210*6fd01894SGreg Roach                ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR'])
211*6fd01894SGreg Roach                ->select(['o_id AS xref', 'o_type AS type']));
212*6fd01894SGreg Roach
213*6fd01894SGreg Roach        $subquery2 = DB::table('change')
214*6fd01894SGreg Roach            ->where('gedcom_id', '<>', $tree->id())
215*6fd01894SGreg Roach            ->select(['xref AS other_xref'])
216*6fd01894SGreg Roach            ->union(DB::table('individuals')
217*6fd01894SGreg Roach                ->where('i_file', '<>', $tree->id())
218*6fd01894SGreg Roach                ->select(['i_id AS xref']))
219*6fd01894SGreg Roach            ->union(DB::table('families')
220*6fd01894SGreg Roach                ->where('f_file', '<>', $tree->id())
221*6fd01894SGreg Roach                ->select(['f_id AS xref']))
222*6fd01894SGreg Roach            ->union(DB::table('sources')
223*6fd01894SGreg Roach                ->where('s_file', '<>', $tree->id())
224*6fd01894SGreg Roach                ->select(['s_id AS xref']))
225*6fd01894SGreg Roach            ->union(DB::table('media')
226*6fd01894SGreg Roach                ->where('m_file', '<>', $tree->id())
227*6fd01894SGreg Roach                ->select(['m_id AS xref']))
228*6fd01894SGreg Roach            ->union(DB::table('other')
229*6fd01894SGreg Roach                ->where('o_file', '<>', $tree->id())
230*6fd01894SGreg Roach                ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR'])
231*6fd01894SGreg Roach                ->select(['o_id AS xref']));
232*6fd01894SGreg Roach
233*6fd01894SGreg Roach        return DB::table(new Expression('(' . $subquery1->toSql() . ') AS sub1'))
234*6fd01894SGreg Roach            ->mergeBindings($subquery1)
235*6fd01894SGreg Roach            ->joinSub($subquery2, 'sub2', 'other_xref', '=', 'xref')
236*6fd01894SGreg Roach            ->pluck('type', 'xref')
237*6fd01894SGreg Roach            ->all();
238*6fd01894SGreg Roach    }
239*6fd01894SGreg Roach
240*6fd01894SGreg Roach    /**
241*6fd01894SGreg Roach     * A list of GEDCOM files in the data folder.
242*6fd01894SGreg Roach     *
243*6fd01894SGreg Roach     * @param FilesystemInterface $filesystem
244*6fd01894SGreg Roach     *
245*6fd01894SGreg Roach     * @return Collection<string>
246*6fd01894SGreg Roach     */
247*6fd01894SGreg Roach    public function gedcomFiles(FilesystemInterface $filesystem): Collection
248*6fd01894SGreg Roach    {
249*6fd01894SGreg Roach        return Collection::make($filesystem->listContents())
250*6fd01894SGreg Roach            ->filter(static function (array $path) use ($filesystem): bool {
251*6fd01894SGreg Roach                if ($path['type'] !== 'file') {
252*6fd01894SGreg Roach                    return false;
253*6fd01894SGreg Roach                }
254*6fd01894SGreg Roach
255*6fd01894SGreg Roach                $stream = $filesystem->readStream($path['path']);
256*6fd01894SGreg Roach
257*6fd01894SGreg Roach                $header = fread($stream, 10);
258*6fd01894SGreg Roach                fclose($stream);
259*6fd01894SGreg Roach
260*6fd01894SGreg Roach                return preg_match('/^(' . Gedcom::UTF8_BOM . ')?0 HEAD/', $header) > 0;
261*6fd01894SGreg Roach            })
262*6fd01894SGreg Roach            ->map(static function (array $path): string {
263*6fd01894SGreg Roach                return $path['path'];
264*6fd01894SGreg Roach            })
265*6fd01894SGreg Roach            ->sort();
266*6fd01894SGreg Roach    }
267*6fd01894SGreg Roach
268*6fd01894SGreg Roach    /**
269*6fd01894SGreg Roach     * Change the behaviour a little, when there are a lot of trees.
270*6fd01894SGreg Roach     *
271*6fd01894SGreg Roach     * @return int
272*6fd01894SGreg Roach     */
273*6fd01894SGreg Roach    public function multipleTreeThreshold(): int
274*6fd01894SGreg Roach    {
275*6fd01894SGreg Roach        return (int) Site::getPreference('MULTIPLE_TREE_THRESHOLD', self::MULTIPLE_TREE_THRESHOLD);
276*6fd01894SGreg Roach    }
277*6fd01894SGreg Roach}
278