Passed
Push — ip-ranges ( 157734...88dad8 )
by MusikAnimal
06:12 queued 01:19
created

User   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 355
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 103
dl 0
loc 355
rs 8.96
c 2
b 0
f 0
wmc 43

24 Methods

Rating   Name   Duplication   Size   Complexity  
A isIPv6() 0 3 1
A getPrettyUsername() 0 6 2
A getCacheKey() 0 3 1
A __construct() 0 10 3
A isIpRange() 0 3 1
A getUsernameIdent() 0 6 2
A getUsername() 0 3 1
A getActorId() 0 5 1
B getIpSubstringFromCidr() 0 48 6
A getEditCount() 0 13 2
A getGlobalUserRights() 0 3 1
A isBlocked() 0 3 1
A hasTooManyEdits() 0 4 2
A isQueryableRange() 0 9 3
A getUserRights() 0 3 1
A countEdits() 0 3 1
A isAdmin() 0 3 1
A getRegistrationDate() 0 10 2
A getBlockExpiry() 0 13 3
A isCurrentlyLoggedIn() 0 8 3
A maxEdits() 0 3 1
A existsOnProject() 0 3 1
A isAnon() 0 3 1
A getId() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like User often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use User, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file contains only the User class.
4
 */
5
6
declare(strict_types = 1);
7
8
namespace AppBundle\Model;
9
10
use DateTime;
11
use Exception;
12
use Wikimedia\IPUtils;
13
14
/**
15
 * A User is a wiki user who has the same username across all projects in an XTools installation.
16
 */
17
class User extends Model
18
{
19
    /** @var int Maximum queryable range for IPv4. */
20
    public const MAX_IPV4_CIDR = 16;
21
22
    /** @var int Maximum queryable range for IPv6. */
23
    public const MAX_IPV6_CIDR = 32;
24
25
    /** @var string The user's username. */
26
    protected $username;
27
28
    /** @var int Quick cache of edit counts, keyed by project domain. */
29
    protected $editCounts = [];
30
31
    /**
32
     * Create a new User given a username.
33
     * @param string $username
34
     */
35
    public function __construct(string $username)
36
    {
37
        if ('ipr-' === substr($username, 0, 4)) {
38
            $username = substr($username, 4);
39
        }
40
        $this->username = ucfirst(str_replace('_', ' ', trim($username)));
41
42
        // IPv6 address are stored as uppercase in the database.
43
        if ($this->isAnon()) {
44
            $this->username = IPUtils::sanitizeIP($this->username);
45
        }
46
    }
47
48
    /**
49
     * Unique identifier for this User, to be used in cache keys. Use of md5 ensures the cache key does not contain
50
     * reserved characters. You could also use the ID, but that may require an unnecessary DB query.
51
     * @see Repository::getCacheKey()
52
     * @return string
53
     */
54
    public function getCacheKey(): string
55
    {
56
        return md5($this->username);
57
    }
58
59
    /**
60
     * Get the username.
61
     * @return string
62
     */
63
    public function getUsername(): string
64
    {
65
        return $this->username;
66
    }
67
68
    /**
69
     * Get a prettified username for IP addresses. For accounts, just the username is returned.
70
     * @return string
71
     */
72
    public function getPrettyUsername(): string
73
    {
74
        if (!$this->isAnon()) {
75
            return $this->username;
76
        }
77
        return IPUtils::prettifyIP($this->username);
78
    }
79
80
    /**
81
     * Get the username identifier that should be used in routing. This only matters for IP ranges,
82
     * which get prefixed with 'ipr-' to ensure they don't conflict with other routing params (such as namespace ID).
83
     * Always use this method when linking to or generating internal routes, and use it nowhere else.
84
     * @return string
85
     */
86
    public function getUsernameIdent(): string
87
    {
88
        if ($this->isIpRange()) {
89
            return 'ipr-'.$this->username;
90
        }
91
        return $this->username;
92
    }
93
94
    /**
95
     * Is this an IP range?
96
     * @return bool
97
     */
98
    public function isIpRange(): bool
99
    {
100
        return IPUtils::isValidRange($this->username);
101
    }
102
103
    /**
104
     * Is this an IPv6 address or range?
105
     * @return bool
106
     */
107
    public function isIPv6(): bool
108
    {
109
        return IPUtils::isIPv6($this->username);
110
    }
111
112
    /**
113
     * Get the common characters between the start and end address of an IPv4 range.
114
     * This is used when running a LIKE query against actor names.
115
     * @return string[]|null
116
     */
117
    public function getIpSubstringFromCidr(): ?string
118
    {
119
        if (!$this->isIpRange()) {
120
            return null;
121
        }
122
123
        if ($this->isIPv6()) {
124
            // Adapted from https://stackoverflow.com/a/10086404/604142 (CC BY-SA 3.0)
125
            [$firstAddrStr, $prefixLen] = explode('/', $this->username);
126
            $firstAddrBin = inet_pton($firstAddrStr);
127
            $firstAddrUnpacked = unpack('H*', $firstAddrBin);
128
            $firstAddrHex = reset($firstAddrUnpacked);
129
            $range[0] = inet_ntop($firstAddrBin);
0 ignored issues
show
Comprehensibility Best Practice introduced by
$range was never initialized. Although not strictly required by PHP, it is generally a good practice to add $range = array(); before regardless.
Loading history...
130
            $flexBits = 128 - $prefixLen;
131
            $lastAddrHex = $firstAddrHex;
132
133
            $pos = 31;
134
            while ($flexBits > 0) {
135
                $orig = substr($lastAddrHex, $pos, 1);
0 ignored issues
show
Bug introduced by
It seems like $lastAddrHex can also be of type array; however, parameter $string of substr() does only seem to accept string, 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

135
                $orig = substr(/** @scrutinizer ignore-type */ $lastAddrHex, $pos, 1);
Loading history...
136
                $origVal = hexdec($orig);
137
                $newVal = $origVal | (pow(2, min(4, $flexBits)) - 1);
138
                $new = dechex($newVal);
139
                $lastAddrHex = substr_replace($lastAddrHex, $new, $pos, 1);
140
                $flexBits -= 4;
141
                $pos -= 1;
142
            }
143
144
            $lastAddrBin = pack('H*', $lastAddrHex);
145
            $range[1] = inet_ntop($lastAddrBin);
146
        } else {
147
            $cidr = explode('/', $this->username);
148
            $range[0] = long2ip(ip2long($cidr[0]) & -1 << (32 - (int)$cidr[1]));
149
            $range[1] = long2ip(ip2long($range[0]) + pow(2, (32 - (int)$cidr[1])) - 1);
150
        }
151
152
        // Find the leftmost common characters between the two addresses.
153
        $common = '';
154
        $startSplit = str_split(strtoupper($range[0]));
155
        $endSplit = str_split(strtoupper($range[1]));
156
        foreach ($startSplit as $index => $char) {
157
            if ($endSplit[$index] === $char) {
158
                $common .= $char;
159
            } else {
160
                break;
161
            }
162
        }
163
164
        return $common;
165
    }
166
167
    /**
168
     * Is this IP range outside the queryable limits?
169
     * @return bool
170
     */
171
    public function isQueryableRange(): bool
172
    {
173
        if (!$this->isIpRange()) {
174
            return true;
175
        }
176
177
        [, $bits] = IPUtils::parseCIDR($this->username);
178
        $limit = $this->isIPv6() ? self::MAX_IPV6_CIDR : self::MAX_IPV4_CIDR;
179
        return (int)$bits >= $limit;
180
    }
181
182
    /**
183
     * Get the user's ID on the given project.
184
     * @param Project $project
185
     * @return int|null
186
     */
187
    public function getId(Project $project): ?int
188
    {
189
        $ret = $this->getRepository()->getIdAndRegistration(
0 ignored issues
show
Bug introduced by
The method getIdAndRegistration() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\UserRepository. ( Ignorable by Annotation )

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

189
        $ret = $this->getRepository()->/** @scrutinizer ignore-call */ getIdAndRegistration(
Loading history...
190
            $project->getDatabaseName(),
191
            $this->getUsername()
192
        );
193
194
        return $ret ? (int)$ret['userId'] : null;
195
    }
196
197
    /**
198
     * Get the user's actor ID on the given project.
199
     * @param Project $project
200
     * @return int
201
     */
202
    public function getActorId(Project $project): int
203
    {
204
        return (int)$this->getRepository()->getActorId(
0 ignored issues
show
Bug introduced by
The method getActorId() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\UserRepository. ( Ignorable by Annotation )

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

204
        return (int)$this->getRepository()->/** @scrutinizer ignore-call */ getActorId(
Loading history...
205
            $project->getDatabaseName(),
206
            $this->getUsername()
207
        );
208
    }
209
210
    /**
211
     * Get the user's registration date on the given project.
212
     * @param Project $project
213
     * @return DateTime|null null if no registration date was found.
214
     */
215
    public function getRegistrationDate(Project $project): ?DateTime
216
    {
217
        $ret = $this->getRepository()->getIdAndRegistration(
218
            $project->getDatabaseName(),
219
            $this->getUsername()
220
        );
221
222
        return null !== $ret['regDate']
223
            ? DateTime::createFromFormat('YmdHis', $ret['regDate'])
224
            : null;
225
    }
226
227
    /**
228
     * Get a user's local user rights on the given Project.
229
     * @param Project $project
230
     * @return string[]
231
     */
232
    public function getUserRights(Project $project): array
233
    {
234
        return $this->getRepository()->getUserRights($project, $this);
0 ignored issues
show
Bug introduced by
The method getUserRights() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\UserRepository. ( Ignorable by Annotation )

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

234
        return $this->getRepository()->/** @scrutinizer ignore-call */ getUserRights($project, $this);
Loading history...
235
    }
236
237
    /**
238
     * Get a list of this user's global rights.
239
     * @param Project|null $project A project to query; if not provided, the default will be used.
240
     * @return string[]
241
     */
242
    public function getGlobalUserRights(?Project $project = null): array
243
    {
244
        return $this->getRepository()->getGlobalUserRights($this->getUsername(), $project);
0 ignored issues
show
Bug introduced by
The method getGlobalUserRights() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\UserRepository. ( Ignorable by Annotation )

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

244
        return $this->getRepository()->/** @scrutinizer ignore-call */ getGlobalUserRights($this->getUsername(), $project);
Loading history...
245
    }
246
247
    /**
248
     * Get the user's (system) edit count.
249
     * @param Project $project
250
     * @return int
251
     */
252
    public function getEditCount(Project $project): int
253
    {
254
        $domain = $project->getDomain();
255
        if (isset($this->editCounts[$domain])) {
256
            return $this->editCounts[$domain];
257
        }
258
259
        $this->editCounts[$domain] = (int)$this->getRepository()->getEditCount(
0 ignored issues
show
Bug introduced by
The method getEditCount() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\UserRepository. ( Ignorable by Annotation )

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

259
        $this->editCounts[$domain] = (int)$this->getRepository()->/** @scrutinizer ignore-call */ getEditCount(
Loading history...
260
            $project->getDatabaseName(),
261
            $this->getUsername()
262
        );
263
264
        return $this->editCounts[$domain];
265
    }
266
267
    /**
268
     * Maximum number of edits to process, based on configuration.
269
     * @return int
270
     */
271
    public function maxEdits(): int
272
    {
273
        return $this->getRepository()->maxEdits();
0 ignored issues
show
Bug introduced by
The method maxEdits() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\UserRepository. ( Ignorable by Annotation )

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

273
        return $this->getRepository()->/** @scrutinizer ignore-call */ maxEdits();
Loading history...
274
    }
275
276
    /**
277
     * Does this user exist on the given project.
278
     * @param Project $project
279
     * @return bool
280
     */
281
    public function existsOnProject(Project $project): bool
282
    {
283
        return $this->getId($project) > 0;
284
    }
285
286
    /**
287
     * Is this user an Administrator on the given project?
288
     * @param Project $project The project.
289
     * @return bool
290
     */
291
    public function isAdmin(Project $project): bool
292
    {
293
        return false !== array_search('sysop', $this->getUserRights($project));
294
    }
295
296
    /**
297
     * Is this user an anonymous user (IP)?
298
     * @return bool
299
     */
300
    public function isAnon(): bool
301
    {
302
        return IPUtils::isIPAddress($this->username);
303
    }
304
305
    /**
306
     * Get the expiry of the current block on the user
307
     * @param Project $project The project.
308
     * @return DateTime|bool Expiry as DateTime, true if indefinite, or false if they are not blocked.
309
     */
310
    public function getBlockExpiry(Project $project)
311
    {
312
        $expiry = $this->getRepository()->getBlockExpiry(
0 ignored issues
show
Bug introduced by
The method getBlockExpiry() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\UserRepository. ( Ignorable by Annotation )

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

312
        $expiry = $this->getRepository()->/** @scrutinizer ignore-call */ getBlockExpiry(
Loading history...
313
            $project->getDatabaseName(),
314
            $this->getUsername()
315
        );
316
317
        if ('infinity' === $expiry) {
318
            return true;
319
        } elseif (false === $expiry) {
320
            return false;
321
        } else {
322
            return new DateTime($expiry);
323
        }
324
    }
325
326
    /**
327
     * Is this user currently blocked on the given project?
328
     * @param Project $project The project.
329
     * @return bool
330
     */
331
    public function isBlocked(Project $project): bool
332
    {
333
        return false !== $this->getBlockExpiry($project);
334
    }
335
336
    /**
337
     * Does the user have more edits than maximum amount allowed for processing?
338
     * @param Project $project
339
     * @return bool
340
     */
341
    public function hasTooManyEdits(Project $project): bool
342
    {
343
        $editCount = $this->getEditCount($project);
344
        return $this->maxEdits() > 0 && $editCount > $this->maxEdits();
345
    }
346
347
    /**
348
     * Get edit count within given timeframe and namespace
349
     * @param Project $project
350
     * @param int|string $namespace Namespace ID or 'all' for all namespaces
351
     * @param int|false $start Start date as Unix timestamp.
352
     * @param int|false $end End date as Unix timestamp.
353
     * @return int
354
     */
355
    public function countEdits(Project $project, $namespace = 'all', $start = false, $end = false): int
356
    {
357
        return (int) $this->getRepository()->countEdits($project, $this, $namespace, $start, $end);
0 ignored issues
show
Bug introduced by
The method countEdits() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\UserRepository. ( Ignorable by Annotation )

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

357
        return (int) $this->getRepository()->/** @scrutinizer ignore-call */ countEdits($project, $this, $namespace, $start, $end);
Loading history...
358
    }
359
360
    /**
361
     * Is this user the same as the current XTools user?
362
     * @return bool
363
     */
364
    public function isCurrentlyLoggedIn(): bool
365
    {
366
        try {
367
            $ident = $this->getRepository()->getXtoolsUserInfo();
0 ignored issues
show
Bug introduced by
The method getXtoolsUserInfo() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\UserRepository. ( Ignorable by Annotation )

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

367
            $ident = $this->getRepository()->/** @scrutinizer ignore-call */ getXtoolsUserInfo();
Loading history...
368
        } catch (Exception $exception) {
369
            return false;
370
        }
371
        return isset($ident->username) && $ident->username === $this->getUsername();
372
    }
373
}
374