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