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