Passed
Push — main ( e740f7...9feb73 )
by Greg
07:16
created

PendingChangesService   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 251
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 136
dl 0
loc 251
rs 10
c 0
b 0
f 0
wmc 26

11 Methods

Rating   Name   Duplication   Size   Complexity  
A acceptChange() 0 22 3
A acceptTree() 0 24 3
A rejectChange() 0 8 1
A acceptRecord() 0 22 3
A pendingChangesExist() 0 9 2
A rejectTree() 0 6 1
A pendingXrefs() 0 8 1
B changesQuery() 0 58 8
A rejectRecord() 0 7 1
A pendingChanges() 0 41 2
A __construct() 0 3 1
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2025 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
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Services;
21
22
use DateInterval;
23
use DateTimeImmutable;
24
use DateTimeZone;
25
use Fisharebest\Webtrees\Auth;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Auth was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
26
use Fisharebest\Webtrees\Contracts\TimestampInterface;
27
use Fisharebest\Webtrees\Contracts\UserInterface;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Contracts\UserInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
28
use Fisharebest\Webtrees\DB;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\DB was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
29
use Fisharebest\Webtrees\Exceptions\GedcomErrorException;
30
use Fisharebest\Webtrees\Family;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Family was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
31
use Fisharebest\Webtrees\Gedcom;
32
use Fisharebest\Webtrees\GedcomRecord;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\GedcomRecord was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
33
use Fisharebest\Webtrees\Header;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Header was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
34
use Fisharebest\Webtrees\Individual;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Individual was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
35
use Fisharebest\Webtrees\Location;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Location was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
36
use Fisharebest\Webtrees\Media;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Media was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
37
use Fisharebest\Webtrees\Note;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Note was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
38
use Fisharebest\Webtrees\Registry;
39
use Fisharebest\Webtrees\Repository;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Repository was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
40
use Fisharebest\Webtrees\Source;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Source was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
41
use Fisharebest\Webtrees\Submission;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Submission was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
42
use Fisharebest\Webtrees\Submitter;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Submitter was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
43
use Fisharebest\Webtrees\Tree;
44
use Illuminate\Database\Query\Builder;
45
use Illuminate\Database\Query\Expression;
46
use Illuminate\Support\Collection;
47
48
use function addcslashes;
49
use function preg_match;
50
51
/**
52
 * Manage pending changes
53
 */
54
class PendingChangesService
55
{
56
    public function __construct(
57
        private readonly GedcomImportService $gedcom_import_service,
58
    ) {
59
    }
60
61
    public function pendingChangesExist(Tree|null $tree = null): bool
62
    {
63
        $query = DB::table(table: 'change')->where(column: 'status', operator: '=', value: 'pending');
64
65
        if ($tree instanceof Tree) {
66
            $query = $query->where(column: 'tree_id', operator: '=', value: $tree->id());
67
        }
68
69
        return $query->exists();
70
    }
71
72
    /**
73
     * @return Collection<int,string>
74
     */
75
    public function pendingXrefs(Tree $tree): Collection
76
    {
77
        return DB::table('change')
78
            ->where('status', '=', 'pending')
79
            ->where('gedcom_id', '=', $tree->id())
80
            ->orderBy('xref')
81
            ->groupBy(['xref'])
82
            ->pluck('xref');
83
    }
84
85
    /**
86
     * @return array<array<object{
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array<object{ at position 4 could not be parsed: Expected '>' at position 4, but found 'object'.
Loading history...
87
     *     xref:string,
88
     *     change_id:string,
89
     *     old_gedcom:string|null,
90
     *     new_gedcom:string|null,
91
     *     change_time:TimestampInterface,
92
     *     record:GedcomRecord,
93
     *     user_name:string,
94
     *     real_name:string
95
     * }>>
96
     */
97
    public function pendingChanges(Tree $tree, int $n): array
98
    {
99
        $xrefs = $this->pendingXrefs($tree);
100
101
        $rows = DB::table('change')
102
            ->join('user', 'user.user_id', '=', 'change.user_id')
103
            ->where('status', '=', 'pending')
104
            ->where('gedcom_id', '=', $tree->id())
105
            ->whereIn('xref', $xrefs->slice(0, $n))
106
            ->orderBy('change.change_id')
107
            ->select(['change.*', 'user.user_name', 'user.real_name'])
108
            ->get();
109
110
        $changes = [];
111
112
        $factories = [
113
            Individual::RECORD_TYPE => Registry::individualFactory(),
114
            Family::RECORD_TYPE     => Registry::familyFactory(),
115
            Source::RECORD_TYPE     => Registry::sourceFactory(),
116
            Repository::RECORD_TYPE => Registry::repositoryFactory(),
117
            Media::RECORD_TYPE      => Registry::mediaFactory(),
118
            Note::RECORD_TYPE       => Registry::noteFactory(),
119
            Submitter::RECORD_TYPE  => Registry::submitterFactory(),
120
            Submission::RECORD_TYPE => Registry::submissionFactory(),
121
            Location::RECORD_TYPE   => Registry::locationFactory(),
122
            Header::RECORD_TYPE     => Registry::headerFactory(),
123
        ];
124
125
        foreach ($rows as $row) {
126
            $row->change_time = Registry::timestampFactory()->fromString($row->change_time);
127
128
            preg_match('/^0 (?:@' . Gedcom::REGEX_XREF . '@ )?(' . Gedcom::REGEX_TAG . ')/', $row->old_gedcom . $row->new_gedcom, $match);
129
130
            $factory = $factories[$match[1]] ?? Registry::gedcomRecordFactory();
131
132
            $row->record = $factory->new($row->xref, $row->old_gedcom, $row->new_gedcom, $tree);
133
134
            $changes[$row->xref][] = $row;
135
        }
136
137
        return $changes;
138
    }
139
140
    public function acceptTree(Tree $tree, int $n): void
141
    {
142
        $xrefs = $this->pendingXrefs($tree);
143
144
        $changes = DB::table('change')
145
            ->where('gedcom_id', '=', $tree->id())
146
            ->where('status', '=', 'pending')
147
            ->whereIn('xref', $xrefs->slice(0, $n))
148
            ->orderBy('change_id')
149
            ->lockForUpdate()
150
            ->get();
151
152
        foreach ($changes as $change) {
153
            if ($change->new_gedcom === '') {
154
                // delete
155
                $this->gedcom_import_service->updateRecord($change->old_gedcom, $tree, true);
156
            } else {
157
                // add/update
158
                $this->gedcom_import_service->updateRecord($change->new_gedcom, $tree, false);
159
            }
160
161
            DB::table('change')
162
                ->where('change_id', '=', $change->change_id)
163
                ->update(['status' => 'accepted']);
164
        }
165
    }
166
167
    public function acceptRecord(GedcomRecord $record): void
168
    {
169
        $changes = DB::table('change')
170
            ->where('gedcom_id', '=', $record->tree()->id())
171
            ->where('xref', '=', $record->xref())
172
            ->where('status', '=', 'pending')
173
            ->orderBy('change_id')
174
            ->lockForUpdate()
175
            ->get();
176
177
        foreach ($changes as $change) {
178
            if ($change->new_gedcom === '') {
179
                // delete
180
                $this->gedcom_import_service->updateRecord($change->old_gedcom, $record->tree(), true);
181
            } else {
182
                // add/update
183
                $this->gedcom_import_service->updateRecord($change->new_gedcom, $record->tree(), false);
184
            }
185
186
            DB::table('change')
187
                ->where('change_id', '=', $change->change_id)
188
                ->update(['status' => 'accepted']);
189
        }
190
    }
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
                $this->gedcom_import_service->updateRecord($change->old_gedcom, $record->tree(), true);
206
            } else {
207
                // add/update
208
                $this->gedcom_import_service->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
    public function rejectTree(Tree $tree): void
218
    {
219
        DB::table('change')
220
            ->where('gedcom_id', '=', $tree->id())
221
            ->where('status', '=', 'pending')
222
            ->update(['status' => 'rejected']);
223
    }
224
225
    public function rejectChange(GedcomRecord $record, string $change_id): void
226
    {
227
        DB::table('change')
228
            ->where('gedcom_id', '=', $record->tree()->id())
229
            ->where('xref', '=', $record->xref())
230
            ->where('change_id', '>=', $change_id)
231
            ->where('status', '=', 'pending')
232
            ->update(['status' => 'rejected']);
233
    }
234
235
    public function rejectRecord(GedcomRecord $record): void
236
    {
237
        DB::table('change')
238
            ->where('gedcom_id', '=', $record->tree()->id())
239
            ->where('xref', '=', $record->xref())
240
            ->where('status', '=', 'pending')
241
            ->update(['status' => 'rejected']);
242
    }
243
244
    /**
245
     * @param array<string,string> $params
246
     */
247
    public function changesQuery(array $params): Builder
248
    {
249
        $tree     = $params['tree'];
250
        $from     = $params['from'] ?? '';
251
        $to       = $params['to'] ?? '';
252
        $type     = $params['type'] ?? '';
253
        $oldged   = $params['oldged'] ?? '';
254
        $newged   = $params['newged'] ?? '';
255
        $xref     = $params['xref'] ?? '';
256
        $username = $params['username'] ?? '';
257
258
        $query = DB::table('change')
259
            ->leftJoin('user', 'user.user_id', '=', 'change.user_id')
260
            ->join('gedcom', 'gedcom.gedcom_id', '=', 'change.gedcom_id')
261
            ->select(['change.*', new Expression("COALESCE(user_name, '<none>') AS user_name"), 'gedcom_name'])
0 ignored issues
show
Bug introduced by
'COALESCE(user_name, '<none>') AS user_name' of type string is incompatible with the type Illuminate\Database\Query\TValue expected by parameter $value of Illuminate\Database\Quer...pression::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

261
            ->select(['change.*', new Expression(/** @scrutinizer ignore-type */ "COALESCE(user_name, '<none>') AS user_name"), 'gedcom_name'])
Loading history...
262
            ->where('gedcom_name', '=', $tree);
263
264
        $tz  = new DateTimeZone(Auth::user()->getPreference(UserInterface::PREF_TIME_ZONE, 'UTC'));
265
        $utc = new DateTimeZone('UTC');
266
267
        if ($from !== '') {
268
            $from_time = DateTimeImmutable::createFromFormat('!Y-m-d', $from, $tz)
269
                ->setTimezone($utc)
270
                ->format('Y-m-d H:i:s');
271
272
            $query->where('change_time', '>=', $from_time);
273
        }
274
275
        if ($to !== '') {
276
            // before end of the day
277
            $to_time = DateTimeImmutable::createFromFormat('!Y-m-d', $to, $tz)
278
                ->add(new DateInterval('P1D'))
279
                ->setTimezone($utc)
280
                ->format('Y-m-d H:i:s');
281
282
            $query->where('change_time', '<', $to_time);
283
        }
284
285
        if ($type !== '') {
286
            $query->where('status', '=', $type);
287
        }
288
289
        if ($oldged !== '') {
290
            $query->where('old_gedcom', 'LIKE', '%' . addcslashes($oldged, '\\%_') . '%');
291
        }
292
        if ($newged !== '') {
293
            $query->where('new_gedcom', 'LIKE', '%' . addcslashes($newged, '\\%_') . '%');
294
        }
295
296
        if ($xref !== '') {
297
            $query->where('xref', '=', $xref);
298
        }
299
300
        if ($username !== '') {
301
            $query->where('user_name', 'LIKE', '%' . addcslashes($username, '\\%_') . '%');
302
        }
303
304
        return $query;
305
    }
306
}
307