Passed
Push — master ( 250650...eb8721 )
by MusikAnimal
10:40
created

UserRights::processRightsChanges()   F

Complexity

Conditions 26
Paths 162

Size

Total Lines 90
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 52.5695

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 26
eloc 51
c 1
b 0
f 0
nc 162
nop 1
dl 0
loc 90
ccs 33
cts 50
cp 0.66
crap 52.5695
rs 3.65

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * This file contains only the UserRights class.
4
 */
5
6
declare(strict_types = 1);
7
8
namespace AppBundle\Model;
9
10
use AppBundle\Helper\I18nHelper;
11
use DateInterval;
12
use Exception;
13
14
/**
15
 * An UserRights provides methods around parsing changes to a user's rights.
16
 */
17
class UserRights extends Model
18
{
19
    /** @var I18nHelper For i18n and l10n. */
20
    protected $i18n;
21
22
    /** @var string[] Rights changes, keyed by timestamp then 'added' and 'removed'. */
23
    protected $rightsChanges;
24
25
    /** @var string[] Localized names of the rights. */
26
    protected $rightsNames;
27
28
    /** @var string[] Global rights changes (log), keyed by timestamp then 'added' and 'removed'. */
29
    protected $globalRightsChanges;
30
31
    /** @var array The current and former rights of the user. */
32
    protected $rightsStates = [];
33
34
    /**
35
     * Get user rights changes of the given user.
36
     * @return string[] Keyed by timestamp then 'added' and 'removed'.
37
     */
38 1
    public function getRightsChanges(?int $limit = null): array
39
    {
40 1
        if (!isset($this->rightsChanges)) {
41 1
            $logData = $this->getRepository()
42 1
                ->getRightsChanges($this->project, $this->user);
0 ignored issues
show
Bug introduced by
The method getRightsChanges() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\UserRightsRepository. ( Ignorable by Annotation )

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

42
                ->/** @scrutinizer ignore-call */ getRightsChanges($this->project, $this->user);
Loading history...
43
44 1
            $this->rightsChanges = $this->processRightsChanges($logData);
45
46 1
            $acDate = $this->getAutoconfirmedTimestamp();
47 1
            if (false !== $acDate) {
48
                $this->rightsChanges[$acDate] = [
49
                    'logId' => null,
50
                    'performer' => null,
51
                    'comment' => null,
52
                    'added' => ['autoconfirmed'],
53
                    'removed' => [],
54
                    'grantType' => strtotime($acDate) > time() ? 'pending' : 'automatic',
55
                    'type' => 'local',
56
                ];
57
                krsort($this->rightsChanges);
58
            }
59
        }
60
61 1
        return array_slice($this->rightsChanges, 0, $limit, true);
62
    }
63
64
    /**
65
     * Checks the user rights log to see whether the user is an admin or used to be one.
66
     * @return string|false One of false (never an admin), 'current' or 'former'.
67
     */
68 1
    public function getAdminStatus()
69
    {
70 1
        $rightsStates = $this->getRightsStates();
71
72 1
        if (in_array('sysop', $rightsStates['local']['current'])) {
73 1
            return 'current';
74
        } elseif (in_array('sysop', $rightsStates['local']['former'])) {
75
            return 'former';
76
        } else {
77
            return false;
78
        }
79
    }
80
81
    /**
82
     * Get a list of the current and former rights of the user.
83
     * @return array With keys 'local' and 'global', each with keys 'current' and 'former'.
84
     */
85 1
    public function getRightsStates(): array
86
    {
87 1
        if (count($this->rightsStates) > 0) {
88 1
            return $this->rightsStates;
89
        }
90
91 1
        foreach (['local', 'global'] as $type) {
92 1
            [$currentRights, $rightsChanges] = $this->getCurrentRightsAndChanges($type);
93
94 1
            $former = [];
95
96
            // We'll keep track of added rights, which we'll later compare with the
97
            // current rights to ensure the list of former rights is complete.
98
            // This is because sometimes rights were removed but there mysteriously
99
            // is no log entry of it.
100 1
            $added = [];
101
102 1
            foreach ($rightsChanges as $change) {
103 1
                $former = array_diff(
104 1
                    array_merge($former, $change['removed']),
105 1
                    $change['added']
106
                );
107
108 1
                $added = array_unique(array_merge($added, $change['added']));
109
            }
110
111
            // Also tag on rights that were previously added but mysteriously
112
            // don't have a log entry for when they were removed.
113 1
            $former = array_merge(
114 1
                array_diff($added, $currentRights),
115 1
                $former
116
            );
117
118
            // Remove the current rights for good measure. Autoconfirmed is a special case -- it can never be former,
119
            // but will end up in $former from the above code.
120 1
            $former = array_diff(array_unique($former), $currentRights, ['autoconfirmed']);
121
122 1
            $this->rightsStates[$type] = [
123 1
                'current' => $currentRights,
124 1
                'former' => $former,
125
            ];
126
        }
127
128 1
        return $this->rightsStates;
129
    }
130
131
    /**
132
     * Get a list of the current rights (of given type) and the log.
133
     * @param string $type 'local' or 'global'
134
     * @return array [string[] current rights, array rights changes].
135
     */
136 1
    private function getCurrentRightsAndChanges(string $type): array
137
    {
138
        // Current rights are not fetched from the log because really old
139
        // log entries contained little or no metadata, and the rights
140
        // changes may be undetectable.
141 1
        if ('local' === $type) {
142 1
            $currentRights = $this->user->getUserRights($this->project);
143 1
            $rightsChanges = $this->getRightsChanges();
144
145 1
            $acDate = $this->getAutoconfirmedTimestamp();
146 1
            if (false !== $acDate && strtotime($acDate) <= time()) {
147 1
                $currentRights[] = 'autoconfirmed';
148
            }
149
        } else {
150 1
            $currentRights = $this->user->getGlobalUserRights($this->project);
151 1
            $rightsChanges = $this->getGlobalRightsChanges();
152
        }
153
154 1
        return [$currentRights, $rightsChanges];
155
    }
156
157
    /**
158
     * Get a list of the current and former global rights of the user.
159
     * @return array With keys 'current' and 'former'.
160
     */
161
    public function getGlobalRightsStates(): array
162
    {
163
        return $this->getRightsStates()['global'];
164
    }
165
166
    /**
167
     * Get global user rights changes of the given user.
168
     * @return string[] Keyed by timestamp then 'added' and 'removed'.
169
     */
170 1
    public function getGlobalRightsChanges(): array
171
    {
172 1
        if (isset($this->globalRightsChanges)) {
173 1
            return $this->globalRightsChanges;
174
        }
175
176 1
        $logData = $this->getRepository()
177 1
            ->getGlobalRightsChanges($this->project, $this->user);
0 ignored issues
show
Bug introduced by
The method getGlobalRightsChanges() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\UserRightsRepository. ( Ignorable by Annotation )

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

177
            ->/** @scrutinizer ignore-call */ getGlobalRightsChanges($this->project, $this->user);
Loading history...
178
179 1
        $this->globalRightsChanges = $this->processRightsChanges($logData);
180
181 1
        return $this->globalRightsChanges;
182
    }
183
184
    /**
185
     * Get the localized names for the user groups, fetched from on-wiki system messages.
186
     * @return string[] Localized names keyed by database value.
187
     */
188
    public function getRightsNames(): array
189
    {
190
        if (isset($this->rightsNames)) {
191
            return $this->rightsNames;
192
        }
193
194
        $this->rightsNames = $this->getRepository()
195
            ->getRightsNames($this->project, $this->i18n->getLang());
0 ignored issues
show
Bug introduced by
The method getRightsNames() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\UserRightsRepository. ( Ignorable by Annotation )

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

195
            ->/** @scrutinizer ignore-call */ getRightsNames($this->project, $this->i18n->getLang());
Loading history...
196
197
        return $this->rightsNames;
198
    }
199
200
    /**
201
     * Get the localized translation for the given user right.
202
     * @param string $name The name of the right, such as 'sysop'.
203
     * @return string
204
     */
205
    public function getRightsName(string $name): string
206
    {
207
        return $this->getRightsNames()[$name] ?? $name;
208
    }
209
210
    /**
211
     * Process the given rights changes, sorting an putting in a human-readable format.
212
     * @param array $logData As fetched with EditCounterRepository::getRightsChanges.
213
     * @return array
214
     */
215 1
    private function processRightsChanges(array $logData): array
216
    {
217 1
        $rightsChanges = [];
218
219 1
        foreach ($logData as $row) {
220
            // Can happen if the log entry has been deleted.
221 1
            if (!isset($row['log_params']) || null === $row['log_params']) {
222
                continue;
223
            }
224
225 1
            $unserialized = @unserialize($row['log_params']);
226
227 1
            if (false !== $unserialized) {
228 1
                $old = $unserialized['4::oldgroups'];
229 1
                $new = $unserialized['5::newgroups'];
230 1
                $added = array_diff($new, $old);
231 1
                $removed = array_diff($old, $new);
232 1
                $oldMetadata = $unserialized['oldmetadata'] ?? null;
233 1
                $newMetadata = $unserialized['newmetadata'] ?? null;
234
235
                // Check for changes only to expiry. If such exists, treat it as added. Various issets are safeguards.
236 1
                if (empty($added) && empty($removed) && isset($oldMetadata) && isset($newMetadata)) {
237
                    foreach ($old as $index => $right) {
238
                        $oldExpiry = $oldMetadata[$index]['expiry'] ?? null;
239
                        $newExpiry = $newMetadata[$index]['expiry'] ?? null;
240
241
                        // Check if an expiry was added, removed, or modified.
242
                        if ((null !== $oldExpiry && null === $newExpiry) ||
243
                            (null === $oldExpiry && null !== $newExpiry) ||
244
                            (null !== $oldExpiry && null !== $newExpiry)
245
                        ) {
246
                            $added[$index] = $right;
247
248
                            // Remove the last auto-removal(s), which must exist.
249
                            foreach (array_reverse($rightsChanges, true) as $timestamp => $change) {
250
                                if (in_array($right, $change['removed']) && !in_array($right, $change['added']) &&
251
                                    'automatic' === $change['grantType']
252
                                ) {
253
                                    unset($rightsChanges[$timestamp]);
254
                                }
255
                            }
256
                        }
257
                    }
258
                }
259
260
                // If a right was removed, remove any previously pending auto-removals.
261 1
                if (count($removed) > 0) {
262 1
                    $this->unsetAutoRemoval($rightsChanges, $removed);
263
                }
264
265 1
                $this->setAutoRemovals($rightsChanges, $row, $unserialized, $added);
266
            } else {
267
                // This is the old school format the most likely contains
268
                // the list of rights additions as a comma-separated list.
269
                try {
270 1
                    [$old, $new] = explode("\n", $row['log_params']);
271 1
                    $old = array_filter(array_map('trim', explode(',', $old)));
272 1
                    $new = array_filter(array_map('trim', explode(',', (string)$new)));
273 1
                    $added = array_diff($new, $old);
274 1
                    $removed = array_diff($old, $new);
275
                } catch (Exception $e) {
276
                    // Really, really old school format that may be missing metadata
277
                    // altogether. Here we'll just leave $added and $removed empty.
278
                    $added = [];
279
                    $removed = [];
280
                }
281
            }
282
283
            // Remove '(none)'.
284 1
            if (in_array('(none)', $added)) {
285
                array_splice($added, array_search('(none)', $added), 1);
0 ignored issues
show
Bug introduced by
It seems like array_search('(none)', $added) can also be of type false and string; however, parameter $offset of array_splice() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

285
                array_splice($added, /** @scrutinizer ignore-type */ array_search('(none)', $added), 1);
Loading history...
286
            }
287 1
            if (in_array('(none)', $removed)) {
288
                array_splice($removed, array_search('(none)', $removed), 1);
289
            }
290
291 1
            $rightsChanges[$row['log_timestamp']] = [
292 1
                'logId' => $row['log_id'],
293 1
                'performer' => 'autopromote' === $row['log_action'] ? null : $row['performer'],
294 1
                'comment' => $row['log_comment'],
295 1
                'added' => array_values($added),
296 1
                'removed' => array_values($removed),
297 1
                'grantType' => 'autopromote' === $row['log_action'] ? 'automatic' : 'manual',
298 1
                'type' => $row['type'],
299
            ];
300
        }
301
302 1
        krsort($rightsChanges);
303
304 1
        return $rightsChanges;
305
    }
306
307
    /**
308
     * Check the given log entry for rights changes that are set to automatically expire,
309
     * and add entries to $rightsChanges accordingly.
310
     * @param array $rightsChanges
311
     * @param array $row Log entry row from database.
312
     * @param array $params Unserialized log params.
313
     * @param string[] $added List of added user rights.
314
     */
315 1
    private function setAutoRemovals(array &$rightsChanges, array $row, array $params, array $added): void
316
    {
317 1
        foreach ($added as $index => $entry) {
318
            // Skip if no expiry was set.
319 1
            if (!isset($params['newmetadata'][$index]) ||
320 1
                !array_key_exists('expiry', $params['newmetadata'][$index]) ||
321 1
                empty($params['newmetadata'][$index]['expiry'])
322
            ) {
323 1
                continue;
324
            }
325
326 1
            $expiry = $params['newmetadata'][$index]['expiry'];
327
328 1
            if (isset($rightsChanges[$expiry]) && !in_array($entry, $rightsChanges[$expiry]['removed'])) {
329
                // Temporary right expired.
330 1
                $rightsChanges[$expiry]['removed'][] = $entry;
331
            } else {
332
                // Temporary right was added.
333 1
                $rightsChanges[$expiry] = [
334 1
                    'logId' => $row['log_id'],
335 1
                    'performer' => $row['performer'],
336
                    'comment' => null,
337
                    'added' => [],
338 1
                    'removed' => [$entry],
339 1
                    'grantType' => strtotime($expiry) > time() ? 'pending' : 'automatic',
340 1
                    'type' => $row['type'],
341
                ];
342
            }
343
        }
344
345
        // Resort because the auto-removal timestamp could be before other rights changes.
346 1
        ksort($rightsChanges);
347 1
    }
348
349 1
    private function unsetAutoRemoval(array &$rightsChanges, array $removed): void
350
    {
351 1
        foreach ($rightsChanges as $timestamp => $change) {
352 1
            if ('pending' === $change['grantType']) {
353
                $rightsChanges[$timestamp]['removed'] = array_diff($rightsChanges[$timestamp]['removed'], $removed);
354
                if (empty($rightsChanges[$timestamp]['removed'])) {
355 1
                    unset($rightsChanges[$timestamp]);
356
                }
357
            }
358
        }
359 1
    }
360
361
    /**
362
     * Get the timestamp of when the user became autoconfirmed.
363
     * @return string|false YmdHis format, or false if date is in the future or if AC status could not be determined.
364
     */
365 1
    private function getAutoconfirmedTimestamp()
366
    {
367 1
        static $acTimestamp = null;
368 1
        if (null !== $acTimestamp) {
369
            return $acTimestamp;
370
        }
371
372 1
        $thresholds = $this->getRepository()->getAutoconfirmedAgeAndCount($this->project);
0 ignored issues
show
Bug introduced by
The method getAutoconfirmedAgeAndCount() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\UserRightsRepository. ( Ignorable by Annotation )

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

372
        $thresholds = $this->getRepository()->/** @scrutinizer ignore-call */ getAutoconfirmedAgeAndCount($this->project);
Loading history...
373
374
        // Happens for non-WMF installations, or if there is no autoconfirmed status.
375 1
        if (null === $thresholds) {
376 1
            return false;
377
        }
378
379
        $registrationDate = $this->user->getRegistrationDate($this->project);
380
381
        // Sometimes for old accounts the registration date is null, in which case
382
        // we won't attempt to find out when they were autoconfirmed.
383
        if (!is_a($registrationDate, 'DateTime')) {
384
            return false;
385
        }
386
387
        $regDateImmutable = new \DateTimeImmutable(
388
            $registrationDate->format('YmdHis')
389
        );
390
391
        $acDate = $regDateImmutable->add(DateInterval::createFromDateString(
392
            $thresholds['wgAutoConfirmAge'].' seconds'
393
        ))->format('YmdHis');
394
395
        // First check if they already had 10 edits made as of $acDate
396
        $editsByAcDate = $this->getRepository()->getNumEditsByTimestamp(
0 ignored issues
show
Bug introduced by
The method getNumEditsByTimestamp() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\UserRightsRepository. ( Ignorable by Annotation )

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

396
        $editsByAcDate = $this->getRepository()->/** @scrutinizer ignore-call */ getNumEditsByTimestamp(
Loading history...
397
            $this->project,
398
            $this->user,
399
            $acDate
400
        );
401
402
        // If more than wgAutoConfirmCount, then $acDate is when they became autoconfirmed.
403
        if ($editsByAcDate >= $thresholds['wgAutoConfirmCount']) {
404
            return $acDate;
405
        }
406
407
        // Now check when the nth edit was made, where n is wgAutoConfirmCount.
408
        // This will be false if they still haven't made 10 edits.
409
        $acTimestamp = $this->getRepository()->getNthEditTimestamp(
0 ignored issues
show
Bug introduced by
The method getNthEditTimestamp() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\UserRightsRepository. ( Ignorable by Annotation )

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

409
        $acTimestamp = $this->getRepository()->/** @scrutinizer ignore-call */ getNthEditTimestamp(
Loading history...
410
            $this->project,
411
            $this->user,
412
            $registrationDate->format('YmdHis'),
413
            $thresholds['wgAutoConfirmCount']
414
        );
415
416
        return $acTimestamp;
417
    }
418
}
419