summaryrefslogtreecommitdiff
path: root/www/wiki/includes/logging
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
committerYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
commitfc7369835258467bf97eb64f184b93691f9a9fd5 (patch)
treedaabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/includes/logging
first commit
Diffstat (limited to 'www/wiki/includes/logging')
-rw-r--r--www/wiki/includes/logging/BlockLogFormatter.php232
-rw-r--r--www/wiki/includes/logging/ContentModelLogFormatter.php34
-rw-r--r--www/wiki/includes/logging/DeleteLogFormatter.php313
-rw-r--r--www/wiki/includes/logging/ImportLogFormatter.php42
-rw-r--r--www/wiki/includes/logging/LogEntry.php865
-rw-r--r--www/wiki/includes/logging/LogEventsList.php797
-rw-r--r--www/wiki/includes/logging/LogFormatter.php993
-rw-r--r--www/wiki/includes/logging/LogPage.php488
-rw-r--r--www/wiki/includes/logging/LogPager.php462
-rw-r--r--www/wiki/includes/logging/MergeLogFormatter.php91
-rw-r--r--www/wiki/includes/logging/MoveLogFormatter.php113
-rw-r--r--www/wiki/includes/logging/NewUsersLogFormatter.php68
-rw-r--r--www/wiki/includes/logging/PageLangLogFormatter.php61
-rw-r--r--www/wiki/includes/logging/PatrolLog.php89
-rw-r--r--www/wiki/includes/logging/PatrolLogFormatter.php88
-rw-r--r--www/wiki/includes/logging/ProtectLogFormatter.php214
-rw-r--r--www/wiki/includes/logging/RightsLogFormatter.php240
-rw-r--r--www/wiki/includes/logging/TagLogFormatter.php53
-rw-r--r--www/wiki/includes/logging/UploadLogFormatter.php49
-rw-r--r--www/wiki/includes/logging/WikitextLogFormatter.php35
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 = '&lrm;' . 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( '&#160;', $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();
+ }
+}