Passed
Push — master ( 4ac0e3...997f64 )
by MusikAnimal
07:16
created

UserRights::getRightsChanges()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3.054

Importance

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

243
                array_splice($added, /** @scrutinizer ignore-type */ array_search('(none)', $added), 1);
Loading history...
244
            }
245 1
            if (in_array('(none)', $removed)) {
246
                array_splice($removed, array_search('(none)', $removed), 1);
247
            }
248
249 1
            $rightsChanges[$row['log_timestamp']] = [
250 1
                'logId' => $row['log_id'],
251 1
                'admin' => $row['log_user_text'],
252 1
                'comment' => $row['log_comment'],
253 1
                'added' => array_values($added),
254 1
                'removed' => array_values($removed),
255 1
                'automatic' => $row['log_action'] === 'autopromote',
256 1
                'type' => $row['type'],
257
            ];
258
        }
259
260 1
        krsort($rightsChanges);
261
262 1
        return $rightsChanges;
263
    }
264
265
    /**
266
     * Check the given log entry for rights changes that are set to automatically expire,
267
     * and add entries to $rightsChanges accordingly.
268
     * @param array $rightsChanges
269
     * @param array $row Log entry row from database.
270
     * @param array $params Unserialized log params.
271
     * @param string[] $added List of added user rights.
272
     * @return array Modified $rightsChanges.
273
     */
274 1
    private function setAutoRemovals($rightsChanges, $row, $params, $added)
275
    {
276 1
        foreach ($added as $index => $entry) {
277 1
            if (!isset($params['newmetadata'][$index]) ||
278 1
                !array_key_exists('expiry', $params['newmetadata'][$index]) ||
279 1
                empty($params['newmetadata'][$index]['expiry'])
280
            ) {
281 1
                continue;
282
            }
283
284 1
            $expiry = $params['newmetadata'][$index]['expiry'];
285
286 1
            if (isset($rightsChanges[$expiry]) && !in_array($entry, $rightsChanges[$expiry]['removed'])) {
287 1
                $rightsChanges[$expiry]['removed'][] = $entry;
288
            } else {
289 1
                $rightsChanges[$expiry] = [
290 1
                    'logId' => $row['log_id'],
291 1
                    'admin' => $row['log_user_text'],
292
                    'comment' => null,
293
                    'added' => [],
294 1
                    'removed' => [$entry],
295
                    'automatic' => true,
296 1
                    'type' => $row['type'],
297
                ];
298
            }
299
        }
300
301 1
        return $rightsChanges;
302
    }
303
304
    /**
305
     * Get the timestamp of when the user became autoconfirmed.
306
     * @return string YYYYMMDDHHMMSS format.
307
     */
308 1
    private function getAutoconfirmedTimestamp()
309
    {
310 1
        static $acTimestamp = null;
311 1
        if ($acTimestamp !== null) {
312
            return $acTimestamp;
313
        }
314
315 1
        $thresholds = $this->getRepository()->getAutoconfirmedAgeAndCount($this->project);
316
317
        // Happens for non-WMF installations, or if there is no autoconfirmed status.
318 1
        if (null === $thresholds) {
319 1
            return null;
320
        }
321
322
        $registrationDate = $this->user->getRegistrationDate($this->project);
323
324
        // Sometimes for old accounts the registration date is null, in which case
325
        // we won't attempt to find out when they were autoconfirmed.
326
        if (!is_a($registrationDate, 'DateTime')) {
327
            return false;
328
        }
329
330
        $regDateImmutable = new \DateTimeImmutable(
331
            $registrationDate->format('YmdHis')
332
        );
333
334
        $acDate = $regDateImmutable->add(DateInterval::createFromDateString(
335
            $thresholds['wgAutoConfirmAge'].' seconds'
336
        ))->format('YmdHis');
337
338
        // First check if they already had 10 edits made as of $acDate
339
        $editsByAcDate = $this->getRepository()->getNumEditsByTimestamp(
340
            $this->project,
341
            $this->user,
342
            $acDate
343
        );
344
345
        // If more than wgAutoConfirmCount, then $acDate is when they became autoconfirmed.
346
        if ($editsByAcDate >= $thresholds['wgAutoConfirmCount']) {
347
            return $acDate;
348
        }
349
350
        // Now check when the nth edit was made, where n is wgAutoConfirmCount.
351
        // This will be false if they still haven't made 10 edits.
352
        $acTimestamp = $this->getRepository()->getNthEditTimestamp(
353
            $this->project,
354
            $this->user,
355
            $registrationDate->format('YmdHis'),
356
            $thresholds['wgAutoConfirmCount']
357
        );
358
359
        return $acTimestamp;
360
    }
361
}
362