Issues (196)

Security Analysis    6 potential vulnerabilities

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  SQL Injection (4)
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection (1)
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Model/AdminStats.php (4 issues)

Labels
Severity
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace App\Model;
6
7
use App\Repository\AdminStatsRepository;
8
9
/**
10
 * AdminStats returns information about users with rights defined in admin_stats.yaml.
11
 */
12
class AdminStats extends Model
13
{
14
15
    /** @var string[][] Keyed by user name, values are arrays containing actions and counts. */
16
    protected array $adminStats;
17
18
    /** @var string[] Keys are user names, values are their user groups. */
19
    protected array $usersAndGroups;
20
21
    /** @var int Number of users in the relevant group who made any actions within the time period. */
22
    protected int $numWithActions = 0;
23
24
    /** @var string[] Usernames of users who are in the relevant user group (sysop for admins, etc.). */
25
    private array $usersInGroup = [];
26
27
    /** @var string Type that we're getting stats for (admin, patroller, steward, etc.). See admin_stats.yaml */
28
    private string $type;
29
30
    /** @var string[] Which actions to show ('block', 'protect', etc.) */
31
    private array $actions;
32
33
    /**
34
     * AdminStats constructor.
35
     * @param AdminStatsRepository $repository
36
     * @param Project $project
37
     * @param int $start as UTC timestamp.
38
     * @param int $end as UTC timestamp.
39
     * @param string $group Which user group to get stats for. Refer to admin_stats.yaml for possible values.
40
     * @param string[] $actions Which actions to query for ('block', 'protect', etc.). Null for all actions.
41
     */
42
    public function __construct(
43
        AdminStatsRepository $repository,
44
        Project $project,
45
        int $start,
46
        int $end,
47
        string $group,
48
        array $actions
49
    ) {
50
        $this->repository = $repository;
51
        $this->project = $project;
52
        $this->start = $start;
53
        $this->end = $end;
54
        $this->type = $group;
55
        $this->actions = $actions;
56
    }
57
58
    /**
59
     * Get the group for this AdminStats.
60
     * @return string
61
     */
62
    public function getType(): string
63
    {
64
        return $this->type;
65
    }
66
67
    /**
68
     * Get the user_group from the config given the 'group'.
69
     * @return string
70
     */
71
    public function getRelevantUserGroup(): string
72
    {
73
        // Quick cache, valid only for the same request.
74
        static $relevantUserGroup = '';
75
        if ('' !== $relevantUserGroup) {
76
            return $relevantUserGroup;
77
        }
78
79
        return $relevantUserGroup = $this->getRepository()->getRelevantUserGroup($this->type);
0 ignored issues
show
The method getRelevantUserGroup() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\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

79
        return $relevantUserGroup = $this->getRepository()->/** @scrutinizer ignore-call */ getRelevantUserGroup($this->type);
Loading history...
80
    }
81
82
    /**
83
     * Get the array of statistics for each qualifying user. This may be called ahead of self::getStats() so certain
84
     * class-level properties will be supplied (such as self::numUsers(), which is called in the view before iterating
85
     * over the master array of statistics).
86
     * @return string[]
87
     */
88
    public function prepareStats(): array
89
    {
90
        if (isset($this->adminStats)) {
91
            return $this->adminStats;
92
        }
93
94
        $stats = $this->getRepository()
95
            ->getStats($this->project, $this->start, $this->end, $this->type, $this->actions);
0 ignored issues
show
The method getStats() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\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

95
            ->/** @scrutinizer ignore-call */ getStats($this->project, $this->start, $this->end, $this->type, $this->actions);
Loading history...
96
97
        // Group by username.
98
        $stats = $this->groupStatsByUsername($stats);
99
100
        // Resort, as for some reason the SQL doesn't do this properly.
101
        uasort($stats, function ($a, $b) {
102
            if ($a['total'] === $b['total']) {
103
                return 0;
104
            }
105
            return $a['total'] < $b['total'] ? 1 : -1;
106
        });
107
108
        $this->adminStats = $stats;
109
        return $this->adminStats;
110
    }
111
112
    /**
113
     * Get users of the project that are capable of making the relevant actions,
114
     * keyed by user name, with the user groups as the values.
115
     * @return string[][]
116
     */
117
    public function getUsersAndGroups(): array
118
    {
119
        if (isset($this->usersAndGroups)) {
120
            return $this->usersAndGroups;
121
        }
122
123
        // All the user groups that are considered capable of making the relevant actions for $this->group.
124
        $groupUserGroups = $this->getRepository()->getUserGroups($this->project, $this->type);
0 ignored issues
show
The method getUserGroups() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\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

124
        $groupUserGroups = $this->getRepository()->/** @scrutinizer ignore-call */ getUserGroups($this->project, $this->type);
Loading history...
125
126
        $this->usersAndGroups = $this->project->getUsersInGroups($groupUserGroups['local'], $groupUserGroups['global']);
127
128
        // Populate $this->usersInGroup with users who are in the relevant user group for $this->group.
129
        $this->usersInGroup = array_keys(array_filter($this->usersAndGroups, function ($groups) {
130
            return in_array($this->getRelevantUserGroup(), $groups);
131
        }));
132
133
        return $this->usersAndGroups;
134
    }
135
136
    /**
137
     * Get all user groups with permissions applicable to the $this->group.
138
     * @param bool $wikiPath Whether to return the title for the on-wiki image, instead of full URL.
139
     * @return array Each entry contains 'name' (user group) and 'rights' (the permissions).
140
     */
141
    public function getUserGroupIcons(bool $wikiPath = false): array
142
    {
143
        // Quick cache, valid only for the same request.
144
        static $userGroupIcons = null;
145
        if (null !== $userGroupIcons) {
146
            $out = $userGroupIcons;
147
        } else {
148
            $out = $userGroupIcons = $this->getRepository()->getUserGroupIcons();
0 ignored issues
show
The method getUserGroupIcons() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\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

148
            $out = $userGroupIcons = $this->getRepository()->/** @scrutinizer ignore-call */ getUserGroupIcons();
Loading history...
149
        }
150
151
        if ($wikiPath) {
152
            $out = array_map(function ($url) {
153
                return str_replace('.svg.png', '.svg', preg_replace('/.*\/18px-/', '', $url));
154
            }, $out);
155
        }
156
157
        return $out;
158
    }
159
160
    /**
161
     * The number of days we're spanning between the start and end date.
162
     * @return int
163
     */
164
    public function numDays(): int
165
    {
166
        return (int)(($this->end - $this->start) / 60 / 60 / 24) + 1;
167
    }
168
169
    /**
170
     * Get the master array of statistics for each qualifying user.
171
     * @return string[]
172
     */
173
    public function getStats(): array
174
    {
175
        if (isset($this->adminStats)) {
176
            $this->adminStats = $this->prepareStats();
177
        }
178
        return $this->adminStats;
179
    }
180
181
    /**
182
     * Get the actions that are shown as columns in the view.
183
     * @return string[] Each the i18n key of the action.
184
     */
185
    public function getActions(): array
186
    {
187
        return count($this->getStats()) > 0
188
            ? array_diff(array_keys(array_values($this->getStats())[0]), ['username', 'user-groups', 'total'])
189
            : [];
190
    }
191
192
    /**
193
     * Given the data returned by AdminStatsRepository::getStats, return the stats keyed by user name,
194
     * adding in a key/value for user groups.
195
     * @param string[][] $data As retrieved by AdminStatsRepository::getStats
196
     * @return string[] Stats keyed by user name.
197
     * Functionality covered in test for self::getStats().
198
     * @codeCoverageIgnore
199
     */
200
    private function groupStatsByUsername(array $data): array
201
    {
202
        $usersAndGroups = $this->getUsersAndGroups();
203
        $users = [];
204
205
        foreach ($data as $datum) {
206
            $username = $datum['username'];
207
208
            // Push to array containing all users with admin actions.
209
            // We also want numerical values to be integers.
210
            $users[$username] = array_map('intval', $datum);
211
212
            // Push back username which was casted to an integer.
213
            $users[$username]['username'] = $username;
214
215
            // Set the 'user-groups' property with the user groups they belong to (if any),
216
            // going off of self::getUsersAndGroups().
217
            if (isset($usersAndGroups[$username])) {
218
                $users[$username]['user-groups'] = $usersAndGroups[$username];
219
            } else {
220
                $users[$username]['user-groups'] = [];
221
            }
222
223
            // Keep track of users who are not in the relevant user group but made applicable actions.
224
            if (in_array($username, $this->usersInGroup)) {
225
                $this->numWithActions++;
226
            }
227
        }
228
229
        return $users;
230
    }
231
232
    /**
233
     * Get the total number of users in the relevant user group.
234
     * @return int
235
     */
236
    public function getNumInRelevantUserGroup(): int
237
    {
238
        return count($this->usersInGroup);
239
    }
240
241
    /**
242
     * Number of users who made any relevant actions within the time period.
243
     * @return int
244
     */
245
    public function getNumWithActions(): int
246
    {
247
        return $this->numWithActions;
248
    }
249
250
    /**
251
     * Number of currently users who made any actions within the time period who are not in the relevant user group.
252
     * @return int
253
     */
254
    public function getNumWithActionsNotInGroup(): int
255
    {
256
        return count($this->adminStats) - $this->numWithActions;
257
    }
258
}
259