Passed
Push — master ( a1ed3f...a64a0e )
by
unknown
05:21
created

UserRights::getRightsStates()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 4

Importance

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

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