xref: /webtrees/app/Services/PendingChangesService.php (revision 090a06287954f43677f06ea778b3f67c029de8fe)
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