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