Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
124 / 124 |
|
100.00% |
6 / 6 |
CRAP | |
100.00% |
1 / 1 |
CheckUserInsert | |
100.00% |
124 / 124 |
|
100.00% |
6 / 6 |
25 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
insertIntoCuLogEventTable | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
5 | |||
insertIntoCuPrivateEventTable | |
100.00% |
37 / 37 |
|
100.00% |
1 / 1 |
6 | |||
insertIntoCuChangesTable | |
100.00% |
44 / 44 |
|
100.00% |
1 / 1 |
6 | |||
getAgent | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
acquireActorId | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 |
1 | <?php |
2 | |
3 | namespace MediaWiki\CheckUser\Services; |
4 | |
5 | use DatabaseLogEntry; |
6 | use Language; |
7 | use LogEntryBase; |
8 | use MediaWiki\CheckUser\CheckUserQueryInterface; |
9 | use MediaWiki\CheckUser\Hook\HookRunner; |
10 | use MediaWiki\CommentStore\CommentStore; |
11 | use MediaWiki\HookContainer\HookContainer; |
12 | use MediaWiki\User\ActorStore; |
13 | use MediaWiki\User\TempUser\TempUserConfig; |
14 | use MediaWiki\User\UserIdentity; |
15 | use RecentChange; |
16 | use RequestContext; |
17 | use WebRequest; |
18 | use Wikimedia\IPUtils; |
19 | use Wikimedia\Rdbms\IConnectionProvider; |
20 | |
21 | /** |
22 | * This service provides methods that can be used |
23 | * to insert data into the CheckUser result tables. |
24 | * |
25 | * Extensions other than CheckUser should not use |
26 | * the methods marked as internal. |
27 | */ |
28 | class CheckUserInsert { |
29 | |
30 | private ActorStore $actorStore; |
31 | private CheckUserUtilityService $checkUserUtilityService; |
32 | private CommentStore $commentStore; |
33 | private HookRunner $hookRunner; |
34 | private IConnectionProvider $connectionProvider; |
35 | private Language $contentLanguage; |
36 | private TempUserConfig $tempUserConfig; |
37 | |
38 | /** |
39 | * The maximum number of bytes that fit in CheckUser's text fields, |
40 | * specifically user agent, XFF strings and action text. |
41 | */ |
42 | public const TEXT_FIELD_LENGTH = 255; |
43 | |
44 | public function __construct( |
45 | ActorStore $actorStore, |
46 | CheckUserUtilityService $checkUserUtilityService, |
47 | CommentStore $commentStore, |
48 | HookContainer $hookContainer, |
49 | IConnectionProvider $connectionProvider, |
50 | Language $contentLanguage, |
51 | TempUserConfig $tempUserConfig |
52 | ) { |
53 | $this->actorStore = $actorStore; |
54 | $this->checkUserUtilityService = $checkUserUtilityService; |
55 | $this->commentStore = $commentStore; |
56 | $this->hookRunner = new HookRunner( $hookContainer ); |
57 | $this->connectionProvider = $connectionProvider; |
58 | $this->contentLanguage = $contentLanguage; |
59 | $this->tempUserConfig = $tempUserConfig; |
60 | } |
61 | |
62 | /** |
63 | * Inserts a row into cu_log_event based on provided log ID and performer. |
64 | * |
65 | * The $user parameter is used to fill the column values about the performer of the log action. |
66 | * The log ID is stored in the table and used to get information to show the CheckUser when |
67 | * running a check. |
68 | * |
69 | * @param DatabaseLogEntry $logEntry the log entry to add to cu_log_event |
70 | * @param string $method the method name that called this, used for the insertion into the DB. |
71 | * @param UserIdentity $user the user who made the request. |
72 | * @param ?RecentChange $rc If triggered by a RecentChange, then this is the associated |
73 | * RecentChange object. Null if not triggered by a RecentChange. |
74 | * @return void |
75 | * @internal Only for use by the CheckUser extension |
76 | */ |
77 | public function insertIntoCuLogEventTable( |
78 | DatabaseLogEntry $logEntry, |
79 | string $method, |
80 | UserIdentity $user, |
81 | ?RecentChange $rc = null |
82 | ): void { |
83 | $request = RequestContext::getMain()->getRequest(); |
84 | |
85 | $ip = $request->getIP(); |
86 | $xff = $request->getHeader( 'X-Forwarded-For' ); |
87 | |
88 | $row = [ |
89 | 'cule_log_id' => $logEntry->getId() |
90 | ]; |
91 | |
92 | // Provide the ip, xff and row to code that hooks onto this so that they can modify the row before |
93 | // it's inserted. The ip and xff are provided separately so that the caller doesn't have to set |
94 | // the hex versions of the IP and XFF and can therefore leave that to this function. |
95 | $this->hookRunner->onCheckUserInsertLogEventRow( $ip, $xff, $row, $user, $logEntry->getId(), $rc ); |
96 | [ $xff_ip, $isSquidOnly, $xff ] = $this->checkUserUtilityService->getClientIPfromXFF( $xff ); |
97 | |
98 | $dbw = $this->connectionProvider->getPrimaryDatabase(); |
99 | $row = array_merge( [ |
100 | 'cule_actor' => $this->acquireActorId( $user, CheckUserQueryInterface::LOG_EVENT_TABLE ), |
101 | 'cule_timestamp' => $dbw->timestamp( $logEntry->getTimestamp() ), |
102 | 'cule_ip' => IPUtils::sanitizeIP( $ip ), |
103 | 'cule_ip_hex' => $ip ? IPUtils::toHex( $ip ) : null, |
104 | 'cule_xff' => !$isSquidOnly ? $xff : '', |
105 | 'cule_xff_hex' => ( $xff_ip && !$isSquidOnly ) ? IPUtils::toHex( $xff_ip ) : null, |
106 | 'cule_agent' => $this->getAgent( $request ), |
107 | ], $row ); |
108 | |
109 | // (T199323) Truncate text fields prior to database insertion |
110 | // Attempting to insert too long text will cause an error in MariaDB/MySQL strict mode |
111 | $row['cule_xff'] = $this->contentLanguage->truncateForDatabase( $row['cule_xff'], self::TEXT_FIELD_LENGTH ); |
112 | |
113 | $dbw->newInsertQueryBuilder() |
114 | ->insertInto( 'cu_log_event' ) |
115 | ->row( $row ) |
116 | ->caller( $method ) |
117 | ->execute(); |
118 | } |
119 | |
120 | /** |
121 | * Inserts a row to cu_private_event based on a provided row and performer of the action. |
122 | * |
123 | * The $row has defaults applied, truncation performed and comment table insertion performed. |
124 | * The $user parameter is used to fill the default for the actor ID column. |
125 | * |
126 | * Provide cupe_comment_id if you have generated a comment table ID for this action, or provide |
127 | * cupe_comment if you want this method to deal with the comment table. |
128 | * |
129 | * @param array $row an array of cu_private_event table column names to their values. Changeable by a hook |
130 | * and for any needed truncation. |
131 | * @param string $method the method name that called this, used for the insertion into the DB. |
132 | * @param UserIdentity $user the user associated with the event |
133 | * @param ?RecentChange $rc If triggered by a RecentChange, then this is the associated |
134 | * RecentChange object. Null if not triggered by a RecentChange. |
135 | * @internal Only for use by the CheckUser extension |
136 | */ |
137 | public function insertIntoCuPrivateEventTable( |
138 | array $row, |
139 | string $method, |
140 | UserIdentity $user, |
141 | ?RecentChange $rc = null |
142 | ): void { |
143 | $request = RequestContext::getMain()->getRequest(); |
144 | |
145 | $ip = $request->getIP(); |
146 | $xff = $request->getHeader( 'X-Forwarded-For' ); |
147 | |
148 | // Provide the ip, xff and row to code that hooks onto this so that they can modify the row before |
149 | // it's inserted. The ip and xff are provided separately so that the caller doesn't have to set |
150 | // the hex versions of the IP and XFF and can therefore leave that to this function. |
151 | $this->hookRunner->onCheckUserInsertPrivateEventRow( $ip, $xff, $row, $user, $rc ); |
152 | [ $xff_ip, $isSquidOnly, $xff ] = $this->checkUserUtilityService->getClientIPfromXFF( $xff ); |
153 | |
154 | $dbw = $this->connectionProvider->getPrimaryDatabase(); |
155 | $row = array_merge( |
156 | [ |
157 | 'cupe_namespace' => 0, |
158 | 'cupe_title' => '', |
159 | 'cupe_log_type' => 'checkuser-private-event', |
160 | 'cupe_log_action' => '', |
161 | 'cupe_params' => LogEntryBase::makeParamBlob( [] ), |
162 | 'cupe_page' => 0, |
163 | 'cupe_actor' => $this->acquireActorId( $user, CheckUserQueryInterface::PRIVATE_LOG_EVENT_TABLE ), |
164 | 'cupe_timestamp' => $dbw->timestamp( wfTimestampNow() ), |
165 | 'cupe_ip' => IPUtils::sanitizeIP( $ip ), |
166 | 'cupe_ip_hex' => $ip ? IPUtils::toHex( $ip ) : null, |
167 | 'cupe_xff' => !$isSquidOnly ? $xff : '', |
168 | 'cupe_xff_hex' => ( $xff_ip && !$isSquidOnly ) ? IPUtils::toHex( $xff_ip ) : null, |
169 | 'cupe_agent' => $this->getAgent( $request ), |
170 | ], |
171 | $row |
172 | ); |
173 | |
174 | // (T199323) Truncate text fields prior to database insertion |
175 | // Attempting to insert too long text will cause an error in MariaDB/MySQL strict mode |
176 | $row['cupe_xff'] = $this->contentLanguage->truncateForDatabase( $row['cupe_xff'], self::TEXT_FIELD_LENGTH ); |
177 | |
178 | if ( !isset( $row['cupe_comment_id'] ) ) { |
179 | $row += $this->commentStore->insert( |
180 | $dbw, |
181 | 'cupe_comment', |
182 | $row['cupe_comment'] ?? '' |
183 | ); |
184 | } |
185 | |
186 | // Remove any defined cupe_comment as this is not a valid column name. |
187 | unset( $row['cupe_comment'] ); |
188 | |
189 | $dbw->newInsertQueryBuilder() |
190 | ->insertInto( 'cu_private_event' ) |
191 | ->row( $row ) |
192 | ->caller( $method ) |
193 | ->execute(); |
194 | } |
195 | |
196 | /** |
197 | * Inserts a row in cu_changes based on the provided $row. |
198 | * |
199 | * The $user parameter is used to generate the default value for cuc_actor. |
200 | * |
201 | * @param array $row an array of cu_change table column names to their values. Overridable by a hook |
202 | * and for any necessary truncation. |
203 | * @param string $method the method name that called this, used for the insertion into the DB. |
204 | * @param UserIdentity $user the user who made the change |
205 | * @param ?RecentChange $rc If triggered by a RecentChange, then this is the associated |
206 | * RecentChange object. Null if not triggered by a RecentChange. |
207 | * @internal Only for use by the CheckUser extension |
208 | */ |
209 | public function insertIntoCuChangesTable( |
210 | array $row, |
211 | string $method, |
212 | UserIdentity $user, |
213 | ?RecentChange $rc = null |
214 | ): void { |
215 | $request = RequestContext::getMain()->getRequest(); |
216 | |
217 | $ip = $request->getIP(); |
218 | $xff = $request->getHeader( 'X-Forwarded-For' ); |
219 | // Provide the ip, xff and row to code that hooks onto this so that they can modify the row before |
220 | // it's inserted. The ip and xff are provided separately so that the caller doesn't have to set |
221 | // the hex versions of the IP and XFF and can therefore leave that to this function. |
222 | $this->hookRunner->onCheckUserInsertChangesRow( $ip, $xff, $row, $user, $rc ); |
223 | [ $xff_ip, $isSquidOnly, $xff ] = $this->checkUserUtilityService->getClientIPfromXFF( $xff ); |
224 | |
225 | $dbw = $this->connectionProvider->getPrimaryDatabase(); |
226 | $row = array_merge( |
227 | [ |
228 | 'cuc_page_id' => 0, |
229 | 'cuc_namespace' => 0, |
230 | 'cuc_minor' => 0, |
231 | 'cuc_title' => '', |
232 | 'cuc_actiontext' => '', |
233 | 'cuc_comment' => '', |
234 | 'cuc_actor' => $this->acquireActorId( $user, CheckUserQueryInterface::CHANGES_TABLE ), |
235 | 'cuc_this_oldid' => 0, |
236 | 'cuc_last_oldid' => 0, |
237 | 'cuc_type' => RC_LOG, |
238 | 'cuc_timestamp' => $dbw->timestamp( wfTimestampNow() ), |
239 | 'cuc_ip' => IPUtils::sanitizeIP( $ip ), |
240 | 'cuc_ip_hex' => $ip ? IPUtils::toHex( $ip ) : null, |
241 | 'cuc_xff' => !$isSquidOnly ? $xff : '', |
242 | 'cuc_xff_hex' => ( $xff_ip && !$isSquidOnly ) ? IPUtils::toHex( $xff_ip ) : null, |
243 | 'cuc_agent' => $this->getAgent( $request ), |
244 | ], |
245 | $row |
246 | ); |
247 | |
248 | // (T199323) Truncate text fields prior to database insertion |
249 | // Attempting to insert too long text will cause an error in MariaDB/MySQL strict mode |
250 | $row['cuc_actiontext'] = $this->contentLanguage->truncateForDatabase( |
251 | $row['cuc_actiontext'], |
252 | self::TEXT_FIELD_LENGTH |
253 | ); |
254 | $row['cuc_xff'] = $this->contentLanguage->truncateForDatabase( $row['cuc_xff'], self::TEXT_FIELD_LENGTH ); |
255 | |
256 | if ( !isset( $row['cuc_comment_id'] ) ) { |
257 | $row += $this->commentStore->insert( |
258 | $dbw, |
259 | 'cuc_comment', |
260 | $row['cuc_comment'] |
261 | ); |
262 | } |
263 | unset( $row['cuc_comment'] ); |
264 | |
265 | $dbw->newInsertQueryBuilder() |
266 | ->insertInto( 'cu_changes' ) |
267 | ->row( $row ) |
268 | ->caller( $method ) |
269 | ->execute(); |
270 | } |
271 | |
272 | /** |
273 | * Get user agent for the given request. |
274 | * |
275 | * @param WebRequest $request |
276 | * @return string |
277 | */ |
278 | private function getAgent( WebRequest $request ): string { |
279 | $agent = $request->getHeader( 'User-Agent' ); |
280 | if ( $agent === false ) { |
281 | // no agent was present, store as an empty string (otherwise, it would |
282 | // end up stored as a zero due to boolean casting done by the DB layer). |
283 | return ''; |
284 | } |
285 | return $this->contentLanguage->truncateForDatabase( $agent, self::TEXT_FIELD_LENGTH ); |
286 | } |
287 | |
288 | /** |
289 | * Generates an integer for insertion into cuc_actor, cule_actor, or cupe_actor. |
290 | * |
291 | * This integer will be an actor ID for the $user unless all the following are true: |
292 | * * The $user is an IP address |
293 | * * $wgAutoCreateTempUser['enabled'] is true |
294 | * * The $table is 'cu_private_event' |
295 | * |
296 | * In all of the above are true, this method will return null as when the first two are true, trying to create an |
297 | * actor ID will cause a CannotCreateActorException exception to be thrown. |
298 | * |
299 | * If the first two are true but the last is not, then the code will try to find an existing actor ID for the IP |
300 | * address (to allow imports) and if this fails then will throw a CannotCreateActorException. |
301 | * |
302 | * @param UserIdentity $user |
303 | * @param string $table The table that the actor ID will be inserted into. |
304 | * @return ?int The value to insert into the actor column (can be null if the table is cu_private_event). |
305 | */ |
306 | private function acquireActorId( UserIdentity $user, string $table ): ?int { |
307 | $dbw = $this->connectionProvider->getPrimaryDatabase(); |
308 | if ( IPUtils::isIPAddress( $user->getName() ) && $this->tempUserConfig->isEnabled() ) { |
309 | if ( $table === CheckUserQueryInterface::PRIVATE_LOG_EVENT_TABLE ) { |
310 | return null; |
311 | } |
312 | $actorId = $this->actorStore->findActorId( $user, $dbw ); |
313 | if ( $actorId !== null ) { |
314 | return $actorId; |
315 | } |
316 | } |
317 | return $this->actorStore->acquireActorId( $user, $dbw ); |
318 | } |
319 | } |