diff options
author | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
---|---|---|
committer | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
commit | fc7369835258467bf97eb64f184b93691f9a9fd5 (patch) | |
tree | daabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/includes/logging |
first commit
Diffstat (limited to 'www/wiki/includes/logging')
20 files changed, 5327 insertions, 0 deletions
diff --git a/www/wiki/includes/logging/BlockLogFormatter.php b/www/wiki/includes/logging/BlockLogFormatter.php new file mode 100644 index 00000000..a5af0269 --- /dev/null +++ b/www/wiki/includes/logging/BlockLogFormatter.php @@ -0,0 +1,232 @@ +<?php +/** + * Formatter for block log entries. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later + * @since 1.25 + */ + +/** + * This class formats block log entries. + * + * @since 1.25 + */ +class BlockLogFormatter extends LogFormatter { + protected function getMessageParameters() { + $params = parent::getMessageParameters(); + + $title = $this->entry->getTarget(); + if ( substr( $title->getText(), 0, 1 ) === '#' ) { + // autoblock - no user link possible + $params[2] = $title->getText(); + $params[3] = ''; // no user name for gender use + } else { + // Create a user link for the blocked + $username = $title->getText(); + // @todo Store the user identifier in the parameters + // to make this faster for future log entries + $targetUser = User::newFromName( $username, false ); + $params[2] = Message::rawParam( $this->makeUserLink( $targetUser, Linker::TOOL_LINKS_NOBLOCK ) ); + $params[3] = $username; // plain user name for gender use + } + + $subtype = $this->entry->getSubtype(); + if ( $subtype === 'block' || $subtype === 'reblock' ) { + if ( !isset( $params[4] ) ) { + // Very old log entry without duration: means infinite + $params[4] = 'infinite'; + } + // Localize the duration, and add a tooltip + // in English to help visitors from other wikis. + // The lrm is needed to make sure that the number + // is shown on the correct side of the tooltip text. + $durationTooltip = '‎' . htmlspecialchars( $params[4] ); + $params[4] = Message::rawParam( + "<span class=\"blockExpiry\" title=\"$durationTooltip\">" . + $this->context->getLanguage()->translateBlockExpiry( + $params[4], + $this->context->getUser(), + wfTimestamp( TS_UNIX, $this->entry->getTimestamp() ) + ) . + '</span>' + ); + $params[5] = isset( $params[5] ) ? + self::formatBlockFlags( $params[5], $this->context->getLanguage() ) : ''; + } + + return $params; + } + + protected function extractParameters() { + $params = parent::extractParameters(); + // Legacy log params returning the params in index 3 and 4, moved to 4 and 5 + if ( $this->entry->isLegacy() && isset( $params[3] ) ) { + if ( isset( $params[4] ) ) { + $params[5] = $params[4]; + } + $params[4] = $params[3]; + $params[3] = ''; + } + return $params; + } + + public function getPreloadTitles() { + $title = $this->entry->getTarget(); + // Preload user page for non-autoblocks + if ( substr( $title->getText(), 0, 1 ) !== '#' ) { + return [ $title->getTalkPage() ]; + } + return []; + } + + public function getActionLinks() { + $subtype = $this->entry->getSubtype(); + $linkRenderer = $this->getLinkRenderer(); + if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) // Action is hidden + || !( $subtype === 'block' || $subtype === 'reblock' ) + || !$this->context->getUser()->isAllowed( 'block' ) + ) { + return ''; + } + + // Show unblock/change block link + $title = $this->entry->getTarget(); + $links = [ + $linkRenderer->makeKnownLink( + SpecialPage::getTitleFor( 'Unblock', $title->getDBkey() ), + $this->msg( 'unblocklink' )->text() + ), + $linkRenderer->makeKnownLink( + SpecialPage::getTitleFor( 'Block', $title->getDBkey() ), + $this->msg( 'change-blocklink' )->text() + ) + ]; + + return $this->msg( 'parentheses' )->rawParams( + $this->context->getLanguage()->pipeList( $links ) )->escaped(); + } + + /** + * Convert a comma-delimited list of block log flags + * into a more readable (and translated) form + * + * @param string $flags Flags to format + * @param Language $lang + * @return string + */ + public static function formatBlockFlags( $flags, $lang ) { + $flags = trim( $flags ); + if ( $flags === '' ) { + return ''; // nothing to do + } + $flags = explode( ',', $flags ); + $flagsCount = count( $flags ); + + for ( $i = 0; $i < $flagsCount; $i++ ) { + $flags[$i] = self::formatBlockFlag( $flags[$i], $lang ); + } + + return wfMessage( 'parentheses' )->inLanguage( $lang ) + ->rawParams( $lang->commaList( $flags ) )->escaped(); + } + + /** + * Translate a block log flag if possible + * + * @param int $flag Flag to translate + * @param Language $lang Language object to use + * @return string + */ + public static function formatBlockFlag( $flag, $lang ) { + static $messages = []; + + if ( !isset( $messages[$flag] ) ) { + $messages[$flag] = htmlspecialchars( $flag ); // Fallback + + // For grepping. The following core messages can be used here: + // * block-log-flags-angry-autoblock + // * block-log-flags-anononly + // * block-log-flags-hiddenname + // * block-log-flags-noautoblock + // * block-log-flags-nocreate + // * block-log-flags-noemail + // * block-log-flags-nousertalk + $msg = wfMessage( 'block-log-flags-' . $flag )->inLanguage( $lang ); + + if ( $msg->exists() ) { + $messages[$flag] = $msg->escaped(); + } + } + + return $messages[$flag]; + } + + protected function getParametersForApi() { + $entry = $this->entry; + $params = $entry->getParameters(); + + static $map = [ + // While this looks wrong to be starting at 5 rather than 4, it's + // because getMessageParameters uses $4 for its own purposes. + '5::duration', + '6:array:flags', + '6::flags' => '6:array:flags', + ]; + foreach ( $map as $index => $key ) { + if ( isset( $params[$index] ) ) { + $params[$key] = $params[$index]; + unset( $params[$index] ); + } + } + + $subtype = $entry->getSubtype(); + if ( $subtype === 'block' || $subtype === 'reblock' ) { + // Defaults for old log entries missing some fields + $params += [ + '5::duration' => 'infinite', + '6:array:flags' => [], + ]; + + if ( !is_array( $params['6:array:flags'] ) ) { + $params['6:array:flags'] = $params['6:array:flags'] === '' + ? [] + : explode( ',', $params['6:array:flags'] ); + } + + if ( !wfIsInfinity( $params['5::duration'] ) ) { + $ts = wfTimestamp( TS_UNIX, $entry->getTimestamp() ); + $expiry = strtotime( $params['5::duration'], $ts ); + if ( $expiry !== false && $expiry > 0 ) { + $params[':timestamp:expiry'] = $expiry; + } + } + } + + return $params; + } + + public function formatParametersForApi() { + $ret = parent::formatParametersForApi(); + if ( isset( $ret['flags'] ) ) { + ApiResult::setIndexedTagName( $ret['flags'], 'f' ); + } + return $ret; + } + +} diff --git a/www/wiki/includes/logging/ContentModelLogFormatter.php b/www/wiki/includes/logging/ContentModelLogFormatter.php new file mode 100644 index 00000000..e05357cd --- /dev/null +++ b/www/wiki/includes/logging/ContentModelLogFormatter.php @@ -0,0 +1,34 @@ +<?php + +class ContentModelLogFormatter extends LogFormatter { + protected function getMessageParameters() { + $lang = $this->context->getLanguage(); + $params = parent::getMessageParameters(); + $params[3] = ContentHandler::getLocalizedName( $params[3], $lang ); + $params[4] = ContentHandler::getLocalizedName( $params[4], $lang ); + return $params; + } + + public function getActionLinks() { + if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) // Action is hidden + || $this->entry->getSubtype() !== 'change' + || !$this->context->getUser()->isAllowed( 'editcontentmodel' ) + ) { + return ''; + } + + $params = $this->extractParameters(); + $revert = $this->getLinkRenderer()->makeKnownLink( + SpecialPage::getTitleFor( 'ChangeContentModel' ), + $this->msg( 'logentry-contentmodel-change-revertlink' )->text(), + [], + [ + 'pagetitle' => $this->entry->getTarget()->getPrefixedText(), + 'model' => $params[3], + 'reason' => $this->msg( 'logentry-contentmodel-change-revert' )->inContentLanguage()->text(), + ] + ); + + return $this->msg( 'parentheses' )->rawParams( $revert )->escaped(); + } +} diff --git a/www/wiki/includes/logging/DeleteLogFormatter.php b/www/wiki/includes/logging/DeleteLogFormatter.php new file mode 100644 index 00000000..ef006345 --- /dev/null +++ b/www/wiki/includes/logging/DeleteLogFormatter.php @@ -0,0 +1,313 @@ +<?php +/** + * Formatter for delete log entries. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @author Niklas Laxström + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later + * @since 1.22 + */ + +/** + * This class formats delete log entries. + * + * @since 1.19 + */ +class DeleteLogFormatter extends LogFormatter { + protected function getMessageKey() { + $key = parent::getMessageKey(); + if ( in_array( $this->entry->getSubtype(), [ 'event', 'revision' ] ) ) { + if ( count( $this->getMessageParameters() ) < 5 ) { + // Messages: logentry-delete-event-legacy, logentry-delete-revision-legacy, + // logentry-suppress-event-legacy, logentry-suppress-revision-legacy + return "$key-legacy"; + } + } elseif ( $this->entry->getSubtype() === 'restore' ) { + $rawParams = $this->entry->getParameters(); + if ( !isset( $rawParams[':assoc:count'] ) ) { + // Message: logentry-delete-restore-nocount + return $key . '-nocount'; + } + } + + return $key; + } + + protected function getMessageParameters() { + if ( isset( $this->parsedParametersDeleteLog ) ) { + return $this->parsedParametersDeleteLog; + } + + $params = parent::getMessageParameters(); + $subtype = $this->entry->getSubtype(); + if ( in_array( $subtype, [ 'event', 'revision' ] ) ) { + // $params[3] here is 'revision' or 'archive' for page revisions, 'oldimage' or + // 'filearchive' for file versions, or a comma-separated list of log_ids for log + // entries. $subtype here is 'revision' for page revisions and file + // versions, or 'event' for log entries. + if ( + ( $subtype === 'event' && count( $params ) === 6 ) + || ( + $subtype === 'revision' && isset( $params[3] ) + && in_array( $params[3], [ 'revision', 'archive', 'oldimage', 'filearchive' ] ) + ) + ) { + // See RevDelList::getLogParams()/RevDelLogList::getLogParams() + $paramStart = $subtype === 'revision' ? 4 : 3; + + $old = $this->parseBitField( $params[$paramStart + 1] ); + $new = $this->parseBitField( $params[$paramStart + 2] ); + list( $hid, $unhid, $extra ) = RevisionDeleter::getChanges( $new, $old ); + $changes = []; + // messages used: revdelete-content-hid, revdelete-summary-hid, revdelete-uname-hid + foreach ( $hid as $v ) { + $changes[] = $this->msg( "$v-hid" )->plain(); + } + // messages used: revdelete-content-unhid, revdelete-summary-unhid, + // revdelete-uname-unhid + foreach ( $unhid as $v ) { + $changes[] = $this->msg( "$v-unhid" )->plain(); + } + foreach ( $extra as $v ) { + $changes[] = $this->msg( $v )->plain(); + } + $changeText = $this->context->getLanguage()->listToText( $changes ); + + $newParams = array_slice( $params, 0, 3 ); + $newParams[3] = $changeText; + $ids = is_array( $params[$paramStart] ) + ? $params[$paramStart] + : explode( ',', $params[$paramStart] ); + $newParams[4] = $this->context->getLanguage()->formatNum( count( $ids ) ); + + $this->parsedParametersDeleteLog = $newParams; + return $this->parsedParametersDeleteLog; + } else { + $this->parsedParametersDeleteLog = array_slice( $params, 0, 3 ); + return $this->parsedParametersDeleteLog; + } + } elseif ( $subtype === 'restore' ) { + $rawParams = $this->entry->getParameters(); + if ( isset( $rawParams[':assoc:count'] ) ) { + $countList = []; + foreach ( $rawParams[':assoc:count'] as $type => $count ) { + if ( $count ) { + // Messages: restore-count-revisions, restore-count-files + $countList[] = $this->context->msg( 'restore-count-' . $type ) + ->numParams( $count )->plain(); + } + } + $params[3] = $this->context->getLanguage()->listToText( $countList ); + } + } + + $this->parsedParametersDeleteLog = $params; + return $this->parsedParametersDeleteLog; + } + + protected function parseBitField( $string ) { + // Input is like ofield=2134 or just the number + if ( strpos( $string, 'field=' ) === 1 ) { + list( , $field ) = explode( '=', $string ); + + return (int)$field; + } else { + return (int)$string; + } + } + + public function getActionLinks() { + $user = $this->context->getUser(); + $linkRenderer = $this->getLinkRenderer(); + if ( !$user->isAllowed( 'deletedhistory' ) + || $this->entry->isDeleted( LogPage::DELETED_ACTION ) + ) { + return ''; + } + + switch ( $this->entry->getSubtype() ) { + case 'delete': // Show undelete link + case 'delete_redir': + if ( $user->isAllowed( 'undelete' ) ) { + $message = 'undeletelink'; + } else { + $message = 'undeleteviewlink'; + } + $revert = $linkRenderer->makeKnownLink( + SpecialPage::getTitleFor( 'Undelete' ), + $this->msg( $message )->text(), + [], + [ 'target' => $this->entry->getTarget()->getPrefixedDBkey() ] + ); + + return $this->msg( 'parentheses' )->rawParams( $revert )->escaped(); + + case 'revision': // If an edit was hidden from a page give a review link to the history + $params = $this->extractParameters(); + if ( !isset( $params[3] ) || !isset( $params[4] ) ) { + return ''; + } + + // Different revision types use different URL params... + $key = $params[3]; + // This is a array or CSV of the IDs + $ids = is_array( $params[4] ) + ? $params[4] + : explode( ',', $params[4] ); + + $links = []; + + // If there's only one item, we can show a diff link + if ( count( $ids ) == 1 ) { + // Live revision diffs... + if ( $key == 'oldid' || $key == 'revision' ) { + $links[] = $linkRenderer->makeKnownLink( + $this->entry->getTarget(), + $this->msg( 'diff' )->text(), + [], + [ + 'diff' => intval( $ids[0] ), + 'unhide' => 1 + ] + ); + // Deleted revision diffs... + } elseif ( $key == 'artimestamp' || $key == 'archive' ) { + $links[] = $linkRenderer->makeKnownLink( + SpecialPage::getTitleFor( 'Undelete' ), + $this->msg( 'diff' )->text(), + [], + [ + 'target' => $this->entry->getTarget()->getPrefixedDBkey(), + 'diff' => 'prev', + 'timestamp' => $ids[0] + ] + ); + } + } + + // View/modify link... + $links[] = $linkRenderer->makeKnownLink( + SpecialPage::getTitleFor( 'Revisiondelete' ), + $this->msg( 'revdel-restore' )->text(), + [], + [ + 'target' => $this->entry->getTarget()->getPrefixedText(), + 'type' => $key, + 'ids' => implode( ',', $ids ), + ] + ); + + return $this->msg( 'parentheses' )->rawParams( + $this->context->getLanguage()->pipeList( $links ) )->escaped(); + + case 'event': // Hidden log items, give review link + $params = $this->extractParameters(); + if ( !isset( $params[3] ) ) { + return ''; + } + // This is a CSV of the IDs + $query = $params[3]; + if ( is_array( $query ) ) { + $query = implode( ',', $query ); + } + // Link to each hidden object ID, $params[1] is the url param + $revert = $linkRenderer->makeKnownLink( + SpecialPage::getTitleFor( 'Revisiondelete' ), + $this->msg( 'revdel-restore' )->text(), + [], + [ + 'target' => $this->entry->getTarget()->getPrefixedText(), + 'type' => 'logging', + 'ids' => $query + ] + ); + + return $this->msg( 'parentheses' )->rawParams( $revert )->escaped(); + default: + return ''; + } + } + + protected function getParametersForApi() { + $entry = $this->entry; + $params = []; + + $subtype = $this->entry->getSubtype(); + if ( in_array( $subtype, [ 'event', 'revision' ] ) ) { + $rawParams = $entry->getParameters(); + if ( $subtype === 'event' ) { + array_unshift( $rawParams, 'logging' ); + } + + static $map = [ + '4::type', + '5::ids', + '6::ofield', + '7::nfield', + '4::ids' => '5::ids', + '5::ofield' => '6::ofield', + '6::nfield' => '7::nfield', + ]; + foreach ( $map as $index => $key ) { + if ( isset( $rawParams[$index] ) ) { + $rawParams[$key] = $rawParams[$index]; + unset( $rawParams[$index] ); + } + } + + $old = $this->parseBitField( $rawParams['6::ofield'] ); + $new = $this->parseBitField( $rawParams['7::nfield'] ); + if ( !is_array( $rawParams['5::ids'] ) ) { + $rawParams['5::ids'] = explode( ',', $rawParams['5::ids'] ); + } + + $params = [ + '::type' => $rawParams['4::type'], + ':array:ids' => $rawParams['5::ids'], + ':assoc:old' => [ 'bitmask' => $old ], + ':assoc:new' => [ 'bitmask' => $new ], + ]; + + static $fields = [ + Revision::DELETED_TEXT => 'content', + Revision::DELETED_COMMENT => 'comment', + Revision::DELETED_USER => 'user', + Revision::DELETED_RESTRICTED => 'restricted', + ]; + foreach ( $fields as $bit => $key ) { + $params[':assoc:old'][$key] = (bool)( $old & $bit ); + $params[':assoc:new'][$key] = (bool)( $new & $bit ); + } + } elseif ( $subtype === 'restore' ) { + $rawParams = $entry->getParameters(); + if ( isset( $rawParams[':assoc:count'] ) ) { + $params[':assoc:count'] = $rawParams[':assoc:count']; + } + } + + return $params; + } + + public function formatParametersForApi() { + $ret = parent::formatParametersForApi(); + if ( isset( $ret['ids'] ) ) { + ApiResult::setIndexedTagName( $ret['ids'], 'id' ); + } + return $ret; + } +} diff --git a/www/wiki/includes/logging/ImportLogFormatter.php b/www/wiki/includes/logging/ImportLogFormatter.php new file mode 100644 index 00000000..a2a899b0 --- /dev/null +++ b/www/wiki/includes/logging/ImportLogFormatter.php @@ -0,0 +1,42 @@ +<?php +/** + * Formatter for import log entries. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later + * @since 1.27 + */ + +/** + * This class formats import log entries. + * + * @since 1.27 + */ +class ImportLogFormatter extends LogFormatter { + protected function getMessageKey() { + $key = parent::getMessageKey(); + $params = $this->extractParameters(); + if ( isset( $params[3] ) ) { + // New log items with more details + // Messages: logentry-import-upload-details, logentry-import-interwiki-details + $key .= '-details'; + } + + return $key; + } +} diff --git a/www/wiki/includes/logging/LogEntry.php b/www/wiki/includes/logging/LogEntry.php new file mode 100644 index 00000000..e17ac032 --- /dev/null +++ b/www/wiki/includes/logging/LogEntry.php @@ -0,0 +1,865 @@ +<?php +/** + * Contain classes for dealing with individual log entries + * + * This is how I see the log system history: + * - appending to plain wiki pages + * - formatting log entries based on database fields + * - user is now part of the action message + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @author Niklas Laxström + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later + * @since 1.19 + */ + +use Wikimedia\Rdbms\IDatabase; + +/** + * Interface for log entries. Every log entry has these methods. + * + * @since 1.19 + */ +interface LogEntry { + + /** + * The main log type. + * + * @return string + */ + public function getType(); + + /** + * The log subtype. + * + * @return string + */ + public function getSubtype(); + + /** + * The full logtype in format maintype/subtype. + * + * @return string + */ + public function getFullType(); + + /** + * Get the extra parameters stored for this message. + * + * @return array + */ + public function getParameters(); + + /** + * Get the user for performed this action. + * + * @return User + */ + public function getPerformer(); + + /** + * Get the target page of this action. + * + * @return Title + */ + public function getTarget(); + + /** + * Get the timestamp when the action was executed. + * + * @return string + */ + public function getTimestamp(); + + /** + * Get the user provided comment. + * + * @return string + */ + public function getComment(); + + /** + * Get the access restriction. + * + * @return string + */ + public function getDeleted(); + + /** + * @param int $field One of LogPage::DELETED_* bitfield constants + * @return bool + */ + public function isDeleted( $field ); +} + +/** + * Extends the LogEntryInterface with some basic functionality + * + * @since 1.19 + */ +abstract class LogEntryBase implements LogEntry { + + public function getFullType() { + return $this->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 ); + } +} + +/** + * This class wraps around database result row. + * + * @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; + } +} + +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 log entries manually, to inject them into the database. + * + * @since 1.19 + */ +class ManualLogEntry extends LogEntryBase { + /** @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 array Change tags add to the log entry */ + protected $tags = null; + + /** @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 User $performer + */ + public function setPerformer( User $performer ) { + $this->performer = $performer; + } + + /** + * Set the title of the object changed. + * + * @since 1.19 + * @param Title $target + */ + public function setTarget( Title $target ) { + $this->target = $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. + * + * @since 1.27 + * @param string|string[] $tags + */ + public function setTags( $tags ) { + if ( is_string( $tags ) ) { + $tags = [ $tags ]; + } + $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 $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 >= MIGRATION_WRITE_BOTH && + 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 >= MIGRATION_WRITE_NEW ) { + 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' ) { + DeferredUpdates::addCallableUpdate( + function () use ( $newId, $to ) { + $log = new LogPage( $this->getType() ); + if ( !$log->isRestricted() ) { + $rc = $this->getRecentChange( $newId ); + + if ( $to === 'rc' || $to === 'rcandudp' ) { + // save RC, passing tags so they are applied there + $tags = $this->getTags(); + if ( is_null( $tags ) ) { + $tags = []; + } + $rc->addTags( $tags ); + $rc->save( 'pleasedontudp' ); + } + + 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 !== null ? $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 array + */ + 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/www/wiki/includes/logging/LogEventsList.php b/www/wiki/includes/logging/LogEventsList.php new file mode 100644 index 00000000..d97ddfdd --- /dev/null +++ b/www/wiki/includes/logging/LogEventsList.php @@ -0,0 +1,797 @@ +<?php +/** + * Contain classes to list log entries + * + * Copyright © 2004 Brion Vibber <brion@pobox.com> + * https://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +use MediaWiki\Linker\LinkRenderer; +use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\IDatabase; + +class LogEventsList extends ContextSource { + const NO_ACTION_LINK = 1; + const NO_EXTRA_USER_LINKS = 2; + const USE_CHECKBOXES = 4; + + public $flags; + + /** + * @var array + */ + protected $mDefaultQuery; + + /** + * @var bool + */ + protected $showTagEditUI; + + /** + * @var array + */ + protected $allowedActions = null; + + /** + * @var LinkRenderer|null + */ + private $linkRenderer; + + /** + * The first two parameters used to be $skin and $out, but now only a context + * is needed, that's why there's a second unused parameter. + * + * @param IContextSource|Skin $context Context to use; formerly it was + * a Skin object. Use of Skin is deprecated. + * @param LinkRenderer|null $linkRenderer previously unused + * @param int $flags Can be a combination of self::NO_ACTION_LINK, + * self::NO_EXTRA_USER_LINKS or self::USE_CHECKBOXES. + */ + public function __construct( $context, $linkRenderer = null, $flags = 0 ) { + if ( $context instanceof IContextSource ) { + $this->setContext( $context ); + } else { + // Old parameters, $context should be a Skin object + $this->setContext( $context->getContext() ); + } + + $this->flags = $flags; + $this->showTagEditUI = ChangeTags::showTagEditingUI( $this->getUser() ); + if ( $linkRenderer instanceof LinkRenderer ) { + $this->linkRenderer = $linkRenderer; + } + } + + /** + * @since 1.30 + * @return LinkRenderer + */ + protected function getLinkRenderer() { + if ( $this->linkRenderer !== null ) { + return $this->linkRenderer; + } else { + return MediaWikiServices::getInstance()->getLinkRenderer(); + } + } + + /** + * Show options for the log list + * + * @param array|string $types + * @param string $user + * @param string $page + * @param string $pattern + * @param int|string $year Use 0 to start with no year preselected. + * @param int|string $month A month in the 1..12 range. Use 0 to start with no month + * preselected. + * @param array $filter + * @param string $tagFilter Tag to select by default + * @param string $action + */ + public function showOptions( $types = [], $user = '', $page = '', $pattern = '', $year = 0, + $month = 0, $filter = null, $tagFilter = '', $action = null + ) { + global $wgScript, $wgMiserMode; + + $title = SpecialPage::getTitleFor( 'Log' ); + + // For B/C, we take strings, but make sure they are converted... + $types = ( $types === '' ) ? [] : (array)$types; + + $tagSelector = ChangeTags::buildTagFilterSelector( $tagFilter, false, $this->getContext() ); + + $html = Html::hidden( 'title', $title->getPrefixedDBkey() ); + + // Basic selectors + $html .= $this->getTypeMenu( $types ) . "\n"; + $html .= $this->getUserInput( $user ) . "\n"; + $html .= $this->getTitleInput( $page ) . "\n"; + $html .= $this->getExtraInputs( $types ) . "\n"; + + // Title pattern, if allowed + if ( !$wgMiserMode ) { + $html .= $this->getTitlePattern( $pattern ) . "\n"; + } + + // date menu + $html .= Xml::tags( 'p', null, Xml::dateMenu( (int)$year, (int)$month ) ); + + // Tag filter + if ( $tagSelector ) { + $html .= Xml::tags( 'p', null, implode( ' ', $tagSelector ) ); + } + + // Filter links + if ( $filter ) { + $html .= Xml::tags( 'p', null, $this->getFilterLinks( $filter ) ); + } + + // Action filter + if ( $action !== null ) { + $html .= Xml::tags( 'p', null, $this->getActionSelector( $types, $action ) ); + } + + // Submit button + $html .= Xml::submitButton( $this->msg( 'logeventslist-submit' )->text() ); + + // Fieldset + $html = Xml::fieldset( $this->msg( 'log' )->text(), $html ); + + // Form wrapping + $html = Xml::tags( 'form', [ 'action' => $wgScript, 'method' => 'get' ], $html ); + + $this->getOutput()->addHTML( $html ); + } + + /** + * @param array $filter + * @return string Formatted HTML + */ + private function getFilterLinks( $filter ) { + // show/hide links + $messages = [ $this->msg( 'show' )->text(), $this->msg( 'hide' )->text() ]; + // Option value -> message mapping + $links = []; + $hiddens = ''; // keep track for "go" button + $linkRenderer = $this->getLinkRenderer(); + foreach ( $filter as $type => $val ) { + // Should the below assignment be outside the foreach? + // Then it would have to be copied. Not certain what is more expensive. + $query = $this->getDefaultQuery(); + $queryKey = "hide_{$type}_log"; + + $hideVal = 1 - intval( $val ); + $query[$queryKey] = $hideVal; + + $link = $linkRenderer->makeKnownLink( + $this->getTitle(), + $messages[$hideVal], + [], + $query + ); + + // Message: log-show-hide-patrol + $links[$type] = $this->msg( "log-show-hide-{$type}" )->rawParams( $link )->escaped(); + $hiddens .= Html::hidden( "hide_{$type}_log", $val ) . "\n"; + } + + // Build links + return '<small>' . $this->getLanguage()->pipeList( $links ) . '</small>' . $hiddens; + } + + private function getDefaultQuery() { + if ( !isset( $this->mDefaultQuery ) ) { + $this->mDefaultQuery = $this->getRequest()->getQueryValues(); + unset( $this->mDefaultQuery['title'] ); + unset( $this->mDefaultQuery['dir'] ); + unset( $this->mDefaultQuery['offset'] ); + unset( $this->mDefaultQuery['limit'] ); + unset( $this->mDefaultQuery['order'] ); + unset( $this->mDefaultQuery['month'] ); + unset( $this->mDefaultQuery['year'] ); + } + + return $this->mDefaultQuery; + } + + /** + * @param array $queryTypes + * @return string Formatted HTML + */ + private function getTypeMenu( $queryTypes ) { + $queryType = count( $queryTypes ) == 1 ? $queryTypes[0] : ''; + $selector = $this->getTypeSelector(); + $selector->setDefault( $queryType ); + + return $selector->getHTML(); + } + + /** + * Returns log page selector. + * @return XmlSelect + * @since 1.19 + */ + public function getTypeSelector() { + $typesByName = []; // Temporary array + // First pass to load the log names + foreach ( LogPage::validTypes() as $type ) { + $page = new LogPage( $type ); + $restriction = $page->getRestriction(); + if ( $this->getUser()->isAllowed( $restriction ) ) { + $typesByName[$type] = $page->getName()->text(); + } + } + + // Second pass to sort by name + asort( $typesByName ); + + // Always put "All public logs" on top + $public = $typesByName['']; + unset( $typesByName[''] ); + $typesByName = [ '' => $public ] + $typesByName; + + $select = new XmlSelect( 'type' ); + foreach ( $typesByName as $type => $name ) { + $select->addOption( $name, $type ); + } + + return $select; + } + + /** + * @param string $user + * @return string Formatted HTML + */ + private function getUserInput( $user ) { + $label = Xml::inputLabel( + $this->msg( 'specialloguserlabel' )->text(), + 'user', + 'mw-log-user', + 15, + $user, + [ 'class' => 'mw-autocomplete-user' ] + ); + + return '<span class="mw-input-with-label">' . $label . '</span>'; + } + + /** + * @param string $title + * @return string Formatted HTML + */ + private function getTitleInput( $title ) { + $label = Xml::inputLabel( + $this->msg( 'speciallogtitlelabel' )->text(), + 'page', + 'mw-log-page', + 20, + $title + ); + + return '<span class="mw-input-with-label">' . $label . '</span>'; + } + + /** + * @param string $pattern + * @return string Checkbox + */ + private function getTitlePattern( $pattern ) { + return '<span class="mw-input-with-label">' . + Xml::checkLabel( $this->msg( 'log-title-wildcard' )->text(), 'pattern', 'pattern', $pattern ) . + '</span>'; + } + + /** + * @param array $types + * @return string + */ + private function getExtraInputs( $types ) { + if ( count( $types ) == 1 ) { + if ( $types[0] == 'suppress' ) { + $offender = $this->getRequest()->getVal( 'offender' ); + $user = User::newFromName( $offender, false ); + if ( !$user || ( $user->getId() == 0 && !IP::isIPAddress( $offender ) ) ) { + $offender = ''; // Blank field if invalid + } + return Xml::inputLabel( $this->msg( 'revdelete-offender' )->text(), 'offender', + 'mw-log-offender', 20, $offender ); + } else { + // Allow extensions to add their own extra inputs + $input = ''; + Hooks::run( 'LogEventsListGetExtraInputs', [ $types[0], $this, &$input ] ); + return $input; + } + } + + return ''; + } + + /** + * Drop down menu for selection of actions that can be used to filter the log + * @param array $types + * @param string $action + * @return string + * @since 1.27 + */ + private function getActionSelector( $types, $action ) { + if ( $this->allowedActions === null || !count( $this->allowedActions ) ) { + return ''; + } + $html = ''; + $html .= Xml::label( wfMessage( 'log-action-filter-' . $types[0] )->text(), + 'action-filter-' .$types[0] ) . "\n"; + $select = new XmlSelect( 'subtype' ); + $select->addOption( wfMessage( 'log-action-filter-all' )->text(), '' ); + foreach ( $this->allowedActions as $value ) { + $msgKey = 'log-action-filter-' . $types[0] . '-' . $value; + $select->addOption( wfMessage( $msgKey )->text(), $value ); + } + $select->setDefault( $action ); + $html .= $select->getHTML(); + return $html; + } + + /** + * Sets the action types allowed for log filtering + * To one action type may correspond several log_actions + * @param array $actions + * @since 1.27 + */ + public function setAllowedActions( $actions ) { + $this->allowedActions = $actions; + } + + /** + * @return string + */ + public function beginLogEventsList() { + return "<ul>\n"; + } + + /** + * @return string + */ + public function endLogEventsList() { + return "</ul>\n"; + } + + /** + * @param stdClass $row A single row from the result set + * @return string Formatted HTML list item + */ + public function logLine( $row ) { + $entry = DatabaseLogEntry::newFromRow( $row ); + $formatter = LogFormatter::newFromEntry( $entry ); + $formatter->setContext( $this->getContext() ); + $formatter->setLinkRenderer( $this->getLinkRenderer() ); + $formatter->setShowUserToolLinks( !( $this->flags & self::NO_EXTRA_USER_LINKS ) ); + + $time = htmlspecialchars( $this->getLanguage()->userTimeAndDate( + $entry->getTimestamp(), $this->getUser() ) ); + + $action = $formatter->getActionText(); + + if ( $this->flags & self::NO_ACTION_LINK ) { + $revert = ''; + } else { + $revert = $formatter->getActionLinks(); + if ( $revert != '' ) { + $revert = '<span class="mw-logevent-actionlink">' . $revert . '</span>'; + } + } + + $comment = $formatter->getComment(); + + // Some user can hide log items and have review links + $del = $this->getShowHideLinks( $row ); + + // Any tags... + list( $tagDisplay, $newClasses ) = ChangeTags::formatSummaryRow( + $row->ts_tags, + 'logevent', + $this->getContext() + ); + $classes = array_merge( + [ 'mw-logline-' . $entry->getType() ], + $newClasses + ); + $attribs = [ + 'data-mw-logid' => $entry->getId(), + 'data-mw-logaction' => $entry->getFullType(), + ]; + $ret = "$del $time $action $comment $revert $tagDisplay"; + + // Let extensions add data + Hooks::run( 'LogEventsListLineEnding', [ $this, &$ret, $entry, &$classes, &$attribs ] ); + $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] ); + $attribs['class'] = implode( ' ', $classes ); + + return Html::rawElement( 'li', $attribs, $ret ) . "\n"; + } + + /** + * @param stdClass $row + * @return string + */ + private function getShowHideLinks( $row ) { + // We don't want to see the links and + if ( $this->flags == self::NO_ACTION_LINK ) { + return ''; + } + + $user = $this->getUser(); + + // If change tag editing is available to this user, return the checkbox + if ( $this->flags & self::USE_CHECKBOXES && $this->showTagEditUI ) { + return Xml::check( + 'showhiderevisions', + false, + [ 'name' => 'ids[' . $row->log_id . ']' ] + ); + } + + // no one can hide items from the suppress log. + if ( $row->log_type == 'suppress' ) { + return ''; + } + + $del = ''; + // Don't show useless checkbox to people who cannot hide log entries + if ( $user->isAllowed( 'deletedhistory' ) ) { + $canHide = $user->isAllowed( 'deletelogentry' ); + $canViewSuppressedOnly = $user->isAllowed( 'viewsuppressed' ) && + !$user->isAllowed( 'suppressrevision' ); + $entryIsSuppressed = self::isDeleted( $row, LogPage::DELETED_RESTRICTED ); + $canViewThisSuppressedEntry = $canViewSuppressedOnly && $entryIsSuppressed; + if ( $row->log_deleted || $canHide ) { + // Show checkboxes instead of links. + if ( $canHide && $this->flags & self::USE_CHECKBOXES && !$canViewThisSuppressedEntry ) { + // If event was hidden from sysops + if ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $user ) ) { + $del = Xml::check( 'deleterevisions', false, [ 'disabled' => 'disabled' ] ); + } else { + $del = Xml::check( + 'showhiderevisions', + false, + [ 'name' => 'ids[' . $row->log_id . ']' ] + ); + } + } else { + // If event was hidden from sysops + if ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $user ) ) { + $del = Linker::revDeleteLinkDisabled( $canHide ); + } else { + $query = [ + 'target' => SpecialPage::getTitleFor( 'Log', $row->log_type )->getPrefixedDBkey(), + 'type' => 'logging', + 'ids' => $row->log_id, + ]; + $del = Linker::revDeleteLink( + $query, + $entryIsSuppressed, + $canHide && !$canViewThisSuppressedEntry + ); + } + } + } + } + + return $del; + } + + /** + * @param stdClass $row + * @param string|array $type + * @param string|array $action + * @param string $right + * @return bool + */ + public static function typeAction( $row, $type, $action, $right = '' ) { + $match = is_array( $type ) ? + in_array( $row->log_type, $type ) : $row->log_type == $type; + if ( $match ) { + $match = is_array( $action ) ? + in_array( $row->log_action, $action ) : $row->log_action == $action; + if ( $match && $right ) { + global $wgUser; + $match = $wgUser->isAllowed( $right ); + } + } + + return $match; + } + + /** + * Determine if the current user is allowed to view a particular + * field of this log row, if it's marked as deleted and/or restricted log type. + * + * @param stdClass $row + * @param int $field + * @param User $user User to check, or null to use $wgUser + * @return bool + */ + public static function userCan( $row, $field, User $user = null ) { + return self::userCanBitfield( $row->log_deleted, $field, $user ) && + self::userCanViewLogType( $row->log_type, $user ); + } + + /** + * Determine if the current user is allowed to view a particular + * field of this log row, if it's marked as deleted. + * + * @param int $bitfield Current field + * @param int $field + * @param User $user User to check, or null to use $wgUser + * @return bool + */ + public static function userCanBitfield( $bitfield, $field, User $user = null ) { + if ( $bitfield & $field ) { + if ( $user === null ) { + global $wgUser; + $user = $wgUser; + } + if ( $bitfield & LogPage::DELETED_RESTRICTED ) { + $permissions = [ 'suppressrevision', 'viewsuppressed' ]; + } else { + $permissions = [ 'deletedhistory' ]; + } + $permissionlist = implode( ', ', $permissions ); + wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" ); + return call_user_func_array( [ $user, 'isAllowedAny' ], $permissions ); + } + return true; + } + + /** + * Determine if the current user is allowed to view a particular + * field of this log row, if it's marked as restricted log type. + * + * @param stdClass $type + * @param User|null $user User to check, or null to use $wgUser + * @return bool + */ + public static function userCanViewLogType( $type, User $user = null ) { + if ( $user === null ) { + global $wgUser; + $user = $wgUser; + } + $logRestrictions = MediaWikiServices::getInstance()->getMainConfig()->get( 'LogRestrictions' ); + if ( isset( $logRestrictions[$type] ) && !$user->isAllowed( $logRestrictions[$type] ) ) { + return false; + } + return true; + } + + /** + * @param stdClass $row + * @param int $field One of DELETED_* bitfield constants + * @return bool + */ + public static function isDeleted( $row, $field ) { + return ( $row->log_deleted & $field ) == $field; + } + + /** + * Show log extract. Either with text and a box (set $msgKey) or without (don't set $msgKey) + * + * @param OutputPage|string &$out + * @param string|array $types Log types to show + * @param string|Title $page The page title to show log entries for + * @param string $user The user who made the log entries + * @param array $param Associative Array with the following additional options: + * - lim Integer Limit of items to show, default is 50 + * - conds Array Extra conditions for the query + * (e.g. 'log_action != ' . $dbr->addQuotes( 'revision' )) + * - showIfEmpty boolean Set to false if you don't want any output in case the loglist is empty + * if set to true (default), "No matching items in log" is displayed if loglist is empty + * - msgKey Array If you want a nice box with a message, set this to the key of the message. + * First element is the message key, additional optional elements are parameters for the key + * that are processed with wfMessage + * - offset Set to overwrite offset parameter in WebRequest + * set to '' to unset offset + * - wrap String Wrap the message in html (usually something like "<div ...>$1</div>"). + * - flags Integer display flags (NO_ACTION_LINK,NO_EXTRA_USER_LINKS) + * - useRequestParams boolean Set true to use Pager-related parameters in the WebRequest + * - useMaster boolean Use master DB + * - extraUrlParams array|bool Additional url parameters for "full log" link (if it is shown) + * @return int Number of total log items (not limited by $lim) + */ + public static function showLogExtract( + &$out, $types = [], $page = '', $user = '', $param = [] + ) { + $defaultParameters = [ + 'lim' => 25, + 'conds' => [], + 'showIfEmpty' => true, + 'msgKey' => [ '' ], + 'wrap' => "$1", + 'flags' => 0, + 'useRequestParams' => false, + 'useMaster' => false, + 'extraUrlParams' => false, + ]; + # The + operator appends elements of remaining keys from the right + # handed array to the left handed, whereas duplicated keys are NOT overwritten. + $param += $defaultParameters; + # Convert $param array to individual variables + $lim = $param['lim']; + $conds = $param['conds']; + $showIfEmpty = $param['showIfEmpty']; + $msgKey = $param['msgKey']; + $wrap = $param['wrap']; + $flags = $param['flags']; + $extraUrlParams = $param['extraUrlParams']; + + $useRequestParams = $param['useRequestParams']; + if ( !is_array( $msgKey ) ) { + $msgKey = [ $msgKey ]; + } + + if ( $out instanceof OutputPage ) { + $context = $out->getContext(); + } else { + $context = RequestContext::getMain(); + } + + // FIXME: Figure out how to inject this + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); + + # Insert list of top 50 (or top $lim) items + $loglist = new LogEventsList( $context, $linkRenderer, $flags ); + $pager = new LogPager( $loglist, $types, $user, $page, '', $conds ); + if ( !$useRequestParams ) { + # Reset vars that may have been taken from the request + $pager->mLimit = 50; + $pager->mDefaultLimit = 50; + $pager->mOffset = ""; + $pager->mIsBackwards = false; + } + + if ( $param['useMaster'] ) { + $pager->mDb = wfGetDB( DB_MASTER ); + } + if ( isset( $param['offset'] ) ) { # Tell pager to ignore WebRequest offset + $pager->setOffset( $param['offset'] ); + } + + if ( $lim > 0 ) { + $pager->mLimit = $lim; + } + // Fetch the log rows and build the HTML if needed + $logBody = $pager->getBody(); + $numRows = $pager->getNumRows(); + + $s = ''; + + if ( $logBody ) { + if ( $msgKey[0] ) { + $dir = $context->getLanguage()->getDir(); + $lang = $context->getLanguage()->getHtmlCode(); + + $s = Xml::openElement( 'div', [ + 'class' => "mw-warning-with-logexcerpt mw-content-$dir", + 'dir' => $dir, + 'lang' => $lang, + ] ); + + if ( count( $msgKey ) == 1 ) { + $s .= $context->msg( $msgKey[0] )->parseAsBlock(); + } else { // Process additional arguments + $args = $msgKey; + array_shift( $args ); + $s .= $context->msg( $msgKey[0], $args )->parseAsBlock(); + } + } + $s .= $loglist->beginLogEventsList() . + $logBody . + $loglist->endLogEventsList(); + } elseif ( $showIfEmpty ) { + $s = Html::rawElement( 'div', [ 'class' => 'mw-warning-logempty' ], + $context->msg( 'logempty' )->parse() ); + } + + if ( $numRows > $pager->mLimit ) { # Show "Full log" link + $urlParam = []; + if ( $page instanceof Title ) { + $urlParam['page'] = $page->getPrefixedDBkey(); + } elseif ( $page != '' ) { + $urlParam['page'] = $page; + } + + if ( $user != '' ) { + $urlParam['user'] = $user; + } + + if ( !is_array( $types ) ) { # Make it an array, if it isn't + $types = [ $types ]; + } + + # If there is exactly one log type, we can link to Special:Log?type=foo + if ( count( $types ) == 1 ) { + $urlParam['type'] = $types[0]; + } + + if ( $extraUrlParams !== false ) { + $urlParam = array_merge( $urlParam, $extraUrlParams ); + } + + $s .= $linkRenderer->makeKnownLink( + SpecialPage::getTitleFor( 'Log' ), + $context->msg( 'log-fulllog' )->text(), + [], + $urlParam + ); + } + + if ( $logBody && $msgKey[0] ) { + $s .= '</div>'; + } + + if ( $wrap != '' ) { // Wrap message in html + $s = str_replace( '$1', $s, $wrap ); + } + + /* hook can return false, if we don't want the message to be emitted (Wikia BugId:7093) */ + if ( Hooks::run( 'LogEventsListShowLogExtract', [ &$s, $types, $page, $user, $param ] ) ) { + // $out can be either an OutputPage object or a String-by-reference + if ( $out instanceof OutputPage ) { + $out->addHTML( $s ); + } else { + $out = $s; + } + } + + return $numRows; + } + + /** + * SQL clause to skip forbidden log types for this user + * + * @param IDatabase $db + * @param string $audience Public/user + * @param User $user User to check, or null to use $wgUser + * @return string|bool String on success, false on failure. + */ + public static function getExcludeClause( $db, $audience = 'public', User $user = null ) { + global $wgLogRestrictions; + + if ( $audience != 'public' && $user === null ) { + global $wgUser; + $user = $wgUser; + } + + // Reset the array, clears extra "where" clauses when $par is used + $hiddenLogs = []; + + // Don't show private logs to unprivileged users + foreach ( $wgLogRestrictions as $logType => $right ) { + if ( $audience == 'public' || !$user->isAllowed( $right ) ) { + $hiddenLogs[] = $logType; + } + } + if ( count( $hiddenLogs ) == 1 ) { + return 'log_type != ' . $db->addQuotes( $hiddenLogs[0] ); + } elseif ( $hiddenLogs ) { + return 'log_type NOT IN (' . $db->makeList( $hiddenLogs ) . ')'; + } + + return false; + } +} diff --git a/www/wiki/includes/logging/LogFormatter.php b/www/wiki/includes/logging/LogFormatter.php new file mode 100644 index 00000000..72f3c54b --- /dev/null +++ b/www/wiki/includes/logging/LogFormatter.php @@ -0,0 +1,993 @@ +<?php +/** + * Contains classes for formatting log entries + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @author Niklas Laxström + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later + * @since 1.19 + */ +use MediaWiki\Linker\LinkRenderer; +use MediaWiki\MediaWikiServices; + +/** + * Implements the default log formatting. + * + * Can be overridden by subclassing and setting: + * + * $wgLogActionsHandlers['type/subtype'] = 'class'; or + * $wgLogActionsHandlers['type/*'] = 'class'; + * + * @since 1.19 + */ +class LogFormatter { + // Audience options for viewing usernames, comments, and actions + const FOR_PUBLIC = 1; + const FOR_THIS_USER = 2; + + // Static-> + + /** + * Constructs a new formatter suitable for given entry. + * @param LogEntry $entry + * @return LogFormatter + */ + public static function newFromEntry( LogEntry $entry ) { + global $wgLogActionsHandlers; + $fulltype = $entry->getFullType(); + $wildcard = $entry->getType() . '/*'; + $handler = ''; + + if ( isset( $wgLogActionsHandlers[$fulltype] ) ) { + $handler = $wgLogActionsHandlers[$fulltype]; + } elseif ( isset( $wgLogActionsHandlers[$wildcard] ) ) { + $handler = $wgLogActionsHandlers[$wildcard]; + } + + if ( $handler !== '' && is_string( $handler ) && class_exists( $handler ) ) { + return new $handler( $entry ); + } + + return new LegacyLogFormatter( $entry ); + } + + /** + * Handy shortcut for constructing a formatter directly from + * database row. + * @param stdClass|array $row + * @see DatabaseLogEntry::getSelectQueryData + * @return LogFormatter + */ + public static function newFromRow( $row ) { + return self::newFromEntry( DatabaseLogEntry::newFromRow( $row ) ); + } + + // Nonstatic-> + + /** @var LogEntryBase */ + protected $entry; + + /** @var int Constant for handling log_deleted */ + protected $audience = self::FOR_PUBLIC; + + /** @var IContextSource Context for logging */ + public $context; + + /** @var bool Whether to output user tool links */ + protected $linkFlood = false; + + /** + * Set to true if we are constructing a message text that is going to + * be included in page history or send to IRC feed. Links are replaced + * with plaintext or with [[pagename]] kind of syntax, that is parsed + * by page histories and IRC feeds. + * @var string + */ + protected $plaintext = false; + + /** @var string */ + protected $irctext = false; + + /** + * @var LinkRenderer|null + */ + private $linkRenderer; + + protected function __construct( LogEntry $entry ) { + $this->entry = $entry; + $this->context = RequestContext::getMain(); + } + + /** + * Replace the default context + * @param IContextSource $context + */ + public function setContext( IContextSource $context ) { + $this->context = $context; + } + + /** + * @since 1.30 + * @param LinkRenderer $linkRenderer + */ + public function setLinkRenderer( LinkRenderer $linkRenderer ) { + $this->linkRenderer = $linkRenderer; + } + + /** + * @since 1.30 + * @return LinkRenderer + */ + public function getLinkRenderer() { + if ( $this->linkRenderer !== null ) { + return $this->linkRenderer; + } else { + return MediaWikiServices::getInstance()->getLinkRenderer(); + } + } + + /** + * Set the visibility restrictions for displaying content. + * If set to public, and an item is deleted, then it will be replaced + * with a placeholder even if the context user is allowed to view it. + * @param int $audience Const self::FOR_THIS_USER or self::FOR_PUBLIC + */ + public function setAudience( $audience ) { + $this->audience = ( $audience == self::FOR_THIS_USER ) + ? self::FOR_THIS_USER + : self::FOR_PUBLIC; + } + + /** + * Check if a log item can be displayed + * @param int $field LogPage::DELETED_* constant + * @return bool + */ + protected function canView( $field ) { + if ( $this->audience == self::FOR_THIS_USER ) { + return LogEventsList::userCanBitfield( + $this->entry->getDeleted(), $field, $this->context->getUser() ); + } else { + return !$this->entry->isDeleted( $field ); + } + } + + /** + * If set to true, will produce user tool links after + * the user name. This should be replaced with generic + * CSS/JS solution. + * @param bool $value + */ + public function setShowUserToolLinks( $value ) { + $this->linkFlood = $value; + } + + /** + * Ugly hack to produce plaintext version of the message. + * Usually you also want to set extraneous request context + * to avoid formatting for any particular user. + * @see getActionText() + * @return string Plain text + */ + public function getPlainActionText() { + $this->plaintext = true; + $text = $this->getActionText(); + $this->plaintext = false; + + return $text; + } + + /** + * Even uglier hack to maintain backwards compatibility with IRC bots + * (T36508). + * @see getActionText() + * @return string Text + */ + public function getIRCActionComment() { + $actionComment = $this->getIRCActionText(); + $comment = $this->entry->getComment(); + + if ( $comment != '' ) { + if ( $actionComment == '' ) { + $actionComment = $comment; + } else { + $actionComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment; + } + } + + return $actionComment; + } + + /** + * Even uglier hack to maintain backwards compatibility with IRC bots + * (T36508). + * @see getActionText() + * @return string Text + */ + public function getIRCActionText() { + global $wgContLang; + + $this->plaintext = true; + $this->irctext = true; + + $entry = $this->entry; + $parameters = $entry->getParameters(); + // @see LogPage::actionText() + // Text of title the action is aimed at. + $target = $entry->getTarget()->getPrefixedText(); + $text = null; + switch ( $entry->getType() ) { + case 'move': + switch ( $entry->getSubtype() ) { + case 'move': + $movesource = $parameters['4::target']; + $text = wfMessage( '1movedto2' ) + ->rawParams( $target, $movesource )->inContentLanguage()->escaped(); + break; + case 'move_redir': + $movesource = $parameters['4::target']; + $text = wfMessage( '1movedto2_redir' ) + ->rawParams( $target, $movesource )->inContentLanguage()->escaped(); + break; + case 'move-noredirect': + break; + case 'move_redir-noredirect': + break; + } + break; + + case 'delete': + switch ( $entry->getSubtype() ) { + case 'delete': + $text = wfMessage( 'deletedarticle' ) + ->rawParams( $target )->inContentLanguage()->escaped(); + break; + case 'restore': + $text = wfMessage( 'undeletedarticle' ) + ->rawParams( $target )->inContentLanguage()->escaped(); + break; + //case 'revision': // Revision deletion + //case 'event': // Log deletion + // see https://github.com/wikimedia/mediawiki/commit/a9c243b7b5289dad204278dbe7ed571fd914e395 + //default: + } + break; + + case 'patrol': + // https://github.com/wikimedia/mediawiki/commit/1a05f8faf78675dc85984f27f355b8825b43efff + // Create a diff link to the patrolled revision + if ( $entry->getSubtype() === 'patrol' ) { + $diffLink = htmlspecialchars( + wfMessage( 'patrol-log-diff', $parameters['4::curid'] ) + ->inContentLanguage()->text() ); + $text = wfMessage( 'patrol-log-line', $diffLink, "[[$target]]", "" ) + ->inContentLanguage()->text(); + } else { + // broken?? + } + break; + + case 'protect': + switch ( $entry->getSubtype() ) { + case 'protect': + $text = wfMessage( 'protectedarticle' ) + ->rawParams( $target . ' ' . $parameters['4::description'] )->inContentLanguage()->escaped(); + break; + case 'unprotect': + $text = wfMessage( 'unprotectedarticle' ) + ->rawParams( $target )->inContentLanguage()->escaped(); + break; + case 'modify': + $text = wfMessage( 'modifiedarticleprotection' ) + ->rawParams( $target . ' ' . $parameters['4::description'] )->inContentLanguage()->escaped(); + break; + case 'move_prot': + $text = wfMessage( 'movedarticleprotection' ) + ->rawParams( $target, $parameters['4::oldtitle'] )->inContentLanguage()->escaped(); + break; + } + break; + + case 'newusers': + switch ( $entry->getSubtype() ) { + case 'newusers': + case 'create': + $text = wfMessage( 'newuserlog-create-entry' ) + ->inContentLanguage()->escaped(); + break; + case 'create2': + case 'byemail': + $text = wfMessage( 'newuserlog-create2-entry' ) + ->rawParams( $target )->inContentLanguage()->escaped(); + break; + case 'autocreate': + $text = wfMessage( 'newuserlog-autocreate-entry' ) + ->inContentLanguage()->escaped(); + break; + } + break; + + case 'upload': + switch ( $entry->getSubtype() ) { + case 'upload': + $text = wfMessage( 'uploadedimage' ) + ->rawParams( $target )->inContentLanguage()->escaped(); + break; + case 'overwrite': + $text = wfMessage( 'overwroteimage' ) + ->rawParams( $target )->inContentLanguage()->escaped(); + break; + } + break; + + case 'rights': + if ( count( $parameters['4::oldgroups'] ) ) { + $oldgroups = implode( ', ', $parameters['4::oldgroups'] ); + } else { + $oldgroups = wfMessage( 'rightsnone' )->inContentLanguage()->escaped(); + } + if ( count( $parameters['5::newgroups'] ) ) { + $newgroups = implode( ', ', $parameters['5::newgroups'] ); + } else { + $newgroups = wfMessage( 'rightsnone' )->inContentLanguage()->escaped(); + } + switch ( $entry->getSubtype() ) { + case 'rights': + $text = wfMessage( 'rightslogentry' ) + ->rawParams( $target, $oldgroups, $newgroups )->inContentLanguage()->escaped(); + break; + case 'autopromote': + $text = wfMessage( 'rightslogentry-autopromote' ) + ->rawParams( $target, $oldgroups, $newgroups )->inContentLanguage()->escaped(); + break; + } + break; + + case 'merge': + $text = wfMessage( 'pagemerge-logentry' ) + ->rawParams( $target, $parameters['4::dest'], $parameters['5::mergepoint'] ) + ->inContentLanguage()->escaped(); + break; + + case 'block': + switch ( $entry->getSubtype() ) { + case 'block': + // Keep compatibility with extensions by checking for + // new key (5::duration/6::flags) or old key (0/optional 1) + if ( $entry->isLegacy() ) { + $rawDuration = $parameters[0]; + $rawFlags = isset( $parameters[1] ) ? $parameters[1] : ''; + } else { + $rawDuration = $parameters['5::duration']; + $rawFlags = $parameters['6::flags']; + } + $duration = $wgContLang->translateBlockExpiry( + $rawDuration, + null, + wfTimestamp( TS_UNIX, $entry->getTimestamp() ) + ); + $flags = BlockLogFormatter::formatBlockFlags( $rawFlags, $wgContLang ); + $text = wfMessage( 'blocklogentry' ) + ->rawParams( $target, $duration, $flags )->inContentLanguage()->escaped(); + break; + case 'unblock': + $text = wfMessage( 'unblocklogentry' ) + ->rawParams( $target )->inContentLanguage()->escaped(); + break; + case 'reblock': + $duration = $wgContLang->translateBlockExpiry( + $parameters['5::duration'], + null, + wfTimestamp( TS_UNIX, $entry->getTimestamp() ) + ); + $flags = BlockLogFormatter::formatBlockFlags( $parameters['6::flags'], $wgContLang ); + $text = wfMessage( 'reblock-logentry' ) + ->rawParams( $target, $duration, $flags )->inContentLanguage()->escaped(); + break; + } + break; + + case 'import': + switch ( $entry->getSubtype() ) { + case 'upload': + $text = wfMessage( 'import-logentry-upload' ) + ->rawParams( $target )->inContentLanguage()->escaped(); + break; + case 'interwiki': + $text = wfMessage( 'import-logentry-interwiki' ) + ->rawParams( $target )->inContentLanguage()->escaped(); + break; + } + break; + // case 'suppress' --private log -- aaron (so we know who to blame in a few years :-D) + // default: + } + if ( is_null( $text ) ) { + $text = $this->getPlainActionText(); + } + + $this->plaintext = false; + $this->irctext = false; + + return $text; + } + + /** + * Gets the log action, including username. + * @return string HTML + */ + public function getActionText() { + if ( $this->canView( LogPage::DELETED_ACTION ) ) { + $element = $this->getActionMessage(); + if ( $element instanceof Message ) { + $element = $this->plaintext ? $element->text() : $element->escaped(); + } + if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) ) { + $element = $this->styleRestricedElement( $element ); + } + } else { + $sep = $this->msg( 'word-separator' ); + $sep = $this->plaintext ? $sep->text() : $sep->escaped(); + $performer = $this->getPerformerElement(); + $element = $performer . $sep . $this->getRestrictedElement( 'rev-deleted-event' ); + } + + return $element; + } + + /** + * Returns a sentence describing the log action. Usually + * a Message object is returned, but old style log types + * and entries might return pre-escaped HTML string. + * @return Message|string Pre-escaped HTML + */ + protected function getActionMessage() { + $message = $this->msg( $this->getMessageKey() ); + $message->params( $this->getMessageParameters() ); + + return $message; + } + + /** + * Returns a key to be used for formatting the action sentence. + * Default is logentry-TYPE-SUBTYPE for modern logs. Legacy log + * types will use custom keys, and subclasses can also alter the + * key depending on the entry itself. + * @return string Message key + */ + protected function getMessageKey() { + $type = $this->entry->getType(); + $subtype = $this->entry->getSubtype(); + + return "logentry-$type-$subtype"; + } + + /** + * Returns extra links that comes after the action text, like "revert", etc. + * + * @return string + */ + public function getActionLinks() { + return ''; + } + + /** + * Extracts the optional extra parameters for use in action messages. + * The array indexes start from number 3. + * @return array + */ + protected function extractParameters() { + $entry = $this->entry; + $params = []; + + if ( $entry->isLegacy() ) { + foreach ( $entry->getParameters() as $index => $value ) { + $params[$index + 3] = $value; + } + } + + // Filter out parameters which are not in format #:foo + foreach ( $entry->getParameters() as $key => $value ) { + if ( strpos( $key, ':' ) === false ) { + continue; + } + list( $index, $type, ) = explode( ':', $key, 3 ); + if ( ctype_digit( $index ) ) { + $params[$index - 1] = $this->formatParameterValue( $type, $value ); + } + } + + /* Message class doesn't like non consecutive numbering. + * Fill in missing indexes with empty strings to avoid + * incorrect renumbering. + */ + if ( count( $params ) ) { + $max = max( array_keys( $params ) ); + // index 0 to 2 are added in getMessageParameters + for ( $i = 3; $i < $max; $i++ ) { + if ( !isset( $params[$i] ) ) { + $params[$i] = ''; + } + } + } + + return $params; + } + + /** + * Formats parameters intented for action message from + * array of all parameters. There are three hardcoded + * parameters (array is zero-indexed, this list not): + * - 1: user name with premade link + * - 2: usable for gender magic function + * - 3: target page with premade link + * @return array + */ + protected function getMessageParameters() { + if ( isset( $this->parsedParameters ) ) { + return $this->parsedParameters; + } + + $entry = $this->entry; + $params = $this->extractParameters(); + $params[0] = Message::rawParam( $this->getPerformerElement() ); + $params[1] = $this->canView( LogPage::DELETED_USER ) ? $entry->getPerformer()->getName() : ''; + $params[2] = Message::rawParam( $this->makePageLink( $entry->getTarget() ) ); + + // Bad things happens if the numbers are not in correct order + ksort( $params ); + + $this->parsedParameters = $params; + return $this->parsedParameters; + } + + /** + * Formats parameters values dependent to their type + * @param string $type The type of the value. + * Valid are currently: + * * - (empty) or plain: The value is returned as-is + * * raw: The value will be added to the log message + * as raw parameter (e.g. no escaping) + * Use this only if there is no other working + * type like user-link or title-link + * * msg: The value is a message-key, the output is + * the message in user language + * * msg-content: The value is a message-key, the output + * is the message in content language + * * user: The value is a user name, e.g. for GENDER + * * user-link: The value is a user name, returns a + * link for the user + * * title: The value is a page title, + * returns name of page + * * title-link: The value is a page title, + * returns link to this page + * * number: Format value as number + * * list: Format value as a comma-separated list + * @param mixed $value The parameter value that should be formatted + * @return string|array Formated value + * @since 1.21 + */ + protected function formatParameterValue( $type, $value ) { + $saveLinkFlood = $this->linkFlood; + + switch ( strtolower( trim( $type ) ) ) { + case 'raw': + $value = Message::rawParam( $value ); + break; + case 'list': + $value = $this->context->getLanguage()->commaList( $value ); + break; + case 'msg': + $value = $this->msg( $value )->text(); + break; + case 'msg-content': + $value = $this->msg( $value )->inContentLanguage()->text(); + break; + case 'number': + $value = Message::numParam( $value ); + break; + case 'user': + $user = User::newFromName( $value ); + $value = $user->getName(); + break; + case 'user-link': + $this->setShowUserToolLinks( false ); + + $user = User::newFromName( $value ); + $value = Message::rawParam( $this->makeUserLink( $user ) ); + + $this->setShowUserToolLinks( $saveLinkFlood ); + break; + case 'title': + $title = Title::newFromText( $value ); + $value = $title->getPrefixedText(); + break; + case 'title-link': + $title = Title::newFromText( $value ); + $value = Message::rawParam( $this->makePageLink( $title ) ); + break; + case 'plain': + // Plain text, nothing to do + default: + // Catch other types and use the old behavior (return as-is) + } + + return $value; + } + + /** + * Helper to make a link to the page, taking the plaintext + * value in consideration. + * @param Title $title The page + * @param array $parameters Query parameters + * @param string|null $html Linktext of the link as raw html + * @return string + */ + protected function makePageLink( Title $title = null, $parameters = [], $html = null ) { + if ( !$title instanceof Title ) { + $msg = $this->msg( 'invalidtitle' )->text(); + if ( !$this->plaintext ) { + return Html::element( 'span', [ 'class' => 'mw-invalidtitle' ], $msg ); + } else { + return $msg; + } + } + + if ( !$this->plaintext ) { + $html = $html !== null ? new HtmlArmor( $html ) : $html; + $link = $this->getLinkRenderer()->makeLink( $title, $html, [], $parameters ); + } else { + $link = '[[' . $title->getPrefixedText() . ']]'; + } + + return $link; + } + + /** + * Provides the name of the user who performed the log action. + * Used as part of log action message or standalone, depending + * which parts of the log entry has been hidden. + * @return string + */ + public function getPerformerElement() { + if ( $this->canView( LogPage::DELETED_USER ) ) { + $performer = $this->entry->getPerformer(); + $element = $this->makeUserLink( $performer ); + if ( $this->entry->isDeleted( LogPage::DELETED_USER ) ) { + $element = $this->styleRestricedElement( $element ); + } + } else { + $element = $this->getRestrictedElement( 'rev-deleted-user' ); + } + + return $element; + } + + /** + * Gets the user provided comment + * @return string HTML + */ + public function getComment() { + if ( $this->canView( LogPage::DELETED_COMMENT ) ) { + $comment = Linker::commentBlock( $this->entry->getComment() ); + // No hard coded spaces thanx + $element = ltrim( $comment ); + if ( $this->entry->isDeleted( LogPage::DELETED_COMMENT ) ) { + $element = $this->styleRestricedElement( $element ); + } + } else { + $element = $this->getRestrictedElement( 'rev-deleted-comment' ); + } + + return $element; + } + + /** + * Helper method for displaying restricted element. + * @param string $message + * @return string HTML or wiki text + */ + protected function getRestrictedElement( $message ) { + if ( $this->plaintext ) { + return $this->msg( $message )->text(); + } + + $content = $this->msg( $message )->escaped(); + $attribs = [ 'class' => 'history-deleted' ]; + + return Html::rawElement( 'span', $attribs, $content ); + } + + /** + * Helper method for styling restricted element. + * @param string $content + * @return string HTML or wiki text + */ + protected function styleRestricedElement( $content ) { + if ( $this->plaintext ) { + return $content; + } + $attribs = [ 'class' => 'history-deleted' ]; + + return Html::rawElement( 'span', $attribs, $content ); + } + + /** + * Shortcut for wfMessage which honors local context. + * @param string $key + * @return Message + */ + protected function msg( $key ) { + return $this->context->msg( $key ); + } + + protected function makeUserLink( User $user, $toolFlags = 0 ) { + if ( $this->plaintext ) { + $element = $user->getName(); + } else { + $element = Linker::userLink( + $user->getId(), + $user->getName() + ); + + if ( $this->linkFlood ) { + $element .= Linker::userToolLinks( + $user->getId(), + $user->getName(), + true, // redContribsWhenNoEdits + $toolFlags, + $user->getEditCount() + ); + } + } + + return $element; + } + + /** + * @return array Array of titles that should be preloaded with LinkBatch + */ + public function getPreloadTitles() { + return []; + } + + /** + * @return array Output of getMessageParameters() for testing + */ + public function getMessageParametersForTesting() { + // This function was added because getMessageParameters() is + // protected and a change from protected to public caused + // problems with extensions + return $this->getMessageParameters(); + } + + /** + * Get the array of parameters, converted from legacy format if necessary. + * @since 1.25 + * @return array + */ + protected function getParametersForApi() { + return $this->entry->getParameters(); + } + + /** + * Format parameters for API output + * + * The result array should generally map named keys to values. Index and + * type should be omitted, e.g. "4::foo" should be returned as "foo" in the + * output. Values should generally be unformatted. + * + * Renames or removals of keys besides from the legacy numeric format to + * modern named style should be avoided. Any renames should be announced to + * the mediawiki-api-announce mailing list. + * + * @since 1.25 + * @return array + */ + public function formatParametersForApi() { + $logParams = []; + foreach ( $this->getParametersForApi() as $key => $value ) { + $vals = explode( ':', $key, 3 ); + if ( count( $vals ) !== 3 ) { + $logParams[$key] = $value; + continue; + } + $logParams += $this->formatParameterValueForApi( $vals[2], $vals[1], $value ); + } + ApiResult::setIndexedTagName( $logParams, 'param' ); + ApiResult::setArrayType( $logParams, 'assoc' ); + + return $logParams; + } + + /** + * Format a single parameter value for API output + * + * @since 1.25 + * @param string $name + * @param string $type + * @param string $value + * @return array + */ + protected function formatParameterValueForApi( $name, $type, $value ) { + $type = strtolower( trim( $type ) ); + switch ( $type ) { + case 'bool': + $value = (bool)$value; + break; + + case 'number': + if ( ctype_digit( $value ) || is_int( $value ) ) { + $value = (int)$value; + } else { + $value = (float)$value; + } + break; + + case 'array': + case 'assoc': + case 'kvp': + if ( is_array( $value ) ) { + ApiResult::setArrayType( $value, $type ); + } + break; + + case 'timestamp': + $value = wfTimestamp( TS_ISO_8601, $value ); + break; + + case 'msg': + case 'msg-content': + $msg = $this->msg( $value ); + if ( $type === 'msg-content' ) { + $msg->inContentLanguage(); + } + $value = []; + $value["{$name}_key"] = $msg->getKey(); + if ( $msg->getParams() ) { + $value["{$name}_params"] = $msg->getParams(); + } + $value["{$name}_text"] = $msg->text(); + return $value; + + case 'title': + case 'title-link': + $title = Title::newFromText( $value ); + if ( !$title ) { + // Huh? Do something halfway sane. + $title = SpecialPage::getTitleFor( 'Badtitle', $value ); + } + $value = []; + ApiQueryBase::addTitleInfo( $value, $title, "{$name}_" ); + return $value; + + case 'user': + case 'user-link': + $user = User::newFromName( $value ); + if ( $user ) { + $value = $user->getName(); + } + break; + + default: + // do nothing + break; + } + + return [ $name => $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; + } + + 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/www/wiki/includes/logging/LogPage.php b/www/wiki/includes/logging/LogPage.php new file mode 100644 index 00000000..28c1a873 --- /dev/null +++ b/www/wiki/includes/logging/LogPage.php @@ -0,0 +1,488 @@ +<?php +/** + * Contain log classes + * + * Copyright © 2002, 2004 Brion Vibber <brion@pobox.com> + * https://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * Class to simplify the use of log pages. + * The logs are now kept in a table which is easier to manage and trim + * than ever-growing wiki pages. + */ +class LogPage { + const DELETED_ACTION = 1; + const DELETED_COMMENT = 2; + const DELETED_USER = 4; + const DELETED_RESTRICTED = 8; + + // Convenience fields + const SUPPRESSED_USER = 12; + const SUPPRESSED_ACTION = 9; + + /** @var bool */ + public $updateRecentChanges; + + /** @var bool */ + public $sendToUDP; + + /** @var string Plaintext version of the message for IRC */ + private $ircActionText; + + /** @var string Plaintext version of the message */ + private $actionText; + + /** @var string One of '', 'block', 'protect', 'rights', 'delete', + * 'upload', 'move' + */ + private $type; + + /** @var string One of '', 'block', 'protect', 'rights', 'delete', + * 'upload', 'move', 'move_redir' */ + private $action; + + /** @var string Comment associated with action */ + private $comment; + + /** @var string Blob made of a parameters array */ + private $params; + + /** @var User The user doing the action */ + private $doer; + + /** @var Title */ + private $target; + + /** + * @param string $type One of '', 'block', 'protect', 'rights', 'delete', + * 'upload', 'move' + * @param bool $rc Whether to update recent changes as well as the logging table + * @param string $udp Pass 'UDP' to send to the UDP feed if NOT sent to RC + */ + public function __construct( $type, $rc = true, $udp = 'skipUDP' ) { + $this->type = $type; + $this->updateRecentChanges = $rc; + $this->sendToUDP = ( $udp == 'UDP' ); + } + + /** + * @return int The log_id of the inserted log entry + */ + protected function saveContent() { + global $wgLogRestrictions; + + $dbw = wfGetDB( DB_MASTER ); + + // @todo FIXME private/protected/public property? + $this->timestamp = $now = wfTimestampNow(); + $data = [ + 'log_type' => $this->type, + 'log_action' => $this->action, + 'log_timestamp' => $dbw->timestamp( $now ), + 'log_namespace' => $this->target->getNamespace(), + 'log_title' => $this->target->getDBkey(), + 'log_page' => $this->target->getArticleID(), + 'log_params' => $this->params + ]; + $data += CommentStore::getStore()->insert( $dbw, 'log_comment', $this->comment ); + $data += ActorMigration::newMigration()->getInsertValues( $dbw, 'log_user', $this->doer ); + $dbw->insert( 'logging', $data, __METHOD__ ); + $newId = $dbw->insertId(); + + # And update recentchanges + if ( $this->updateRecentChanges ) { + $titleObj = SpecialPage::getTitleFor( 'Log', $this->type ); + + RecentChange::notifyLog( + $now, $titleObj, $this->doer, $this->getRcComment(), '', + $this->type, $this->action, $this->target, $this->comment, + $this->params, $newId, $this->getRcCommentIRC() + ); + } elseif ( $this->sendToUDP ) { + # Don't send private logs to UDP + if ( isset( $wgLogRestrictions[$this->type] ) && $wgLogRestrictions[$this->type] != '*' ) { + return $newId; + } + + # Notify external application via UDP. + # We send this to IRC but do not want to add it the RC table. + $titleObj = SpecialPage::getTitleFor( 'Log', $this->type ); + $rc = RecentChange::newLogEntry( + $now, $titleObj, $this->doer, $this->getRcComment(), '', + $this->type, $this->action, $this->target, $this->comment, + $this->params, $newId, $this->getRcCommentIRC() + ); + $rc->notifyRCFeeds(); + } + + return $newId; + } + + /** + * Get the RC comment from the last addEntry() call + * + * @return string + */ + public function getRcComment() { + $rcComment = $this->actionText; + + if ( $this->comment != '' ) { + if ( $rcComment == '' ) { + $rcComment = $this->comment; + } else { + $rcComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . + $this->comment; + } + } + + return $rcComment; + } + + /** + * Get the RC comment from the last addEntry() call for IRC + * + * @return string + */ + public function getRcCommentIRC() { + $rcComment = $this->ircActionText; + + if ( $this->comment != '' ) { + if ( $rcComment == '' ) { + $rcComment = $this->comment; + } else { + $rcComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . + $this->comment; + } + } + + return $rcComment; + } + + /** + * Get the comment from the last addEntry() call + * @return string + */ + public function getComment() { + return $this->comment; + } + + /** + * Get the list of valid log types + * + * @return array Array of strings + */ + public static function validTypes() { + global $wgLogTypes; + + return $wgLogTypes; + } + + /** + * Is $type a valid log type + * + * @param string $type Log type to check + * @return bool + */ + public static function isLogType( $type ) { + return in_array( $type, self::validTypes() ); + } + + /** + * Generate text for a log entry. + * Only LogFormatter should call this function. + * + * @param string $type Log type + * @param string $action Log action + * @param Title|null $title Title object or null + * @param Skin|null $skin Skin object or null. If null, we want to use the wiki + * content language, since that will go to the IRC feed. + * @param array $params Parameters + * @param bool $filterWikilinks Whether to filter wiki links + * @return string HTML + */ + public static function actionText( $type, $action, $title = null, $skin = null, + $params = [], $filterWikilinks = false + ) { + global $wgLang, $wgContLang, $wgLogActions; + + if ( is_null( $skin ) ) { + $langObj = $wgContLang; + $langObjOrNull = null; + } else { + $langObj = $wgLang; + $langObjOrNull = $wgLang; + } + + $key = "$type/$action"; + + if ( isset( $wgLogActions[$key] ) ) { + if ( is_null( $title ) ) { + $rv = wfMessage( $wgLogActions[$key] )->inLanguage( $langObj )->escaped(); + } else { + $titleLink = self::getTitleLink( $type, $langObjOrNull, $title, $params ); + + if ( count( $params ) == 0 ) { + $rv = wfMessage( $wgLogActions[$key] )->rawParams( $titleLink ) + ->inLanguage( $langObj )->escaped(); + } else { + array_unshift( $params, $titleLink ); + + $rv = wfMessage( $wgLogActions[$key] )->rawParams( $params ) + ->inLanguage( $langObj )->escaped(); + } + } + } else { + global $wgLogActionsHandlers; + + if ( isset( $wgLogActionsHandlers[$key] ) ) { + $args = func_get_args(); + $rv = call_user_func_array( $wgLogActionsHandlers[$key], $args ); + } else { + wfDebug( "LogPage::actionText - unknown action $key\n" ); + $rv = "$action"; + } + } + + // For the perplexed, this feature was added in r7855 by Erik. + // The feature was added because we liked adding [[$1]] in our log entries + // but the log entries are parsed as Wikitext on RecentChanges but as HTML + // on Special:Log. The hack is essentially that [[$1]] represented a link + // to the title in question. The first parameter to the HTML version (Special:Log) + // is that link in HTML form, and so this just gets rid of the ugly [[]]. + // However, this is a horrible hack and it doesn't work like you expect if, say, + // you want to link to something OTHER than the title of the log entry. + // The real problem, which Erik was trying to fix (and it sort-of works now) is + // that the same messages are being treated as both wikitext *and* HTML. + if ( $filterWikilinks ) { + $rv = str_replace( '[[', '', $rv ); + $rv = str_replace( ']]', '', $rv ); + } + + return $rv; + } + + /** + * @todo Document + * @param string $type + * @param Language|null $lang + * @param Title $title + * @param array &$params + * @return string + */ + protected static function getTitleLink( $type, $lang, $title, &$params ) { + if ( !$lang ) { + return $title->getPrefixedText(); + } + + if ( $title->isSpecialPage() ) { + list( $name, $par ) = SpecialPageFactory::resolveAlias( $title->getDBkey() ); + + # Use the language name for log titles, rather than Log/X + if ( $name == 'Log' ) { + $logPage = new LogPage( $par ); + $titleLink = Linker::link( $title, $logPage->getName()->escaped() ); + $titleLink = wfMessage( 'parentheses' ) + ->inLanguage( $lang ) + ->rawParams( $titleLink ) + ->escaped(); + } else { + $titleLink = Linker::link( $title ); + } + } else { + $titleLink = Linker::link( $title ); + } + + return $titleLink; + } + + /** + * Add a log entry + * + * @param string $action One of '', 'block', 'protect', 'rights', 'delete', + * 'upload', 'move', 'move_redir' + * @param Title $target + * @param string $comment Description associated + * @param array $params Parameters passed later to wfMessage function + * @param null|int|User $doer The user doing the action. null for $wgUser + * + * @return int The log_id of the inserted log entry + */ + public function addEntry( $action, $target, $comment, $params = [], $doer = null ) { + if ( !is_array( $params ) ) { + $params = [ $params ]; + } + + if ( $comment === null ) { + $comment = ''; + } + + # Trim spaces on user supplied text + $comment = trim( $comment ); + + $this->action = $action; + $this->target = $target; + $this->comment = $comment; + $this->params = self::makeParamBlob( $params ); + + if ( $doer === null ) { + global $wgUser; + $doer = $wgUser; + } elseif ( !is_object( $doer ) ) { + $doer = User::newFromId( $doer ); + } + + $this->doer = $doer; + + $logEntry = new ManualLogEntry( $this->type, $action ); + $logEntry->setTarget( $target ); + $logEntry->setPerformer( $doer ); + $logEntry->setParameters( $params ); + // All log entries using the LogPage to insert into the logging table + // are using the old logging system and therefore the legacy flag is + // needed to say the LogFormatter the parameters have numeric keys + $logEntry->setLegacy( true ); + + $formatter = LogFormatter::newFromEntry( $logEntry ); + $context = RequestContext::newExtraneousContext( $target ); + $formatter->setContext( $context ); + + $this->actionText = $formatter->getPlainActionText(); + $this->ircActionText = $formatter->getIRCActionText(); + + return $this->saveContent(); + } + + /** + * Add relations to log_search table + * + * @param string $field + * @param array $values + * @param int $logid + * @return bool + */ + public function addRelations( $field, $values, $logid ) { + if ( !strlen( $field ) || empty( $values ) ) { + return false; // nothing + } + + $data = []; + + foreach ( $values as $value ) { + $data[] = [ + 'ls_field' => $field, + 'ls_value' => $value, + 'ls_log_id' => $logid + ]; + } + + $dbw = wfGetDB( DB_MASTER ); + $dbw->insert( 'log_search', $data, __METHOD__, 'IGNORE' ); + + return true; + } + + /** + * Create a blob from a parameter array + * + * @param array $params + * @return string + */ + public static function makeParamBlob( $params ) { + return implode( "\n", $params ); + } + + /** + * Extract a parameter array from a blob + * + * @param string $blob + * @return array + */ + public static function extractParams( $blob ) { + if ( $blob === '' ) { + return []; + } else { + return explode( "\n", $blob ); + } + } + + /** + * Name of the log. + * @return Message + * @since 1.19 + */ + public function getName() { + global $wgLogNames; + + // BC + if ( isset( $wgLogNames[$this->type] ) ) { + $key = $wgLogNames[$this->type]; + } else { + $key = 'log-name-' . $this->type; + } + + return wfMessage( $key ); + } + + /** + * Description of this log type. + * @return Message + * @since 1.19 + */ + public function getDescription() { + global $wgLogHeaders; + // BC + if ( isset( $wgLogHeaders[$this->type] ) ) { + $key = $wgLogHeaders[$this->type]; + } else { + $key = 'log-description-' . $this->type; + } + + return wfMessage( $key ); + } + + /** + * Returns the right needed to read this log type. + * @return string + * @since 1.19 + */ + public function getRestriction() { + global $wgLogRestrictions; + if ( isset( $wgLogRestrictions[$this->type] ) ) { + $restriction = $wgLogRestrictions[$this->type]; + } else { + // '' always returns true with $user->isAllowed() + $restriction = ''; + } + + return $restriction; + } + + /** + * Tells if this log is not viewable by all. + * @return bool + * @since 1.19 + */ + public function isRestricted() { + $restriction = $this->getRestriction(); + + return $restriction !== '' && $restriction !== '*'; + } +} diff --git a/www/wiki/includes/logging/LogPager.php b/www/wiki/includes/logging/LogPager.php new file mode 100644 index 00000000..c047e96e --- /dev/null +++ b/www/wiki/includes/logging/LogPager.php @@ -0,0 +1,462 @@ +<?php +/** + * Contain classes to list log entries + * + * Copyright © 2004 Brion Vibber <brion@pobox.com> + * https://www.mediawiki.org/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * @ingroup Pager + */ +class LogPager extends ReverseChronologicalPager { + /** @var array Log types */ + private $types = []; + + /** @var string Events limited to those by performer when set */ + private $performer = ''; + + /** @var string|Title Events limited to those about Title when set */ + private $title = ''; + + /** @var string */ + private $pattern = ''; + + /** @var string */ + private $typeCGI = ''; + + /** @var string */ + private $action = ''; + + /** @var bool */ + private $performerRestrictionsEnforced = false; + + /** @var bool */ + private $actionRestrictionsEnforced = false; + + /** @var LogEventsList */ + public $mLogEventsList; + + /** + * @param LogEventsList $list + * @param string|array $types Log types to show + * @param string $performer The user who made the log entries + * @param string|Title $title The page title the log entries are for + * @param string $pattern Do a prefix search rather than an exact title match + * @param array $conds Extra conditions for the query + * @param int|bool $year The year to start from. Default: false + * @param int|bool $month The month to start from. Default: false + * @param string $tagFilter Tag + * @param string $action Specific action (subtype) requested + * @param int $logId Log entry ID, to limit to a single log entry. + */ + public function __construct( $list, $types = [], $performer = '', $title = '', + $pattern = '', $conds = [], $year = false, $month = false, $tagFilter = '', + $action = '', $logId = false + ) { + parent::__construct( $list->getContext() ); + $this->mConds = $conds; + + $this->mLogEventsList = $list; + + $this->limitType( $types ); // also excludes hidden types + $this->limitPerformer( $performer ); + $this->limitTitle( $title, $pattern ); + $this->limitAction( $action ); + $this->getDateCond( $year, $month ); + $this->mTagFilter = $tagFilter; + $this->limitLogId( $logId ); + + $this->mDb = wfGetDB( DB_REPLICA, 'logpager' ); + } + + public function getDefaultQuery() { + $query = parent::getDefaultQuery(); + $query['type'] = $this->typeCGI; // arrays won't work here + $query['user'] = $this->performer; + $query['month'] = $this->mMonth; + $query['year'] = $this->mYear; + + return $query; + } + + // Call ONLY after calling $this->limitType() already! + public function getFilterParams() { + global $wgFilterLogTypes; + $filters = []; + if ( count( $this->types ) ) { + return $filters; + } + foreach ( $wgFilterLogTypes as $type => $default ) { + $hide = $this->getRequest()->getInt( "hide_{$type}_log", $default ); + + $filters[$type] = $hide; + if ( $hide ) { + $this->mConds[] = 'log_type != ' . $this->mDb->addQuotes( $type ); + } + } + + return $filters; + } + + /** + * Set the log reader to return only entries of the given type. + * Type restrictions enforced here + * + * @param string|array $types Log types ('upload', 'delete', etc); + * empty string means no restriction + */ + private function limitType( $types ) { + global $wgLogRestrictions; + + $user = $this->getUser(); + // If $types is not an array, make it an array + $types = ( $types === '' ) ? [] : (array)$types; + // Don't even show header for private logs; don't recognize it... + $needReindex = false; + foreach ( $types as $type ) { + if ( isset( $wgLogRestrictions[$type] ) + && !$user->isAllowed( $wgLogRestrictions[$type] ) + ) { + $needReindex = true; + $types = array_diff( $types, [ $type ] ); + } + } + if ( $needReindex ) { + // Lots of this code makes assumptions that + // the first entry in the array is $types[0]. + $types = array_values( $types ); + } + $this->types = $types; + // Don't show private logs to unprivileged users. + // Also, only show them upon specific request to avoid suprises. + $audience = $types ? 'user' : 'public'; + $hideLogs = LogEventsList::getExcludeClause( $this->mDb, $audience, $user ); + if ( $hideLogs !== false ) { + $this->mConds[] = $hideLogs; + } + if ( count( $types ) ) { + $this->mConds['log_type'] = $types; + // Set typeCGI; used in url param for paging + if ( count( $types ) == 1 ) { + $this->typeCGI = $types[0]; + } + } + } + + /** + * Set the log reader to return only entries by the given user. + * + * @param string $name (In)valid user name + * @return void + */ + private function limitPerformer( $name ) { + if ( $name == '' ) { + return; + } + $usertitle = Title::makeTitleSafe( NS_USER, $name ); + if ( is_null( $usertitle ) ) { + return; + } + // Normalize username first so that non-existent users used + // in maintenance scripts work + $name = $usertitle->getText(); + + // Assume no joins required for log_user + $this->mConds[] = ActorMigration::newMigration()->getWhere( + wfGetDB( DB_REPLICA ), 'log_user', User::newFromName( $name, false ) + )['conds']; + + $this->enforcePerformerRestrictions(); + + $this->performer = $name; + } + + /** + * Set the log reader to return only entries affecting the given page. + * (For the block and rights logs, this is a user page.) + * + * @param string|Title $page Title name + * @param string $pattern + * @return void + */ + private function limitTitle( $page, $pattern ) { + global $wgMiserMode, $wgUserrightsInterwikiDelimiter; + + if ( $page instanceof Title ) { + $title = $page; + } else { + $title = Title::newFromText( $page ); + if ( strlen( $page ) == 0 || !$title instanceof Title ) { + return; + } + } + + $this->title = $title->getPrefixedText(); + $ns = $title->getNamespace(); + $db = $this->mDb; + + $doUserRightsLogLike = false; + if ( $this->types == [ 'rights' ] ) { + $parts = explode( $wgUserrightsInterwikiDelimiter, $title->getDBkey() ); + if ( count( $parts ) == 2 ) { + list( $name, $database ) = array_map( 'trim', $parts ); + if ( strstr( $database, '*' ) ) { // Search for wildcard in database name + $doUserRightsLogLike = true; + } + } + } + + /** + * Using the (log_namespace, log_title, log_timestamp) index with a + * range scan (LIKE) on the first two parts, instead of simple equality, + * makes it unusable for sorting. Sorted retrieval using another index + * would be possible, but then we might have to scan arbitrarily many + * nodes of that index. Therefore, we need to avoid this if $wgMiserMode + * is on. + * + * This is not a problem with simple title matches, because then we can + * use the page_time index. That should have no more than a few hundred + * log entries for even the busiest pages, so it can be safely scanned + * in full to satisfy an impossible condition on user or similar. + */ + $this->mConds['log_namespace'] = $ns; + if ( $doUserRightsLogLike ) { + $params = [ $name . $wgUserrightsInterwikiDelimiter ]; + foreach ( explode( '*', $database ) as $databasepart ) { + $params[] = $databasepart; + $params[] = $db->anyString(); + } + array_pop( $params ); // Get rid of the last % we added. + $this->mConds[] = 'log_title' . $db->buildLike( $params ); + } elseif ( $pattern && !$wgMiserMode ) { + $this->mConds[] = 'log_title' . $db->buildLike( $title->getDBkey(), $db->anyString() ); + $this->pattern = $pattern; + } else { + $this->mConds['log_title'] = $title->getDBkey(); + } + $this->enforceActionRestrictions(); + } + + /** + * Set the log_action field to a specified value (or values) + * + * @param string $action + */ + private function limitAction( $action ) { + global $wgActionFilteredLogs; + // Allow to filter the log by actions + $type = $this->typeCGI; + if ( $type === '' ) { + // nothing to do + return; + } + $actions = $wgActionFilteredLogs; + if ( isset( $actions[$type] ) ) { + // log type can be filtered by actions + $this->mLogEventsList->setAllowedActions( array_keys( $actions[$type] ) ); + if ( $action !== '' && isset( $actions[$type][$action] ) ) { + // add condition to query + $this->mConds['log_action'] = $actions[$type][$action]; + $this->action = $action; + } + } + } + + /** + * Limit to the (single) specified log ID. + * @param int $logId The log entry ID. + */ + protected function limitLogId( $logId ) { + if ( !$logId ) { + return; + } + $this->mConds['log_id'] = $logId; + } + + /** + * Constructs the most part of the query. Extra conditions are sprinkled in + * all over this class. + * @return array + */ + public function getQueryInfo() { + $basic = DatabaseLogEntry::getSelectQueryData(); + + $tables = $basic['tables']; + $fields = $basic['fields']; + $conds = $basic['conds']; + $options = $basic['options']; + $joins = $basic['join_conds']; + + # Add log_search table if there are conditions on it. + # This filters the results to only include log rows that have + # log_search records with the specified ls_field and ls_value values. + if ( array_key_exists( 'ls_field', $this->mConds ) ) { + $tables[] = 'log_search'; + $options['IGNORE INDEX'] = [ 'log_search' => 'ls_log_id' ]; + $options['USE INDEX'] = [ 'logging' => 'PRIMARY' ]; + if ( !$this->hasEqualsClause( 'ls_field' ) + || !$this->hasEqualsClause( 'ls_value' ) + ) { + # Since (ls_field,ls_value,ls_logid) is unique, if the condition is + # to match a specific (ls_field,ls_value) tuple, then there will be + # no duplicate log rows. Otherwise, we need to remove the duplicates. + $options[] = 'DISTINCT'; + } + } + # Don't show duplicate rows when using log_search + $joins['log_search'] = [ 'INNER JOIN', 'ls_log_id=log_id' ]; + + $info = [ + 'tables' => $tables, + 'fields' => $fields, + 'conds' => array_merge( $conds, $this->mConds ), + 'options' => $options, + 'join_conds' => $joins, + ]; + # Add ChangeTags filter query + ChangeTags::modifyDisplayQuery( $info['tables'], $info['fields'], $info['conds'], + $info['join_conds'], $info['options'], $this->mTagFilter ); + + return $info; + } + + /** + * Checks if $this->mConds has $field matched to a *single* value + * @param string $field + * @return bool + */ + protected function hasEqualsClause( $field ) { + return ( + array_key_exists( $field, $this->mConds ) && + ( !is_array( $this->mConds[$field] ) || count( $this->mConds[$field] ) == 1 ) + ); + } + + function getIndexField() { + return 'log_timestamp'; + } + + public function getStartBody() { + # Do a link batch query + if ( $this->getNumRows() > 0 ) { + $lb = new LinkBatch; + foreach ( $this->mResult as $row ) { + $lb->add( $row->log_namespace, $row->log_title ); + $lb->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) ); + $lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_name ) ); + $formatter = LogFormatter::newFromRow( $row ); + foreach ( $formatter->getPreloadTitles() as $title ) { + $lb->addObj( $title ); + } + } + $lb->execute(); + $this->mResult->seek( 0 ); + } + + return ''; + } + + public function formatRow( $row ) { + return $this->mLogEventsList->logLine( $row ); + } + + public function getType() { + return $this->types; + } + + /** + * Guaranteed to either return a valid title string or a Zero-Length String + * + * @return string + */ + public function getPerformer() { + return $this->performer; + } + + /** + * @return string + */ + public function getPage() { + return $this->title; + } + + public function getPattern() { + return $this->pattern; + } + + public function getYear() { + return $this->mYear; + } + + public function getMonth() { + return $this->mMonth; + } + + public function getTagFilter() { + return $this->mTagFilter; + } + + public function getAction() { + return $this->action; + } + + public function doQuery() { + // Workaround MySQL optimizer bug + $this->mDb->setBigSelects(); + parent::doQuery(); + $this->mDb->setBigSelects( 'default' ); + } + + /** + * Paranoia: avoid brute force searches (T19342) + */ + private function enforceActionRestrictions() { + if ( $this->actionRestrictionsEnforced ) { + return; + } + $this->actionRestrictionsEnforced = true; + $user = $this->getUser(); + if ( !$user->isAllowed( 'deletedhistory' ) ) { + $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_ACTION ) . ' = 0'; + } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::SUPPRESSED_ACTION ) . + ' != ' . LogPage::SUPPRESSED_USER; + } + } + + /** + * Paranoia: avoid brute force searches (T19342) + */ + private function enforcePerformerRestrictions() { + // Same as enforceActionRestrictions(), except for _USER instead of _ACTION bits. + if ( $this->performerRestrictionsEnforced ) { + return; + } + $this->performerRestrictionsEnforced = true; + $user = $this->getUser(); + if ( !$user->isAllowed( 'deletedhistory' ) ) { + $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::DELETED_USER ) . ' = 0'; + } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + $this->mConds[] = $this->mDb->bitAnd( 'log_deleted', LogPage::SUPPRESSED_USER ) . + ' != ' . LogPage::SUPPRESSED_ACTION; + } + } +} diff --git a/www/wiki/includes/logging/MergeLogFormatter.php b/www/wiki/includes/logging/MergeLogFormatter.php new file mode 100644 index 00000000..8775097d --- /dev/null +++ b/www/wiki/includes/logging/MergeLogFormatter.php @@ -0,0 +1,91 @@ +<?php +/** + * Formatter for merge log entries. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later + * @since 1.25 + */ + +/** + * This class formats merge log entries. + * + * @since 1.25 + */ +class MergeLogFormatter extends LogFormatter { + public function getPreloadTitles() { + $params = $this->extractParameters(); + + return [ Title::newFromText( $params[3] ) ]; + } + + protected function getMessageParameters() { + $params = parent::getMessageParameters(); + $oldname = $this->makePageLink( $this->entry->getTarget(), [ 'redirect' => 'no' ] ); + $newname = $this->makePageLink( Title::newFromText( $params[3] ) ); + $params[2] = Message::rawParam( $oldname ); + $params[3] = Message::rawParam( $newname ); + $params[4] = $this->context->getLanguage() + ->userTimeAndDate( $params[4], $this->context->getUser() ); + return $params; + } + + public function getActionLinks() { + if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) // Action is hidden + || !$this->context->getUser()->isAllowed( 'mergehistory' ) + ) { + return ''; + } + + // Show unmerge link + $params = $this->extractParameters(); + $revert = $this->getLinkRenderer()->makeKnownLink( + SpecialPage::getTitleFor( 'MergeHistory' ), + $this->msg( 'revertmerge' )->text(), + [], + [ + 'target' => $params[3], + 'dest' => $this->entry->getTarget()->getPrefixedDBkey(), + 'mergepoint' => $params[4], + 'submitted' => 1 // show the revisions immediately + ] + ); + + return $this->msg( 'parentheses' )->rawParams( $revert )->escaped(); + } + + protected function getParametersForApi() { + $entry = $this->entry; + $params = $entry->getParameters(); + + static $map = [ + '4:title:dest', + '5:timestamp:mergepoint', + '4::dest' => '4:title:dest', + '5::mergepoint' => '5:timestamp:mergepoint', + ]; + foreach ( $map as $index => $key ) { + if ( isset( $params[$index] ) ) { + $params[$key] = $params[$index]; + unset( $params[$index] ); + } + } + + return $params; + } +} diff --git a/www/wiki/includes/logging/MoveLogFormatter.php b/www/wiki/includes/logging/MoveLogFormatter.php new file mode 100644 index 00000000..43ca0ea1 --- /dev/null +++ b/www/wiki/includes/logging/MoveLogFormatter.php @@ -0,0 +1,113 @@ +<?php +/** + * Formatter for move log entries. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @author Niklas Laxström + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later + * @since 1.22 + */ + +/** + * This class formats move log entries. + * + * @since 1.19 + */ +class MoveLogFormatter extends LogFormatter { + public function getPreloadTitles() { + $params = $this->extractParameters(); + + return [ Title::newFromText( $params[3] ) ]; + } + + protected function getMessageKey() { + $key = parent::getMessageKey(); + $params = $this->extractParameters(); + if ( isset( $params[4] ) && $params[4] === '1' ) { + // Messages: logentry-move-move-noredirect, logentry-move-move_redir-noredirect + $key .= '-noredirect'; + } + + return $key; + } + + protected function getMessageParameters() { + $params = parent::getMessageParameters(); + $oldname = $this->makePageLink( $this->entry->getTarget(), [ 'redirect' => 'no' ] ); + $newname = $this->makePageLink( Title::newFromText( $params[3] ) ); + $params[2] = Message::rawParam( $oldname ); + $params[3] = Message::rawParam( $newname ); + unset( $params[4] ); // handled in getMessageKey + + return $params; + } + + public function getActionLinks() { + if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) // Action is hidden + || $this->entry->getSubtype() !== 'move' + || !$this->context->getUser()->isAllowed( 'move' ) + ) { + return ''; + } + + $params = $this->extractParameters(); + $destTitle = Title::newFromText( $params[3] ); + if ( !$destTitle ) { + return ''; + } + + $revert = $this->getLinkRenderer()->makeKnownLink( + SpecialPage::getTitleFor( 'Movepage' ), + $this->msg( 'revertmove' )->text(), + [], + [ + 'wpOldTitle' => $destTitle->getPrefixedDBkey(), + 'wpNewTitle' => $this->entry->getTarget()->getPrefixedDBkey(), + 'wpReason' => $this->msg( 'revertmove' )->inContentLanguage()->text(), + 'wpMovetalk' => 0 + ] + ); + + return $this->msg( 'parentheses' )->rawParams( $revert )->escaped(); + } + + protected function getParametersForApi() { + $entry = $this->entry; + $params = $entry->getParameters(); + + static $map = [ + '4:title:target', + '5:bool:suppressredirect', + '4::target' => '4:title:target', + '5::noredir' => '5:bool:suppressredirect', + ]; + foreach ( $map as $index => $key ) { + if ( isset( $params[$index] ) ) { + $params[$key] = $params[$index]; + unset( $params[$index] ); + } + } + + if ( !isset( $params['5:bool:suppressredirect'] ) ) { + $params['5:bool:suppressredirect'] = false; + } + + return $params; + } + +} diff --git a/www/wiki/includes/logging/NewUsersLogFormatter.php b/www/wiki/includes/logging/NewUsersLogFormatter.php new file mode 100644 index 00000000..382e4adb --- /dev/null +++ b/www/wiki/includes/logging/NewUsersLogFormatter.php @@ -0,0 +1,68 @@ +<?php +/** + * Formatter for new user log entries. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @author Niklas Laxström + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later + * @since 1.22 + */ + +/** + * This class formats new user log entries. + * + * @since 1.19 + */ +class NewUsersLogFormatter extends LogFormatter { + protected function getMessageParameters() { + $params = parent::getMessageParameters(); + $subtype = $this->entry->getSubtype(); + if ( $subtype === 'create2' || $subtype === 'byemail' ) { + if ( isset( $params[3] ) ) { + $target = User::newFromId( $params[3] ); + } else { + $target = User::newFromName( $this->entry->getTarget()->getText(), false ); + } + $params[2] = Message::rawParam( $this->makeUserLink( $target ) ); + $params[3] = $target->getName(); + } + + return $params; + } + + public function getComment() { + $timestamp = wfTimestamp( TS_MW, $this->entry->getTimestamp() ); + if ( $timestamp < '20080129000000' ) { + # Suppress $comment from old entries (before 2008-01-29), + # not needed and can contain incorrect links + return ''; + } + + return parent::getComment(); + } + + public function getPreloadTitles() { + $subtype = $this->entry->getSubtype(); + if ( $subtype === 'create2' || $subtype === 'byemail' ) { + // add the user talk to LinkBatch for the userLink + return [ Title::makeTitle( NS_USER_TALK, $this->entry->getTarget()->getText() ) ]; + } + + return []; + } +} diff --git a/www/wiki/includes/logging/PageLangLogFormatter.php b/www/wiki/includes/logging/PageLangLogFormatter.php new file mode 100644 index 00000000..694fa7f3 --- /dev/null +++ b/www/wiki/includes/logging/PageLangLogFormatter.php @@ -0,0 +1,61 @@ +<?php +/** + * Formatter for changelang log entries. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @author Kunal Grover + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later + * @since 1.24 + */ + +/** + * This class formats language change log entries. + * + * @since 1.24 + */ +class PageLangLogFormatter extends LogFormatter { + protected function getMessageParameters() { + // Get the user language for displaying language names + $userLang = $this->context->getLanguage()->getCode(); + $params = parent::getMessageParameters(); + + // Get the language codes from log + $oldLang = $params[3]; + $kOld = strrpos( $oldLang, '[' ); + if ( $kOld ) { + $oldLang = substr( $oldLang, 0, $kOld ); + } + + $newLang = $params[4]; + $kNew = strrpos( $newLang, '[' ); + if ( $kNew ) { + $newLang = substr( $newLang, 0, $kNew ); + } + + // Convert language codes to names in user language + $logOld = Language::fetchLanguageName( $oldLang, $userLang ) + . ' (' . $oldLang . ')'; + $logNew = Language::fetchLanguageName( $newLang, $userLang ) + . ' (' . $newLang . ')'; + + // Add the default message to languages if required + $params[3] = !$kOld ? $logOld : $logOld . ' [' . $this->msg( 'default' ) . ']'; + $params[4] = !$kNew ? $logNew : $logNew . ' [' . $this->msg( 'default' ) . ']'; + return $params; + } +} diff --git a/www/wiki/includes/logging/PatrolLog.php b/www/wiki/includes/logging/PatrolLog.php new file mode 100644 index 00000000..9b2e0982 --- /dev/null +++ b/www/wiki/includes/logging/PatrolLog.php @@ -0,0 +1,89 @@ +<?php +/** + * Specific methods for the patrol log. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @author Rob Church <robchur@gmail.com> + * @author Niklas Laxström + */ + +/** + * Class containing static functions for working with + * logs of patrol events + */ +class PatrolLog { + + /** + * Record a log event for a change being patrolled + * + * @param int|RecentChange $rc Change identifier or RecentChange object + * @param bool $auto Was this patrol event automatic? + * @param User $user User performing the action or null to use $wgUser + * @param string|string[] $tags Change tags to add to the patrol log entry + * ($user should be able to add the specified tags before this is called) + * + * @return bool + */ + public static function record( $rc, $auto = false, User $user = null, $tags = null ) { + // Do not log autopatrol actions: T184485 + if ( $auto ) { + return false; + } + + if ( !$rc instanceof RecentChange ) { + $rc = RecentChange::newFromId( $rc ); + if ( !is_object( $rc ) ) { + return false; + } + } + + if ( !$user ) { + global $wgUser; + $user = $wgUser; + } + + $action = $auto ? 'autopatrol' : 'patrol'; + + $entry = new ManualLogEntry( 'patrol', $action ); + $entry->setTarget( $rc->getTitle() ); + $entry->setParameters( self::buildParams( $rc, $auto ) ); + $entry->setPerformer( $user ); + $entry->setTags( $tags ); + $logid = $entry->insert(); + if ( !$auto ) { + $entry->publish( $logid, 'udp' ); + } + + return true; + } + + /** + * Prepare log parameters for a patrolled change + * + * @param RecentChange $change RecentChange to represent + * @param bool $auto Whether the patrol event was automatic + * @return array + */ + private static function buildParams( $change, $auto ) { + return [ + '4::curid' => $change->getAttribute( 'rc_this_oldid' ), + '5::previd' => $change->getAttribute( 'rc_last_oldid' ), + '6::auto' => (int)$auto + ]; + } +} diff --git a/www/wiki/includes/logging/PatrolLogFormatter.php b/www/wiki/includes/logging/PatrolLogFormatter.php new file mode 100644 index 00000000..894f59b0 --- /dev/null +++ b/www/wiki/includes/logging/PatrolLogFormatter.php @@ -0,0 +1,88 @@ +<?php +/** + * Formatter for new user log entries. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @author Niklas Laxström + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later + * @since 1.22 + */ + +/** + * This class formats patrol log entries. + * + * @since 1.19 + */ +class PatrolLogFormatter extends LogFormatter { + protected function getMessageKey() { + $params = $this->getMessageParameters(); + if ( isset( $params[5] ) && $params[5] ) { + $key = 'logentry-patrol-patrol-auto'; + } else { + $key = 'logentry-patrol-patrol'; + } + + return $key; + } + + protected function getMessageParameters() { + $params = parent::getMessageParameters(); + + $target = $this->entry->getTarget(); + $oldid = $params[3]; + $revision = $this->context->getLanguage()->formatNum( $oldid, true ); + + if ( $this->plaintext ) { + $revlink = $revision; + } elseif ( $target->exists() ) { + $query = [ + 'oldid' => $oldid, + 'diff' => 'prev' + ]; + $revlink = $this->getLinkRenderer()->makeLink( $target, $revision, [], $query ); + } else { + $revlink = htmlspecialchars( $revision ); + } + + $params[3] = Message::rawParam( $revlink ); + + return $params; + } + + protected function getParametersForApi() { + $entry = $this->entry; + $params = $entry->getParameters(); + + static $map = [ + '4:number:curid', + '5:number:previd', + '6:bool:auto', + '4::curid' => '4:number:curid', + '5::previd' => '5:number:previd', + '6::auto' => '6:bool:auto', + ]; + foreach ( $map as $index => $key ) { + if ( isset( $params[$index] ) ) { + $params[$key] = $params[$index]; + unset( $params[$index] ); + } + } + + return $params; + } +} diff --git a/www/wiki/includes/logging/ProtectLogFormatter.php b/www/wiki/includes/logging/ProtectLogFormatter.php new file mode 100644 index 00000000..64ec6269 --- /dev/null +++ b/www/wiki/includes/logging/ProtectLogFormatter.php @@ -0,0 +1,214 @@ +<?php +/** + * Formatter for protect log entries. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later + * @since 1.26 + */ + +/** + * This class formats protect log entries. + * + * @since 1.26 + */ +class ProtectLogFormatter extends LogFormatter { + public function getPreloadTitles() { + $subtype = $this->entry->getSubtype(); + if ( $subtype === 'move_prot' ) { + $params = $this->extractParameters(); + return [ Title::newFromText( $params[3] ) ]; + } + return []; + } + + protected function getMessageKey() { + $key = parent::getMessageKey(); + $params = $this->extractParameters(); + if ( isset( $params[4] ) && $params[4] ) { + // Messages: logentry-protect-protect-cascade, logentry-protect-modify-cascade + $key .= '-cascade'; + } + + return $key; + } + + protected function getMessageParameters() { + $params = parent::getMessageParameters(); + + $subtype = $this->entry->getSubtype(); + if ( $subtype === 'protect' || $subtype === 'modify' ) { + $rawParams = $this->entry->getParameters(); + if ( isset( $rawParams['details'] ) ) { + $params[3] = $this->createProtectDescription( $rawParams['details'] ); + } elseif ( isset( $params[3] ) ) { + // Old way of Restrictions and expiries + $params[3] = $this->context->getLanguage()->getDirMark() . $params[3]; + } else { + // Very old way (nothing set) + $params[3] = ''; + } + // Cascading flag + if ( isset( $params[4] ) ) { + // handled in getMessageKey + unset( $params[4] ); + } + } elseif ( $subtype === 'move_prot' ) { + $oldname = $this->makePageLink( Title::newFromText( $params[3] ), [ 'redirect' => 'no' ] ); + $params[3] = Message::rawParam( $oldname ); + } + + return $params; + } + + public function getActionLinks() { + $linkRenderer = $this->getLinkRenderer(); + $subtype = $this->entry->getSubtype(); + if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) // Action is hidden + || $subtype === 'move_prot' // the move log entry has the right action link + ) { + return ''; + } + + // Show history link for all changes after the protection + $title = $this->entry->getTarget(); + $links = [ + $linkRenderer->makeLink( $title, + $this->msg( 'hist' )->text(), + [], + [ + 'action' => 'history', + 'offset' => $this->entry->getTimestamp(), + ] + ) + ]; + + // Show change protection link + if ( $this->context->getUser()->isAllowed( 'protect' ) ) { + $links[] = $linkRenderer->makeKnownLink( + $title, + $this->msg( 'protect_change' )->text(), + [], + [ 'action' => 'protect' ] + ); + } + + return $this->msg( 'parentheses' )->rawParams( + $this->context->getLanguage()->pipeList( $links ) )->escaped(); + } + + protected function getParametersForApi() { + $entry = $this->entry; + $subtype = $this->entry->getSubtype(); + $params = $entry->getParameters(); + + $map = []; + if ( $subtype === 'protect' || $subtype === 'modify' ) { + $map = [ + '4::description', + '5:bool:cascade', + 'details' => ':array:details', + ]; + } elseif ( $subtype === 'move_prot' ) { + $map = [ + '4:title:oldtitle', + '4::oldtitle' => '4:title:oldtitle', + ]; + } + foreach ( $map as $index => $key ) { + if ( isset( $params[$index] ) ) { + $params[$key] = $params[$index]; + unset( $params[$index] ); + } + } + + // Change string to explicit boolean + if ( isset( $params['5:bool:cascade'] ) && is_string( $params['5:bool:cascade'] ) ) { + $params['5:bool:cascade'] = $params['5:bool:cascade'] === 'cascade'; + } + + return $params; + } + + public function formatParametersForApi() { + global $wgContLang; + + $ret = parent::formatParametersForApi(); + if ( isset( $ret['details'] ) && is_array( $ret['details'] ) ) { + foreach ( $ret['details'] as &$detail ) { + if ( isset( $detail['expiry'] ) ) { + $detail['expiry'] = $wgContLang->formatExpiry( $detail['expiry'], TS_ISO_8601, 'infinite' ); + } + } + } + + return $ret; + } + + /** + * Create the protect description to show in the log formatter + * + * @param array $details + * @return string + */ + public function createProtectDescription( array $details ) { + $protectDescription = ''; + + foreach ( $details as $param ) { + $expiryText = $this->formatExpiry( $param['expiry'] ); + + // Messages: restriction-edit, restriction-move, restriction-create, + // restriction-upload + $action = $this->context->msg( 'restriction-' . $param['type'] )->escaped(); + + $protectionLevel = $param['level']; + // Messages: protect-level-autoconfirmed, protect-level-sysop + $message = $this->context->msg( 'protect-level-' . $protectionLevel ); + if ( $message->isDisabled() ) { + // Require "$1" permission + $restrictions = $this->context->msg( "protect-fallback", $protectionLevel )->parse(); + } else { + $restrictions = $message->escaped(); + } + + if ( $protectDescription !== '' ) { + $protectDescription .= $this->context->msg( 'word-separator' )->escaped(); + } + + $protectDescription .= $this->context->msg( 'protect-summary-desc' ) + ->params( $action, $restrictions, $expiryText )->escaped(); + } + + return $protectDescription; + } + + private function formatExpiry( $expiry ) { + if ( wfIsInfinity( $expiry ) ) { + return $this->context->msg( 'protect-expiry-indefinite' )->text(); + } + $lang = $this->context->getLanguage(); + $user = $this->context->getUser(); + return $this->context->msg( + 'protect-expiring-local', + $lang->userTimeAndDate( $expiry, $user ), + $lang->userDate( $expiry, $user ), + $lang->userTime( $expiry, $user ) + )->text(); + } + +} diff --git a/www/wiki/includes/logging/RightsLogFormatter.php b/www/wiki/includes/logging/RightsLogFormatter.php new file mode 100644 index 00000000..4b4d19f4 --- /dev/null +++ b/www/wiki/includes/logging/RightsLogFormatter.php @@ -0,0 +1,240 @@ +<?php +/** + * Formatter for user rights log entries. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @author Alexandre Emsenhuber + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later + * @since 1.22 + */ + +/** + * This class formats rights log entries. + * + * @since 1.21 + */ +class RightsLogFormatter extends LogFormatter { + protected function makePageLink( Title $title = null, $parameters = [], $html = null ) { + global $wgContLang, $wgUserrightsInterwikiDelimiter; + + if ( !$this->plaintext ) { + $text = $wgContLang->ucfirst( $title->getDBkey() ); + $parts = explode( $wgUserrightsInterwikiDelimiter, $text, 2 ); + + if ( count( $parts ) === 2 ) { + $titleLink = WikiMap::foreignUserLink( + $parts[1], + $parts[0], + htmlspecialchars( + strtr( $parts[0], '_', ' ' ) . + $wgUserrightsInterwikiDelimiter . + $parts[1] + ) + ); + + if ( $titleLink !== false ) { + return $titleLink; + } + } + } + + return parent::makePageLink( $title, $parameters, $title ? $title->getText() : null ); + } + + protected function getMessageKey() { + $key = parent::getMessageKey(); + $params = $this->getMessageParameters(); + if ( !isset( $params[3] ) && !isset( $params[4] ) ) { + // Messages: logentry-rights-rights-legacy + $key .= '-legacy'; + } + + return $key; + } + + protected function getMessageParameters() { + $params = parent::getMessageParameters(); + + // Really old entries that lack old/new groups + if ( !isset( $params[3] ) && !isset( $params[4] ) ) { + return $params; + } + + $oldGroups = $this->makeGroupArray( $params[3] ); + $newGroups = $this->makeGroupArray( $params[4] ); + + $userName = $this->entry->getTarget()->getText(); + if ( !$this->plaintext && count( $oldGroups ) ) { + foreach ( $oldGroups as &$group ) { + $group = UserGroupMembership::getGroupMemberName( $group, $userName ); + } + } + if ( !$this->plaintext && count( $newGroups ) ) { + foreach ( $newGroups as &$group ) { + $group = UserGroupMembership::getGroupMemberName( $group, $userName ); + } + } + + // fetch the metadata about each group membership + $allParams = $this->entry->getParameters(); + + if ( count( $oldGroups ) ) { + $params[3] = [ 'raw' => $this->formatRightsList( $oldGroups, + isset( $allParams['oldmetadata'] ) ? $allParams['oldmetadata'] : [] ) ]; + } else { + $params[3] = $this->msg( 'rightsnone' )->text(); + } + if ( count( $newGroups ) ) { + // Array_values is used here because of T44211 + // see use of array_unique in UserrightsPage::doSaveUserGroups on $newGroups. + $params[4] = [ 'raw' => $this->formatRightsList( array_values( $newGroups ), + isset( $allParams['newmetadata'] ) ? $allParams['newmetadata'] : [] ) ]; + } else { + $params[4] = $this->msg( 'rightsnone' )->text(); + } + + $params[5] = $userName; + + return $params; + } + + protected function formatRightsList( $groups, $serializedUGMs = [] ) { + $uiLanguage = $this->context->getLanguage(); + $uiUser = $this->context->getUser(); + // separate arrays of temporary and permanent memberships + $tempList = $permList = []; + + reset( $groups ); + reset( $serializedUGMs ); + while ( current( $groups ) ) { + $group = current( $groups ); + + if ( current( $serializedUGMs ) && + isset( current( $serializedUGMs )['expiry'] ) && + current( $serializedUGMs )['expiry'] + ) { + // there is an expiry date; format the group and expiry into a friendly string + $expiry = current( $serializedUGMs )['expiry']; + $expiryFormatted = $uiLanguage->userTimeAndDate( $expiry, $uiUser ); + $expiryFormattedD = $uiLanguage->userDate( $expiry, $uiUser ); + $expiryFormattedT = $uiLanguage->userTime( $expiry, $uiUser ); + $tempList[] = $this->msg( 'rightslogentry-temporary-group' )->params( $group, + $expiryFormatted, $expiryFormattedD, $expiryFormattedT )->parse(); + } else { + // the right does not expire; just insert the group name + $permList[] = $group; + } + + next( $groups ); + next( $serializedUGMs ); + } + + // place all temporary memberships first, to avoid the ambiguity of + // "adinistrator, bureaucrat and importer (temporary, until X time)" + return $uiLanguage->listToText( array_merge( $tempList, $permList ) ); + } + + protected function getParametersForApi() { + $entry = $this->entry; + $params = $entry->getParameters(); + + static $map = [ + '4:array:oldgroups', + '5:array:newgroups', + '4::oldgroups' => '4:array:oldgroups', + '5::newgroups' => '5:array:newgroups', + ]; + foreach ( $map as $index => $key ) { + if ( isset( $params[$index] ) ) { + $params[$key] = $params[$index]; + unset( $params[$index] ); + } + } + + // Really old entries do not have log params, so form them from whatever info + // we have. + // Also walk through the parallel arrays of groups and metadata, combining each + // metadata array with the name of the group it pertains to + if ( isset( $params['4:array:oldgroups'] ) ) { + $params['4:array:oldgroups'] = $this->makeGroupArray( $params['4:array:oldgroups'] ); + + $oldmetadata =& $params['oldmetadata']; + // unset old metadata entry to ensure metadata goes at the end of the params array + unset( $params['oldmetadata'] ); + $params['oldmetadata'] = array_map( function ( $index ) use ( $params, $oldmetadata ) { + $result = [ 'group' => $params['4:array:oldgroups'][$index] ]; + if ( isset( $oldmetadata[$index] ) ) { + $result += $oldmetadata[$index]; + } + $result['expiry'] = ApiResult::formatExpiry( isset( $result['expiry'] ) ? + $result['expiry'] : null ); + + return $result; + }, array_keys( $params['4:array:oldgroups'] ) ); + } + + if ( isset( $params['5:array:newgroups'] ) ) { + $params['5:array:newgroups'] = $this->makeGroupArray( $params['5:array:newgroups'] ); + + $newmetadata =& $params['newmetadata']; + // unset old metadata entry to ensure metadata goes at the end of the params array + unset( $params['newmetadata'] ); + $params['newmetadata'] = array_map( function ( $index ) use ( $params, $newmetadata ) { + $result = [ 'group' => $params['5:array:newgroups'][$index] ]; + if ( isset( $newmetadata[$index] ) ) { + $result += $newmetadata[$index]; + } + $result['expiry'] = ApiResult::formatExpiry( isset( $result['expiry'] ) ? + $result['expiry'] : null ); + + return $result; + }, array_keys( $params['5:array:newgroups'] ) ); + } + + return $params; + } + + public function formatParametersForApi() { + $ret = parent::formatParametersForApi(); + if ( isset( $ret['oldgroups'] ) ) { + ApiResult::setIndexedTagName( $ret['oldgroups'], 'g' ); + } + if ( isset( $ret['newgroups'] ) ) { + ApiResult::setIndexedTagName( $ret['newgroups'], 'g' ); + } + if ( isset( $ret['oldmetadata'] ) ) { + ApiResult::setArrayType( $ret['oldmetadata'], 'array' ); + ApiResult::setIndexedTagName( $ret['oldmetadata'], 'g' ); + } + if ( isset( $ret['newmetadata'] ) ) { + ApiResult::setArrayType( $ret['newmetadata'], 'array' ); + ApiResult::setIndexedTagName( $ret['newmetadata'], 'g' ); + } + return $ret; + } + + private function makeGroupArray( $group ) { + // Migrate old group params from string to array + if ( $group === '' ) { + $group = []; + } elseif ( is_string( $group ) ) { + $group = array_map( 'trim', explode( ',', $group ) ); + } + return $group; + } +} diff --git a/www/wiki/includes/logging/TagLogFormatter.php b/www/wiki/includes/logging/TagLogFormatter.php new file mode 100644 index 00000000..230d13b6 --- /dev/null +++ b/www/wiki/includes/logging/TagLogFormatter.php @@ -0,0 +1,53 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +/** + * This class formats tag log entries. + * + * Parameters (one-based indexes): + * 4::revid + * 5::logid + * 6:list:tagsAdded + * 7:number:tagsAddedCount + * 8:list:tagsRemoved + * 9:number:tagsRemovedCount + * + * @since 1.25 + */ +class TagLogFormatter extends LogFormatter { + protected function getMessageKey() { + $key = parent::getMessageKey(); + $params = $this->getMessageParameters(); + + $add = ( isset( $params[6] ) && isset( $params[6]['num'] ) && $params[6]['num'] ); + $remove = ( isset( $params[8] ) && isset( $params[8]['num'] ) && $params[8]['num'] ); + $key .= ( $remove ? ( $add ? '' : '-remove' ) : '-add' ); + + if ( isset( $params[3] ) && $params[3] ) { + // Messages: logentry-tag-update-add-revision, logentry-tag-update-remove-revision, + // logentry-tag-update-revision + $key .= '-revision'; + } else { + // Messages: logentry-tag-update-add-logentry, logentry-tag-update-remove-logentry, + // logentry-tag-update-logentry + $key .= '-logentry'; + } + + return $key; + } +} diff --git a/www/wiki/includes/logging/UploadLogFormatter.php b/www/wiki/includes/logging/UploadLogFormatter.php new file mode 100644 index 00000000..6c536717 --- /dev/null +++ b/www/wiki/includes/logging/UploadLogFormatter.php @@ -0,0 +1,49 @@ +<?php +/** + * Formatter for upload log entries. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later + * @since 1.25 + */ + +/** + * This class formats upload log entries. + * + * @since 1.25 + */ +class UploadLogFormatter extends LogFormatter { + + protected function getParametersForApi() { + $entry = $this->entry; + $params = $entry->getParameters(); + + static $map = [ + 'img_timestamp' => ':timestamp:img_timestamp', + ]; + foreach ( $map as $index => $key ) { + if ( isset( $params[$index] ) ) { + $params[$key] = $params[$index]; + unset( $params[$index] ); + } + } + + return $params; + } + +} diff --git a/www/wiki/includes/logging/WikitextLogFormatter.php b/www/wiki/includes/logging/WikitextLogFormatter.php new file mode 100644 index 00000000..13b55595 --- /dev/null +++ b/www/wiki/includes/logging/WikitextLogFormatter.php @@ -0,0 +1,35 @@ +<?php +/** + * Formatter to allow log entries to contain formatted wikitext. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later + */ + +/** + * Log formatter specifically for log entries containing wikitext. + * @since 1.31 + */ +class WikitextLogFormatter extends LogFormatter { + /** + * @return string + */ + public function getActionMessage() { + return parent::getActionMessage()->parse(); + } +} |