Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
93.62% |
88 / 94 |
|
80.00% |
4 / 5 |
CRAP | |
0.00% |
0 / 1 |
CheckUserLogService | |
93.62% |
88 / 94 |
|
80.00% |
4 / 5 |
17.08 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
addLogEntry | |
87.23% |
41 / 47 |
|
0.00% |
0 / 1 |
4.03 | |||
getPlaintextReason | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getTargetSearchConds | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
6 | |||
verifyTarget | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 |
1 | <?php |
2 | |
3 | namespace MediaWiki\CheckUser\Services; |
4 | |
5 | use LogicException; |
6 | use MediaWiki\CommentFormatter\CommentFormatter; |
7 | use MediaWiki\CommentStore\CommentStore; |
8 | use MediaWiki\Deferred\DeferredUpdates; |
9 | use MediaWiki\Parser\Sanitizer; |
10 | use MediaWiki\Title\Title; |
11 | use MediaWiki\User\ActorStore; |
12 | use MediaWiki\User\UserIdentity; |
13 | use MediaWiki\User\UserIdentityLookup; |
14 | use Psr\Log\LoggerInterface; |
15 | use Wikimedia\IPUtils; |
16 | use Wikimedia\Rdbms\DBError; |
17 | use Wikimedia\Rdbms\IConnectionProvider; |
18 | use Wikimedia\Timestamp\ConvertibleTimestamp; |
19 | |
20 | /** |
21 | * A service for methods that interact with the cu_log table, either for insertion or |
22 | * reading log entries. |
23 | */ |
24 | class CheckUserLogService { |
25 | |
26 | private IConnectionProvider $dbProvider; |
27 | private CommentStore $commentStore; |
28 | private CommentFormatter $commentFormatter; |
29 | private LoggerInterface $logger; |
30 | private ActorStore $actorStore; |
31 | private UserIdentityLookup $userIdentityLookup; |
32 | |
33 | /** |
34 | * @param IConnectionProvider $dbProvider |
35 | * @param CommentStore $commentStore |
36 | * @param CommentFormatter $commentFormatter |
37 | * @param LoggerInterface $logger |
38 | * @param ActorStore $actorStore |
39 | * @param UserIdentityLookup $userIdentityLookup |
40 | */ |
41 | public function __construct( |
42 | IConnectionProvider $dbProvider, |
43 | CommentStore $commentStore, |
44 | CommentFormatter $commentFormatter, |
45 | LoggerInterface $logger, |
46 | ActorStore $actorStore, |
47 | UserIdentityLookup $userIdentityLookup |
48 | ) { |
49 | $this->dbProvider = $dbProvider; |
50 | $this->commentStore = $commentStore; |
51 | $this->commentFormatter = $commentFormatter; |
52 | $this->logger = $logger; |
53 | $this->actorStore = $actorStore; |
54 | $this->userIdentityLookup = $userIdentityLookup; |
55 | } |
56 | |
57 | /** |
58 | * Adds a log entry to the CheckUserLog. |
59 | * |
60 | * @param UserIdentity $user |
61 | * @param string $logType |
62 | * @param string $targetType |
63 | * @param string $target |
64 | * @param string $reason |
65 | * @param int $targetID |
66 | * @return void |
67 | */ |
68 | public function addLogEntry( |
69 | UserIdentity $user, string $logType, string $targetType, string $target, string $reason, int $targetID = 0 |
70 | ) { |
71 | if ( $targetType == 'ip' ) { |
72 | [ $rangeStart, $rangeEnd ] = IPUtils::parseRange( $target ); |
73 | $targetHex = $rangeStart; |
74 | if ( $rangeStart == $rangeEnd ) { |
75 | $rangeStart = ''; |
76 | $rangeEnd = ''; |
77 | } |
78 | } else { |
79 | $targetHex = ''; |
80 | $rangeStart = ''; |
81 | $rangeEnd = ''; |
82 | } |
83 | |
84 | $timestamp = ConvertibleTimestamp::now(); |
85 | $dbw = $this->dbProvider->getPrimaryDatabase(); |
86 | |
87 | $data = [ |
88 | 'cul_actor' => $this->actorStore->acquireActorId( $user, $dbw ), |
89 | 'cul_type' => $logType, |
90 | 'cul_target_id' => $targetID, |
91 | 'cul_target_text' => trim( $target ), |
92 | 'cul_target_hex' => $targetHex, |
93 | 'cul_range_start' => $rangeStart, |
94 | 'cul_range_end' => $rangeEnd |
95 | ]; |
96 | |
97 | $plaintextReason = $this->getPlaintextReason( $reason ); |
98 | |
99 | $fname = __METHOD__; |
100 | $commentStore = $this->commentStore; |
101 | $logger = $this->logger; |
102 | |
103 | DeferredUpdates::addCallableUpdate( |
104 | static function () use ( |
105 | $data, $timestamp, $reason, $plaintextReason, $fname, $dbw, $commentStore, $logger |
106 | ) { |
107 | try { |
108 | $data += $commentStore->insert( $dbw, 'cul_reason', $reason ); |
109 | $data += $commentStore->insert( $dbw, 'cul_reason_plaintext', $plaintextReason ); |
110 | $dbw->newInsertQueryBuilder() |
111 | ->insertInto( 'cu_log' ) |
112 | ->row( |
113 | [ |
114 | 'cul_timestamp' => $dbw->timestamp( $timestamp ) |
115 | ] + $data |
116 | ) |
117 | ->caller( $fname ) |
118 | ->execute(); |
119 | } catch ( DBError $e ) { |
120 | $logger->critical( |
121 | 'CheckUserLog entry was not recorded. This means checks can occur without being auditable. ' . |
122 | 'Immediate fix required.' |
123 | ); |
124 | throw $e; |
125 | } |
126 | } |
127 | ); |
128 | } |
129 | |
130 | /** |
131 | * Get the plaintext reason |
132 | * |
133 | * @param string $reason |
134 | * @return string |
135 | */ |
136 | public function getPlaintextReason( $reason ) { |
137 | return Sanitizer::stripAllTags( |
138 | $this->commentFormatter->formatBlock( |
139 | $reason, Title::newFromText( 'Special:CheckUserLog' ), |
140 | false, false, false |
141 | ) |
142 | ); |
143 | } |
144 | |
145 | /** |
146 | * Get DB search conditions for the cu_log table according to the target given. |
147 | * |
148 | * @param string $target the username, IP address or range of the target. |
149 | * @return array|null array if valid target, null if invalid target given |
150 | */ |
151 | public function getTargetSearchConds( string $target ): ?array { |
152 | $result = $this->verifyTarget( $target ); |
153 | if ( is_array( $result ) ) { |
154 | $dbr = $this->dbProvider->getReplicaDatabase(); |
155 | switch ( count( $result ) ) { |
156 | case 1: |
157 | return [ |
158 | 'cul_target_hex = ' . $dbr->addQuotes( $result[0] ) . ' OR ' . |
159 | '(cul_range_end >= ' . $dbr->addQuotes( $result[0] ) . ' AND ' . |
160 | 'cul_range_start <= ' . $dbr->addQuotes( $result[0] ) . ')' |
161 | ]; |
162 | case 2: |
163 | return [ |
164 | '(cul_target_hex >= ' . $dbr->addQuotes( $result[0] ) . ' AND ' . |
165 | 'cul_target_hex <= ' . $dbr->addQuotes( $result[1] ) . ') OR ' . |
166 | '(cul_range_end >= ' . $dbr->addQuotes( $result[0] ) . ' AND ' . |
167 | 'cul_range_start <= ' . $dbr->addQuotes( $result[1] ) . ')' |
168 | ]; |
169 | default: |
170 | throw new LogicException( |
171 | "Array returned from ::verifyTarget had the wrong number of items." |
172 | ); |
173 | } |
174 | } elseif ( is_int( $result ) ) { |
175 | return [ |
176 | 'cul_type' => [ 'userips', 'useredits', 'investigate' ], |
177 | 'cul_target_id' => $result, |
178 | ]; |
179 | } |
180 | return null; |
181 | } |
182 | |
183 | /** |
184 | * Verify if the target is a valid IP, IP range or user. |
185 | * |
186 | * @param string $target |
187 | * @return false|int|array If the target is a user, then the user's ID is returned. |
188 | * If the target is valid IP address, then the IP address |
189 | * in hexadecimal is returned as a one item array. |
190 | * If the target is a valid IP address range, then the |
191 | * start and end of the range in hexadecimal is returned |
192 | * as an array. |
193 | * Returns false for an invalid target. |
194 | */ |
195 | public function verifyTarget( string $target ) { |
196 | [ $start, $end ] = IPUtils::parseRange( $target ); |
197 | |
198 | if ( $start !== false ) { |
199 | if ( $start === $end ) { |
200 | return [ $start ]; |
201 | } |
202 | |
203 | return [ $start, $end ]; |
204 | } |
205 | |
206 | $user = $this->userIdentityLookup->getUserIdentityByName( $target ); |
207 | if ( $user && $user->getId() ) { |
208 | return $user->getId(); |
209 | } |
210 | |
211 | return false; |
212 | } |
213 | } |