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