Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 144 |
|
0.00% |
0 / 28 |
CRAP | |
0.00% |
0 / 1 |
ManualLogEntry | |
0.00% |
0 / 144 |
|
0.00% |
0 / 28 |
2756 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setParameters | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setRelations | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setPerformer | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setTarget | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
setTimestamp | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setComment | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setAssociatedRevId | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addTags | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
setIsPatrollable | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setLegacy | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setDeleted | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setForceBotFlag | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
insert | |
0.00% |
0 / 50 |
|
0.00% |
0 / 1 |
90 | |||
getRecentChange | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
2 | |||
publish | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
182 | |||
getType | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSubtype | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getParameters | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPerformerIdentity | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTarget | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTimestamp | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getComment | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getAssociatedRevId | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTags | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getIsPatrollable | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isLegacy | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDeleted | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Contains a class for dealing with manual log entries |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | * @author Niklas Laxström |
22 | * @license GPL-2.0-or-later |
23 | * @since 1.19 |
24 | */ |
25 | |
26 | use MediaWiki\ChangeTags\Taggable; |
27 | use MediaWiki\Context\RequestContext; |
28 | use MediaWiki\Deferred\DeferredUpdates; |
29 | use MediaWiki\HookContainer\HookRunner; |
30 | use MediaWiki\Linker\LinkTarget; |
31 | use MediaWiki\MediaWikiServices; |
32 | use MediaWiki\Page\PageReference; |
33 | use MediaWiki\SpecialPage\SpecialPage; |
34 | use MediaWiki\Title\Title; |
35 | use MediaWiki\User\UserIdentity; |
36 | use Wikimedia\Assert\Assert; |
37 | use Wikimedia\Rdbms\IDatabase; |
38 | |
39 | /** |
40 | * Class for creating new log entries and inserting them into the database. |
41 | * |
42 | * @newable |
43 | * @note marked as newable in 1.35 for lack of a better alternative, |
44 | * but should be changed to use the builder pattern or the |
45 | * command pattern. |
46 | * @since 1.19 |
47 | * @see https://www.mediawiki.org/wiki/Manual:Logging_to_Special:Log |
48 | */ |
49 | class ManualLogEntry extends LogEntryBase implements Taggable { |
50 | /** @var string Type of log entry */ |
51 | protected $type; |
52 | |
53 | /** @var string Sub type of log entry */ |
54 | protected $subtype; |
55 | |
56 | /** @var array Parameters for log entry */ |
57 | protected $parameters = []; |
58 | |
59 | /** @var array */ |
60 | protected $relations = []; |
61 | |
62 | /** @var UserIdentity Performer of the action for the log entry */ |
63 | protected $performer; |
64 | |
65 | /** @var Title Target title for the log entry */ |
66 | protected $target; |
67 | |
68 | /** @var string Timestamp of creation of the log entry */ |
69 | protected $timestamp; |
70 | |
71 | /** @var string Comment for the log entry */ |
72 | protected $comment = ''; |
73 | |
74 | /** @var int A rev id associated to the log entry */ |
75 | protected $revId = 0; |
76 | |
77 | /** @var string[] Change tags add to the log entry */ |
78 | protected $tags = []; |
79 | |
80 | /** @var int Deletion state of the log entry */ |
81 | protected $deleted; |
82 | |
83 | /** @var int ID of the log entry */ |
84 | protected $id; |
85 | |
86 | /** @var bool Can this log entry be patrolled? */ |
87 | protected $isPatrollable = false; |
88 | |
89 | /** @var bool Whether this is a legacy log entry */ |
90 | protected $legacy = false; |
91 | |
92 | /** @var bool|null The bot flag in the recent changes will be set to this value */ |
93 | protected $forceBotFlag = null; |
94 | |
95 | /** |
96 | * @stable to call |
97 | * @since 1.19 |
98 | * @param string $type Log type. Should match $wgLogTypes. |
99 | * @param string $subtype Log subtype (action). Should match $wgLogActions or |
100 | * (together with $type) $wgLogActionsHandlers. |
101 | * @note |
102 | */ |
103 | public function __construct( $type, $subtype ) { |
104 | $this->type = $type; |
105 | $this->subtype = $subtype; |
106 | } |
107 | |
108 | /** |
109 | * Set extra log parameters. |
110 | * |
111 | * Takes an array in a parameter name => parameter value format. The array |
112 | * will be converted to string via serialize() and stored in the log_params |
113 | * database field. (If you want to store parameters in such a way that they |
114 | * can be targeted by DB queries, use setRelations() instead.) |
115 | * |
116 | * You can pass these parameters to the log action message by prefixing the |
117 | * keys with a number and optional type, using colons to separate the fields. |
118 | * The numbering should start with number 4 (matching the $4 message |
119 | * parameter), as the first three parameters are hardcoded for every message |
120 | * ($1 is a link to the username and user talk page of the performing user, |
121 | * $2 is just the username (for determining gender), $3 is a link to the |
122 | * target page). |
123 | * |
124 | * If you want to store stuff that should not be available in messages, don't |
125 | * prefix the array key with a number and don't use the colons. (Note that |
126 | * such parameters will still be publicly viewable via the API.) |
127 | * |
128 | * Example: |
129 | * $entry->setParameters( [ |
130 | * // store and use in messages as $4 |
131 | * '4::color' => 'blue', |
132 | * // store as is, use in messages as $5 with Message::numParam() |
133 | * '5:number:count' => 3000, |
134 | * // store but do not use in messages |
135 | * 'animal' => 'dog' |
136 | * ] ); |
137 | * |
138 | * Typically, these parameters will be used in the logentry-<type>-<subtype> |
139 | * message, but custom formatters, declared via $wgLogActionsHandlers, can |
140 | * override that. |
141 | * |
142 | * @since 1.19 |
143 | * @param array $parameters Associative array |
144 | * @see LogFormatter::formatParameterValue() for valid parameter types and |
145 | * their meanings. |
146 | * @see self::setRelations() for storing parameters in a way that can be searched. |
147 | * @see LogFormatter::getMessageKey() for determining which message these |
148 | * parameters will be used in. |
149 | */ |
150 | public function setParameters( $parameters ) { |
151 | $this->parameters = $parameters; |
152 | } |
153 | |
154 | /** |
155 | * Declare arbitrary tag/value relations to this log entry. |
156 | * These will be stored in the log_search table and can be used |
157 | * to filter log entries later on. |
158 | * |
159 | * @param array $relations Map of (tag => (list of values|value)); values must be string. |
160 | * When an array of values is given, a separate DB row will be created for each value. |
161 | * @since 1.22 |
162 | */ |
163 | public function setRelations( array $relations ) { |
164 | $this->relations = $relations; |
165 | } |
166 | |
167 | /** |
168 | * Set the user that performed the action being logged. |
169 | * |
170 | * @since 1.19 |
171 | * @param UserIdentity $performer |
172 | */ |
173 | public function setPerformer( UserIdentity $performer ) { |
174 | $this->performer = $performer; |
175 | } |
176 | |
177 | /** |
178 | * Set the title of the object changed. |
179 | * |
180 | * @param LinkTarget|PageReference $target calling with LinkTarget |
181 | * is deprecated since 1.37 |
182 | * @since 1.19 |
183 | */ |
184 | public function setTarget( $target ) { |
185 | if ( $target instanceof PageReference ) { |
186 | $this->target = Title::newFromPageReference( $target ); |
187 | } elseif ( $target instanceof LinkTarget ) { |
188 | $this->target = Title::newFromLinkTarget( $target ); |
189 | } else { |
190 | throw new InvalidArgumentException( "Invalid target provided" ); |
191 | } |
192 | } |
193 | |
194 | /** |
195 | * Set the timestamp of when the logged action took place. |
196 | * |
197 | * @since 1.19 |
198 | * @param string $timestamp Can be in any format accepted by ConvertibleTimestamp |
199 | */ |
200 | public function setTimestamp( $timestamp ) { |
201 | $this->timestamp = $timestamp; |
202 | } |
203 | |
204 | /** |
205 | * Set a comment associated with the action being logged. |
206 | * |
207 | * @since 1.19 |
208 | * @param string $comment |
209 | */ |
210 | public function setComment( string $comment ) { |
211 | $this->comment = $comment; |
212 | } |
213 | |
214 | /** |
215 | * Set an associated revision id. |
216 | * |
217 | * For example, the ID of the revision that was inserted to mark a page move |
218 | * or protection, file upload, etc. |
219 | * |
220 | * @since 1.27 |
221 | * @param int $revId |
222 | */ |
223 | public function setAssociatedRevId( $revId ) { |
224 | $this->revId = $revId; |
225 | } |
226 | |
227 | /** |
228 | * Add change tags for the log entry |
229 | * |
230 | * @since 1.33 |
231 | * @param string|string[]|null $tags Tags to apply |
232 | */ |
233 | public function addTags( $tags ) { |
234 | if ( $tags === null ) { |
235 | return; |
236 | } |
237 | |
238 | if ( is_string( $tags ) ) { |
239 | $tags = [ $tags ]; |
240 | } |
241 | Assert::parameterElementType( 'string', $tags, 'tags' ); |
242 | $this->tags = array_unique( array_merge( $this->tags, $tags ) ); |
243 | } |
244 | |
245 | /** |
246 | * Set whether this log entry should be made patrollable |
247 | * This shouldn't depend on config, only on whether there is full support |
248 | * in the software for patrolling this log entry. |
249 | * False by default |
250 | * |
251 | * @since 1.27 |
252 | * @param bool $patrollable |
253 | */ |
254 | public function setIsPatrollable( $patrollable ) { |
255 | $this->isPatrollable = (bool)$patrollable; |
256 | } |
257 | |
258 | /** |
259 | * Set the 'legacy' flag |
260 | * |
261 | * @since 1.25 |
262 | * @param bool $legacy |
263 | */ |
264 | public function setLegacy( $legacy ) { |
265 | $this->legacy = $legacy; |
266 | } |
267 | |
268 | /** |
269 | * Set the 'deleted' flag. |
270 | * |
271 | * @since 1.19 |
272 | * @param int $deleted One of LogPage::DELETED_* bitfield constants |
273 | */ |
274 | public function setDeleted( $deleted ) { |
275 | $this->deleted = $deleted; |
276 | } |
277 | |
278 | /** |
279 | * Set the bot flag in the recent changes to this value. |
280 | * |
281 | * @since 1.40 |
282 | * @param bool $forceBotFlag |
283 | */ |
284 | public function setForceBotFlag( bool $forceBotFlag ): void { |
285 | $this->forceBotFlag = $forceBotFlag; |
286 | } |
287 | |
288 | /** |
289 | * Insert the entry into the `logging` table. |
290 | * |
291 | * @param IDatabase|null $dbw |
292 | * @return int ID of the log entry |
293 | */ |
294 | public function insert( IDatabase $dbw = null ) { |
295 | $services = MediaWikiServices::getInstance(); |
296 | $dbw = $dbw ?: $services->getConnectionProvider()->getPrimaryDatabase(); |
297 | |
298 | $this->timestamp ??= wfTimestampNow(); |
299 | $actorId = $services->getActorStore()->acquireActorId( $this->getPerformerIdentity(), $dbw ); |
300 | |
301 | // Trim spaces on user supplied text |
302 | $comment = trim( $this->getComment() ?? '' ); |
303 | |
304 | $params = $this->getParameters(); |
305 | $relations = $this->relations; |
306 | |
307 | // Additional fields for which there's no space in the database table schema |
308 | $revId = $this->getAssociatedRevId(); |
309 | if ( $revId ) { |
310 | $params['associated_rev_id'] = $revId; |
311 | $relations['associated_rev_id'] = $revId; |
312 | } |
313 | |
314 | $row = [ |
315 | 'log_type' => $this->getType(), |
316 | 'log_action' => $this->getSubtype(), |
317 | 'log_timestamp' => $dbw->timestamp( $this->getTimestamp() ), |
318 | 'log_actor' => $actorId, |
319 | 'log_namespace' => $this->getTarget()->getNamespace(), |
320 | 'log_title' => $this->getTarget()->getDBkey(), |
321 | 'log_page' => $this->getTarget()->getArticleID(), |
322 | 'log_params' => LogEntryBase::makeParamBlob( $params ), |
323 | ]; |
324 | if ( isset( $this->deleted ) ) { |
325 | $row['log_deleted'] = $this->deleted; |
326 | } |
327 | $row += $services->getCommentStore()->insert( $dbw, 'log_comment', $comment ); |
328 | |
329 | $dbw->newInsertQueryBuilder() |
330 | ->insertInto( 'logging' ) |
331 | ->row( $row ) |
332 | ->caller( __METHOD__ ) |
333 | ->execute(); |
334 | $this->id = $dbw->insertId(); |
335 | |
336 | $rows = []; |
337 | foreach ( $relations as $tag => $values ) { |
338 | if ( !strlen( $tag ) ) { |
339 | throw new UnexpectedValueException( "Got empty log search tag." ); |
340 | } |
341 | |
342 | if ( !is_array( $values ) ) { |
343 | $values = [ $values ]; |
344 | } |
345 | |
346 | foreach ( $values as $value ) { |
347 | $rows[] = [ |
348 | 'ls_field' => $tag, |
349 | 'ls_value' => $value, |
350 | 'ls_log_id' => $this->id |
351 | ]; |
352 | } |
353 | } |
354 | if ( count( $rows ) ) { |
355 | $dbw->newInsertQueryBuilder() |
356 | ->insertInto( 'log_search' ) |
357 | ->ignore() |
358 | ->rows( $rows ) |
359 | ->caller( __METHOD__ ) |
360 | ->execute(); |
361 | } |
362 | |
363 | return $this->id; |
364 | } |
365 | |
366 | /** |
367 | * Get a RecentChanges object for the log entry |
368 | * |
369 | * @param int $newId |
370 | * @return RecentChange |
371 | * @since 1.23 |
372 | */ |
373 | public function getRecentChange( $newId = 0 ) { |
374 | $formatter = MediaWikiServices::getInstance()->getLogFormatterFactory()->newFromEntry( $this ); |
375 | $context = RequestContext::newExtraneousContext( $this->getTarget() ); |
376 | $formatter->setContext( $context ); |
377 | |
378 | $logpage = SpecialPage::getTitleFor( 'Log', $this->getType() ); |
379 | |
380 | return RecentChange::newLogEntry( |
381 | $this->getTimestamp(), |
382 | $logpage, |
383 | $this->getPerformerIdentity(), |
384 | $formatter->getPlainActionText(), |
385 | '', |
386 | $this->getType(), |
387 | $this->getSubtype(), |
388 | $this->getTarget(), |
389 | $this->getComment(), |
390 | LogEntryBase::makeParamBlob( $this->getParameters() ), |
391 | $newId, |
392 | $formatter->getIRCActionComment(), // Used for IRC feeds |
393 | $this->getAssociatedRevId(), // Used for e.g. moves and uploads |
394 | $this->getIsPatrollable(), |
395 | $this->forceBotFlag |
396 | ); |
397 | } |
398 | |
399 | /** |
400 | * Publish the log entry. |
401 | * |
402 | * @param int $newId Id of the log entry. |
403 | * @param string $to One of: rcandudp (default), rc, udp |
404 | */ |
405 | public function publish( $newId, $to = 'rcandudp' ) { |
406 | $canAddTags = true; |
407 | // FIXME: this code should be removed once all callers properly call publish() |
408 | if ( $to === 'udp' && !$newId && !$this->getAssociatedRevId() ) { |
409 | \MediaWiki\Logger\LoggerFactory::getInstance( 'logging' )->warning( |
410 | 'newId and/or revId must be set when calling ManualLogEntry::publish()', |
411 | [ |
412 | 'newId' => $newId, |
413 | 'to' => $to, |
414 | 'revId' => $this->getAssociatedRevId(), |
415 | // pass a new exception to register the stack trace |
416 | 'exception' => new RuntimeException() |
417 | ] |
418 | ); |
419 | $canAddTags = false; |
420 | } |
421 | |
422 | DeferredUpdates::addCallableUpdate( |
423 | function () use ( $newId, $to, $canAddTags ) { |
424 | $log = new LogPage( $this->getType() ); |
425 | if ( !$log->isRestricted() ) { |
426 | ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) ) |
427 | ->onManualLogEntryBeforePublish( $this ); |
428 | $rc = $this->getRecentChange( $newId ); |
429 | |
430 | if ( $to === 'rc' || $to === 'rcandudp' ) { |
431 | // save RC, passing tags so they are applied there |
432 | $rc->addTags( $this->getTags() ); |
433 | $rc->save( $rc::SEND_NONE ); |
434 | } else { |
435 | $tags = $this->getTags(); |
436 | if ( $tags && $canAddTags ) { |
437 | $revId = $this->getAssociatedRevId(); |
438 | MediaWikiServices::getInstance()->getChangeTagsStore()->addTags( |
439 | $tags, |
440 | null, |
441 | $revId > 0 ? $revId : null, |
442 | $newId > 0 ? $newId : null |
443 | ); |
444 | } |
445 | } |
446 | |
447 | if ( $to === 'udp' || $to === 'rcandudp' ) { |
448 | $rc->notifyRCFeeds(); |
449 | } |
450 | } |
451 | }, |
452 | DeferredUpdates::POSTSEND, |
453 | MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase() |
454 | ); |
455 | } |
456 | |
457 | /** |
458 | * @return string |
459 | */ |
460 | public function getType() { |
461 | return $this->type; |
462 | } |
463 | |
464 | /** |
465 | * @return string |
466 | */ |
467 | public function getSubtype() { |
468 | return $this->subtype; |
469 | } |
470 | |
471 | /** |
472 | * @return array |
473 | */ |
474 | public function getParameters() { |
475 | return $this->parameters; |
476 | } |
477 | |
478 | /** |
479 | * @return UserIdentity |
480 | */ |
481 | public function getPerformerIdentity(): UserIdentity { |
482 | return $this->performer; |
483 | } |
484 | |
485 | /** |
486 | * @return Title |
487 | */ |
488 | public function getTarget() { |
489 | return $this->target; |
490 | } |
491 | |
492 | /** |
493 | * @return string|false TS_MW timestamp, a string with 14 digits |
494 | */ |
495 | public function getTimestamp() { |
496 | $ts = $this->timestamp ?? wfTimestampNow(); |
497 | |
498 | return wfTimestamp( TS_MW, $ts ); |
499 | } |
500 | |
501 | /** |
502 | * @return string |
503 | */ |
504 | public function getComment() { |
505 | return $this->comment; |
506 | } |
507 | |
508 | /** |
509 | * @since 1.27 |
510 | * @return int |
511 | */ |
512 | public function getAssociatedRevId() { |
513 | return $this->revId; |
514 | } |
515 | |
516 | /** |
517 | * @since 1.27 |
518 | * @return string[] |
519 | */ |
520 | public function getTags() { |
521 | return $this->tags; |
522 | } |
523 | |
524 | /** |
525 | * Whether this log entry is patrollable |
526 | * |
527 | * @since 1.27 |
528 | * @return bool |
529 | */ |
530 | public function getIsPatrollable() { |
531 | return $this->isPatrollable; |
532 | } |
533 | |
534 | /** |
535 | * @since 1.25 |
536 | * @return bool |
537 | */ |
538 | public function isLegacy() { |
539 | return $this->legacy; |
540 | } |
541 | |
542 | /** |
543 | * @return int |
544 | */ |
545 | public function getDeleted() { |
546 | return (int)$this->deleted; |
547 | } |
548 | } |