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