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