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