From: Reedy Date: Sun, 14 Apr 2019 01:05:34 +0000 (+0100) Subject: Split logging classes to individual files X-Git-Tag: 1.34.0-rc.0~1997^2 X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=commitdiff_plain;h=f032d27d0cfc16d93d9164498c3967c77c60cfc8;hp=f950cc221ed58a83c08fb816aa8faee8fbcbe09d Split logging classes to individual files Change-Id: I3eaaf23612fe1aed65e49c06a9e5e565399af9ce --- diff --git a/.phpcs.xml b/.phpcs.xml index 7f4ec1b04d..236b7e75ca 100644 --- a/.phpcs.xml +++ b/.phpcs.xml @@ -212,8 +212,6 @@ */includes/libs/filebackend/FileBackendStore\.php */includes/libs/filebackend/FSFileBackend\.php */includes/libs/filebackend/SwiftFileBackend\.php - */includes/logging/LogEntry\.php - */includes/logging/LogFormatter\.php */includes/parser/Preprocessor_DOM\.php */includes/parser/Preprocessor_Hash\.php */includes/parser/Preprocessor\.php diff --git a/autoload.php b/autoload.php index 1bd32a74e6..f58a3d6500 100644 --- a/autoload.php +++ b/autoload.php @@ -353,7 +353,7 @@ $wgAutoloadLocalClasses = [ 'DatabaseBase' => __DIR__ . '/includes/libs/rdbms/database/Database.php', 'DatabaseInstaller' => __DIR__ . '/includes/installer/DatabaseInstaller.php', 'DatabaseLag' => __DIR__ . '/maintenance/lag.php', - 'DatabaseLogEntry' => __DIR__ . '/includes/logging/LogEntry.php', + 'DatabaseLogEntry' => __DIR__ . '/includes/logging/DatabaseLogEntry.php', 'DatabaseMssql' => __DIR__ . '/includes/libs/rdbms/database/DatabaseMssql.php', 'DatabaseMysqlBase' => __DIR__ . '/includes/libs/rdbms/database/DatabaseMysqlBase.php', 'DatabaseMysqli' => __DIR__ . '/includes/libs/rdbms/database/DatabaseMysqli.php', @@ -772,7 +772,7 @@ $wgAutoloadLocalClasses = [ 'LanguageZh_hans' => __DIR__ . '/languages/classes/LanguageZh_hans.php', 'Languages' => __DIR__ . '/maintenance/language/languages.inc', 'LayeredParameterizedPassword' => __DIR__ . '/includes/password/LayeredParameterizedPassword.php', - 'LegacyLogFormatter' => __DIR__ . '/includes/logging/LogFormatter.php', + 'LegacyLogFormatter' => __DIR__ . '/includes/logging/LegacyLogFormatter.php', 'License' => __DIR__ . '/includes/specials/helpers/License.php', 'Licenses' => __DIR__ . '/includes/specials/formfields/Licenses.php', 'LinkBatch' => __DIR__ . '/includes/cache/LinkBatch.php', @@ -803,7 +803,7 @@ $wgAutoloadLocalClasses = [ 'LockManager' => __DIR__ . '/includes/libs/lockmanager/LockManager.php', 'LockManagerGroup' => __DIR__ . '/includes/filebackend/lockmanager/LockManagerGroup.php', 'LogEntry' => __DIR__ . '/includes/logging/LogEntry.php', - 'LogEntryBase' => __DIR__ . '/includes/logging/LogEntry.php', + 'LogEntryBase' => __DIR__ . '/includes/logging/LogEntryBase.php', 'LogEventsList' => __DIR__ . '/includes/logging/LogEventsList.php', 'LogFormatter' => __DIR__ . '/includes/logging/LogFormatter.php', 'LogPage' => __DIR__ . '/includes/logging/LogPage.php', @@ -851,7 +851,7 @@ $wgAutoloadLocalClasses = [ 'MalformedTitleException' => __DIR__ . '/includes/title/MalformedTitleException.php', 'ManageForeignResources' => __DIR__ . '/maintenance/manageForeignResources.php', 'ManageJobs' => __DIR__ . '/maintenance/manageJobs.php', - 'ManualLogEntry' => __DIR__ . '/includes/logging/LogEntry.php', + 'ManualLogEntry' => __DIR__ . '/includes/logging/ManualLogEntry.php', 'MapCacheLRU' => __DIR__ . '/includes/libs/MapCacheLRU.php', 'MappedIterator' => __DIR__ . '/includes/libs/MappedIterator.php', 'MarkpatrolledAction' => __DIR__ . '/includes/actions/MarkpatrolledAction.php', @@ -1179,7 +1179,7 @@ $wgAutoloadLocalClasses = [ 'QuorumLockManager' => __DIR__ . '/includes/libs/lockmanager/QuorumLockManager.php', 'RCCacheEntry' => __DIR__ . '/includes/changes/RCCacheEntry.php', 'RCCacheEntryFactory' => __DIR__ . '/includes/changes/RCCacheEntryFactory.php', - 'RCDatabaseLogEntry' => __DIR__ . '/includes/logging/LogEntry.php', + 'RCDatabaseLogEntry' => __DIR__ . '/includes/logging/RCDatabaseLogEntry.php', 'RCFeed' => __DIR__ . '/includes/rcfeed/RCFeed.php', 'RCFeedEngine' => __DIR__ . '/includes/rcfeed/RCFeedEngine.php', 'RCFeedFormatter' => __DIR__ . '/includes/rcfeed/RCFeedFormatter.php', diff --git a/includes/logging/DatabaseLogEntry.php b/includes/logging/DatabaseLogEntry.php new file mode 100644 index 0000000000..db0f5996ff --- /dev/null +++ b/includes/logging/DatabaseLogEntry.php @@ -0,0 +1,231 @@ +getJoin( 'log_comment' ); + $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' ); + + $tables = array_merge( + [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ] + ); + $fields = [ + 'log_id', 'log_type', 'log_action', 'log_timestamp', + 'log_namespace', 'log_title', // unused log_page + 'log_params', 'log_deleted', + 'user_id', 'user_name', 'user_editcount', + ] + $commentQuery['fields'] + $actorQuery['fields']; + + $joins = [ + // IPs don't have an entry in user table + 'user' => [ 'LEFT JOIN', 'user_id=' . $actorQuery['fields']['log_user'] ], + ] + $commentQuery['joins'] + $actorQuery['joins']; + + return [ + 'tables' => $tables, + 'fields' => $fields, + 'conds' => [], + 'options' => [], + 'join_conds' => $joins, + ]; + } + + /** + * Constructs new LogEntry from database result row. + * Supports rows from both logging and recentchanges table. + * + * @param stdClass|array $row + * @return DatabaseLogEntry + */ + public static function newFromRow( $row ) { + $row = (object)$row; + if ( isset( $row->rc_logid ) ) { + return new RCDatabaseLogEntry( $row ); + } else { + return new self( $row ); + } + } + + /** + * Loads a LogEntry with the given id from database + * + * @param int $id + * @param IDatabase $db + * @return DatabaseLogEntry|null + */ + public static function newFromId( $id, IDatabase $db ) { + $queryInfo = self::getSelectQueryData(); + $queryInfo['conds'] += [ 'log_id' => $id ]; + $row = $db->selectRow( + $queryInfo['tables'], + $queryInfo['fields'], + $queryInfo['conds'], + __METHOD__, + $queryInfo['options'], + $queryInfo['join_conds'] + ); + if ( !$row ) { + return null; + } + return self::newFromRow( $row ); + } + + /** @var stdClass Database result row. */ + protected $row; + + /** @var User */ + protected $performer; + + /** @var array Parameters for log entry */ + protected $params; + + /** @var int A rev id associated to the log entry */ + protected $revId = null; + + /** @var bool Whether the parameters for this log entry are stored in new or old format. */ + protected $legacy; + + protected function __construct( $row ) { + $this->row = $row; + } + + /** + * Returns the unique database id. + * + * @return int + */ + public function getId() { + return (int)$this->row->log_id; + } + + /** + * Returns whatever is stored in the database field. + * + * @return string + */ + protected function getRawParameters() { + return $this->row->log_params; + } + + public function isLegacy() { + // This extracts the property + $this->getParameters(); + return $this->legacy; + } + + public function getType() { + return $this->row->log_type; + } + + public function getSubtype() { + return $this->row->log_action; + } + + public function getParameters() { + if ( !isset( $this->params ) ) { + $blob = $this->getRawParameters(); + Wikimedia\suppressWarnings(); + $params = LogEntryBase::extractParams( $blob ); + Wikimedia\restoreWarnings(); + if ( $params !== false ) { + $this->params = $params; + $this->legacy = false; + } else { + $this->params = LogPage::extractParams( $blob ); + $this->legacy = true; + } + + if ( isset( $this->params['associated_rev_id'] ) ) { + $this->revId = $this->params['associated_rev_id']; + unset( $this->params['associated_rev_id'] ); + } + } + + return $this->params; + } + + public function getAssociatedRevId() { + // This extracts the property + $this->getParameters(); + return $this->revId; + } + + public function getPerformer() { + if ( !$this->performer ) { + $actorId = isset( $this->row->log_actor ) ? (int)$this->row->log_actor : 0; + $userId = (int)$this->row->log_user; + if ( $userId !== 0 || $actorId !== 0 ) { + // logged-in users + if ( isset( $this->row->user_name ) ) { + $this->performer = User::newFromRow( $this->row ); + } elseif ( $actorId !== 0 ) { + $this->performer = User::newFromActorId( $actorId ); + } else { + $this->performer = User::newFromId( $userId ); + } + } else { + // IP users + $userText = $this->row->log_user_text; + $this->performer = User::newFromName( $userText, false ); + } + } + + return $this->performer; + } + + public function getTarget() { + $namespace = $this->row->log_namespace; + $page = $this->row->log_title; + return Title::makeTitle( $namespace, $page ); + } + + public function getTimestamp() { + return wfTimestamp( TS_MW, $this->row->log_timestamp ); + } + + public function getComment() { + return CommentStore::getStore()->getComment( 'log_comment', $this->row )->text; + } + + public function getDeleted() { + return $this->row->log_deleted; + } +} diff --git a/includes/logging/LegacyLogFormatter.php b/includes/logging/LegacyLogFormatter.php new file mode 100644 index 0000000000..61104db625 --- /dev/null +++ b/includes/logging/LegacyLogFormatter.php @@ -0,0 +1,127 @@ +comment === null ) { + $this->comment = parent::getComment(); + } + + // Make sure we execute the LogLine hook so that we immediately return + // the correct value. + if ( $this->revert === null ) { + $this->getActionLinks(); + } + + return $this->comment; + } + + /** + * @return string + * @return-taint onlysafefor_html + */ + protected function getActionMessage() { + $entry = $this->entry; + $action = LogPage::actionText( + $entry->getType(), + $entry->getSubtype(), + $entry->getTarget(), + $this->plaintext ? null : $this->context->getSkin(), + (array)$entry->getParameters(), + !$this->plaintext // whether to filter [[]] links + ); + + $performer = $this->getPerformerElement(); + if ( !$this->irctext ) { + $sep = $this->msg( 'word-separator' ); + $sep = $this->plaintext ? $sep->text() : $sep->escaped(); + $action = $performer . $sep . $action; + } + + return $action; + } + + public function getActionLinks() { + if ( $this->revert !== null ) { + return $this->revert; + } + + if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) ) { + $this->revert = ''; + return $this->revert; + } + + $title = $this->entry->getTarget(); + $type = $this->entry->getType(); + $subtype = $this->entry->getSubtype(); + + // Do nothing. The implementation is handled by the hook modifiying the + // passed-by-ref parameters. This also changes the default value so that + // getComment() and getActionLinks() do not call them indefinitely. + $this->revert = ''; + + // This is to populate the $comment member of this instance so that it + // can be modified when calling the hook just below. + if ( $this->comment === null ) { + $this->getComment(); + } + + $params = $this->entry->getParameters(); + + Hooks::run( 'LogLine', [ $type, $subtype, $title, $params, + &$this->comment, &$this->revert, $this->entry->getTimestamp() ] ); + + return $this->revert; + } +} diff --git a/includes/logging/LogEntry.php b/includes/logging/LogEntry.php index c5e4a9204d..17f72bd574 100644 --- a/includes/logging/LogEntry.php +++ b/includes/logging/LogEntry.php @@ -1,11 +1,6 @@ getType() . '/' . $this->getSubtype(); - } - - public function isDeleted( $field ) { - return ( $this->getDeleted() & $field ) === $field; - } - - /** - * Whether the parameters for this log are stored in new or - * old format. - * - * @return bool - */ - public function isLegacy() { - return false; - } - - /** - * Create a blob from a parameter array - * - * @since 1.26 - * @param array $params - * @return string - */ - public static function makeParamBlob( $params ) { - return serialize( (array)$params ); - } - - /** - * Extract a parameter array from a blob - * - * @since 1.26 - * @param string $blob - * @return array - */ - public static function extractParams( $blob ) { - return unserialize( $blob ); - } -} - -/** - * A value class to process existing log entries. In other words, this class caches a log - * entry from the database and provides an immutable object-oriented representation of it. - * - * @since 1.19 - */ -class DatabaseLogEntry extends LogEntryBase { - - /** - * Returns array of information that is needed for querying - * log entries. Array contains the following keys: - * tables, fields, conds, options and join_conds - * - * @return array - */ - public static function getSelectQueryData() { - $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' ); - $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' ); - - $tables = array_merge( - [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ] - ); - $fields = [ - 'log_id', 'log_type', 'log_action', 'log_timestamp', - 'log_namespace', 'log_title', // unused log_page - 'log_params', 'log_deleted', - 'user_id', 'user_name', 'user_editcount', - ] + $commentQuery['fields'] + $actorQuery['fields']; - - $joins = [ - // IPs don't have an entry in user table - 'user' => [ 'LEFT JOIN', 'user_id=' . $actorQuery['fields']['log_user'] ], - ] + $commentQuery['joins'] + $actorQuery['joins']; - - return [ - 'tables' => $tables, - 'fields' => $fields, - 'conds' => [], - 'options' => [], - 'join_conds' => $joins, - ]; - } - - /** - * Constructs new LogEntry from database result row. - * Supports rows from both logging and recentchanges table. - * - * @param stdClass|array $row - * @return DatabaseLogEntry - */ - public static function newFromRow( $row ) { - $row = (object)$row; - if ( isset( $row->rc_logid ) ) { - return new RCDatabaseLogEntry( $row ); - } else { - return new self( $row ); - } - } - - /** - * Loads a LogEntry with the given id from database - * - * @param int $id - * @param IDatabase $db - * @return DatabaseLogEntry|null - */ - public static function newFromId( $id, IDatabase $db ) { - $queryInfo = self::getSelectQueryData(); - $queryInfo['conds'] += [ 'log_id' => $id ]; - $row = $db->selectRow( - $queryInfo['tables'], - $queryInfo['fields'], - $queryInfo['conds'], - __METHOD__, - $queryInfo['options'], - $queryInfo['join_conds'] - ); - if ( !$row ) { - return null; - } - return self::newFromRow( $row ); - } - - /** @var stdClass Database result row. */ - protected $row; - - /** @var User */ - protected $performer; - - /** @var array Parameters for log entry */ - protected $params; - - /** @var int A rev id associated to the log entry */ - protected $revId = null; - - /** @var bool Whether the parameters for this log entry are stored in new or old format. */ - protected $legacy; - - protected function __construct( $row ) { - $this->row = $row; - } - - /** - * Returns the unique database id. - * - * @return int - */ - public function getId() { - return (int)$this->row->log_id; - } - - /** - * Returns whatever is stored in the database field. - * - * @return string - */ - protected function getRawParameters() { - return $this->row->log_params; - } - - public function isLegacy() { - // This extracts the property - $this->getParameters(); - return $this->legacy; - } - - public function getType() { - return $this->row->log_type; - } - - public function getSubtype() { - return $this->row->log_action; - } - - public function getParameters() { - if ( !isset( $this->params ) ) { - $blob = $this->getRawParameters(); - Wikimedia\suppressWarnings(); - $params = LogEntryBase::extractParams( $blob ); - Wikimedia\restoreWarnings(); - if ( $params !== false ) { - $this->params = $params; - $this->legacy = false; - } else { - $this->params = LogPage::extractParams( $blob ); - $this->legacy = true; - } - - if ( isset( $this->params['associated_rev_id'] ) ) { - $this->revId = $this->params['associated_rev_id']; - unset( $this->params['associated_rev_id'] ); - } - } - - return $this->params; - } - - public function getAssociatedRevId() { - // This extracts the property - $this->getParameters(); - return $this->revId; - } - - public function getPerformer() { - if ( !$this->performer ) { - $actorId = isset( $this->row->log_actor ) ? (int)$this->row->log_actor : 0; - $userId = (int)$this->row->log_user; - if ( $userId !== 0 || $actorId !== 0 ) { - // logged-in users - if ( isset( $this->row->user_name ) ) { - $this->performer = User::newFromRow( $this->row ); - } elseif ( $actorId !== 0 ) { - $this->performer = User::newFromActorId( $actorId ); - } else { - $this->performer = User::newFromId( $userId ); - } - } else { - // IP users - $userText = $this->row->log_user_text; - $this->performer = User::newFromName( $userText, false ); - } - } - - return $this->performer; - } - - public function getTarget() { - $namespace = $this->row->log_namespace; - $page = $this->row->log_title; - $title = Title::makeTitle( $namespace, $page ); - - return $title; - } - - public function getTimestamp() { - return wfTimestamp( TS_MW, $this->row->log_timestamp ); - } - - public function getComment() { - return CommentStore::getStore()->getComment( 'log_comment', $this->row )->text; - } - - public function getDeleted() { - return $this->row->log_deleted; - } -} - -/** - * A subclass of DatabaseLogEntry for objects constructed from entries in the - * recentchanges table (rather than the logging table). - */ -class RCDatabaseLogEntry extends DatabaseLogEntry { - - public function getId() { - return $this->row->rc_logid; - } - - protected function getRawParameters() { - return $this->row->rc_params; - } - - public function getAssociatedRevId() { - return $this->row->rc_this_oldid; - } - - public function getType() { - return $this->row->rc_log_type; - } - - public function getSubtype() { - return $this->row->rc_log_action; - } - - public function getPerformer() { - if ( !$this->performer ) { - $actorId = isset( $this->row->rc_actor ) ? (int)$this->row->rc_actor : 0; - $userId = (int)$this->row->rc_user; - if ( $actorId !== 0 ) { - $this->performer = User::newFromActorId( $actorId ); - } elseif ( $userId !== 0 ) { - $this->performer = User::newFromId( $userId ); - } else { - $userText = $this->row->rc_user_text; - // Might be an IP, don't validate the username - $this->performer = User::newFromName( $userText, false ); - } - } - - return $this->performer; - } - - public function getTarget() { - $namespace = $this->row->rc_namespace; - $page = $this->row->rc_title; - $title = Title::makeTitle( $namespace, $page ); - - return $title; - } - - public function getTimestamp() { - return wfTimestamp( TS_MW, $this->row->rc_timestamp ); - } - - public function getComment() { - return CommentStore::getStore() - // Legacy because the row may have used RecentChange::selectFields() - ->getCommentLegacy( wfGetDB( DB_REPLICA ), 'rc_comment', $this->row )->text; - } - - public function getDeleted() { - return $this->row->rc_deleted; - } -} - -/** - * Class for creating new log entries and inserting them into the database. - * - * @since 1.19 - */ -class ManualLogEntry extends LogEntryBase implements Taggable { - /** @var string Type of log entry */ - protected $type; - - /** @var string Sub type of log entry */ - protected $subtype; - - /** @var array Parameters for log entry */ - protected $parameters = []; - - /** @var array */ - protected $relations = []; - - /** @var User Performer of the action for the log entry */ - protected $performer; - - /** @var Title Target title for the log entry */ - protected $target; - - /** @var string Timestamp of creation of the log entry */ - protected $timestamp; - - /** @var string Comment for the log entry */ - protected $comment = ''; - - /** @var int A rev id associated to the log entry */ - protected $revId = 0; - - /** @var string[] Change tags add to the log entry */ - protected $tags = []; - - /** @var int Deletion state of the log entry */ - protected $deleted; - - /** @var int ID of the log entry */ - protected $id; - - /** @var bool Can this log entry be patrolled? */ - protected $isPatrollable = false; - - /** @var bool Whether this is a legacy log entry */ - protected $legacy = false; - - /** - * @since 1.19 - * @param string $type - * @param string $subtype - */ - public function __construct( $type, $subtype ) { - $this->type = $type; - $this->subtype = $subtype; - } - - /** - * Set extra log parameters. - * - * You can pass params to the log action message by prefixing the keys with - * a number and optional type, using colons to separate the fields. The - * numbering should start with number 4, the first three parameters are - * hardcoded for every message. - * - * If you want to store stuff that should not be available in messages, don't - * prefix the array key with a number and don't use the colons. - * - * Example: - * $entry->setParameters( - * '4::color' => 'blue', - * '5:number:count' => 3000, - * 'animal' => 'dog' - * ); - * - * @since 1.19 - * @param array $parameters Associative array - */ - public function setParameters( $parameters ) { - $this->parameters = $parameters; - } - - /** - * Declare arbitrary tag/value relations to this log entry. - * These can be used to filter log entries later on. - * - * @param array $relations Map of (tag => (list of values|value)) - * @since 1.22 - */ - public function setRelations( array $relations ) { - $this->relations = $relations; - } - - /** - * Set the user that performed the action being logged. - * - * @since 1.19 - * @param UserIdentity $performer - */ - public function setPerformer( UserIdentity $performer ) { - $this->performer = User::newFromIdentity( $performer ); - } - - /** - * Set the title of the object changed. - * - * @since 1.19 - * @param LinkTarget $target - */ - public function setTarget( LinkTarget $target ) { - $this->target = Title::newFromLinkTarget( $target ); - } - - /** - * Set the timestamp of when the logged action took place. - * - * @since 1.19 - * @param string $timestamp - */ - public function setTimestamp( $timestamp ) { - $this->timestamp = $timestamp; - } - - /** - * Set a comment associated with the action being logged. - * - * @since 1.19 - * @param string $comment - */ - public function setComment( $comment ) { - $this->comment = $comment; - } - - /** - * Set an associated revision id. - * - * For example, the ID of the revision that was inserted to mark a page move - * or protection, file upload, etc. - * - * @since 1.27 - * @param int $revId - */ - public function setAssociatedRevId( $revId ) { - $this->revId = $revId; - } - - /** - * Set change tags for the log entry. - * - * Passing `null` means the same as empty array, - * for compatibility with WikiPage::doUpdateRestrictions(). - * - * @since 1.27 - * @param string|string[]|null $tags - * @deprecated since 1.33 Please use addTags() instead - */ - public function setTags( $tags ) { - if ( $this->tags ) { - wfDebug( 'Overwriting existing ManualLogEntry tags' ); - } - $this->tags = []; - if ( $tags !== null ) { - $this->addTags( $tags ); - } - } - - /** - * Add change tags for the log entry - * - * @since 1.33 - * @param string|string[] $tags Tags to apply - */ - public function addTags( $tags ) { - if ( is_string( $tags ) ) { - $tags = [ $tags ]; - } - Assert::parameterElementType( 'string', $tags, 'tags' ); - $this->tags = array_unique( array_merge( $this->tags, $tags ) ); - } - - /** - * Set whether this log entry should be made patrollable - * This shouldn't depend on config, only on whether there is full support - * in the software for patrolling this log entry. - * False by default - * - * @since 1.27 - * @param bool $patrollable - */ - public function setIsPatrollable( $patrollable ) { - $this->isPatrollable = (bool)$patrollable; - } - - /** - * Set the 'legacy' flag - * - * @since 1.25 - * @param bool $legacy - */ - public function setLegacy( $legacy ) { - $this->legacy = $legacy; - } - - /** - * Set the 'deleted' flag. - * - * @since 1.19 - * @param int $deleted One of LogPage::DELETED_* bitfield constants - */ - public function setDeleted( $deleted ) { - $this->deleted = $deleted; - } - - /** - * Insert the entry into the `logging` table. - * - * @param IDatabase|null $dbw - * @return int ID of the log entry - * @throws MWException - */ - public function insert( IDatabase $dbw = null ) { - global $wgActorTableSchemaMigrationStage; - - $dbw = $dbw ?: wfGetDB( DB_MASTER ); - - if ( $this->timestamp === null ) { - $this->timestamp = wfTimestampNow(); - } - - // Trim spaces on user supplied text - $comment = trim( $this->getComment() ); - - $params = $this->getParameters(); - $relations = $this->relations; - - // Ensure actor relations are set - if ( ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) && - empty( $relations['target_author_actor'] ) - ) { - $actorIds = []; - if ( !empty( $relations['target_author_id'] ) ) { - foreach ( $relations['target_author_id'] as $id ) { - $actorIds[] = User::newFromId( $id )->getActorId( $dbw ); - } - } - if ( !empty( $relations['target_author_ip'] ) ) { - foreach ( $relations['target_author_ip'] as $ip ) { - $actorIds[] = User::newFromName( $ip, false )->getActorId( $dbw ); - } - } - if ( $actorIds ) { - $relations['target_author_actor'] = $actorIds; - $params['authorActors'] = $actorIds; - } - } - if ( !( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) ) { - unset( $relations['target_author_id'], $relations['target_author_ip'] ); - unset( $params['authorIds'], $params['authorIPs'] ); - } - - // Additional fields for which there's no space in the database table schema - $revId = $this->getAssociatedRevId(); - if ( $revId ) { - $params['associated_rev_id'] = $revId; - $relations['associated_rev_id'] = $revId; - } - - $data = [ - 'log_type' => $this->getType(), - 'log_action' => $this->getSubtype(), - 'log_timestamp' => $dbw->timestamp( $this->getTimestamp() ), - 'log_namespace' => $this->getTarget()->getNamespace(), - 'log_title' => $this->getTarget()->getDBkey(), - 'log_page' => $this->getTarget()->getArticleID(), - 'log_params' => LogEntryBase::makeParamBlob( $params ), - ]; - if ( isset( $this->deleted ) ) { - $data['log_deleted'] = $this->deleted; - } - $data += CommentStore::getStore()->insert( $dbw, 'log_comment', $comment ); - $data += ActorMigration::newMigration() - ->getInsertValues( $dbw, 'log_user', $this->getPerformer() ); - - $dbw->insert( 'logging', $data, __METHOD__ ); - $this->id = $dbw->insertId(); - - $rows = []; - foreach ( $relations as $tag => $values ) { - if ( !strlen( $tag ) ) { - throw new MWException( "Got empty log search tag." ); - } - - if ( !is_array( $values ) ) { - $values = [ $values ]; - } - - foreach ( $values as $value ) { - $rows[] = [ - 'ls_field' => $tag, - 'ls_value' => $value, - 'ls_log_id' => $this->id - ]; - } - } - if ( count( $rows ) ) { - $dbw->insert( 'log_search', $rows, __METHOD__, 'IGNORE' ); - } - - return $this->id; - } - - /** - * Get a RecentChanges object for the log entry - * - * @param int $newId - * @return RecentChange - * @since 1.23 - */ - public function getRecentChange( $newId = 0 ) { - $formatter = LogFormatter::newFromEntry( $this ); - $context = RequestContext::newExtraneousContext( $this->getTarget() ); - $formatter->setContext( $context ); - - $logpage = SpecialPage::getTitleFor( 'Log', $this->getType() ); - $user = $this->getPerformer(); - $ip = ""; - if ( $user->isAnon() ) { - // "MediaWiki default" and friends may have - // no IP address in their name - if ( IP::isIPAddress( $user->getName() ) ) { - $ip = $user->getName(); - } - } - - return RecentChange::newLogEntry( - $this->getTimestamp(), - $logpage, - $user, - $formatter->getPlainActionText(), - $ip, - $this->getType(), - $this->getSubtype(), - $this->getTarget(), - $this->getComment(), - LogEntryBase::makeParamBlob( $this->getParameters() ), - $newId, - $formatter->getIRCActionComment(), // Used for IRC feeds - $this->getAssociatedRevId(), // Used for e.g. moves and uploads - $this->getIsPatrollable() - ); - } - - /** - * Publish the log entry. - * - * @param int $newId Id of the log entry. - * @param string $to One of: rcandudp (default), rc, udp - */ - public function publish( $newId, $to = 'rcandudp' ) { - $canAddTags = true; - // FIXME: this code should be removed once all callers properly call publish() - if ( $to === 'udp' && !$newId && !$this->getAssociatedRevId() ) { - \MediaWiki\Logger\LoggerFactory::getInstance( 'logging' )->warning( - 'newId and/or revId must be set when calling ManualLogEntry::publish()', - [ - 'newId' => $newId, - 'to' => $to, - 'revId' => $this->getAssociatedRevId(), - // pass a new exception to register the stack trace - 'exception' => new RuntimeException() - ] - ); - $canAddTags = false; - } - - DeferredUpdates::addCallableUpdate( - function () use ( $newId, $to, $canAddTags ) { - $log = new LogPage( $this->getType() ); - if ( !$log->isRestricted() ) { - Hooks::runWithoutAbort( 'ManualLogEntryBeforePublish', [ $this ] ); - $rc = $this->getRecentChange( $newId ); - - if ( $to === 'rc' || $to === 'rcandudp' ) { - // save RC, passing tags so they are applied there - $rc->addTags( $this->getTags() ); - $rc->save( $rc::SEND_NONE ); - } else { - $tags = $this->getTags(); - if ( $tags && $canAddTags ) { - $revId = $this->getAssociatedRevId(); - ChangeTags::addTags( - $tags, - null, - $revId > 0 ? $revId : null, - $newId > 0 ? $newId : null - ); - } - } - - if ( $to === 'udp' || $to === 'rcandudp' ) { - $rc->notifyRCFeeds(); - } - } - }, - DeferredUpdates::POSTSEND, - wfGetDB( DB_MASTER ) - ); - } - - public function getType() { - return $this->type; - } - - public function getSubtype() { - return $this->subtype; - } - - public function getParameters() { - return $this->parameters; - } - - /** - * @return User - */ - public function getPerformer() { - return $this->performer; - } - - /** - * @return Title - */ - public function getTarget() { - return $this->target; - } - - public function getTimestamp() { - $ts = $this->timestamp ?? wfTimestampNow(); - - return wfTimestamp( TS_MW, $ts ); - } - - public function getComment() { - return $this->comment; - } - - /** - * @since 1.27 - * @return int - */ - public function getAssociatedRevId() { - return $this->revId; - } - - /** - * @since 1.27 - * @return string[] - */ - public function getTags() { - return $this->tags; - } - - /** - * Whether this log entry is patrollable - * - * @since 1.27 - * @return bool - */ - public function getIsPatrollable() { - return $this->isPatrollable; - } - - /** - * @since 1.25 - * @return bool - */ - public function isLegacy() { - return $this->legacy; - } - - public function getDeleted() { - return (int)$this->deleted; - } -} diff --git a/includes/logging/LogEntryBase.php b/includes/logging/LogEntryBase.php new file mode 100644 index 0000000000..170fc2975f --- /dev/null +++ b/includes/logging/LogEntryBase.php @@ -0,0 +1,72 @@ +getType() . '/' . $this->getSubtype(); + } + + public function isDeleted( $field ) { + return ( $this->getDeleted() & $field ) === $field; + } + + /** + * Whether the parameters for this log are stored in new or + * old format. + * + * @return bool + */ + public function isLegacy() { + return false; + } + + /** + * Create a blob from a parameter array + * + * @since 1.26 + * @param array $params + * @return string + */ + public static function makeParamBlob( $params ) { + return serialize( (array)$params ); + } + + /** + * Extract a parameter array from a blob + * + * @since 1.26 + * @param string $blob + * @return array + */ + public static function extractParams( $blob ) { + return unserialize( $blob ); + } +} diff --git a/includes/logging/LogFormatter.php b/includes/logging/LogFormatter.php index b9bb70c321..3e942ae08d 100644 --- a/includes/logging/LogFormatter.php +++ b/includes/logging/LogFormatter.php @@ -1,6 +1,6 @@ $value ]; } } - -/** - * This class formats all log entries for log types - * which have not been converted to the new system. - * This is not about old log entries which store - * parameters in a different format - the new - * LogFormatter classes have code to support formatting - * those too. - * @since 1.19 - */ -class LegacyLogFormatter extends LogFormatter { - /** - * Backward compatibility for extension changing the comment from - * the LogLine hook. This will be set by the first call on getComment(), - * then it might be modified by the hook when calling getActionLinks(), - * so that the modified value will be returned when calling getComment() - * a second time. - * - * @var string|null - */ - private $comment = null; - - /** - * Cache for the result of getActionLinks() so that it does not need to - * run multiple times depending on the order that getComment() and - * getActionLinks() are called. - * - * @var string|null - */ - private $revert = null; - - public function getComment() { - if ( $this->comment === null ) { - $this->comment = parent::getComment(); - } - - // Make sure we execute the LogLine hook so that we immediately return - // the correct value. - if ( $this->revert === null ) { - $this->getActionLinks(); - } - - return $this->comment; - } - - /** - * @return string - * @return-taint onlysafefor_html - */ - protected function getActionMessage() { - $entry = $this->entry; - $action = LogPage::actionText( - $entry->getType(), - $entry->getSubtype(), - $entry->getTarget(), - $this->plaintext ? null : $this->context->getSkin(), - (array)$entry->getParameters(), - !$this->plaintext // whether to filter [[]] links - ); - - $performer = $this->getPerformerElement(); - if ( !$this->irctext ) { - $sep = $this->msg( 'word-separator' ); - $sep = $this->plaintext ? $sep->text() : $sep->escaped(); - $action = $performer . $sep . $action; - } - - return $action; - } - - public function getActionLinks() { - if ( $this->revert !== null ) { - return $this->revert; - } - - if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) ) { - $this->revert = ''; - return $this->revert; - } - - $title = $this->entry->getTarget(); - $type = $this->entry->getType(); - $subtype = $this->entry->getSubtype(); - - // Do nothing. The implementation is handled by the hook modifiying the - // passed-by-ref parameters. This also changes the default value so that - // getComment() and getActionLinks() do not call them indefinitely. - $this->revert = ''; - - // This is to populate the $comment member of this instance so that it - // can be modified when calling the hook just below. - if ( $this->comment === null ) { - $this->getComment(); - } - - $params = $this->entry->getParameters(); - - Hooks::run( 'LogLine', [ $type, $subtype, $title, $params, - &$this->comment, &$this->revert, $this->entry->getTimestamp() ] ); - - return $this->revert; - } -} diff --git a/includes/logging/ManualLogEntry.php b/includes/logging/ManualLogEntry.php new file mode 100644 index 0000000000..90c0a72292 --- /dev/null +++ b/includes/logging/ManualLogEntry.php @@ -0,0 +1,515 @@ +type = $type; + $this->subtype = $subtype; + } + + /** + * Set extra log parameters. + * + * You can pass params to the log action message by prefixing the keys with + * a number and optional type, using colons to separate the fields. The + * numbering should start with number 4, the first three parameters are + * hardcoded for every message. + * + * If you want to store stuff that should not be available in messages, don't + * prefix the array key with a number and don't use the colons. + * + * Example: + * $entry->setParameters( + * '4::color' => 'blue', + * '5:number:count' => 3000, + * 'animal' => 'dog' + * ); + * + * @since 1.19 + * @param array $parameters Associative array + */ + public function setParameters( $parameters ) { + $this->parameters = $parameters; + } + + /** + * Declare arbitrary tag/value relations to this log entry. + * These can be used to filter log entries later on. + * + * @param array $relations Map of (tag => (list of values|value)) + * @since 1.22 + */ + public function setRelations( array $relations ) { + $this->relations = $relations; + } + + /** + * Set the user that performed the action being logged. + * + * @since 1.19 + * @param UserIdentity $performer + */ + public function setPerformer( UserIdentity $performer ) { + $this->performer = User::newFromIdentity( $performer ); + } + + /** + * Set the title of the object changed. + * + * @since 1.19 + * @param LinkTarget $target + */ + public function setTarget( LinkTarget $target ) { + $this->target = Title::newFromLinkTarget( $target ); + } + + /** + * Set the timestamp of when the logged action took place. + * + * @since 1.19 + * @param string $timestamp + */ + public function setTimestamp( $timestamp ) { + $this->timestamp = $timestamp; + } + + /** + * Set a comment associated with the action being logged. + * + * @since 1.19 + * @param string $comment + */ + public function setComment( $comment ) { + $this->comment = $comment; + } + + /** + * Set an associated revision id. + * + * For example, the ID of the revision that was inserted to mark a page move + * or protection, file upload, etc. + * + * @since 1.27 + * @param int $revId + */ + public function setAssociatedRevId( $revId ) { + $this->revId = $revId; + } + + /** + * Set change tags for the log entry. + * + * Passing `null` means the same as empty array, + * for compatibility with WikiPage::doUpdateRestrictions(). + * + * @since 1.27 + * @param string|string[]|null $tags + * @deprecated since 1.33 Please use addTags() instead + */ + public function setTags( $tags ) { + if ( $this->tags ) { + wfDebug( 'Overwriting existing ManualLogEntry tags' ); + } + $this->tags = []; + if ( $tags !== null ) { + $this->addTags( $tags ); + } + } + + /** + * Add change tags for the log entry + * + * @since 1.33 + * @param string|string[] $tags Tags to apply + */ + public function addTags( $tags ) { + if ( is_string( $tags ) ) { + $tags = [ $tags ]; + } + Assert::parameterElementType( 'string', $tags, 'tags' ); + $this->tags = array_unique( array_merge( $this->tags, $tags ) ); + } + + /** + * Set whether this log entry should be made patrollable + * This shouldn't depend on config, only on whether there is full support + * in the software for patrolling this log entry. + * False by default + * + * @since 1.27 + * @param bool $patrollable + */ + public function setIsPatrollable( $patrollable ) { + $this->isPatrollable = (bool)$patrollable; + } + + /** + * Set the 'legacy' flag + * + * @since 1.25 + * @param bool $legacy + */ + public function setLegacy( $legacy ) { + $this->legacy = $legacy; + } + + /** + * Set the 'deleted' flag. + * + * @since 1.19 + * @param int $deleted One of LogPage::DELETED_* bitfield constants + */ + public function setDeleted( $deleted ) { + $this->deleted = $deleted; + } + + /** + * Insert the entry into the `logging` table. + * + * @param IDatabase|null $dbw + * @return int ID of the log entry + * @throws MWException + */ + public function insert( IDatabase $dbw = null ) { + global $wgActorTableSchemaMigrationStage; + + $dbw = $dbw ?: wfGetDB( DB_MASTER ); + + if ( $this->timestamp === null ) { + $this->timestamp = wfTimestampNow(); + } + + // Trim spaces on user supplied text + $comment = trim( $this->getComment() ); + + $params = $this->getParameters(); + $relations = $this->relations; + + // Ensure actor relations are set + if ( ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) && + empty( $relations['target_author_actor'] ) + ) { + $actorIds = []; + if ( !empty( $relations['target_author_id'] ) ) { + foreach ( $relations['target_author_id'] as $id ) { + $actorIds[] = User::newFromId( $id )->getActorId( $dbw ); + } + } + if ( !empty( $relations['target_author_ip'] ) ) { + foreach ( $relations['target_author_ip'] as $ip ) { + $actorIds[] = User::newFromName( $ip, false )->getActorId( $dbw ); + } + } + if ( $actorIds ) { + $relations['target_author_actor'] = $actorIds; + $params['authorActors'] = $actorIds; + } + } + if ( !( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) ) { + unset( $relations['target_author_id'], $relations['target_author_ip'] ); + unset( $params['authorIds'], $params['authorIPs'] ); + } + + // Additional fields for which there's no space in the database table schema + $revId = $this->getAssociatedRevId(); + if ( $revId ) { + $params['associated_rev_id'] = $revId; + $relations['associated_rev_id'] = $revId; + } + + $data = [ + 'log_type' => $this->getType(), + 'log_action' => $this->getSubtype(), + 'log_timestamp' => $dbw->timestamp( $this->getTimestamp() ), + 'log_namespace' => $this->getTarget()->getNamespace(), + 'log_title' => $this->getTarget()->getDBkey(), + 'log_page' => $this->getTarget()->getArticleID(), + 'log_params' => LogEntryBase::makeParamBlob( $params ), + ]; + if ( isset( $this->deleted ) ) { + $data['log_deleted'] = $this->deleted; + } + $data += CommentStore::getStore()->insert( $dbw, 'log_comment', $comment ); + $data += ActorMigration::newMigration() + ->getInsertValues( $dbw, 'log_user', $this->getPerformer() ); + + $dbw->insert( 'logging', $data, __METHOD__ ); + $this->id = $dbw->insertId(); + + $rows = []; + foreach ( $relations as $tag => $values ) { + if ( !strlen( $tag ) ) { + throw new MWException( "Got empty log search tag." ); + } + + if ( !is_array( $values ) ) { + $values = [ $values ]; + } + + foreach ( $values as $value ) { + $rows[] = [ + 'ls_field' => $tag, + 'ls_value' => $value, + 'ls_log_id' => $this->id + ]; + } + } + if ( count( $rows ) ) { + $dbw->insert( 'log_search', $rows, __METHOD__, 'IGNORE' ); + } + + return $this->id; + } + + /** + * Get a RecentChanges object for the log entry + * + * @param int $newId + * @return RecentChange + * @since 1.23 + */ + public function getRecentChange( $newId = 0 ) { + $formatter = LogFormatter::newFromEntry( $this ); + $context = RequestContext::newExtraneousContext( $this->getTarget() ); + $formatter->setContext( $context ); + + $logpage = SpecialPage::getTitleFor( 'Log', $this->getType() ); + $user = $this->getPerformer(); + $ip = ""; + if ( $user->isAnon() ) { + // "MediaWiki default" and friends may have + // no IP address in their name + if ( IP::isIPAddress( $user->getName() ) ) { + $ip = $user->getName(); + } + } + + return RecentChange::newLogEntry( + $this->getTimestamp(), + $logpage, + $user, + $formatter->getPlainActionText(), + $ip, + $this->getType(), + $this->getSubtype(), + $this->getTarget(), + $this->getComment(), + LogEntryBase::makeParamBlob( $this->getParameters() ), + $newId, + $formatter->getIRCActionComment(), // Used for IRC feeds + $this->getAssociatedRevId(), // Used for e.g. moves and uploads + $this->getIsPatrollable() + ); + } + + /** + * Publish the log entry. + * + * @param int $newId Id of the log entry. + * @param string $to One of: rcandudp (default), rc, udp + */ + public function publish( $newId, $to = 'rcandudp' ) { + $canAddTags = true; + // FIXME: this code should be removed once all callers properly call publish() + if ( $to === 'udp' && !$newId && !$this->getAssociatedRevId() ) { + \MediaWiki\Logger\LoggerFactory::getInstance( 'logging' )->warning( + 'newId and/or revId must be set when calling ManualLogEntry::publish()', + [ + 'newId' => $newId, + 'to' => $to, + 'revId' => $this->getAssociatedRevId(), + // pass a new exception to register the stack trace + 'exception' => new RuntimeException() + ] + ); + $canAddTags = false; + } + + DeferredUpdates::addCallableUpdate( + function () use ( $newId, $to, $canAddTags ) { + $log = new LogPage( $this->getType() ); + if ( !$log->isRestricted() ) { + Hooks::runWithoutAbort( 'ManualLogEntryBeforePublish', [ $this ] ); + $rc = $this->getRecentChange( $newId ); + + if ( $to === 'rc' || $to === 'rcandudp' ) { + // save RC, passing tags so they are applied there + $rc->addTags( $this->getTags() ); + $rc->save( $rc::SEND_NONE ); + } else { + $tags = $this->getTags(); + if ( $tags && $canAddTags ) { + $revId = $this->getAssociatedRevId(); + ChangeTags::addTags( + $tags, + null, + $revId > 0 ? $revId : null, + $newId > 0 ? $newId : null + ); + } + } + + if ( $to === 'udp' || $to === 'rcandudp' ) { + $rc->notifyRCFeeds(); + } + } + }, + DeferredUpdates::POSTSEND, + wfGetDB( DB_MASTER ) + ); + } + + public function getType() { + return $this->type; + } + + public function getSubtype() { + return $this->subtype; + } + + public function getParameters() { + return $this->parameters; + } + + /** + * @return User + */ + public function getPerformer() { + return $this->performer; + } + + /** + * @return Title + */ + public function getTarget() { + return $this->target; + } + + public function getTimestamp() { + $ts = $this->timestamp ?? wfTimestampNow(); + + return wfTimestamp( TS_MW, $ts ); + } + + public function getComment() { + return $this->comment; + } + + /** + * @since 1.27 + * @return int + */ + public function getAssociatedRevId() { + return $this->revId; + } + + /** + * @since 1.27 + * @return string[] + */ + public function getTags() { + return $this->tags; + } + + /** + * Whether this log entry is patrollable + * + * @since 1.27 + * @return bool + */ + public function getIsPatrollable() { + return $this->isPatrollable; + } + + /** + * @since 1.25 + * @return bool + */ + public function isLegacy() { + return $this->legacy; + } + + public function getDeleted() { + return (int)$this->deleted; + } +} diff --git a/includes/logging/RCDatabaseLogEntry.php b/includes/logging/RCDatabaseLogEntry.php new file mode 100644 index 0000000000..4dc40373ff --- /dev/null +++ b/includes/logging/RCDatabaseLogEntry.php @@ -0,0 +1,89 @@ +row->rc_logid; + } + + protected function getRawParameters() { + return $this->row->rc_params; + } + + public function getAssociatedRevId() { + return $this->row->rc_this_oldid; + } + + public function getType() { + return $this->row->rc_log_type; + } + + public function getSubtype() { + return $this->row->rc_log_action; + } + + public function getPerformer() { + if ( !$this->performer ) { + $actorId = isset( $this->row->rc_actor ) ? (int)$this->row->rc_actor : 0; + $userId = (int)$this->row->rc_user; + if ( $actorId !== 0 ) { + $this->performer = User::newFromActorId( $actorId ); + } elseif ( $userId !== 0 ) { + $this->performer = User::newFromId( $userId ); + } else { + $userText = $this->row->rc_user_text; + // Might be an IP, don't validate the username + $this->performer = User::newFromName( $userText, false ); + } + } + + return $this->performer; + } + + public function getTarget() { + $namespace = $this->row->rc_namespace; + $page = $this->row->rc_title; + return Title::makeTitle( $namespace, $page ); + } + + public function getTimestamp() { + return wfTimestamp( TS_MW, $this->row->rc_timestamp ); + } + + public function getComment() { + return CommentStore::getStore() + // Legacy because the row may have used RecentChange::selectFields() + ->getCommentLegacy( wfGetDB( DB_REPLICA ), 'rc_comment', $this->row )->text; + } + + public function getDeleted() { + return $this->row->rc_deleted; + } +}