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