Passed
Push — master ( 619316...3e4776 )
by MusikAnimal
07:30
created

AdminStats::getActions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 0
dl 0
loc 5
rs 10
c 0
b 0
f 0
ccs 0
cts 4
cp 0
crap 6
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 $adminsAndGroups;
27
28
    /** @var int Number of admins who made any actions within the time period. */
29
    protected $numAdminsWithActions = 0;
30
31
    /** @var string[] Usernames of proper sysops. */
32
    private $admins = [];
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 users of the project that are capable of making 'admin actions',
73
     * keyed by user name with abbreviations for the user groups as the values.
74
     * @param bool $abbreviate If set, the keys of the result with be a string containing
75
     *   abbreviated versions of their user groups, such as 'A' instead of administrator,
76
     *   'CU' instead of CheckUser, etc. If $abbreviate is false, the keys of the result
77
     *   will be an array of the full-named user groups.
78
     * @see Project::getAdmins()
79
     * @return string[][]
80
     */
81 3
    public function getAdminsAndGroups(bool $abbreviate = true): array
82
    {
83 3
        if ($this->adminsAndGroups) {
84
            return $this->adminsAndGroups;
85
        }
86
87
        /**
88
         * Each user group that is considered capable of making 'admin actions'.
89
         * @var string[]
90
         */
91 3
        $adminGroups = $this->getRepository()->getAdminGroups($this->project, $this->group);
0 ignored issues
show
Bug introduced by
The method getAdminGroups() 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

91
        $adminGroups = $this->getRepository()->/** @scrutinizer ignore-call */ getAdminGroups($this->project, $this->group);
Loading history...
92
93
        /** @var array $admins Keys are the usernames, values are their user groups. */
94 3
        $admins = $this->project->getUsersInGroups($adminGroups);
95
96 3
        if (false === $abbreviate || 0 === count($admins)) {
97 1
            return $admins;
98
        }
99
100
        /**
101
         * Keys are the database-stored names, values are the abbreviations.
102
         * FIXME: i18n this somehow.
103
         * @var string[]
104
         */
105
        $userGroupAbbrMap = [
106 3
            'sysop' => 'A',
107
            'bureaucrat' => 'B',
108
            'steward' => 'S',
109
            'checkuser' => 'CU',
110
            'oversight' => 'OS',
111
            'interface-admin' => 'IA',
112
            'bot' => 'Bot',
113
        ];
114
115 3
        foreach ($admins as $admin => $groups) {
116 3
            $abbrGroups = [];
117
118
            // Keep track of actual number of sysops.
119 3
            if (in_array('sysop', $groups)) {
120 3
                $this->admins[] = $admin;
121
            }
122
123 3
            foreach ($groups as $group) {
124 3
                if (isset($userGroupAbbrMap[$group])) {
125 3
                    $abbrGroups[] = $userGroupAbbrMap[$group];
126
                }
127
            }
128
129
            // Make 'A' (admin) come before 'CU' (CheckUser), etc.
130 3
            sort($abbrGroups);
131
132 3
            $this->adminsAndGroups[$admin] = implode('/', $abbrGroups);
133
        }
134
135 3
        return $this->adminsAndGroups;
136
    }
137
138
    /**
139
     * Get the array of statistics for each qualifying user. This may be called ahead of self::getStats() so certain
140
     * class-level properties will be supplied (such as self::numUsers(), which is called in the view before iterating
141
     * over the master array of statistics).
142
     * @param bool $abbreviateGroups If set, the 'user-groups' list will be a string with abbreivated user groups names,
143
     *   as opposed to an array of full-named user groups.
144
     * @return string[]
145
     */
146 2
    public function prepareStats(bool $abbreviateGroups = true): array
147
    {
148 2
        if (isset($this->adminStats)) {
149 1
            return $this->adminStats;
150
        }
151
152
        // UTC to YYYYMMDDHHMMSS.
153 2
        $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

153
        $startDb = date('Ymd000000', /** @scrutinizer ignore-type */ $this->start);
Loading history...
154 2
        $endDb = date('Ymd235959', $this->end);
155
156 2
        $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

156
        $stats = $this->getRepository()->/** @scrutinizer ignore-call */ getStats($this->project, $startDb, $endDb, $this->group, $this->actions);
Loading history...
157
158
        // Group by username.
159 2
        $stats = $this->groupAdminStatsByUsername($stats, $abbreviateGroups);
160
161
        // Resort, as for some reason the SQL isn't doing this properly.
162 2
        uasort($stats, function ($a, $b) {
163 2
            if ($a['total'] === $b['total']) {
164
                return 0;
165
            }
166 2
            return $a['total'] < $b['total'] ? 1 : -1;
167 2
        });
168
169 2
        $this->adminStats = $stats;
170 2
        return $this->adminStats;
171
    }
172
173
    /**
174
     * The number of days we're spanning between the start and end date.
175
     * @return int
176
     */
177 1
    public function numDays(): int
178
    {
179 1
        return (int)(($this->end - $this->start) / 60 / 60 / 24);
180
    }
181
182
    /**
183
     * Get the master array of statistics for each qualifying user.
184
     * @param bool $abbreviateGroups If set, the 'user-groups' list will be a string with abbreviated user groups names,
185
     *   as opposed to an array of full-named user groups.
186
     * @return string[]
187
     */
188 1
    public function getStats(bool $abbreviateGroups = true): array
189
    {
190 1
        if (isset($this->adminStats)) {
191 1
            $this->adminStats = $this->prepareStats($abbreviateGroups);
192
        }
193 1
        return $this->adminStats;
194
    }
195
196
    /**
197
     * Get the actions that are shown as columns in the view.
198
     * @return string[] Each the i18n key of the action.
199
     */
200
    public function getActions(): array
201
    {
202
        return count($this->getStats()) > 0
203
            ? array_diff(array_keys(array_values($this->getStats())[0]), ['username', 'user-groups', 'total'])
204
            : [];
205
    }
206
207
    /**
208
     * Given the data returned by AdminStatsRepository::getStats, return the stats keyed by user name,
209
     * adding in a key/value for user groups.
210
     * @param string[][] $data As retrieved by AdminStatsRepository::getStats
211
     * @param bool $abbreviateGroups If set, the 'user-groups' list will be a string with abbreviated user groups names,
212
     *   as opposed to an array of full-named user groups.
213
     * @return string[] Stats keyed by user name.
214
     * Functionality covered in test for self::getStats().
215
     * @codeCoverageIgnore
216
     */
217
    private function groupAdminStatsByUsername(array $data, bool $abbreviateGroups = true): array
218
    {
219
        $adminsAndGroups = $this->getAdminsAndGroups($abbreviateGroups);
220
        $users = [];
221
222
        foreach ($data as $datum) {
223
            $username = $datum['username'];
224
225
            // Push to array containing all users with admin actions.
226
            // We also want numerical values to be integers.
227
            $users[$username] = array_map('intval', $datum);
228
229
            // Push back username which was casted to an integer.
230
            $users[$username]['username'] = $username;
231
232
            // Set the 'user-groups' property with the user groups they belong to (if any),
233
            // going off of self::getAdminsAndGroups().
234
            if (isset($adminsAndGroups[$username])) {
235
                $users[$username]['user-groups'] = $adminsAndGroups[$username];
236
            } else {
237
                $users[$username]['user-groups'] = $abbreviateGroups ? '' : [];
238
            }
239
240
            // Keep track of non-admins who made admin actions.
241
            if (in_array($username, $this->admins)) {
242
                $this->numAdminsWithActions++;
243
            }
244
        }
245
246
        return $users;
247
    }
248
249
    /**
250
     * Get the total number of admins (users currently with qualifying permissions).
251
     * @return int
252
     */
253 1
    public function numAdmins(): int
254
    {
255 1
        return count($this->admins);
256
    }
257
258
    /**
259
     * Number of admins who made any actions within the time period.
260
     * @return int
261
     */
262
    public function getNumAdminsWithActions(): int
263
    {
264
        return $this->numAdminsWithActions;
265
    }
266
267
    /**
268
     * Number of currently non-admins who made any actions within the time period.
269
     * @return int
270
     */
271 1
    public function getNumNonAdminsWithActions(): int
272
    {
273 1
        return count($this->adminStats) - $this->numAdminsWithActions;
274
    }
275
}
276