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