. */ declare(strict_types=1); namespace Fisharebest\Webtrees\Services; use DateInterval; use DateTimeImmutable; use DateTimeZone; use Fisharebest\Webtrees\Auth; use Fisharebest\Webtrees\Contracts\UserInterface; use Fisharebest\Webtrees\DB; use Fisharebest\Webtrees\Exceptions\GedcomErrorException; use Fisharebest\Webtrees\Family; use Fisharebest\Webtrees\Gedcom; use Fisharebest\Webtrees\GedcomRecord; use Fisharebest\Webtrees\Header; use Fisharebest\Webtrees\Individual; use Fisharebest\Webtrees\Location; use Fisharebest\Webtrees\Media; use Fisharebest\Webtrees\Note; use Fisharebest\Webtrees\Registry; use Fisharebest\Webtrees\Repository; use Fisharebest\Webtrees\Source; use Fisharebest\Webtrees\Submission; use Fisharebest\Webtrees\Submitter; use Fisharebest\Webtrees\Tree; use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\Expression; use Illuminate\Support\Collection; use function addcslashes; use function preg_match; use function var_dump; /** * Manage pending changes */ class PendingChangesService { private GedcomImportService $gedcom_import_service; /** * @param GedcomImportService $gedcom_import_service */ public function __construct(GedcomImportService $gedcom_import_service) { $this->gedcom_import_service = $gedcom_import_service; } /** * Which records have pending changes * * @param Tree $tree * * @return Collection */ public function pendingXrefs(Tree $tree): Collection { return DB::table('change') ->where('status', '=', 'pending') ->where('gedcom_id', '=', $tree->id()) ->orderBy('xref') ->groupBy(['xref']) ->pluck('xref'); } /** * @param Tree $tree * @param int $n * * @return array> */ public function pendingChanges(Tree $tree, int $n): array { $xrefs = $this->pendingXrefs($tree); $rows = DB::table('change') ->join('user', 'user.user_id', '=', 'change.user_id') ->where('status', '=', 'pending') ->where('gedcom_id', '=', $tree->id()) ->whereIn('xref', $xrefs->slice(0, $n)) ->orderBy('change.change_id') ->select(['change.*', 'user.user_name', 'user.real_name']) ->get(); $changes = []; $factories = [ Individual::RECORD_TYPE => Registry::individualFactory(), Family::RECORD_TYPE => Registry::familyFactory(), Source::RECORD_TYPE => Registry::sourceFactory(), Repository::RECORD_TYPE => Registry::repositoryFactory(), Media::RECORD_TYPE => Registry::mediaFactory(), Note::RECORD_TYPE => Registry::noteFactory(), Submitter::RECORD_TYPE => Registry::submitterFactory(), Submission::RECORD_TYPE => Registry::submissionFactory(), Location::RECORD_TYPE => Registry::locationFactory(), Header::RECORD_TYPE => Registry::headerFactory(), ]; foreach ($rows as $row) { $row->change_time = Registry::timestampFactory()->fromString($row->change_time); preg_match('/^0 (?:@' . Gedcom::REGEX_XREF . '@ )?(' . Gedcom::REGEX_TAG . ')/', $row->old_gedcom . $row->new_gedcom, $match); $factory = $factories[$match[1]] ?? Registry::gedcomRecordFactory(); $row->record = $factory->new($row->xref, $row->old_gedcom, $row->new_gedcom, $tree); $changes[$row->xref][] = $row; } return $changes; } /** * Accept all changes to a tree. * * @param Tree $tree * * @param int $n * * @return void * @throws GedcomErrorException */ public function acceptTree(Tree $tree, int $n): void { $xrefs = $this->pendingXrefs($tree); $changes = DB::table('change') ->where('gedcom_id', '=', $tree->id()) ->where('status', '=', 'pending') ->whereIn('xref', $xrefs->slice(0, $n)) ->orderBy('change_id') ->lockForUpdate() ->get(); foreach ($changes as $change) { if ($change->new_gedcom === '') { // delete $this->gedcom_import_service->updateRecord($change->old_gedcom, $tree, true); } else { // add/update $this->gedcom_import_service->updateRecord($change->new_gedcom, $tree, false); } DB::table('change') ->where('change_id', '=', $change->change_id) ->update(['status' => 'accepted']); } } /** * Accept all changes to a record. * * @param GedcomRecord $record */ public function acceptRecord(GedcomRecord $record): void { $changes = DB::table('change') ->where('gedcom_id', '=', $record->tree()->id()) ->where('xref', '=', $record->xref()) ->where('status', '=', 'pending') ->orderBy('change_id') ->lockForUpdate() ->get(); foreach ($changes as $change) { if ($change->new_gedcom === '') { // delete $this->gedcom_import_service->updateRecord($change->old_gedcom, $record->tree(), true); } else { // add/update $this->gedcom_import_service->updateRecord($change->new_gedcom, $record->tree(), false); } DB::table('change') ->where('change_id', '=', $change->change_id) ->update(['status' => 'accepted']); } } /** * Accept a change (and previous changes) to a record. * * @param GedcomRecord $record * @param string $change_id */ public function acceptChange(GedcomRecord $record, string $change_id): void { $changes = DB::table('change') ->where('gedcom_id', '=', $record->tree()->id()) ->where('xref', '=', $record->xref()) ->where('change_id', '<=', $change_id) ->where('status', '=', 'pending') ->orderBy('change_id') ->get(); foreach ($changes as $change) { if ($change->new_gedcom === '') { // delete $this->gedcom_import_service->updateRecord($change->old_gedcom, $record->tree(), true); } else { // add/update $this->gedcom_import_service->updateRecord($change->new_gedcom, $record->tree(), false); } DB::table('change') ->where('change_id', '=', $change->change_id) ->update(['status' => 'accepted']); } } /** * Reject all changes to a tree. * * @param Tree $tree */ public function rejectTree(Tree $tree): void { DB::table('change') ->where('gedcom_id', '=', $tree->id()) ->where('status', '=', 'pending') ->update(['status' => 'rejected']); } /** * Reject a change (subsequent changes) to a record. * * @param GedcomRecord $record * @param string $change_id */ public function rejectChange(GedcomRecord $record, string $change_id): void { DB::table('change') ->where('gedcom_id', '=', $record->tree()->id()) ->where('xref', '=', $record->xref()) ->where('change_id', '>=', $change_id) ->where('status', '=', 'pending') ->update(['status' => 'rejected']); } /** * Reject all changes to a record. * * @param GedcomRecord $record */ public function rejectRecord(GedcomRecord $record): void { DB::table('change') ->where('gedcom_id', '=', $record->tree()->id()) ->where('xref', '=', $record->xref()) ->where('status', '=', 'pending') ->update(['status' => 'rejected']); } /** * Generate a query for filtering the changes log. * * @param array $params * * @return Builder */ public function changesQuery(array $params): Builder { $tree = $params['tree']; $from = $params['from'] ?? ''; $to = $params['to'] ?? ''; $type = $params['type'] ?? ''; $oldged = $params['oldged'] ?? ''; $newged = $params['newged'] ?? ''; $xref = $params['xref'] ?? ''; $username = $params['username'] ?? ''; $query = DB::table('change') ->leftJoin('user', 'user.user_id', '=', 'change.user_id') ->join('gedcom', 'gedcom.gedcom_id', '=', 'change.gedcom_id') ->select(['change.*', new Expression("COALESCE(user_name, '') AS user_name"), 'gedcom_name']) ->where('gedcom_name', '=', $tree); $tz = new DateTimeZone(Auth::user()->getPreference(UserInterface::PREF_TIME_ZONE, 'UTC')); $utc = new DateTimeZone('UTC'); if ($from !== '') { $from_time = DateTimeImmutable::createFromFormat('Y-m-d', $from, $tz) ->setTime(0, 0) ->setTimezone($utc) ->format('Y-m-d H:i:s'); $query->where('change_time', '>=', $from_time); } if ($to !== '') { // before end of the day $to_time = DateTimeImmutable::createFromFormat('Y-m-d', $from, $tz) ->setTime(0, 0) ->add(new DateInterval('P1D')) ->setTimezone($utc) ->format('Y-m-d H:i:s'); $query->where('change_time', '<', $to_time); } if ($type !== '') { $query->where('status', '=', $type); } if ($oldged !== '') { $query->where('old_gedcom', 'LIKE', '%' . addcslashes($oldged, '\\%_') . '%'); } if ($newged !== '') { $query->where('new_gedcom', 'LIKE', '%' . addcslashes($newged, '\\%_') . '%'); } if ($xref !== '') { $query->where('xref', '=', $xref); } if ($username !== '') { $query->where('user_name', 'LIKE', '%' . addcslashes($username, '\\%_') . '%'); } return $query; } }