class LogEventsList {
const NO_ACTION_LINK = 1;
+ const NO_EXTRA_USER_LINKS = 2;
private $skin;
private $out;
// Precache various messages
if( !isset( $this->message ) ) {
$messages = array( 'revertmerge', 'protect_change', 'unblocklink', 'change-blocklink',
- 'revertmove', 'undeletelink', 'undeleteviewlink', 'revdel-restore', 'rev-delundel', 'hist', 'diff',
+ 'revertmove', 'undeletelink', 'undeleteviewlink', 'revdel-restore', 'hist', 'diff',
'pipe-separator' );
foreach( $messages as $msg ) {
$this->message[$msg] = wfMsgExt( $msg, array( 'escapenoentities' ) );
// First pass to load the log names
foreach( $validTypes as $type ) {
$text = LogPage::logName( $type );
- $typesByName[$text] = $type;
+ $typesByName[$type] = $text;
}
// Second pass to sort by name
- ksort($typesByName);
+ asort($typesByName);
// Note the query type
$queryType = count($queryTypes) == 1 ? $queryTypes[0] : '';
+
+ // Always put "All public logs" on top
+ if ( isset( $typesByName[''] ) ) {
+ $all = $typesByName[''];
+ unset( $typesByName[''] );
+ $typesByName = array( '' => $all ) + $typesByName;
+ }
+
// Third pass generates sorted XHTML content
- foreach( $typesByName as $text => $type ) {
+ foreach( $typesByName as $type => $text ) {
$selected = ($type == $queryType);
// Restricted types
if ( isset($wgLogRestrictions[$type]) ) {
* @return String: Formatted HTML
*/
private function getUserInput( $user ) {
- return Xml::inputLabel( wfMsg( 'specialloguserlabel' ), 'user', 'mw-log-user', 15, $user );
+ return '<span style="white-space: nowrap">' .
+ Xml::inputLabel( wfMsg( 'specialloguserlabel' ), 'user', 'mw-log-user', 15, $user ) .
+ '</span>';
}
/**
* @return String: Formatted HTML
*/
private function getTitleInput( $title ) {
- return Xml::inputLabel( wfMsg( 'speciallogtitlelabel' ), 'page', 'mw-log-page', 20, $title );
+ return '<span style="white-space: nowrap">' .
+ Xml::inputLabel( wfMsg( 'speciallogtitlelabel' ), 'page', 'mw-log-page', 20, $title ) .
+ '</span>';
}
/**
* @return String: Formatted HTML list item
*/
public function logLine( $row ) {
- global $wgLang, $wgUser, $wgContLang;
-
+ $classes = array( 'mw-logline-' . $row->log_type );
$title = Title::makeTitle( $row->log_namespace, $row->log_title );
- $classes = array( "mw-logline-{$row->log_type}" );
- $time = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->log_timestamp ), true );
+ // Log time
+ $time = $this->logTimestamp( $row );
// User links
+ $userLink = $this->logUserLinks( $row );
+ // Extract extra parameters
+ $paramArray = LogPage::extractParams( $row->log_params );
+ // Event description
+ $action = $this->logAction( $row, $title, $paramArray );
+ // Log comment
+ $comment = $this->logComment( $row );
+ // Add review/revert links and such...
+ $revert = $this->logActionLinks( $row, $title, $paramArray, $comment );
+
+ // Some user can hide log items and have review links
+ $del = $this->getShowHideLinks( $row );
+ if( $del != '' ) $del .= ' ';
+
+ // Any tags...
+ list( $tagDisplay, $newClasses ) = ChangeTags::formatSummaryRow( $row->ts_tags, 'logevent' );
+ $classes = array_merge( $classes, $newClasses );
+
+ return Xml::tags( 'li', array( "class" => implode( ' ', $classes ) ),
+ $del . "$time $userLink $action $comment $revert $tagDisplay" ) . "\n";
+ }
+
+ private function logTimestamp( $row ) {
+ global $wgLang;
+ $time = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->log_timestamp ), true );
+ return htmlspecialchars( $time );
+ }
+
+ private function logUserLinks( $row ) {
+ $userLinks = '';
if( self::isDeleted( $row, LogPage::DELETED_USER ) ) {
- $userLink = '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
+ $userLinks = '<span class="history-deleted">' .
+ wfMsgHtml( 'rev-deleted-user' ) . '</span>';
+ } else {
+ $userLinks = $this->skin->userLink( $row->log_user, $row->user_name );
+ // Talk|Contribs links...
+ if( !( $this->flags & self::NO_EXTRA_USER_LINKS ) ) {
+ $userLinks .= $this->skin->userToolLinks(
+ $row->log_user, $row->user_name, true, 0, $row->user_editcount );
+ }
+ }
+ return $userLinks;
+ }
+
+ private function logAction( $row, $title, $paramArray ) {
+ $action = '';
+ if( self::isDeleted( $row, LogPage::DELETED_ACTION ) ) {
+ $action = '<span class="history-deleted">' .
+ wfMsgHtml( 'rev-deleted-event' ) . '</span>';
} else {
- $userLink = $this->skin->userLink( $row->log_user, $row->user_name ) .
- $this->skin->userToolLinks( $row->log_user, $row->user_name, true, 0, $row->user_editcount );
+ $action = LogPage::actionText(
+ $row->log_type, $row->log_action, $title, $this->skin, $paramArray, true );
}
- // Comment
+ return $action;
+ }
+
+ private function logComment( $row ) {
+ global $wgContLang;
+ $comment = '';
if( self::isDeleted( $row, LogPage::DELETED_COMMENT ) ) {
- $comment = '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span>';
+ $comment = '<span class="history-deleted">' .
+ wfMsgHtml( 'rev-deleted-comment' ) . '</span>';
} else {
- $comment = $wgContLang->getDirMark() . $this->skin->commentBlock( $row->log_comment );
+ $comment = $wgContLang->getDirMark() .
+ $this->skin->commentBlock( $row->log_comment );
}
- // Extract extra parameters
- $paramArray = LogPage::extractParams( $row->log_params );
- $revert = $del = '';
- // Some user can hide log items and have review links
- if( !( $this->flags & self::NO_ACTION_LINK ) && $wgUser->isAllowed( 'deletedhistory' ) ) {
- // Don't show useless link to people who cannot hide revisions
- if( $row->log_deleted || $wgUser->isAllowed( 'deleterevision' ) ) {
- $del = $this->getShowHideLinks( $row ) . ' ';
- }
+ return $comment;
+ }
+
+ // @TODO: split up!
+ private function logActionLinks( $row, $title, $paramArray, &$comment ) {
+ global $wgUser, $wgLang;
+ if( ( $this->flags & self::NO_ACTION_LINK ) // we don't want to see the action
+ || self::isDeleted( $row, LogPage::DELETED_ACTION ) ) // action is hidden
+ {
+ return '';
}
- // Add review links and such...
- if( ( $this->flags & self::NO_ACTION_LINK ) || ( $row->log_deleted & LogPage::DELETED_ACTION ) ) {
- // Action text is suppressed...
- } else if( self::typeAction( $row, 'move', 'move', 'move' ) && !empty( $paramArray[0] ) ) {
+ $revert = '';
+ if( self::typeAction( $row, 'move', 'move', 'move' ) && !empty( $paramArray[0] ) ) {
$destTitle = Title::newFromText( $paramArray[0] );
if( $destTitle ) {
$revert = '(' . $this->skin->link(
array(
'wpOldTitle' => $destTitle->getPrefixedDBkey(),
'wpNewTitle' => $title->getPrefixedDBkey(),
- 'wpReason' => wfMsgForContent( 'revertmove' ),
+ 'wpReason' => wfMsgForContent( 'revertmove' ),
'wpMovetalk' => 0
),
array( 'known', 'noclasses' )
} else {
$viewdeleted = $this->message['undeletelink'];
}
-
$revert = '(' . $this->skin->link(
SpecialPage::getTitleFor( 'Undelete' ),
$viewdeleted,
$revert .= ')';
// Show unmerge link
} else if( self::typeAction( $row, 'merge', 'merge', 'mergehistory' ) ) {
- $merge = SpecialPage::getTitleFor( 'Mergehistory' );
$revert = '(' . $this->skin->link(
- $merge,
+ SpecialPage::getTitleFor( 'MergeHistory' ),
$this->message['revertmerge'],
array(),
array(
# Fall back to a blue contributions link
$revert = $this->skin->userToolLinks( 1, $title->getDBkey() );
}
- if( $time < '20080129000000' ) {
+ $ts = wfTimestamp( TS_UNIX, $row->log_timestamp );
+ if( $ts < '20080129000000' ) {
# Suppress $comment from old entries (before 2008-01-29),
# not needed and can contain incorrect links
$comment = '';
wfRunHooks( 'LogLine', array( $row->log_type, $row->log_action, $title, $paramArray,
&$comment, &$revert, $row->log_timestamp ) );
}
- // Event description
- if( self::isDeleted( $row, LogPage::DELETED_ACTION ) ) {
- $action = '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-event' ) . '</span>';
- } else {
- $action = LogPage::actionText( $row->log_type, $row->log_action, $title,
- $this->skin, $paramArray, true );
- }
-
- // Any tags...
- list( $tagDisplay, $newClasses ) = ChangeTags::formatSummaryRow( $row->ts_tags, 'logevent' );
- $classes = array_merge( $classes, $newClasses );
-
if( $revert != '' ) {
$revert = '<span class="mw-logevent-actionlink">' . $revert . '</span>';
}
-
- $time = htmlspecialchars( $time );
-
- return Xml::tags( 'li', array( "class" => implode( ' ', $classes ) ),
- $del . $time . ' ' . $userLink . ' ' . $action . ' ' . $comment . ' ' . $revert . " $tagDisplay" ) . "\n";
+ return $revert;
}
/**
* @return string
*/
private function getShowHideLinks( $row ) {
- // If event was hidden from sysops
- if( !self::userCan( $row, LogPage::DELETED_RESTRICTED ) ) {
- $del = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ),
- '(' . $this->message['rev-delundel'] . ')' );
- } else if( $row->log_type == 'suppress' ) {
- $del = ''; // No one should be hiding from the oversight log
- } else {
- $target = SpecialPage::getTitleFor( 'Log', $row->log_type );
- $page = Title::makeTitle( $row->log_namespace, $row->log_title );
- $query = array(
- 'target' => $target->getPrefixedDBkey(),
- 'type' => 'logging',
- 'ids' => $row->log_id,
- );
- $del = $this->skin->revDeleteLink( $query,
- self::isDeleted( $row, LogPage::DELETED_RESTRICTED ) );
+ global $wgUser;
+ if( ( $this->flags & self::NO_ACTION_LINK ) // we don't want to see the links
+ || $row->log_type == 'suppress' ) // no one can hide items from the suppress log
+ {
+ return '';
+ }
+ $del = '';
+ // Don't show useless link to people who cannot hide revisions
+ if( $wgUser->isAllowed( 'deletedhistory' ) ) {
+ if( $row->log_deleted || $wgUser->isAllowed( 'deleterevision' ) ) {
+ $canHide = $wgUser->isAllowed( 'deleterevision' );
+ // If event was hidden from sysops
+ if( !self::userCan( $row, LogPage::DELETED_RESTRICTED ) ) {
+ $del = $this->skin->revDeleteLinkDisabled( $canHide );
+ } else {
+ $target = SpecialPage::getTitleFor( 'Log', $row->log_type );
+ $query = array(
+ 'target' => $target->getPrefixedDBkey(),
+ 'type' => 'logging',
+ 'ids' => $row->log_id,
+ );
+ $del = $this->skin->revDeleteLink( $query,
+ self::isDeleted( $row, LogPage::DELETED_RESTRICTED ), $canHide );
+ }
+ }
}
return $del;
}
* @return Boolean
*/
public static function userCan( $row, $field ) {
- if( $row->log_deleted & $field ) {
+ return self::userCanBitfield( $row->log_deleted, $field );
+ }
+
+ /**
+ * Determine if the current user is allowed to view a particular
+ * field of this log row, if it's marked as deleted.
+ * @param $bitfield Integer (current field)
+ * @param $field Integer
+ * @return Boolean
+ */
+ public static function userCanBitfield( $bitfield, $field ) {
+ if( $bitfield & $field ) {
global $wgUser;
$permission = '';
- if ( $row->log_deleted & LogPage::DELETED_RESTRICTED ) {
+ if ( $bitfield & LogPage::DELETED_RESTRICTED ) {
$permission = 'suppressrevision';
- } elseif ( $field & LogPage::DELETED_TEXT ) {
- $permission = 'deletedtext';
} else {
$permission = 'deletedhistory';
}
- wfDebug( "Checking for $permission due to $field match on $row->log_deleted\n" );
+ wfDebug( "Checking for $permission due to $field match on $bitfield\n" );
return $wgUser->isAllowed( $permission );
} else {
return true;
* that are processed with wgMsgExt and option 'parse'
* - offset Set to overwrite offset parameter in $wgRequest
* 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)
* @return Integer Number of total log items (not limited by $lim)
*/
- public static function showLogExtract( &$out, $types=array(), $page='', $user='',
- $param = array() ) {
-
+ public static function showLogExtract(
+ &$out, $types=array(), $page='', $user='', $param = array()
+ ) {
+ global $wgUser, $wgOut;
$defaultParameters = array(
'lim' => 25,
'conds' => array(),
'showIfEmpty' => true,
- 'msgKey' => array('')
+ 'msgKey' => array(''),
+ 'wrap' => "$1",
+ 'flags' => 0
);
-
# The + operator appends elements of remaining keys from the right
# handed array to the left handed, whereas duplicated keys are NOT overwritten.
$param += $defaultParameters;
-
- global $wgUser, $wgOut;
# Convert $param array to individual variables
$lim = $param['lim'];
$conds = $param['conds'];
$showIfEmpty = $param['showIfEmpty'];
$msgKey = $param['msgKey'];
- if ( !is_array( $msgKey ) )
+ $wrap = $param['wrap'];
+ $flags = $param['flags'];
+ if ( !is_array( $msgKey ) ) {
$msgKey = array( $msgKey );
+ }
# Insert list of top 50 (or top $lim) items
- $loglist = new LogEventsList( $wgUser->getSkin(), $wgOut, 0 );
+ $loglist = new LogEventsList( $wgUser->getSkin(), $wgOut, $flags );
$pager = new LogPager( $loglist, $types, $user, $page, '', $conds );
- if ( isset( $param['offset'] ) ) # Tell pager to ignore $wgRequest offset
+ if ( isset( $param['offset'] ) ) { # Tell pager to ignore $wgRequest offset
$pager->setOffset( $param['offset'] );
+ }
if( $lim > 0 ) $pager->mLimit = $lim;
$logBody = $pager->getBody();
$s = '';
$loglist->endLogEventsList();
} else {
if ( $showIfEmpty )
- $s = wfMsgExt( 'logempty', array('parse') );
+ $s = Html::rawElement( 'div', array( 'class' => 'mw-warning-logempty' ),
+ wfMsgExt( 'logempty', array( 'parseinline' ) ) );
}
if( $pager->getNumRows() > $pager->mLimit ) { # Show "Full log" link
$urlParam = array();
array(),
$urlParam
);
-
}
- if ( $logBody && $msgKey[0] )
+ if ( $logBody && $msgKey[0] ) {
$s .= '</div>';
+ }
+ if ( $wrap!='' ) { // Wrap message in html
+ $s = str_replace( '$1', $s, $wrap );
+ }
+
+ // $out can be either an OutputPage object or a String-by-reference
if( $out instanceof OutputPage ){
$out->addHTML( $s );
} else {
$types = ($types === '') ? array() : (array)$types;
// Don't even show header for private logs; don't recognize it...
foreach ( $types as $type ) {
- if( isset( $wgLogRestrictions[$type] ) && !$wgUser->isAllowed($wgLogRestrictions[$type]) ) {
+ if( isset( $wgLogRestrictions[$type] )
+ && !$wgUser->isAllowed($wgLogRestrictions[$type])
+ ) {
$types = array_diff( $types, array( $type ) );
}
}
+ $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';
$this->mConds[] = $hideLogs;
}
if( count($types) ) {
- $this->types = $types;
$this->mConds['log_type'] = $types;
// Set typeCGI; used in url param for paging
if( count($types) == 1 ) $this->typeCGI = $types[0];
$this->title = $title->getPrefixedText();
$ns = $title->getNamespace();
+ $db = $this->mDb;
+
# 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
# log entries for even the busiest pages, so it can be safely scanned
# in full to satisfy an impossible condition on user or similar.
if( $pattern && !$wgMiserMode ) {
- # use escapeLike to avoid expensive search patterns like 't%st%'
- $safetitle = $this->mDb->escapeLike( $title->getDBkey() );
$this->mConds['log_namespace'] = $ns;
- $this->mConds[] = "log_title LIKE '$safetitle%'";
+ $this->mConds[] = 'log_title ' . $db->buildLike( $title->getDBkey(), $db->anyString() );
$this->pattern = $pattern;
} else {
$this->mConds['log_namespace'] = $ns;
}
// Paranoia: avoid brute force searches (bug 17342)
if( !$wgUser->isAllowed( 'deletedhistory' ) ) {
- $this->mConds[] = $this->mDb->bitAnd('log_deleted', LogPage::DELETED_ACTION) . ' = 0';
+ $this->mConds[] = $db->bitAnd('log_deleted', LogPage::DELETED_ACTION) . ' = 0';
} else if( !$wgUser->isAllowed( 'suppressrevision' ) ) {
- $this->mConds[] = $this->mDb->bitAnd('log_deleted', LogPage::SUPPRESSED_ACTION) .
+ $this->mConds[] = $db->bitAnd('log_deleted', LogPage::SUPPRESSED_ACTION) .
' != ' . LogPage::SUPPRESSED_ACTION;
}
}
public function getQueryInfo() {
+ global $wgOut;
$tables = array( 'logging', 'user' );
$this->mConds[] = 'user_id = log_user';
- $groupBy = false;
+ $index = array();
+ $options = array();
# Add log_search table if there are conditions on it
if( array_key_exists('ls_field',$this->mConds) ) {
$tables[] = 'log_search';
- $index = array( 'log_search' => 'ls_field_val', 'logging' => 'PRIMARY' );
- $groupBy = 'ls_log_id';
- # Don't use the wrong logging index
+ $index['log_search'] = 'ls_field_val';
+ $index['logging'] = 'PRIMARY';
+ $options[] = 'DISTINCT';
+ # Avoid usage of the wrong index by limiting
+ # the choices of available indexes. This mainly
+ # avoids site-breaking filesorts.
} else if( $this->title || $this->pattern || $this->user ) {
- $index = array( 'logging' => array('page_time','user_time') );
- } else if( $this->types ) {
- $index = array( 'logging' => 'type_time' );
+ $index['logging'] = array( 'page_time', 'user_time' );
+ if( count($this->types) == 1 ) {
+ $index['logging'][] = 'log_user_type_time';
+ }
+ } else if( count($this->types) == 1 ) {
+ $index['logging'] = 'type_time';
} else {
- $index = array( 'logging' => 'times' );
+ $index['logging'] = 'times';
}
- $options = array( 'USE INDEX' => $index );
+ $options['USE INDEX'] = $index;
# Don't show duplicate rows when using log_search
- if( $groupBy ) $options['GROUP BY'] = $groupBy;
$info = array(
'tables' => $tables,
'fields' => array( 'log_type', 'log_action', 'log_user', 'log_namespace',
# Add ChangeTags filter query
ChangeTags::modifyDisplayQuery( $info['tables'], $info['fields'], $info['conds'],
$info['join_conds'], $info['options'], $this->mTagFilter );
-
return $info;
}