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