Passed
Push — master ( dbf8d3...48ba9f )
by MusikAnimal
04:17
created

UserRights   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 352
Duplicated Lines 0 %

Test Coverage

Coverage 68.28%

Importance

Changes 0
Metric Value
eloc 151
dl 0
loc 352
ccs 99
cts 145
cp 0.6828
rs 8.96
c 0
b 0
f 0
wmc 43

11 Methods

Rating   Name   Duplication   Size   Complexity  
A getAdminStatus() 0 10 3
A getGlobalRightsStates() 0 3 1
A getRightsName() 0 5 2
B setAutoRemovals() 0 28 8
B processRightsChanges() 0 52 8
A getCurrentRightsAndChanges() 0 19 4
A getAutoconfirmedTimestamp() 0 52 5
A getRightsChanges() 0 26 4
A getGlobalRightsChanges() 0 12 2
A getRightsNames() 0 10 2
A getRightsStates() 0 44 4

How to fix   Complexity   

Complex Class

Complex classes like UserRights often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use UserRights, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file contains only the UserRights class.
4
 */
5
6
namespace Xtools;
7
8
use AppBundle\Helper\I18nHelper;
9
use DateInterval;
10
use Exception;
11
12
/**
13
 * An UserRights provides methods around parsing changes to a user's rights.
14
 */
15
class UserRights extends Model
16
{
17
    /** @var I18nHelper For i18n and l10n. */
18
    protected $i18n;
19
20
    /** @var string[] Rights changes, keyed by timestamp then 'added' and 'removed'. */
21
    protected $rightsChanges;
22
23
    /** @var string[] Localized names of the rights. */
24
    protected $rightsNames;
25
26
    /** @var string[] Global rights changes (log), keyed by timestamp then 'added' and 'removed'. */
27
    protected $globalRightsChanges;
28
29
    /** @var array The current and former rights of the user. */
30
    protected $rightsStates = [];
31
32
    /**
33
     * Get user rights changes of the given user.
34
     * @return string[] Keyed by timestamp then 'added' and 'removed'.
35
     */
36 1
    public function getRightsChanges()
37
    {
38 1
        if (isset($this->rightsChanges)) {
39 1
            return $this->rightsChanges;
40
        }
41
42 1
        $logData = $this->getRepository()
43 1
            ->getRightsChanges($this->project, $this->user);
44
45 1
        $this->rightsChanges = $this->processRightsChanges($logData);
46
47 1
        $acDate = $this->getAutoconfirmedTimestamp();
48 1
        if ($acDate !== false) {
49
            $this->rightsChanges[$acDate] = [
50
                'logId' => null,
51
                'admin' => null,
52
                'comment' => null,
53
                'added' => ['autoconfirmed'],
54
                'removed' => [],
55
                'grantType' => strtotime($acDate) > time() ? 'pending' : 'automatic',
56
                'type' => 'local',
57
            ];
58
            krsort($this->rightsChanges);
59
        }
60
61 1
        return $this->rightsChanges;
62
    }
63
64
    /**
65
     * Checks the user rights log to see whether the user is an admin
66
     * or used to be one.
67
     * @return string|false One of false (never an admin), 'current' or 'former'.
68
     */
69 1
    public function getAdminStatus()
70
    {
71 1
        $rightsStates = $this->getRightsStates();
72
73 1
        if (in_array('sysop', $rightsStates['local']['current'])) {
74 1
            return 'current';
75
        } elseif (in_array('sysop', $rightsStates['local']['former'])) {
76
            return 'former';
77
        } else {
78
            return false;
79
        }
80
    }
81
82
    /**
83
     * Get a list of the current and former rights of the user.
84
     * @return array With keys 'local' and 'global', each with keys 'current' and 'former'.
85
     */
86 1
    public function getRightsStates()
87
    {
88 1
        if (count($this->rightsStates) > 0) {
89 1
            return $this->rightsStates;
90
        }
91
92 1
        foreach (['local', 'global'] as $type) {
93 1
            list($currentRights, $rightsChanges) = $this->getCurrentRightsAndChanges($type);
94
95 1
            $former = [];
96
97
            // We'll keep track of added rights, which we'll later compare with the
98
            // current rights to ensure the list of former rights is complete.
99
            // This is because sometimes rights were removed but there mysteriously
100
            // is no log entry of it.
101 1
            $added = [];
102
103 1
            foreach ($rightsChanges as $change) {
104 1
                $former = array_diff(
105 1
                    array_merge($former, $change['removed']),
106 1
                    $change['added']
107
                );
108
109 1
                $added = array_unique(array_merge($added, $change['added']));
110
            }
111
112
            // Also tag on rights that were previously added but mysteriously
113
            // don't have a log entry for when they were removed.
114 1
            $former = array_merge(
115 1
                array_diff($added, $currentRights),
116 1
                $former
117
            );
118
119
            // Remove the current rights for good measure. Autoconfirmed is a special case -- it can never be former,
120
            // but will end up in $former from the above code.
121 1
            $former = array_diff(array_unique($former), $currentRights, ['autoconfirmed']);
122
123 1
            $this->rightsStates[$type] = [
124 1
                'current' => $currentRights,
125 1
                'former' => $former,
126
            ];
127
        }
128
129 1
        return $this->rightsStates;
130
    }
131
132
    /**
133
     * Get a list of the current rights (of given type) and the log.
134
     * @param string $type 'local' or 'global'
135
     * @return array [string[] current rights, array rights changes].
136
     */
137 1
    private function getCurrentRightsAndChanges($type)
138
    {
139
        // Current rights are not fetched from the log because really old
140
        // log entries contained little or no metadata, and the rights
141
        // changes may be undetectable.
142 1
        if ($type === 'local') {
143 1
            $currentRights = $this->user->getUserRights($this->project);
144 1
            $rightsChanges = $this->getRightsChanges();
145
146 1
            $acDate = $this->getAutoconfirmedTimestamp();
147 1
            if (false !== $acDate && strtotime($acDate) <= time()) {
148 1
                $currentRights[] = 'autoconfirmed';
149
            }
150
        } else {
151 1
            $currentRights = $this->user->getGlobalUserRights($this->project);
152 1
            $rightsChanges = $this->getGlobalRightsChanges();
153
        }
154
155 1
        return [$currentRights, $rightsChanges];
156
    }
157
158
    /**
159
     * Get a list of the current and former global rights of the user.
160
     * @return array With keys 'current' and 'former'.
161
     */
162
    public function getGlobalRightsStates()
163
    {
164
        return $this->getRightsStates()['global'];
165
    }
166
167
    /**
168
     * Get global user rights changes of the given user.
169
     * @return string[] Keyed by timestamp then 'added' and 'removed'.
170
     */
171 1
    public function getGlobalRightsChanges()
172
    {
173 1
        if (isset($this->globalRightsChanges)) {
174 1
            return $this->globalRightsChanges;
175
        }
176
177 1
        $logData = $this->getRepository()
178 1
            ->getGlobalRightsChanges($this->project, $this->user);
179
180 1
        $this->globalRightsChanges = $this->processRightsChanges($logData);
181
182 1
        return $this->globalRightsChanges;
183
    }
184
185
    /**
186
     * Get the localized names for the user groups, fetched from on-wiki system messages.
187
     * @return string[] Localized names keyed by database value.
188
     */
189
    public function getRightsNames()
190
    {
191
        if (isset($this->rightsNames)) {
192
            return $this->rightsNames;
193
        }
194
195
        $this->rightsNames = $this->getRepository()
196
            ->getRightsNames($this->project, $this->i18n->getLang());
197
198
        return $this->rightsNames;
199
    }
200
201
    /**
202
     * Get the localized translation for the given user right.
203
     * @param string $name The name of the right, such as 'sysop'.
204
     * @return string
205
     */
206
    public function getRightsName($name)
207
    {
208
        return isset($this->getRightsNames()[$name])
209
            ? $this->getRightsNames()[$name]
210
            : $name;
211
    }
212
213
    /**
214
     * Process the given rights changes, sorting an putting in a human-readable format.
215
     * @param array $logData As fetched with EditCounterRepository::getRightsChanges.
216
     * @return array
217
     */
218 1
    private function processRightsChanges($logData)
219
    {
220 1
        $rightsChanges = [];
221
222 1
        foreach ($logData as $row) {
223 1
            $unserialized = @unserialize($row['log_params']);
224 1
            if ($unserialized !== false) {
225 1
                $old = $unserialized['4::oldgroups'];
226 1
                $new = $unserialized['5::newgroups'];
227 1
                $added = array_diff($new, $old);
228 1
                $removed = array_diff($old, $new);
229
230 1
                $rightsChanges = $this->setAutoRemovals($rightsChanges, $row, $unserialized, $added);
231
            } else {
232
                // This is the old school format the most likely contains
233
                // the list of rights additions as a comma-separated list.
234
                try {
235 1
                    list($old, $new) = explode("\n", $row['log_params']);
236 1
                    $old = array_filter(array_map('trim', explode(',', $old)));
237 1
                    $new = array_filter(array_map('trim', explode(',', $new)));
238 1
                    $added = array_diff($new, $old);
239 1
                    $removed = array_diff($old, $new);
240
                } catch (Exception $e) {
241
                    // Really, really old school format that may be missing metadata
242
                    // altogether. Here we'll just leave $added and $removed empty.
243
                    $added = [];
244
                    $removed = [];
245
                }
246
            }
247
248
            // Remove '(none)'.
249 1
            if (in_array('(none)', $added)) {
250
                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 string and false; 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

250
                array_splice($added, /** @scrutinizer ignore-type */ array_search('(none)', $added), 1);
Loading history...
251
            }
252 1
            if (in_array('(none)', $removed)) {
253
                array_splice($removed, array_search('(none)', $removed), 1);
254
            }
255
256 1
            $rightsChanges[$row['log_timestamp']] = [
257 1
                'logId' => $row['log_id'],
258 1
                'admin' => $row['log_action'] === 'autopromote' ? null : $row['log_user_text'],
259 1
                'comment' => $row['log_comment'],
260 1
                'added' => array_values($added),
261 1
                'removed' => array_values($removed),
262 1
                'grantType' => $row['log_action'] === 'autopromote' ? 'automatic' : 'manual',
263 1
                'type' => $row['type'],
264
            ];
265
        }
266
267 1
        krsort($rightsChanges);
268
269 1
        return $rightsChanges;
270
    }
271
272
    /**
273
     * Check the given log entry for rights changes that are set to automatically expire,
274
     * and add entries to $rightsChanges accordingly.
275
     * @param array $rightsChanges
276
     * @param array $row Log entry row from database.
277
     * @param array $params Unserialized log params.
278
     * @param string[] $added List of added user rights.
279
     * @return array Modified $rightsChanges.
280
     */
281 1
    private function setAutoRemovals($rightsChanges, $row, $params, $added)
282
    {
283 1
        foreach ($added as $index => $entry) {
284 1
            if (!isset($params['newmetadata'][$index]) ||
285 1
                !array_key_exists('expiry', $params['newmetadata'][$index]) ||
286 1
                empty($params['newmetadata'][$index]['expiry'])
287
            ) {
288 1
                continue;
289
            }
290
291 1
            $expiry = $params['newmetadata'][$index]['expiry'];
292
293 1
            if (isset($rightsChanges[$expiry]) && !in_array($entry, $rightsChanges[$expiry]['removed'])) {
294 1
                $rightsChanges[$expiry]['removed'][] = $entry;
295
            } else {
296 1
                $rightsChanges[$expiry] = [
297 1
                    'logId' => $row['log_id'],
298 1
                    'admin' => $row['log_user_text'],
299
                    'comment' => null,
300
                    'added' => [],
301 1
                    'removed' => [$entry],
302 1
                    'grantType' => strtotime($expiry) > time() ? 'pending' : 'automatic',
303 1
                    'type' => $row['type'],
304
                ];
305
            }
306
        }
307
308 1
        return $rightsChanges;
309
    }
310
311
    /**
312
     * Get the timestamp of when the user became autoconfirmed.
313
     * @return string|false YmdHis format, or false if date is in the future or if AC status could not be determined.
314
     */
315 1
    private function getAutoconfirmedTimestamp()
316
    {
317 1
        static $acTimestamp = null;
318 1
        if ($acTimestamp !== null) {
319
            return $acTimestamp;
320
        }
321
322 1
        $thresholds = $this->getRepository()->getAutoconfirmedAgeAndCount($this->project);
323
324
        // Happens for non-WMF installations, or if there is no autoconfirmed status.
325 1
        if (null === $thresholds) {
326 1
            return false;
327
        }
328
329
        $registrationDate = $this->user->getRegistrationDate($this->project);
330
331
        // Sometimes for old accounts the registration date is null, in which case
332
        // we won't attempt to find out when they were autoconfirmed.
333
        if (!is_a($registrationDate, 'DateTime')) {
0 ignored issues
show
Bug introduced by
It seems like $registrationDate can also be of type false; however, parameter $object of is_a() does only seem to accept object|string, 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

333
        if (!is_a(/** @scrutinizer ignore-type */ $registrationDate, 'DateTime')) {
Loading history...
334
            return false;
335
        }
336
337
        $regDateImmutable = new \DateTimeImmutable(
338
            $registrationDate->format('YmdHis')
339
        );
340
341
        $acDate = $regDateImmutable->add(DateInterval::createFromDateString(
342
            $thresholds['wgAutoConfirmAge'].' seconds'
343
        ))->format('YmdHis');
344
345
        // First check if they already had 10 edits made as of $acDate
346
        $editsByAcDate = $this->getRepository()->getNumEditsByTimestamp(
347
            $this->project,
348
            $this->user,
349
            $acDate
350
        );
351
352
        // If more than wgAutoConfirmCount, then $acDate is when they became autoconfirmed.
353
        if ($editsByAcDate >= $thresholds['wgAutoConfirmCount']) {
354
            return $acDate;
355
        }
356
357
        // Now check when the nth edit was made, where n is wgAutoConfirmCount.
358
        // This will be false if they still haven't made 10 edits.
359
        $acTimestamp = $this->getRepository()->getNthEditTimestamp(
360
            $this->project,
361
            $this->user,
362
            $registrationDate->format('YmdHis'),
363
            $thresholds['wgAutoConfirmCount']
364
        );
365
366
        return $acTimestamp;
367
    }
368
}
369