xref: /webtrees/app/Services/AdminService.php (revision 859d33aa4dff138865614f38342eda5e1902daba)
16fd01894SGreg Roach<?php
26fd01894SGreg Roach
36fd01894SGreg Roach/**
46fd01894SGreg Roach * webtrees: online genealogy
5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team
66fd01894SGreg Roach * This program is free software: you can redistribute it and/or modify
76fd01894SGreg Roach * it under the terms of the GNU General Public License as published by
86fd01894SGreg Roach * the Free Software Foundation, either version 3 of the License, or
96fd01894SGreg Roach * (at your option) any later version.
106fd01894SGreg Roach * This program is distributed in the hope that it will be useful,
116fd01894SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
126fd01894SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
136fd01894SGreg Roach * GNU General Public License for more details.
146fd01894SGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
166fd01894SGreg Roach */
176fd01894SGreg Roach
186fd01894SGreg Roachdeclare(strict_types=1);
196fd01894SGreg Roach
206fd01894SGreg Roachnamespace Fisharebest\Webtrees\Services;
216fd01894SGreg Roach
226f4ec3caSGreg Roachuse Fisharebest\Webtrees\DB;
231c6adce8SGreg Roachuse Fisharebest\Webtrees\Encodings\UTF8;
246fd01894SGreg Roachuse Fisharebest\Webtrees\Family;
256fd01894SGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
266fd01894SGreg Roachuse Fisharebest\Webtrees\Header;
276fd01894SGreg Roachuse Fisharebest\Webtrees\I18N;
286fd01894SGreg Roachuse Fisharebest\Webtrees\Individual;
296fd01894SGreg Roachuse Fisharebest\Webtrees\Media;
30f0c88a96SGreg Roachuse Fisharebest\Webtrees\Registry;
316fd01894SGreg Roachuse Fisharebest\Webtrees\Site;
326fd01894SGreg Roachuse Fisharebest\Webtrees\Source;
336fd01894SGreg Roachuse Fisharebest\Webtrees\Tree;
346fd01894SGreg Roachuse Illuminate\Database\Query\Expression;
356fd01894SGreg Roachuse Illuminate\Database\Query\JoinClause;
366fd01894SGreg Roachuse Illuminate\Support\Collection;
37f7cf8a15SGreg Roachuse League\Flysystem\FilesystemException;
38f7cf8a15SGreg Roachuse League\Flysystem\FilesystemOperator;
39f7cf8a15SGreg Roachuse League\Flysystem\StorageAttributes;
406fd01894SGreg Roach
416fd01894SGreg Roachuse function array_map;
42*859d33aaSGreg Roachuse function array_unique;
436fd01894SGreg Roachuse function explode;
446fd01894SGreg Roachuse function fclose;
456fd01894SGreg Roachuse function fread;
46*859d33aaSGreg Roachuse function implode;
476fd01894SGreg Roachuse function preg_match;
48*859d33aaSGreg Roachuse function sort;
496fd01894SGreg Roach
506fd01894SGreg Roach/**
516fd01894SGreg Roach * Utilities for the control panel.
526fd01894SGreg Roach */
536fd01894SGreg Roachclass AdminService
546fd01894SGreg Roach{
556fd01894SGreg Roach    /**
566fd01894SGreg Roach     * Count of XREFs used by two trees at the same time.
576fd01894SGreg Roach     *
586fd01894SGreg Roach     * @param Tree $tree1
596fd01894SGreg Roach     * @param Tree $tree2
606fd01894SGreg Roach     *
616fd01894SGreg Roach     * @return int
626fd01894SGreg Roach     */
636fd01894SGreg Roach    public function countCommonXrefs(Tree $tree1, Tree $tree2): int
646fd01894SGreg Roach    {
656fd01894SGreg Roach        $subquery1 = DB::table('individuals')
666fd01894SGreg Roach            ->where('i_file', '=', $tree1->id())
676fd01894SGreg Roach            ->select(['i_id AS xref'])
686fd01894SGreg Roach            ->union(DB::table('families')
696fd01894SGreg Roach                ->where('f_file', '=', $tree1->id())
706fd01894SGreg Roach                ->select(['f_id AS xref']))
716fd01894SGreg Roach            ->union(DB::table('sources')
726fd01894SGreg Roach                ->where('s_file', '=', $tree1->id())
736fd01894SGreg Roach                ->select(['s_id AS xref']))
746fd01894SGreg Roach            ->union(DB::table('media')
756fd01894SGreg Roach                ->where('m_file', '=', $tree1->id())
766fd01894SGreg Roach                ->select(['m_id AS xref']))
776fd01894SGreg Roach            ->union(DB::table('other')
786fd01894SGreg Roach                ->where('o_file', '=', $tree1->id())
796fd01894SGreg Roach                ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR'])
806fd01894SGreg Roach                ->select(['o_id AS xref']));
816fd01894SGreg Roach
826fd01894SGreg Roach        $subquery2 = DB::table('change')
836fd01894SGreg Roach            ->where('gedcom_id', '=', $tree2->id())
846fd01894SGreg Roach            ->select(['xref AS other_xref'])
856fd01894SGreg Roach            ->union(DB::table('individuals')
866fd01894SGreg Roach                ->where('i_file', '=', $tree2->id())
876fd01894SGreg Roach                ->select(['i_id AS xref']))
886fd01894SGreg Roach            ->union(DB::table('families')
896fd01894SGreg Roach                ->where('f_file', '=', $tree2->id())
906fd01894SGreg Roach                ->select(['f_id AS xref']))
916fd01894SGreg Roach            ->union(DB::table('sources')
926fd01894SGreg Roach                ->where('s_file', '=', $tree2->id())
936fd01894SGreg Roach                ->select(['s_id AS xref']))
946fd01894SGreg Roach            ->union(DB::table('media')
956fd01894SGreg Roach                ->where('m_file', '=', $tree2->id())
966fd01894SGreg Roach                ->select(['m_id AS xref']))
976fd01894SGreg Roach            ->union(DB::table('other')
986fd01894SGreg Roach                ->where('o_file', '=', $tree2->id())
996fd01894SGreg Roach                ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR'])
1006fd01894SGreg Roach                ->select(['o_id AS xref']));
1016fd01894SGreg Roach
1026fd01894SGreg Roach        return DB::table(new Expression('(' . $subquery1->toSql() . ') AS sub1'))
1036fd01894SGreg Roach            ->mergeBindings($subquery1)
1046fd01894SGreg Roach            ->joinSub($subquery2, 'sub2', 'other_xref', '=', 'xref')
1056fd01894SGreg Roach            ->count();
1066fd01894SGreg Roach    }
1076fd01894SGreg Roach
1086fd01894SGreg Roach    /**
1096fd01894SGreg Roach     * @param Tree $tree
1106fd01894SGreg Roach     *
1117c2c99faSGreg Roach     * @return array<string,array<int,array<int,GedcomRecord>>>
1126fd01894SGreg Roach     */
1136fd01894SGreg Roach    public function duplicateRecords(Tree $tree): array
1146fd01894SGreg Roach    {
1156fd01894SGreg Roach        // We can't do any reasonable checks using MySQL.
1166fd01894SGreg Roach        // Will need to wait for a "repositories" table.
1176fd01894SGreg Roach        $repositories = [];
1186fd01894SGreg Roach
1196fd01894SGreg Roach        $sources = DB::table('sources')
1206fd01894SGreg Roach            ->where('s_file', '=', $tree->id())
1216fd01894SGreg Roach            ->groupBy(['s_name'])
1226fd01894SGreg Roach            ->having(new Expression('COUNT(s_id)'), '>', '1')
123*859d33aaSGreg Roach            ->select([new Expression(DB::groupConcat('s_id') . ' AS xrefs')])
12492ae7c02SGreg Roach            ->orderBy('xrefs')
1256fd01894SGreg Roach            ->pluck('xrefs')
1266fd01894SGreg Roach            ->map(static function (string $xrefs) use ($tree): array {
1276fd01894SGreg Roach                return array_map(static function (string $xref) use ($tree): Source {
1286b9cb339SGreg Roach                    return Registry::sourceFactory()->make($xref, $tree);
1296fd01894SGreg Roach                }, explode(',', $xrefs));
1306fd01894SGreg Roach            })
1316fd01894SGreg Roach            ->all();
1326fd01894SGreg Roach
133*859d33aaSGreg Roach        // Database agnostic way to do GROUP_CONCAT(DISTINCT x ORDER BY x)
134*859d33aaSGreg Roach        $distinct_order_by = static function (string $xrefs): string {
135*859d33aaSGreg Roach            $array = explode(',', $xrefs);
136*859d33aaSGreg Roach            sort($array);
137*859d33aaSGreg Roach
138*859d33aaSGreg Roach            return implode(',', array_unique($array));
139*859d33aaSGreg Roach        };
140*859d33aaSGreg Roach
1416fd01894SGreg Roach        $individuals = DB::table('dates')
1426fd01894SGreg Roach            ->join('name', static function (JoinClause $join): void {
1436fd01894SGreg Roach                $join
1446fd01894SGreg Roach                    ->on('d_file', '=', 'n_file')
1456fd01894SGreg Roach                    ->on('d_gid', '=', 'n_id');
1466fd01894SGreg Roach            })
1476fd01894SGreg Roach            ->where('d_file', '=', $tree->id())
1486fd01894SGreg Roach            ->whereIn('d_fact', ['BIRT', 'CHR', 'BAPM', 'DEAT', 'BURI'])
1496fd01894SGreg Roach            ->groupBy(['d_year', 'd_month', 'd_day', 'd_type', 'd_fact', 'n_type', 'n_full'])
1506fd01894SGreg Roach            ->having(new Expression('COUNT(DISTINCT d_gid)'), '>', '1')
151*859d33aaSGreg Roach            ->select([new Expression(DB::groupConcat('d_gid') . ' AS xrefs')])
15292ae7c02SGreg Roach            ->orderBy('xrefs')
1536fd01894SGreg Roach            ->pluck('xrefs')
154*859d33aaSGreg Roach            ->map($distinct_order_by)
155*859d33aaSGreg Roach            ->unique()
1566fd01894SGreg Roach            ->map(static function (string $xrefs) use ($tree): array {
1576fd01894SGreg Roach                return array_map(static function (string $xref) use ($tree): Individual {
1586b9cb339SGreg Roach                    return Registry::individualFactory()->make($xref, $tree);
1596fd01894SGreg Roach                }, explode(',', $xrefs));
1606fd01894SGreg Roach            })
1616fd01894SGreg Roach            ->all();
1626fd01894SGreg Roach
1636fd01894SGreg Roach        $families = DB::table('families')
1646fd01894SGreg Roach            ->where('f_file', '=', $tree->id())
1656fd01894SGreg Roach            ->groupBy([new Expression('LEAST(f_husb, f_wife)')])
1666fd01894SGreg Roach            ->groupBy([new Expression('GREATEST(f_husb, f_wife)')])
1676fd01894SGreg Roach            ->having(new Expression('COUNT(f_id)'), '>', '1')
168*859d33aaSGreg Roach            ->select([new Expression(DB::groupConcat('f_id') . ' AS xrefs')])
16992ae7c02SGreg Roach            ->orderBy('xrefs')
1706fd01894SGreg Roach            ->pluck('xrefs')
1716fd01894SGreg Roach            ->map(static function (string $xrefs) use ($tree): array {
1726fd01894SGreg Roach                return array_map(static function (string $xref) use ($tree): Family {
1736b9cb339SGreg Roach                    return Registry::familyFactory()->make($xref, $tree);
1746fd01894SGreg Roach                }, explode(',', $xrefs));
1756fd01894SGreg Roach            })
1766fd01894SGreg Roach            ->all();
1776fd01894SGreg Roach
1786fd01894SGreg Roach        $media = DB::table('media_file')
1796fd01894SGreg Roach            ->where('m_file', '=', $tree->id())
1806fd01894SGreg Roach            ->where('descriptive_title', '<>', '')
1816fd01894SGreg Roach            ->groupBy(['descriptive_title'])
182f48369e0SGreg Roach            ->having(new Expression('COUNT(DISTINCT m_id)'), '>', '1')
183*859d33aaSGreg Roach            ->select([new Expression(DB::groupConcat('m_id') .' AS xrefs')])
18492ae7c02SGreg Roach            ->orderBy('xrefs')
1856fd01894SGreg Roach            ->pluck('xrefs')
1866fd01894SGreg Roach            ->map(static function (string $xrefs) use ($tree): array {
1876fd01894SGreg Roach                return array_map(static function (string $xref) use ($tree): Media {
1886b9cb339SGreg Roach                    return Registry::mediaFactory()->make($xref, $tree);
1896fd01894SGreg Roach                }, explode(',', $xrefs));
1906fd01894SGreg Roach            })
1916fd01894SGreg Roach            ->all();
1926fd01894SGreg Roach
1936fd01894SGreg Roach        return [
1946fd01894SGreg Roach            I18N::translate('Repositories')  => $repositories,
1956fd01894SGreg Roach            I18N::translate('Sources')       => $sources,
1966fd01894SGreg Roach            I18N::translate('Individuals')   => $individuals,
1976fd01894SGreg Roach            I18N::translate('Families')      => $families,
1986fd01894SGreg Roach            I18N::translate('Media objects') => $media,
1996fd01894SGreg Roach        ];
2006fd01894SGreg Roach    }
2016fd01894SGreg Roach
2026fd01894SGreg Roach    /**
2036fd01894SGreg Roach     * Every XREF used by this tree and also used by some other tree
2046fd01894SGreg Roach     *
2056fd01894SGreg Roach     * @param Tree $tree
2066fd01894SGreg Roach     *
20724f2a3afSGreg Roach     * @return array<string>
2086fd01894SGreg Roach     */
2096fd01894SGreg Roach    public function duplicateXrefs(Tree $tree): array
2106fd01894SGreg Roach    {
2116fd01894SGreg Roach        $subquery1 = DB::table('individuals')
2126fd01894SGreg Roach            ->where('i_file', '=', $tree->id())
2136fd01894SGreg Roach            ->select(['i_id AS xref', new Expression("'INDI' AS type")])
2146fd01894SGreg Roach            ->union(DB::table('families')
2156fd01894SGreg Roach                ->where('f_file', '=', $tree->id())
2166fd01894SGreg Roach                ->select(['f_id AS xref', new Expression("'FAM' AS type")]))
2176fd01894SGreg Roach            ->union(DB::table('sources')
2186fd01894SGreg Roach                ->where('s_file', '=', $tree->id())
2196fd01894SGreg Roach                ->select(['s_id AS xref', new Expression("'SOUR' AS type")]))
2206fd01894SGreg Roach            ->union(DB::table('media')
2216fd01894SGreg Roach                ->where('m_file', '=', $tree->id())
2226fd01894SGreg Roach                ->select(['m_id AS xref', new Expression("'OBJE' AS type")]))
2236fd01894SGreg Roach            ->union(DB::table('other')
2246fd01894SGreg Roach                ->where('o_file', '=', $tree->id())
2256fd01894SGreg Roach                ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR'])
2266fd01894SGreg Roach                ->select(['o_id AS xref', 'o_type AS type']));
2276fd01894SGreg Roach
2286fd01894SGreg Roach        $subquery2 = DB::table('change')
2296fd01894SGreg Roach            ->where('gedcom_id', '<>', $tree->id())
2306fd01894SGreg Roach            ->select(['xref AS other_xref'])
2316fd01894SGreg Roach            ->union(DB::table('individuals')
2326fd01894SGreg Roach                ->where('i_file', '<>', $tree->id())
2336fd01894SGreg Roach                ->select(['i_id AS xref']))
2346fd01894SGreg Roach            ->union(DB::table('families')
2356fd01894SGreg Roach                ->where('f_file', '<>', $tree->id())
2366fd01894SGreg Roach                ->select(['f_id AS xref']))
2376fd01894SGreg Roach            ->union(DB::table('sources')
2386fd01894SGreg Roach                ->where('s_file', '<>', $tree->id())
2396fd01894SGreg Roach                ->select(['s_id AS xref']))
2406fd01894SGreg Roach            ->union(DB::table('media')
2416fd01894SGreg Roach                ->where('m_file', '<>', $tree->id())
2426fd01894SGreg Roach                ->select(['m_id AS xref']))
2436fd01894SGreg Roach            ->union(DB::table('other')
2446fd01894SGreg Roach                ->where('o_file', '<>', $tree->id())
2456fd01894SGreg Roach                ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR'])
2466fd01894SGreg Roach                ->select(['o_id AS xref']));
2476fd01894SGreg Roach
2489b80d2d9SGreg Roach        return DB::query()
2499b80d2d9SGreg Roach            ->fromSub($subquery1, 'sub1')
2506fd01894SGreg Roach            ->joinSub($subquery2, 'sub2', 'other_xref', '=', 'xref')
2516fd01894SGreg Roach            ->pluck('type', 'xref')
2526fd01894SGreg Roach            ->all();
2536fd01894SGreg Roach    }
2546fd01894SGreg Roach
2556fd01894SGreg Roach    /**
2566fd01894SGreg Roach     * A list of GEDCOM files in the data folder.
2576fd01894SGreg Roach     *
258f7cf8a15SGreg Roach     * @param FilesystemOperator $filesystem
2596fd01894SGreg Roach     *
26036779af1SGreg Roach     * @return Collection<int,string>
2616fd01894SGreg Roach     */
262f7cf8a15SGreg Roach    public function gedcomFiles(FilesystemOperator $filesystem): Collection
2636fd01894SGreg Roach    {
264f0448b68SGreg Roach        try {
265f7cf8a15SGreg Roach            $files = $filesystem->listContents('')
266f7cf8a15SGreg Roach                ->filter(static function (StorageAttributes $attributes) use ($filesystem) {
267f7cf8a15SGreg Roach                    if (!$attributes->isFile()) {
2686fd01894SGreg Roach                        return false;
2696fd01894SGreg Roach                    }
2706fd01894SGreg Roach
271f7cf8a15SGreg Roach                    $stream = $filesystem->readStream($attributes->path());
2726fd01894SGreg Roach
2736fd01894SGreg Roach                    $header = fread($stream, 10);
2746fd01894SGreg Roach                    fclose($stream);
2756fd01894SGreg Roach
2761c6adce8SGreg Roach                    return preg_match('/^(' . UTF8::BYTE_ORDER_MARK . ')?0 HEAD/', $header) > 0;
2776fd01894SGreg Roach                })
278f7cf8a15SGreg Roach                ->map(function (StorageAttributes $attributes) {
279f7cf8a15SGreg Roach                    return $attributes->path();
2806fd01894SGreg Roach                })
281f7cf8a15SGreg Roach                ->toArray();
28228d026adSGreg Roach        } catch (FilesystemException) {
283f0448b68SGreg Roach            $files = [];
284f0448b68SGreg Roach        }
285f7cf8a15SGreg Roach
286f7cf8a15SGreg Roach        return Collection::make($files)->sort();
2876fd01894SGreg Roach    }
2886fd01894SGreg Roach
2896fd01894SGreg Roach    /**
2906fd01894SGreg Roach     * Change the behaviour a little, when there are a lot of trees.
2916fd01894SGreg Roach     *
2926fd01894SGreg Roach     * @return int
2936fd01894SGreg Roach     */
2946fd01894SGreg Roach    public function multipleTreeThreshold(): int
2956fd01894SGreg Roach    {
2968a07c98eSGreg Roach        return (int) Site::getPreference('MULTIPLE_TREE_THRESHOLD');
2976fd01894SGreg Roach    }
2986fd01894SGreg Roach}
299