Test Failed
Push — master ( dc8c67...628500 )
by MusikAnimal
06:31
created

AdminStats::getNumInRelevantUserGroup()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
ccs 0
cts 0
cp 0
crap 2
1
<?php
2
/**
3
 * This file contains only the AdminStats class.
4
 */
5
6
declare(strict_types = 1);
7
8
namespace AppBundle\Model;
9
10
/**
11
 * AdminStats returns information about users with administrative
12
 * rights on a given wiki.
13
 */
14
class AdminStats extends Model
15
{
16
17
    /** @var string[][] Keyed by user name, values are arrays containing actions and counts. */
18
    protected $adminStats;
19
20
    /**
21
     * Keys are user names, values are their abbreviated user groups.
22
     * If abbreviations are turned on, this will instead be a string of the abbreviated
23
     * user groups, separated by slashes.
24
     * @var string[]|string
25
     */
26
    protected $usersAndGroups;
27
28
    /** @var int Number of users in the relevant group who made any actions within the time period. */
29
    protected $numWithActions = 0;
30
31
    /** @var string[] Usernames of users who are in the relevant user group (sysop for admins, etc.). */
32
    private $usersInGroup = [];
33
34
    /** @var string Group that we're getting stats for (admin, patrollers, stewards, etc.). See admin_stats.yml */
35
    private $group;
36
37
    /** @var string[] Which actions to show ('block', 'protect', etc.) */
38
    private $actions;
39
40
    /**
41
     * AdminStats constructor.
42
     * @param Project $project
43
     * @param int $start as UTC timestamp.
44
     * @param int $end as UTC timestamp.
45
     * @param string $group Which user group to get stats for. Refer to admin_stats.yml for possible values.
46
     * @param string[]|null $actions Which actions to query for ('block', 'protect', etc.). Null for all actions.
47
     */
48 3
    public function __construct(
49
        Project $project,
50
        int $start,
51
        int $end,
52
        string $group = 'admin',
53
        ?array $actions = null
54
    ) {
55 3
        $this->project = $project;
56 3
        $this->start = $start;
57 3
        $this->end = $end;
58 3
        $this->group = $group;
59 3
        $this->actions = $actions;
60 3
    }
61
62
    /**
63
     * Get the group for this AdminStats.
64
     * @return string
65
     */
66
    public function getGroup(): string
67
    {
68
        return $this->group;
69
    }
70
71
    /**
72
     * Get the user_group from the config given the 'group'.
73
     * @return string
74
     */
75
    public function getRelevantUserGroup(): string
76
    {
77
        // Quick cache, valid only for the same request.
78
        static $relevantUserGroup = '';
79
        if ('' !== $relevantUserGroup) {
80
            return $relevantUserGroup;
81 3
        }
82
83 3
        return $relevantUserGroup = $this->getRepository()->getRelevantUserGroup($this->group);
0 ignored issues
show
Bug introduced by
The method getRelevantUserGroup() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\AdminStatsRepository. ( Ignorable by Annotation )

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

83
        return $relevantUserGroup = $this->getRepository()->/** @scrutinizer ignore-call */ getRelevantUserGroup($this->group);
Loading history...
84
    }
85
86
    /**
87
     * Get the array of statistics for each qualifying user. This may be called ahead of self::getStats() so certain
88
     * class-level properties will be supplied (such as self::numUsers(), which is called in the view before iterating
89
     * over the master array of statistics).
90
     * @param bool $abbreviateGroups If set, the 'user-groups' list will be a string with abbreivated user groups names,
91 3
     *   as opposed to an array of full-named user groups.
92
     * @return string[]
93
     */
94 3
    public function prepareStats(bool $abbreviateGroups = true): array
95
    {
96 3
        if (isset($this->adminStats)) {
97 1
            return $this->adminStats;
98
        }
99
100
        // UTC to YYYYMMDDHHMMSS.
101
        $startDb = date('Ymd000000', $this->start);
0 ignored issues
show
Bug introduced by
It seems like $this->start can also be of type boolean and string; however, parameter $timestamp of date() 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

101
        $startDb = date('Ymd000000', /** @scrutinizer ignore-type */ $this->start);
Loading history...
102
        $endDb = date('Ymd235959', $this->end);
103
104
        $stats = $this->getRepository()->getStats($this->project, $startDb, $endDb, $this->group, $this->actions);
0 ignored issues
show
Bug introduced by
The method getStats() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\AdminStatsRepository. ( Ignorable by Annotation )

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

104
        $stats = $this->getRepository()->/** @scrutinizer ignore-call */ getStats($this->project, $startDb, $endDb, $this->group, $this->actions);
Loading history...
105
106 3
        // Group by username.
107
        $stats = $this->groupStatsByUsername($stats, $abbreviateGroups);
108
109
        // Resort, as for some reason the SQL isn't doing this properly.
110
        uasort($stats, function ($a, $b) {
111
            if ($a['total'] === $b['total']) {
112
                return 0;
113
            }
114
            return $a['total'] < $b['total'] ? 1 : -1;
115 3
        });
116 3
117
        $this->adminStats = $stats;
118
        return $this->adminStats;
119 3
    }
120 3
121
    /**
122
     * Get users of the project that are capable of making the relevant actions,
123 3
     * keyed by user name with abbreviations for the user groups as the values.
124 3
     * @param bool $abbreviate If set, the keys of the result with be a string containing
125 3
     *   abbreviated versions of their user groups, such as 'A' instead of administrator,
126
     *   'CU' instead of CheckUser, etc. If $abbreviate is false, the keys of the result
127
     *   will be an array of the full-named user groups.
128
     * @return string[][]
129
     */
130 3
    public function getUsersAndGroups(bool $abbreviate = true): array
131
    {
132 3
        if ($this->usersAndGroups) {
133
            return $this->usersAndGroups;
134
        }
135 3
136
        /**
137
         * Each user group that is considered capable of making 'admin actions'.
138
         * @var string[]
139
         */
140
        $adminGroups = $this->getRepository()->getUserGroups($this->project, $this->group);
0 ignored issues
show
Bug introduced by
The method getUserGroups() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\AdminStatsRepository. ( Ignorable by Annotation )

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

140
        $adminGroups = $this->getRepository()->/** @scrutinizer ignore-call */ getUserGroups($this->project, $this->group);
Loading history...
141
142
        /** @var array $usersAndGroups Keys are the usernames, values are their user groups. */
143
        $usersAndGroups = $this->project->getUsersInGroups($adminGroups);
144
145
        if (false === $abbreviate || 0 === count($usersAndGroups)) {
146 2
            return $usersAndGroups;
147
        }
148 2
149 1
        /**
150
         * Keys are the database-stored names, values are the abbreviations.
151
         * FIXME: i18n this somehow.
152
         * @var string[]
153 2
         */
154 2
        $userGroupAbbrMap = [
155
            'sysop' => 'A',
156 2
            'bureaucrat' => 'B',
157
            'steward' => 'S',
158
            'checkuser' => 'CU',
159 2
            'oversight' => 'OS',
160
            'interface-admin' => 'IA',
161
            'bot' => 'Bot',
162 2
            'global-renamer' => 'GR',
163 2
        ];
164
165
        foreach ($usersAndGroups as $user => $groups) {
166 2
            $abbrGroups = [];
167 2
168
            // Keep track of actual number of sysops.
169 2
            if (in_array($this->getRelevantUserGroup(), $groups)) {
170 2
                $this->usersInGroup[] = $user;
171
            }
172
173
            foreach ($groups as $group) {
174
                if (isset($userGroupAbbrMap[$group])) {
175
                    $abbrGroups[] = $userGroupAbbrMap[$group];
176
                }
177 1
            }
178
179 1
            // Make 'A' (admin) come before 'CU' (CheckUser), etc.
180
            sort($abbrGroups);
181
182
            $this->usersAndGroups[$user] = implode('/', $abbrGroups);
183
        }
184
185
        return $this->usersAndGroups;
186
    }
187
188 1
    /**
189
     * The number of days we're spanning between the start and end date.
190 1
     * @return int
191 1
     */
192
    public function numDays(): int
193 1
    {
194
        return (int)(($this->end - $this->start) / 60 / 60 / 24);
195
    }
196
197
    /**
198
     * Get the master array of statistics for each qualifying user.
199
     * @param bool $abbreviateGroups If set, the 'user-groups' list will be a string with abbreviated user groups names,
200
     *   as opposed to an array of full-named user groups.
201
     * @return string[]
202
     */
203
    public function getStats(bool $abbreviateGroups = true): array
204
    {
205
        if (isset($this->adminStats)) {
206
            $this->adminStats = $this->prepareStats($abbreviateGroups);
207
        }
208
        return $this->adminStats;
209
    }
210
211
    /**
212
     * Get the actions that are shown as columns in the view.
213
     * @return string[] Each the i18n key of the action.
214
     */
215
    public function getActions(): array
216
    {
217
        return count($this->getStats()) > 0
218
            ? array_diff(array_keys(array_values($this->getStats())[0]), ['username', 'user-groups', 'total'])
219
            : [];
220
    }
221
222
    /**
223
     * Given the data returned by AdminStatsRepository::getStats, return the stats keyed by user name,
224
     * adding in a key/value for user groups.
225
     * @param string[][] $data As retrieved by AdminStatsRepository::getStats
226
     * @param bool $abbreviateGroups If set, the 'user-groups' list will be a string with abbreviated user groups names,
227
     *   as opposed to an array of full-named user groups.
228
     * @return string[] Stats keyed by user name.
229
     * Functionality covered in test for self::getStats().
230
     * @codeCoverageIgnore
231
     */
232
    private function groupStatsByUsername(array $data, bool $abbreviateGroups = true): array
233
    {
234
        $usersAndGroups = $this->getUsersAndGroups($abbreviateGroups);
235
        $users = [];
236
237
        foreach ($data as $datum) {
238
            $username = $datum['username'];
239
240
            // Push to array containing all users with admin actions.
241
            // We also want numerical values to be integers.
242
            $users[$username] = array_map('intval', $datum);
243
244
            // Push back username which was casted to an integer.
245
            $users[$username]['username'] = $username;
246
247
            // Set the 'user-groups' property with the user groups they belong to (if any),
248
            // going off of self::getUsersAndGroups().
249
            if (isset($usersAndGroups[$username])) {
250
                $users[$username]['user-groups'] = $usersAndGroups[$username];
251
            } else {
252
                $users[$username]['user-groups'] = $abbreviateGroups ? '' : [];
253 1
            }
254
255 1
            // Keep track of non-admins who made admin actions.
256
            if (in_array($username, $this->usersInGroup)) {
257
                $this->numWithActions++;
258
            }
259
        }
260
261
        return $users;
262
    }
263
264
    /**
265
     * Get the total number of users in the relevant user group.
266
     * @return int
267
     */
268
    public function getNumInRelevantUserGroup(): int
269
    {
270
        return count($this->usersInGroup);
271 1
    }
272
273 1
    /**
274
     * Number of users who made any relevant actions within the time period.
275
     * @return int
276
     */
277
    public function getNumWithActions(): int
278
    {
279
        return $this->numWithActions;
280
    }
281
282
    /**
283
     * Number of currently users who made any actions within the time period who are not in the relevant user group.
284
     * @return int
285
     */
286
    public function getNumWithActionsNotInGroup(): int
287
    {
288
        return count($this->adminStats) - $this->numWithActions;
289
    }
290
}
291