122e73debSGreg Roach<?php 222e73debSGreg Roach 322e73debSGreg Roach/** 422e73debSGreg Roach * webtrees: online genealogy 5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team 622e73debSGreg Roach * This program is free software: you can redistribute it and/or modify 722e73debSGreg Roach * it under the terms of the GNU General Public License as published by 822e73debSGreg Roach * the Free Software Foundation, either version 3 of the License, or 922e73debSGreg Roach * (at your option) any later version. 1022e73debSGreg Roach * This program is distributed in the hope that it will be useful, 1122e73debSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 1222e73debSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 1322e73debSGreg Roach * GNU General Public License for more details. 1422e73debSGreg 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/>. 1622e73debSGreg Roach */ 1722e73debSGreg Roach 1822e73debSGreg Roachdeclare(strict_types=1); 1922e73debSGreg Roach 2022e73debSGreg Roachnamespace Fisharebest\Webtrees\Services; 2122e73debSGreg Roach 225cac87aeSGreg Roachuse DateInterval; 235cac87aeSGreg Roachuse DateTimeImmutable; 245cac87aeSGreg Roachuse DateTimeZone; 255cac87aeSGreg Roachuse Fisharebest\Webtrees\Auth; 265cac87aeSGreg Roachuse Fisharebest\Webtrees\Contracts\UserInterface; 276f4ec3caSGreg Roachuse Fisharebest\Webtrees\DB; 280d047a8cSGreg Roachuse Fisharebest\Webtrees\Exceptions\GedcomErrorException; 2922ad3b5bSGreg Roachuse Fisharebest\Webtrees\Family; 3022ad3b5bSGreg Roachuse Fisharebest\Webtrees\Gedcom; 3122e73debSGreg Roachuse Fisharebest\Webtrees\GedcomRecord; 3222ad3b5bSGreg Roachuse Fisharebest\Webtrees\Header; 3322ad3b5bSGreg Roachuse Fisharebest\Webtrees\Individual; 3422ad3b5bSGreg Roachuse Fisharebest\Webtrees\Location; 3522ad3b5bSGreg Roachuse Fisharebest\Webtrees\Media; 3622ad3b5bSGreg Roachuse Fisharebest\Webtrees\Note; 3781b729d3SGreg Roachuse Fisharebest\Webtrees\Registry; 3822ad3b5bSGreg Roachuse Fisharebest\Webtrees\Repository; 3922ad3b5bSGreg Roachuse Fisharebest\Webtrees\Source; 4022ad3b5bSGreg Roachuse Fisharebest\Webtrees\Submission; 4122ad3b5bSGreg Roachuse Fisharebest\Webtrees\Submitter; 4222e73debSGreg Roachuse Fisharebest\Webtrees\Tree; 4322e73debSGreg Roachuse Illuminate\Database\Query\Builder; 4422e73debSGreg Roachuse Illuminate\Database\Query\Expression; 4522ad3b5bSGreg Roachuse Illuminate\Support\Collection; 4622ad3b5bSGreg Roach 47b5961194SGreg Roachuse function addcslashes; 4822ad3b5bSGreg Roachuse function preg_match; 49b5961194SGreg Roach 5022e73debSGreg Roach/** 5122e73debSGreg Roach * Manage pending changes 5222e73debSGreg Roach */ 5322e73debSGreg Roachclass PendingChangesService 5422e73debSGreg Roach{ 552c685d76SGreg Roach private GedcomImportService $gedcom_import_service; 562c685d76SGreg Roach 572c685d76SGreg Roach /** 582c685d76SGreg Roach * @param GedcomImportService $gedcom_import_service 592c685d76SGreg Roach */ 602c685d76SGreg Roach public function __construct(GedcomImportService $gedcom_import_service) 612c685d76SGreg Roach { 622c685d76SGreg Roach $this->gedcom_import_service = $gedcom_import_service; 632c685d76SGreg Roach } 642c685d76SGreg Roach 6522e73debSGreg Roach /** 6622ad3b5bSGreg Roach * Which records have pending changes 6722ad3b5bSGreg Roach * 6822ad3b5bSGreg Roach * @param Tree $tree 6922ad3b5bSGreg Roach * 7036779af1SGreg Roach * @return Collection<int,string> 7122ad3b5bSGreg Roach */ 7222ad3b5bSGreg Roach public function pendingXrefs(Tree $tree): Collection 7322ad3b5bSGreg Roach { 7422ad3b5bSGreg Roach return DB::table('change') 7522ad3b5bSGreg Roach ->where('status', '=', 'pending') 7622ad3b5bSGreg Roach ->where('gedcom_id', '=', $tree->id()) 7722ad3b5bSGreg Roach ->orderBy('xref') 7822ad3b5bSGreg Roach ->groupBy(['xref']) 7922ad3b5bSGreg Roach ->pluck('xref'); 8022ad3b5bSGreg Roach } 8122ad3b5bSGreg Roach 8222ad3b5bSGreg Roach /** 8322ad3b5bSGreg Roach * @param Tree $tree 8422ad3b5bSGreg Roach * @param int $n 8522ad3b5bSGreg Roach * 86f70bcff5SGreg Roach * @return array<array<object>> 8722ad3b5bSGreg Roach */ 8822ad3b5bSGreg Roach public function pendingChanges(Tree $tree, int $n): array 8922ad3b5bSGreg Roach { 9022ad3b5bSGreg Roach $xrefs = $this->pendingXrefs($tree); 9122ad3b5bSGreg Roach 9222ad3b5bSGreg Roach $rows = DB::table('change') 9322ad3b5bSGreg Roach ->join('user', 'user.user_id', '=', 'change.user_id') 9422ad3b5bSGreg Roach ->where('status', '=', 'pending') 9522ad3b5bSGreg Roach ->where('gedcom_id', '=', $tree->id()) 9622ad3b5bSGreg Roach ->whereIn('xref', $xrefs->slice(0, $n)) 9722ad3b5bSGreg Roach ->orderBy('change.change_id') 9822ad3b5bSGreg Roach ->select(['change.*', 'user.user_name', 'user.real_name']) 9922ad3b5bSGreg Roach ->get(); 10022ad3b5bSGreg Roach 10122ad3b5bSGreg Roach $changes = []; 10222ad3b5bSGreg Roach 10322ad3b5bSGreg Roach $factories = [ 1046b9cb339SGreg Roach Individual::RECORD_TYPE => Registry::individualFactory(), 1056b9cb339SGreg Roach Family::RECORD_TYPE => Registry::familyFactory(), 1066b9cb339SGreg Roach Source::RECORD_TYPE => Registry::sourceFactory(), 1076b9cb339SGreg Roach Repository::RECORD_TYPE => Registry::repositoryFactory(), 1086b9cb339SGreg Roach Media::RECORD_TYPE => Registry::mediaFactory(), 1096b9cb339SGreg Roach Note::RECORD_TYPE => Registry::noteFactory(), 1106b9cb339SGreg Roach Submitter::RECORD_TYPE => Registry::submitterFactory(), 1116b9cb339SGreg Roach Submission::RECORD_TYPE => Registry::submissionFactory(), 1126b9cb339SGreg Roach Location::RECORD_TYPE => Registry::locationFactory(), 1136b9cb339SGreg Roach Header::RECORD_TYPE => Registry::headerFactory(), 11422ad3b5bSGreg Roach ]; 11522ad3b5bSGreg Roach 11622ad3b5bSGreg Roach foreach ($rows as $row) { 117d97083feSGreg Roach $row->change_time = Registry::timestampFactory()->fromString($row->change_time); 11822ad3b5bSGreg Roach 11922ad3b5bSGreg Roach preg_match('/^0 (?:@' . Gedcom::REGEX_XREF . '@ )?(' . Gedcom::REGEX_TAG . ')/', $row->old_gedcom . $row->new_gedcom, $match); 12022ad3b5bSGreg Roach 1216b9cb339SGreg Roach $factory = $factories[$match[1]] ?? Registry::gedcomRecordFactory(); 12222ad3b5bSGreg Roach 12322ad3b5bSGreg Roach $row->record = $factory->new($row->xref, $row->old_gedcom, $row->new_gedcom, $tree); 12422ad3b5bSGreg Roach 12522ad3b5bSGreg Roach $changes[$row->xref][] = $row; 12622ad3b5bSGreg Roach } 12722ad3b5bSGreg Roach 12822ad3b5bSGreg Roach return $changes; 12922ad3b5bSGreg Roach } 13022ad3b5bSGreg Roach 13122ad3b5bSGreg Roach /** 13222e73debSGreg Roach * Accept all changes to a tree. 13322e73debSGreg Roach * 13422e73debSGreg Roach * @param Tree $tree 13522e73debSGreg Roach * 13622ad3b5bSGreg Roach * @param int $n 13722ad3b5bSGreg Roach * 13822e73debSGreg Roach * @return void 1390d047a8cSGreg Roach * @throws GedcomErrorException 14022e73debSGreg Roach */ 14122ad3b5bSGreg Roach public function acceptTree(Tree $tree, int $n): void 14222e73debSGreg Roach { 14322ad3b5bSGreg Roach $xrefs = $this->pendingXrefs($tree); 14422ad3b5bSGreg Roach 14522e73debSGreg Roach $changes = DB::table('change') 14622e73debSGreg Roach ->where('gedcom_id', '=', $tree->id()) 14722e73debSGreg Roach ->where('status', '=', 'pending') 14822ad3b5bSGreg Roach ->whereIn('xref', $xrefs->slice(0, $n)) 14922e73debSGreg Roach ->orderBy('change_id') 15022ad3b5bSGreg Roach ->lockForUpdate() 15122e73debSGreg Roach ->get(); 15222e73debSGreg Roach 15322e73debSGreg Roach foreach ($changes as $change) { 15422e73debSGreg Roach if ($change->new_gedcom === '') { 15522e73debSGreg Roach // delete 1562c685d76SGreg Roach $this->gedcom_import_service->updateRecord($change->old_gedcom, $tree, true); 15722e73debSGreg Roach } else { 15822e73debSGreg Roach // add/update 1592c685d76SGreg Roach $this->gedcom_import_service->updateRecord($change->new_gedcom, $tree, false); 16022e73debSGreg Roach } 16122e73debSGreg Roach 16222e73debSGreg Roach DB::table('change') 16322e73debSGreg Roach ->where('change_id', '=', $change->change_id) 16422e73debSGreg Roach ->update(['status' => 'accepted']); 16522e73debSGreg Roach } 16622e73debSGreg Roach } 16722e73debSGreg Roach 16822e73debSGreg Roach /** 16922e73debSGreg Roach * Accept all changes to a record. 17022e73debSGreg Roach * 17122e73debSGreg Roach * @param GedcomRecord $record 17222e73debSGreg Roach */ 17322e73debSGreg Roach public function acceptRecord(GedcomRecord $record): void 17422e73debSGreg Roach { 17522e73debSGreg Roach $changes = DB::table('change') 17622e73debSGreg Roach ->where('gedcom_id', '=', $record->tree()->id()) 17722e73debSGreg Roach ->where('xref', '=', $record->xref()) 17822e73debSGreg Roach ->where('status', '=', 'pending') 17922e73debSGreg Roach ->orderBy('change_id') 18022ad3b5bSGreg Roach ->lockForUpdate() 18122e73debSGreg Roach ->get(); 18222e73debSGreg Roach 18322e73debSGreg Roach foreach ($changes as $change) { 18422e73debSGreg Roach if ($change->new_gedcom === '') { 18522e73debSGreg Roach // delete 1862c685d76SGreg Roach $this->gedcom_import_service->updateRecord($change->old_gedcom, $record->tree(), true); 18722e73debSGreg Roach } else { 18822e73debSGreg Roach // add/update 1892c685d76SGreg Roach $this->gedcom_import_service->updateRecord($change->new_gedcom, $record->tree(), false); 19022e73debSGreg Roach } 19122e73debSGreg Roach 19222e73debSGreg Roach DB::table('change') 19322e73debSGreg Roach ->where('change_id', '=', $change->change_id) 19422e73debSGreg Roach ->update(['status' => 'accepted']); 19522e73debSGreg Roach } 19622e73debSGreg Roach } 19722e73debSGreg Roach 19822e73debSGreg Roach /** 19922e73debSGreg Roach * Accept a change (and previous changes) to a record. 20022e73debSGreg Roach * 20122e73debSGreg Roach * @param GedcomRecord $record 20222e73debSGreg Roach * @param string $change_id 20322e73debSGreg Roach */ 20422e73debSGreg Roach public function acceptChange(GedcomRecord $record, string $change_id): void 20522e73debSGreg Roach { 20622e73debSGreg Roach $changes = DB::table('change') 20722e73debSGreg Roach ->where('gedcom_id', '=', $record->tree()->id()) 20822e73debSGreg Roach ->where('xref', '=', $record->xref()) 20922e73debSGreg Roach ->where('change_id', '<=', $change_id) 21022e73debSGreg Roach ->where('status', '=', 'pending') 21122e73debSGreg Roach ->orderBy('change_id') 21222e73debSGreg Roach ->get(); 21322e73debSGreg Roach 21422e73debSGreg Roach foreach ($changes as $change) { 21522e73debSGreg Roach if ($change->new_gedcom === '') { 21622e73debSGreg Roach // delete 2172c685d76SGreg Roach $this->gedcom_import_service->updateRecord($change->old_gedcom, $record->tree(), true); 21822e73debSGreg Roach } else { 21922e73debSGreg Roach // add/update 2202c685d76SGreg Roach $this->gedcom_import_service->updateRecord($change->new_gedcom, $record->tree(), false); 22122e73debSGreg Roach } 22222e73debSGreg Roach 22322e73debSGreg Roach DB::table('change') 22422e73debSGreg Roach ->where('change_id', '=', $change->change_id) 22522e73debSGreg Roach ->update(['status' => 'accepted']); 22622e73debSGreg Roach } 22722e73debSGreg Roach } 22822e73debSGreg Roach 22922e73debSGreg Roach /** 23022e73debSGreg Roach * Reject all changes to a tree. 23122e73debSGreg Roach * 23222e73debSGreg Roach * @param Tree $tree 23322e73debSGreg Roach */ 23422e73debSGreg Roach public function rejectTree(Tree $tree): void 23522e73debSGreg Roach { 23622e73debSGreg Roach DB::table('change') 23722e73debSGreg Roach ->where('gedcom_id', '=', $tree->id()) 23822e73debSGreg Roach ->where('status', '=', 'pending') 23922e73debSGreg Roach ->update(['status' => 'rejected']); 24022e73debSGreg Roach } 24122e73debSGreg Roach 24222e73debSGreg Roach /** 24322e73debSGreg Roach * Reject a change (subsequent changes) to a record. 24422e73debSGreg Roach * 24522e73debSGreg Roach * @param GedcomRecord $record 24622e73debSGreg Roach * @param string $change_id 24722e73debSGreg Roach */ 24822e73debSGreg Roach public function rejectChange(GedcomRecord $record, string $change_id): void 24922e73debSGreg Roach { 25022e73debSGreg Roach DB::table('change') 25122e73debSGreg Roach ->where('gedcom_id', '=', $record->tree()->id()) 25222e73debSGreg Roach ->where('xref', '=', $record->xref()) 25322e73debSGreg Roach ->where('change_id', '>=', $change_id) 25422e73debSGreg Roach ->where('status', '=', 'pending') 25522e73debSGreg Roach ->update(['status' => 'rejected']); 25622e73debSGreg Roach } 25722e73debSGreg Roach 25822e73debSGreg Roach /** 25922e73debSGreg Roach * Reject all changes to a record. 26022e73debSGreg Roach * 26122e73debSGreg Roach * @param GedcomRecord $record 26222e73debSGreg Roach */ 26322e73debSGreg Roach public function rejectRecord(GedcomRecord $record): void 26422e73debSGreg Roach { 26522e73debSGreg Roach DB::table('change') 26622e73debSGreg Roach ->where('gedcom_id', '=', $record->tree()->id()) 26722e73debSGreg Roach ->where('xref', '=', $record->xref()) 26822e73debSGreg Roach ->where('status', '=', 'pending') 26922e73debSGreg Roach ->update(['status' => 'rejected']); 27022e73debSGreg Roach } 27122e73debSGreg Roach 27222e73debSGreg Roach /** 27322e73debSGreg Roach * Generate a query for filtering the changes log. 27422e73debSGreg Roach * 27509482a55SGreg Roach * @param array<string> $params 27622e73debSGreg Roach * 27722e73debSGreg Roach * @return Builder 27822e73debSGreg Roach */ 27957bfa969SGreg Roach public function changesQuery(array $params): Builder 28022e73debSGreg Roach { 28157bfa969SGreg Roach $tree = $params['tree']; 28257bfa969SGreg Roach $from = $params['from'] ?? ''; 28357bfa969SGreg Roach $to = $params['to'] ?? ''; 28457bfa969SGreg Roach $type = $params['type'] ?? ''; 28557bfa969SGreg Roach $oldged = $params['oldged'] ?? ''; 28657bfa969SGreg Roach $newged = $params['newged'] ?? ''; 28757bfa969SGreg Roach $xref = $params['xref'] ?? ''; 28857bfa969SGreg Roach $username = $params['username'] ?? ''; 28922e73debSGreg Roach 29022e73debSGreg Roach $query = DB::table('change') 29122e73debSGreg Roach ->leftJoin('user', 'user.user_id', '=', 'change.user_id') 29222e73debSGreg Roach ->join('gedcom', 'gedcom.gedcom_id', '=', 'change.gedcom_id') 29322e73debSGreg Roach ->select(['change.*', new Expression("COALESCE(user_name, '<none>') AS user_name"), 'gedcom_name']) 29457bfa969SGreg Roach ->where('gedcom_name', '=', $tree); 29522e73debSGreg Roach 2965cac87aeSGreg Roach $tz = new DateTimeZone(Auth::user()->getPreference(UserInterface::PREF_TIME_ZONE, 'UTC')); 2975cac87aeSGreg Roach $utc = new DateTimeZone('UTC'); 2985cac87aeSGreg Roach 29922e73debSGreg Roach if ($from !== '') { 3006bd19c8cSGreg Roach $from_time = DateTimeImmutable::createFromFormat('!Y-m-d', $from, $tz) 3015cac87aeSGreg Roach ->setTimezone($utc) 3025cac87aeSGreg Roach ->format('Y-m-d H:i:s'); 3035cac87aeSGreg Roach 3045cac87aeSGreg Roach $query->where('change_time', '>=', $from_time); 30522e73debSGreg Roach } 30622e73debSGreg Roach 30722e73debSGreg Roach if ($to !== '') { 30822e73debSGreg Roach // before end of the day 309*27465b29SGreg Roach $to_time = DateTimeImmutable::createFromFormat('!Y-m-d', $to, $tz) 3105cac87aeSGreg Roach ->add(new DateInterval('P1D')) 3115cac87aeSGreg Roach ->setTimezone($utc) 3125cac87aeSGreg Roach ->format('Y-m-d H:i:s'); 3135cac87aeSGreg Roach 3145cac87aeSGreg Roach $query->where('change_time', '<', $to_time); 31522e73debSGreg Roach } 31622e73debSGreg Roach 31722e73debSGreg Roach if ($type !== '') { 31822e73debSGreg Roach $query->where('status', '=', $type); 31922e73debSGreg Roach } 32022e73debSGreg Roach 32122e73debSGreg Roach if ($oldged !== '') { 322b5961194SGreg Roach $query->where('old_gedcom', 'LIKE', '%' . addcslashes($oldged, '\\%_') . '%'); 32322e73debSGreg Roach } 32422e73debSGreg Roach if ($newged !== '') { 325b5961194SGreg Roach $query->where('new_gedcom', 'LIKE', '%' . addcslashes($newged, '\\%_') . '%'); 32622e73debSGreg Roach } 32722e73debSGreg Roach 32822e73debSGreg Roach if ($xref !== '') { 32922e73debSGreg Roach $query->where('xref', '=', $xref); 33022e73debSGreg Roach } 33122e73debSGreg Roach 33222e73debSGreg Roach if ($username !== '') { 333b5961194SGreg Roach $query->where('user_name', 'LIKE', '%' . addcslashes($username, '\\%_') . '%'); 33422e73debSGreg Roach } 33522e73debSGreg Roach 33622e73debSGreg Roach return $query; 33722e73debSGreg Roach } 33822e73debSGreg Roach} 339