xref: /webtrees/app/Services/PendingChangesService.php (revision 6bd19c8ce39244f770147102c818d9c7d9e13667)
122e73debSGreg Roach<?php
222e73debSGreg Roach
322e73debSGreg Roach/**
422e73debSGreg Roach * webtrees: online genealogy
5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team
622e73debSGreg Roach * This program is free software: you can redistribute it and/or modify
722e73debSGreg Roach * it under the terms of the GNU General Public License as published by
822e73debSGreg Roach * the Free Software Foundation, either version 3 of the License, or
922e73debSGreg Roach * (at your option) any later version.
1022e73debSGreg Roach * This program is distributed in the hope that it will be useful,
1122e73debSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
1222e73debSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1322e73debSGreg Roach * GNU General Public License for more details.
1422e73debSGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
1622e73debSGreg Roach */
1722e73debSGreg Roach
1822e73debSGreg Roachdeclare(strict_types=1);
1922e73debSGreg Roach
2022e73debSGreg Roachnamespace Fisharebest\Webtrees\Services;
2122e73debSGreg Roach
225cac87aeSGreg Roachuse DateInterval;
235cac87aeSGreg Roachuse DateTimeImmutable;
245cac87aeSGreg Roachuse DateTimeZone;
255cac87aeSGreg Roachuse Fisharebest\Webtrees\Auth;
265cac87aeSGreg Roachuse Fisharebest\Webtrees\Contracts\UserInterface;
276f4ec3caSGreg Roachuse Fisharebest\Webtrees\DB;
280d047a8cSGreg Roachuse Fisharebest\Webtrees\Exceptions\GedcomErrorException;
2922ad3b5bSGreg Roachuse Fisharebest\Webtrees\Family;
3022ad3b5bSGreg Roachuse Fisharebest\Webtrees\Gedcom;
3122e73debSGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
3222ad3b5bSGreg Roachuse Fisharebest\Webtrees\Header;
3322ad3b5bSGreg Roachuse Fisharebest\Webtrees\Individual;
3422ad3b5bSGreg Roachuse Fisharebest\Webtrees\Location;
3522ad3b5bSGreg Roachuse Fisharebest\Webtrees\Media;
3622ad3b5bSGreg Roachuse Fisharebest\Webtrees\Note;
3781b729d3SGreg Roachuse Fisharebest\Webtrees\Registry;
3822ad3b5bSGreg Roachuse Fisharebest\Webtrees\Repository;
3922ad3b5bSGreg Roachuse Fisharebest\Webtrees\Source;
4022ad3b5bSGreg Roachuse Fisharebest\Webtrees\Submission;
4122ad3b5bSGreg Roachuse Fisharebest\Webtrees\Submitter;
4222e73debSGreg Roachuse Fisharebest\Webtrees\Tree;
4322e73debSGreg Roachuse Illuminate\Database\Query\Builder;
4422e73debSGreg Roachuse Illuminate\Database\Query\Expression;
4522ad3b5bSGreg Roachuse Illuminate\Support\Collection;
4622ad3b5bSGreg Roach
47b5961194SGreg Roachuse function addcslashes;
4822ad3b5bSGreg Roachuse function preg_match;
495cac87aeSGreg Roachuse function var_dump;
50b5961194SGreg Roach
5122e73debSGreg Roach/**
5222e73debSGreg Roach * Manage pending changes
5322e73debSGreg Roach */
5422e73debSGreg Roachclass PendingChangesService
5522e73debSGreg Roach{
562c685d76SGreg Roach    private GedcomImportService $gedcom_import_service;
572c685d76SGreg Roach
582c685d76SGreg Roach    /**
592c685d76SGreg Roach     * @param GedcomImportService $gedcom_import_service
602c685d76SGreg Roach     */
612c685d76SGreg Roach    public function __construct(GedcomImportService $gedcom_import_service)
622c685d76SGreg Roach    {
632c685d76SGreg Roach        $this->gedcom_import_service = $gedcom_import_service;
642c685d76SGreg Roach    }
652c685d76SGreg Roach
6622e73debSGreg Roach    /**
6722ad3b5bSGreg Roach     * Which records have pending changes
6822ad3b5bSGreg Roach     *
6922ad3b5bSGreg Roach     * @param Tree $tree
7022ad3b5bSGreg Roach     *
7136779af1SGreg Roach     * @return Collection<int,string>
7222ad3b5bSGreg Roach     */
7322ad3b5bSGreg Roach    public function pendingXrefs(Tree $tree): Collection
7422ad3b5bSGreg Roach    {
7522ad3b5bSGreg Roach        return DB::table('change')
7622ad3b5bSGreg Roach            ->where('status', '=', 'pending')
7722ad3b5bSGreg Roach            ->where('gedcom_id', '=', $tree->id())
7822ad3b5bSGreg Roach            ->orderBy('xref')
7922ad3b5bSGreg Roach            ->groupBy(['xref'])
8022ad3b5bSGreg Roach            ->pluck('xref');
8122ad3b5bSGreg Roach    }
8222ad3b5bSGreg Roach
8322ad3b5bSGreg Roach    /**
8422ad3b5bSGreg Roach     * @param Tree $tree
8522ad3b5bSGreg Roach     * @param int  $n
8622ad3b5bSGreg Roach     *
87f70bcff5SGreg Roach     * @return array<array<object>>
8822ad3b5bSGreg Roach     */
8922ad3b5bSGreg Roach    public function pendingChanges(Tree $tree, int $n): array
9022ad3b5bSGreg Roach    {
9122ad3b5bSGreg Roach        $xrefs = $this->pendingXrefs($tree);
9222ad3b5bSGreg Roach
9322ad3b5bSGreg Roach        $rows = DB::table('change')
9422ad3b5bSGreg Roach            ->join('user', 'user.user_id', '=', 'change.user_id')
9522ad3b5bSGreg Roach            ->where('status', '=', 'pending')
9622ad3b5bSGreg Roach            ->where('gedcom_id', '=', $tree->id())
9722ad3b5bSGreg Roach            ->whereIn('xref', $xrefs->slice(0, $n))
9822ad3b5bSGreg Roach            ->orderBy('change.change_id')
9922ad3b5bSGreg Roach            ->select(['change.*', 'user.user_name', 'user.real_name'])
10022ad3b5bSGreg Roach            ->get();
10122ad3b5bSGreg Roach
10222ad3b5bSGreg Roach        $changes = [];
10322ad3b5bSGreg Roach
10422ad3b5bSGreg Roach        $factories = [
1056b9cb339SGreg Roach            Individual::RECORD_TYPE => Registry::individualFactory(),
1066b9cb339SGreg Roach            Family::RECORD_TYPE     => Registry::familyFactory(),
1076b9cb339SGreg Roach            Source::RECORD_TYPE     => Registry::sourceFactory(),
1086b9cb339SGreg Roach            Repository::RECORD_TYPE => Registry::repositoryFactory(),
1096b9cb339SGreg Roach            Media::RECORD_TYPE      => Registry::mediaFactory(),
1106b9cb339SGreg Roach            Note::RECORD_TYPE       => Registry::noteFactory(),
1116b9cb339SGreg Roach            Submitter::RECORD_TYPE  => Registry::submitterFactory(),
1126b9cb339SGreg Roach            Submission::RECORD_TYPE => Registry::submissionFactory(),
1136b9cb339SGreg Roach            Location::RECORD_TYPE   => Registry::locationFactory(),
1146b9cb339SGreg Roach            Header::RECORD_TYPE     => Registry::headerFactory(),
11522ad3b5bSGreg Roach        ];
11622ad3b5bSGreg Roach
11722ad3b5bSGreg Roach        foreach ($rows as $row) {
118d97083feSGreg Roach            $row->change_time = Registry::timestampFactory()->fromString($row->change_time);
11922ad3b5bSGreg Roach
12022ad3b5bSGreg Roach            preg_match('/^0 (?:@' . Gedcom::REGEX_XREF . '@ )?(' . Gedcom::REGEX_TAG . ')/', $row->old_gedcom . $row->new_gedcom, $match);
12122ad3b5bSGreg Roach
1226b9cb339SGreg Roach            $factory = $factories[$match[1]] ?? Registry::gedcomRecordFactory();
12322ad3b5bSGreg Roach
12422ad3b5bSGreg Roach            $row->record = $factory->new($row->xref, $row->old_gedcom, $row->new_gedcom, $tree);
12522ad3b5bSGreg Roach
12622ad3b5bSGreg Roach            $changes[$row->xref][] = $row;
12722ad3b5bSGreg Roach        }
12822ad3b5bSGreg Roach
12922ad3b5bSGreg Roach        return $changes;
13022ad3b5bSGreg Roach    }
13122ad3b5bSGreg Roach
13222ad3b5bSGreg Roach    /**
13322e73debSGreg Roach     * Accept all changes to a tree.
13422e73debSGreg Roach     *
13522e73debSGreg Roach     * @param Tree $tree
13622e73debSGreg Roach     *
13722ad3b5bSGreg Roach     * @param int  $n
13822ad3b5bSGreg Roach     *
13922e73debSGreg Roach     * @return void
1400d047a8cSGreg Roach     * @throws GedcomErrorException
14122e73debSGreg Roach     */
14222ad3b5bSGreg Roach    public function acceptTree(Tree $tree, int $n): void
14322e73debSGreg Roach    {
14422ad3b5bSGreg Roach        $xrefs = $this->pendingXrefs($tree);
14522ad3b5bSGreg Roach
14622e73debSGreg Roach        $changes = DB::table('change')
14722e73debSGreg Roach            ->where('gedcom_id', '=', $tree->id())
14822e73debSGreg Roach            ->where('status', '=', 'pending')
14922ad3b5bSGreg Roach            ->whereIn('xref', $xrefs->slice(0, $n))
15022e73debSGreg Roach            ->orderBy('change_id')
15122ad3b5bSGreg Roach            ->lockForUpdate()
15222e73debSGreg Roach            ->get();
15322e73debSGreg Roach
15422e73debSGreg Roach        foreach ($changes as $change) {
15522e73debSGreg Roach            if ($change->new_gedcom === '') {
15622e73debSGreg Roach                // delete
1572c685d76SGreg Roach                $this->gedcom_import_service->updateRecord($change->old_gedcom, $tree, true);
15822e73debSGreg Roach            } else {
15922e73debSGreg Roach                // add/update
1602c685d76SGreg Roach                $this->gedcom_import_service->updateRecord($change->new_gedcom, $tree, false);
16122e73debSGreg Roach            }
16222e73debSGreg Roach
16322e73debSGreg Roach            DB::table('change')
16422e73debSGreg Roach                ->where('change_id', '=', $change->change_id)
16522e73debSGreg Roach                ->update(['status' => 'accepted']);
16622e73debSGreg Roach        }
16722e73debSGreg Roach    }
16822e73debSGreg Roach
16922e73debSGreg Roach    /**
17022e73debSGreg Roach     * Accept all changes to a record.
17122e73debSGreg Roach     *
17222e73debSGreg Roach     * @param GedcomRecord $record
17322e73debSGreg Roach     */
17422e73debSGreg Roach    public function acceptRecord(GedcomRecord $record): void
17522e73debSGreg Roach    {
17622e73debSGreg Roach        $changes = DB::table('change')
17722e73debSGreg Roach            ->where('gedcom_id', '=', $record->tree()->id())
17822e73debSGreg Roach            ->where('xref', '=', $record->xref())
17922e73debSGreg Roach            ->where('status', '=', 'pending')
18022e73debSGreg Roach            ->orderBy('change_id')
18122ad3b5bSGreg Roach            ->lockForUpdate()
18222e73debSGreg Roach            ->get();
18322e73debSGreg Roach
18422e73debSGreg Roach        foreach ($changes as $change) {
18522e73debSGreg Roach            if ($change->new_gedcom === '') {
18622e73debSGreg Roach                // delete
1872c685d76SGreg Roach                $this->gedcom_import_service->updateRecord($change->old_gedcom, $record->tree(), true);
18822e73debSGreg Roach            } else {
18922e73debSGreg Roach                // add/update
1902c685d76SGreg Roach                $this->gedcom_import_service->updateRecord($change->new_gedcom, $record->tree(), false);
19122e73debSGreg Roach            }
19222e73debSGreg Roach
19322e73debSGreg Roach            DB::table('change')
19422e73debSGreg Roach                ->where('change_id', '=', $change->change_id)
19522e73debSGreg Roach                ->update(['status' => 'accepted']);
19622e73debSGreg Roach        }
19722e73debSGreg Roach    }
19822e73debSGreg Roach
19922e73debSGreg Roach    /**
20022e73debSGreg Roach     * Accept a change (and previous changes) to a record.
20122e73debSGreg Roach     *
20222e73debSGreg Roach     * @param GedcomRecord $record
20322e73debSGreg Roach     * @param string $change_id
20422e73debSGreg Roach     */
20522e73debSGreg Roach    public function acceptChange(GedcomRecord $record, string $change_id): void
20622e73debSGreg Roach    {
20722e73debSGreg Roach        $changes = DB::table('change')
20822e73debSGreg Roach            ->where('gedcom_id', '=', $record->tree()->id())
20922e73debSGreg Roach            ->where('xref', '=', $record->xref())
21022e73debSGreg Roach            ->where('change_id', '<=', $change_id)
21122e73debSGreg Roach            ->where('status', '=', 'pending')
21222e73debSGreg Roach            ->orderBy('change_id')
21322e73debSGreg Roach            ->get();
21422e73debSGreg Roach
21522e73debSGreg Roach        foreach ($changes as $change) {
21622e73debSGreg Roach            if ($change->new_gedcom === '') {
21722e73debSGreg Roach                // delete
2182c685d76SGreg Roach                $this->gedcom_import_service->updateRecord($change->old_gedcom, $record->tree(), true);
21922e73debSGreg Roach            } else {
22022e73debSGreg Roach                // add/update
2212c685d76SGreg Roach                $this->gedcom_import_service->updateRecord($change->new_gedcom, $record->tree(), false);
22222e73debSGreg Roach            }
22322e73debSGreg Roach
22422e73debSGreg Roach            DB::table('change')
22522e73debSGreg Roach                ->where('change_id', '=', $change->change_id)
22622e73debSGreg Roach                ->update(['status' => 'accepted']);
22722e73debSGreg Roach        }
22822e73debSGreg Roach    }
22922e73debSGreg Roach
23022e73debSGreg Roach    /**
23122e73debSGreg Roach     * Reject all changes to a tree.
23222e73debSGreg Roach     *
23322e73debSGreg Roach     * @param Tree $tree
23422e73debSGreg Roach     */
23522e73debSGreg Roach    public function rejectTree(Tree $tree): void
23622e73debSGreg Roach    {
23722e73debSGreg Roach        DB::table('change')
23822e73debSGreg Roach            ->where('gedcom_id', '=', $tree->id())
23922e73debSGreg Roach            ->where('status', '=', 'pending')
24022e73debSGreg Roach            ->update(['status' => 'rejected']);
24122e73debSGreg Roach    }
24222e73debSGreg Roach
24322e73debSGreg Roach    /**
24422e73debSGreg Roach     * Reject a change (subsequent changes) to a record.
24522e73debSGreg Roach     *
24622e73debSGreg Roach     * @param GedcomRecord $record
24722e73debSGreg Roach     * @param string       $change_id
24822e73debSGreg Roach     */
24922e73debSGreg Roach    public function rejectChange(GedcomRecord $record, string $change_id): void
25022e73debSGreg Roach    {
25122e73debSGreg Roach        DB::table('change')
25222e73debSGreg Roach            ->where('gedcom_id', '=', $record->tree()->id())
25322e73debSGreg Roach            ->where('xref', '=', $record->xref())
25422e73debSGreg Roach            ->where('change_id', '>=', $change_id)
25522e73debSGreg Roach            ->where('status', '=', 'pending')
25622e73debSGreg Roach            ->update(['status' => 'rejected']);
25722e73debSGreg Roach    }
25822e73debSGreg Roach
25922e73debSGreg Roach    /**
26022e73debSGreg Roach     * Reject all changes to a record.
26122e73debSGreg Roach     *
26222e73debSGreg Roach     * @param GedcomRecord $record
26322e73debSGreg Roach     */
26422e73debSGreg Roach    public function rejectRecord(GedcomRecord $record): void
26522e73debSGreg Roach    {
26622e73debSGreg Roach        DB::table('change')
26722e73debSGreg Roach            ->where('gedcom_id', '=', $record->tree()->id())
26822e73debSGreg Roach            ->where('xref', '=', $record->xref())
26922e73debSGreg Roach            ->where('status', '=', 'pending')
27022e73debSGreg Roach            ->update(['status' => 'rejected']);
27122e73debSGreg Roach    }
27222e73debSGreg Roach
27322e73debSGreg Roach    /**
27422e73debSGreg Roach     * Generate a query for filtering the changes log.
27522e73debSGreg Roach     *
27609482a55SGreg Roach     * @param array<string> $params
27722e73debSGreg Roach     *
27822e73debSGreg Roach     * @return Builder
27922e73debSGreg Roach     */
28057bfa969SGreg Roach    public function changesQuery(array $params): Builder
28122e73debSGreg Roach    {
28257bfa969SGreg Roach        $tree     = $params['tree'];
28357bfa969SGreg Roach        $from     = $params['from'] ?? '';
28457bfa969SGreg Roach        $to       = $params['to'] ?? '';
28557bfa969SGreg Roach        $type     = $params['type'] ?? '';
28657bfa969SGreg Roach        $oldged   = $params['oldged'] ?? '';
28757bfa969SGreg Roach        $newged   = $params['newged'] ?? '';
28857bfa969SGreg Roach        $xref     = $params['xref'] ?? '';
28957bfa969SGreg Roach        $username = $params['username'] ?? '';
29022e73debSGreg Roach
29122e73debSGreg Roach        $query = DB::table('change')
29222e73debSGreg Roach            ->leftJoin('user', 'user.user_id', '=', 'change.user_id')
29322e73debSGreg Roach            ->join('gedcom', 'gedcom.gedcom_id', '=', 'change.gedcom_id')
29422e73debSGreg Roach            ->select(['change.*', new Expression("COALESCE(user_name, '<none>') AS user_name"), 'gedcom_name'])
29557bfa969SGreg Roach            ->where('gedcom_name', '=', $tree);
29622e73debSGreg Roach
2975cac87aeSGreg Roach        $tz  = new DateTimeZone(Auth::user()->getPreference(UserInterface::PREF_TIME_ZONE, 'UTC'));
2985cac87aeSGreg Roach        $utc = new DateTimeZone('UTC');
2995cac87aeSGreg Roach
30022e73debSGreg Roach        if ($from !== '') {
301*6bd19c8cSGreg Roach            $from_time = DateTimeImmutable::createFromFormat('!Y-m-d', $from, $tz)
3025cac87aeSGreg Roach                ->setTimezone($utc)
3035cac87aeSGreg Roach                ->format('Y-m-d H:i:s');
3045cac87aeSGreg Roach
3055cac87aeSGreg Roach            $query->where('change_time', '>=', $from_time);
30622e73debSGreg Roach        }
30722e73debSGreg Roach
30822e73debSGreg Roach        if ($to !== '') {
30922e73debSGreg Roach            // before end of the day
310*6bd19c8cSGreg Roach            $to_time = DateTimeImmutable::createFromFormat('!Y-m-d', $from, $tz)
3115cac87aeSGreg Roach                ->add(new DateInterval('P1D'))
3125cac87aeSGreg Roach                ->setTimezone($utc)
3135cac87aeSGreg Roach                ->format('Y-m-d H:i:s');
3145cac87aeSGreg Roach
3155cac87aeSGreg Roach            $query->where('change_time', '<', $to_time);
31622e73debSGreg Roach        }
31722e73debSGreg Roach
31822e73debSGreg Roach        if ($type !== '') {
31922e73debSGreg Roach            $query->where('status', '=', $type);
32022e73debSGreg Roach        }
32122e73debSGreg Roach
32222e73debSGreg Roach        if ($oldged !== '') {
323b5961194SGreg Roach            $query->where('old_gedcom', 'LIKE', '%' . addcslashes($oldged, '\\%_') . '%');
32422e73debSGreg Roach        }
32522e73debSGreg Roach        if ($newged !== '') {
326b5961194SGreg Roach            $query->where('new_gedcom', 'LIKE', '%' . addcslashes($newged, '\\%_') . '%');
32722e73debSGreg Roach        }
32822e73debSGreg Roach
32922e73debSGreg Roach        if ($xref !== '') {
33022e73debSGreg Roach            $query->where('xref', '=', $xref);
33122e73debSGreg Roach        }
33222e73debSGreg Roach
33322e73debSGreg Roach        if ($username !== '') {
334b5961194SGreg Roach            $query->where('user_name', 'LIKE', '%' . addcslashes($username, '\\%_') . '%');
33522e73debSGreg Roach        }
33622e73debSGreg Roach
33722e73debSGreg Roach        return $query;
33822e73debSGreg Roach    }
33922e73debSGreg Roach}
340