1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2023 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 <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Services; 21 22use Fisharebest\Webtrees\Exceptions\GedcomErrorException; 23use Fisharebest\Webtrees\Family; 24use Fisharebest\Webtrees\Gedcom; 25use Fisharebest\Webtrees\GedcomRecord; 26use Fisharebest\Webtrees\Header; 27use Fisharebest\Webtrees\Individual; 28use Fisharebest\Webtrees\Location; 29use Fisharebest\Webtrees\Media; 30use Fisharebest\Webtrees\Note; 31use Fisharebest\Webtrees\Registry; 32use Fisharebest\Webtrees\Repository; 33use Fisharebest\Webtrees\Source; 34use Fisharebest\Webtrees\Submission; 35use Fisharebest\Webtrees\Submitter; 36use Fisharebest\Webtrees\Tree; 37use Illuminate\Database\Capsule\Manager as DB; 38use Illuminate\Database\Query\Builder; 39use Illuminate\Database\Query\Expression; 40use Illuminate\Support\Collection; 41 42use function addcslashes; 43use function preg_match; 44 45/** 46 * Manage pending changes 47 */ 48class PendingChangesService 49{ 50 private GedcomImportService $gedcom_import_service; 51 52 /** 53 * @param GedcomImportService $gedcom_import_service 54 */ 55 public function __construct(GedcomImportService $gedcom_import_service) 56 { 57 $this->gedcom_import_service = $gedcom_import_service; 58 } 59 60 /** 61 * Which records have pending changes 62 * 63 * @param Tree $tree 64 * 65 * @return Collection<int,string> 66 */ 67 public function pendingXrefs(Tree $tree): Collection 68 { 69 return DB::table('change') 70 ->where('status', '=', 'pending') 71 ->where('gedcom_id', '=', $tree->id()) 72 ->orderBy('xref') 73 ->groupBy(['xref']) 74 ->pluck('xref'); 75 } 76 77 /** 78 * @param Tree $tree 79 * @param int $n 80 * 81 * @return array<array<object>> 82 */ 83 public function pendingChanges(Tree $tree, int $n): array 84 { 85 $xrefs = $this->pendingXrefs($tree); 86 87 $rows = DB::table('change') 88 ->join('user', 'user.user_id', '=', 'change.user_id') 89 ->where('status', '=', 'pending') 90 ->where('gedcom_id', '=', $tree->id()) 91 ->whereIn('xref', $xrefs->slice(0, $n)) 92 ->orderBy('change.change_id') 93 ->select(['change.*', 'user.user_name', 'user.real_name']) 94 ->get(); 95 96 $changes = []; 97 98 $factories = [ 99 Individual::RECORD_TYPE => Registry::individualFactory(), 100 Family::RECORD_TYPE => Registry::familyFactory(), 101 Source::RECORD_TYPE => Registry::sourceFactory(), 102 Repository::RECORD_TYPE => Registry::repositoryFactory(), 103 Media::RECORD_TYPE => Registry::mediaFactory(), 104 Note::RECORD_TYPE => Registry::noteFactory(), 105 Submitter::RECORD_TYPE => Registry::submitterFactory(), 106 Submission::RECORD_TYPE => Registry::submissionFactory(), 107 Location::RECORD_TYPE => Registry::locationFactory(), 108 Header::RECORD_TYPE => Registry::headerFactory(), 109 ]; 110 111 foreach ($rows as $row) { 112 $row->change_time = Registry::timestampFactory()->fromString($row->change_time); 113 114 preg_match('/^0 (?:@' . Gedcom::REGEX_XREF . '@ )?(' . Gedcom::REGEX_TAG . ')/', $row->old_gedcom . $row->new_gedcom, $match); 115 116 $factory = $factories[$match[1]] ?? Registry::gedcomRecordFactory(); 117 118 $row->record = $factory->new($row->xref, $row->old_gedcom, $row->new_gedcom, $tree); 119 120 $changes[$row->xref][] = $row; 121 } 122 123 return $changes; 124 } 125 126 /** 127 * Accept all changes to a tree. 128 * 129 * @param Tree $tree 130 * 131 * @param int $n 132 * 133 * @return void 134 * @throws GedcomErrorException 135 */ 136 public function acceptTree(Tree $tree, int $n): void 137 { 138 $xrefs = $this->pendingXrefs($tree); 139 140 $changes = DB::table('change') 141 ->where('gedcom_id', '=', $tree->id()) 142 ->where('status', '=', 'pending') 143 ->whereIn('xref', $xrefs->slice(0, $n)) 144 ->orderBy('change_id') 145 ->lockForUpdate() 146 ->get(); 147 148 foreach ($changes as $change) { 149 if ($change->new_gedcom === '') { 150 // delete 151 $this->gedcom_import_service->updateRecord($change->old_gedcom, $tree, true); 152 } else { 153 // add/update 154 $this->gedcom_import_service->updateRecord($change->new_gedcom, $tree, false); 155 } 156 157 DB::table('change') 158 ->where('change_id', '=', $change->change_id) 159 ->update(['status' => 'accepted']); 160 } 161 } 162 163 /** 164 * Accept all changes to a record. 165 * 166 * @param GedcomRecord $record 167 */ 168 public function acceptRecord(GedcomRecord $record): void 169 { 170 $changes = DB::table('change') 171 ->where('gedcom_id', '=', $record->tree()->id()) 172 ->where('xref', '=', $record->xref()) 173 ->where('status', '=', 'pending') 174 ->orderBy('change_id') 175 ->lockForUpdate() 176 ->get(); 177 178 foreach ($changes as $change) { 179 if ($change->new_gedcom === '') { 180 // delete 181 $this->gedcom_import_service->updateRecord($change->old_gedcom, $record->tree(), true); 182 } else { 183 // add/update 184 $this->gedcom_import_service->updateRecord($change->new_gedcom, $record->tree(), false); 185 } 186 187 DB::table('change') 188 ->where('change_id', '=', $change->change_id) 189 ->update(['status' => 'accepted']); 190 } 191 } 192 193 /** 194 * Accept a change (and previous changes) to a record. 195 * 196 * @param GedcomRecord $record 197 * @param string $change_id 198 */ 199 public function acceptChange(GedcomRecord $record, string $change_id): void 200 { 201 $changes = DB::table('change') 202 ->where('gedcom_id', '=', $record->tree()->id()) 203 ->where('xref', '=', $record->xref()) 204 ->where('change_id', '<=', $change_id) 205 ->where('status', '=', 'pending') 206 ->orderBy('change_id') 207 ->get(); 208 209 foreach ($changes as $change) { 210 if ($change->new_gedcom === '') { 211 // delete 212 $this->gedcom_import_service->updateRecord($change->old_gedcom, $record->tree(), true); 213 } else { 214 // add/update 215 $this->gedcom_import_service->updateRecord($change->new_gedcom, $record->tree(), false); 216 } 217 218 DB::table('change') 219 ->where('change_id', '=', $change->change_id) 220 ->update(['status' => 'accepted']); 221 } 222 } 223 224 /** 225 * Reject all changes to a tree. 226 * 227 * @param Tree $tree 228 */ 229 public function rejectTree(Tree $tree): void 230 { 231 DB::table('change') 232 ->where('gedcom_id', '=', $tree->id()) 233 ->where('status', '=', 'pending') 234 ->update(['status' => 'rejected']); 235 } 236 237 /** 238 * Reject a change (subsequent changes) to a record. 239 * 240 * @param GedcomRecord $record 241 * @param string $change_id 242 */ 243 public function rejectChange(GedcomRecord $record, string $change_id): void 244 { 245 DB::table('change') 246 ->where('gedcom_id', '=', $record->tree()->id()) 247 ->where('xref', '=', $record->xref()) 248 ->where('change_id', '>=', $change_id) 249 ->where('status', '=', 'pending') 250 ->update(['status' => 'rejected']); 251 } 252 253 /** 254 * Reject all changes to a record. 255 * 256 * @param GedcomRecord $record 257 */ 258 public function rejectRecord(GedcomRecord $record): void 259 { 260 DB::table('change') 261 ->where('gedcom_id', '=', $record->tree()->id()) 262 ->where('xref', '=', $record->xref()) 263 ->where('status', '=', 'pending') 264 ->update(['status' => 'rejected']); 265 } 266 267 /** 268 * Generate a query for filtering the changes log. 269 * 270 * @param array<string> $params 271 * 272 * @return Builder 273 */ 274 public function changesQuery(array $params): Builder 275 { 276 $tree = $params['tree']; 277 $from = $params['from'] ?? ''; 278 $to = $params['to'] ?? ''; 279 $type = $params['type'] ?? ''; 280 $oldged = $params['oldged'] ?? ''; 281 $newged = $params['newged'] ?? ''; 282 $xref = $params['xref'] ?? ''; 283 $username = $params['username'] ?? ''; 284 285 $query = DB::table('change') 286 ->leftJoin('user', 'user.user_id', '=', 'change.user_id') 287 ->join('gedcom', 'gedcom.gedcom_id', '=', 'change.gedcom_id') 288 ->select(['change.*', new Expression("COALESCE(user_name, '<none>') AS user_name"), 'gedcom_name']) 289 ->where('gedcom_name', '=', $tree); 290 291 if ($from !== '') { 292 $query->where('change_time', '>=', Registry::timestampFactory()->fromString($from, 'Y-m-d')->toDateString()); 293 } 294 295 if ($to !== '') { 296 // before end of the day 297 $query->where('change_time', '<', Registry::timestampFactory()->fromString($to, 'Y-m-d')->addDays(1)->toDateString()); 298 } 299 300 if ($type !== '') { 301 $query->where('status', '=', $type); 302 } 303 304 if ($oldged !== '') { 305 $query->where('old_gedcom', 'LIKE', '%' . addcslashes($oldged, '\\%_') . '%'); 306 } 307 if ($newged !== '') { 308 $query->where('new_gedcom', 'LIKE', '%' . addcslashes($newged, '\\%_') . '%'); 309 } 310 311 if ($xref !== '') { 312 $query->where('xref', '=', $xref); 313 } 314 315 if ($username !== '') { 316 $query->where('user_name', 'LIKE', '%' . addcslashes($username, '\\%_') . '%'); 317 } 318 319 return $query; 320 } 321} 322