-- Introduce tagging of individual changes (revisions, logs, and on recentchanges). The tags are customisable, and currently settable by the Abuse Filter and by the TorBlock extension. The tags can be styled on the various pages on which they appear.
-- Introduces a schema change, three new tables (valid_tag, change_tag, and tag_summary).
'LinksUpdateConstructed': At the end of LinksUpdate() is contruction.
&$linksUpdate: the LinkUpdate object
+'ListDefinedTags': When trying to find all defined tags.
+&$tags: The list of tags.
+
'LoadAllMessages': called by MessageCache::loadAllMessages() to load extensions messages
'LoadExtensionSchemaUpdates': called by maintenance/updaters.inc when upgrading database schema
'CategoryViewer' => 'includes/CategoryPage.php',
'ChangesList' => 'includes/ChangesList.php',
'ChangesFeed' => 'includes/ChangesFeed.php',
+ 'ChangeTags' => 'includes/ChangeTags.php',
'ChannelFeed' => 'includes/Feed.php',
'ConcatenatedGzipHistoryBlob' => 'includes/HistoryBlob.php',
'ConstantDependency' => 'includes/CacheDependency.php',
'SpecialSearch' => 'includes/specials/SpecialSearch.php',
'SpecialSearchOld' => 'includes/specials/SpecialSearch.php',
'SpecialStatistics' => 'includes/specials/SpecialStatistics.php',
+ 'SpecialTags' => 'includes/specials/SpecialTags.php',
'SpecialVersion' => 'includes/specials/SpecialVersion.php',
'UncategorizedCategoriesPage' => 'includes/specials/SpecialUncategorizedcategories.php',
'UncategorizedPagesPage' => 'includes/specials/SpecialUncategorizedpages.php',
--- /dev/null
+<?php
+
+if (!defined( 'MEDIAWIKI' ))
+ die;
+
+class ChangeTags {
+ static function formatSummaryRow( $tags, $page ) {
+ if (!$tags)
+ return array('',array());
+
+ $classes = array();
+
+ $tags = explode( ',', $tags );
+ $displayTags = array();
+ foreach( $tags as $tag ) {
+ $displayTags[] = self::tagDescription( $tag );
+ $classes[] = "mw-tag-$tag";
+ }
+
+ return array( '(' . implode( ', ', $displayTags ) . ')', $classes );
+ }
+
+ static function tagDescription( $tag ) {
+ $msg = wfMsgExt( "tag-$tag", 'parseinline' );
+ if ( wfEmptyMsg( "tag-$tag", $msg ) ) {
+ return htmlspecialchars($tag);
+ }
+ return $msg;
+ }
+
+ ## Basic utility method to add tags to a particular change, given its rc_id, rev_id and/or log_id.
+ static function addTags( $tags, $rc_id=null, $rev_id=null, $log_id=null, $params = null ) {
+ if ( !is_array($tags) ) {
+ $tags = array( $tags );
+ }
+
+ $tags = array_filter( $tags ); // Make sure we're submitting all tags...
+
+ if (!$rc_id && !$rev_id && !$log_id) {
+ throw new MWException( "At least one of: RCID, revision ID, and log ID MUST be specified when adding a tag to a change!" );
+ }
+
+ $dbr = wfGetDB( DB_SLAVE );
+
+ // Might as well look for rcids and so on.
+ if (!$rc_id) {
+ $dbr = wfGetDB( DB_MASTER ); // Info might be out of date, somewhat fractionally, on slave.
+ if ($log_id) {
+ $rc_id = $dbr->selectField( 'recentchanges', 'rc_id', array( 'rc_logid' => $log_id ), __METHOD__ );
+ } elseif ($rev_id) {
+ $rc_id = $dbr->selectField( 'recentchanges', 'rc_id', array( 'rc_this_oldid' => $rev_id ), __METHOD__ );
+ }
+ } elseif (!$log_id && !$rev_id) {
+ $dbr = wfGetDB( DB_MASTER ); // Info might be out of date, somewhat fractionally, on slave.
+ $log_id = $dbr->selectField( 'recentchanges', 'rc_logid', array( 'rc_id' => $rc_id ), __METHOD__ );
+ $rev_id = $dbr->selectField( 'recentchanges', 'rc_this_oldid', array( 'rc_id' => $rc_id ), __METHOD__ );
+ }
+
+ $tsConds = array_filter( array( 'ts_rc_id' => $rc_id, 'ts_rev_id' => $rev_id, 'ts_log_id' => $log_id ) );
+
+ ## Update the summary row.
+ $prevTags = $dbr->selectField( 'tag_summary', 'ts_tags', $tsConds, __METHOD__ );
+ $prevTags = $prevTags ? $prevTags : '';
+ $prevTags = array_filter( explode( ',', $prevTags ) );
+ $newTags = array_unique( array_merge( $prevTags, $tags ) );
+ sort($prevTags);
+ sort($newTags);
+
+ if ( $prevTags == $newTags ) {
+ // No change.
+ return false;
+ }
+
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->replace( 'tag_summary', array( 'ts_rev_id', 'ts_rc_id', 'ts_log_id' ), array_filter( array_merge( $tsConds, array( 'ts_tags' => implode( ',', $newTags ) ) ) ), __METHOD__ );
+
+ // Insert the tags rows.
+ $tagsRows = array();
+ foreach( $tags as $tag ) { // Filter so we don't insert NULLs as zero accidentally.
+ $tagsRows[] = array_filter( array( 'ct_tag' => $tag, 'ct_rc_id' => $rc_id, 'ct_log_id' => $log_id, 'ct_rev_id' => $rev_id, 'ct_params' => $params ) );
+ }
+
+ $dbw->insert( 'change_tag', $tagsRows, __METHOD__, array('IGNORE') );
+
+ return true;
+ }
+
+ /**
+ * Applies all tags-related changes to a query.
+ * Handles selecting tags, and filtering.
+ * Needs $tables to be set up properly, so we can figure out which join conditions to use.
+ */
+ static function modifyDisplayQuery( &$tables, &$fields, &$conds, &$join_conds, $filter_tag = false ) {
+ global $wgRequest;
+
+ if ($filter_tag === false) {
+ $filter_tag = $wgRequest->getVal( 'tagfilter' );
+ }
+
+ // Figure out which conditions can be done.
+ $join_field = '';
+ if ( in_array('recentchanges', $tables) ) {
+ $join_cond = 'rc_id';
+ } elseif( in_array('logging', $tables) ) {
+ $join_cond = 'log_id';
+ } elseif ( in_array('revision', $tables) ) {
+ $join_cond = 'rev_id';
+ } else {
+ throw new MWException( "Unable to determine appropriate JOIN condition for tagging." );
+ }
+
+ // JOIN on tag_summary
+ $tables[] = 'tag_summary';
+ $join_conds['tag_summary'] = array( 'LEFT JOIN', "ts_$join_cond=$join_cond" );
+ $fields[] = 'ts_tags';
+
+ if ($filter_tag) {
+ // Somebody wants to filter on a tag.
+ // Add an INNER JOIN on change_tag
+
+ $tables[] = 'change_tag';
+ $join_conds['change_tag'] = array( 'INNER JOIN', "ct_$join_cond=$join_cond" );
+ $conds['ct_tag'] = $filter_tag;
+ }
+ }
+
+ /**
+ * If $fullForm is set to false, then it returns an array of (label, form).
+ * If $fullForm is true, it returns an entire form.
+ */
+ static function buildTagFilterSelector( $selected='', $fullForm = false /* used to put a full form around the selector */ ) {
+ global $wgTitle;
+
+ $data = array( wfMsgExt( 'tag-filter', 'parseinline' ), Xml::input( 'tagfilter', 20, $selected ) );
+
+ if (!$fullForm) {
+ return $data;
+ }
+
+ $html = implode( ' ', $data );
+ $html .= "\n" . Xml::element( 'input', array( 'type' => 'submit', 'value' => wfMsg( 'tag-filter-submit' ) ) );
+ $html .= "\n" . Xml::hidden( 'title', $wgTitle-> getPrefixedText() );
+ $html = Xml::tags( 'form', array( 'action' => $wgTitle->getLocalURL(), 'method' => 'get' ), $html );
+
+ return $html;
+ }
+
+ /** Basically lists defined tags which count even if they aren't applied to anything */
+ static function listDefinedTags() {
+ $emptyTags = array();
+
+ // Some DB stuff
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'valid_tag', 'vt_tag', array(), __METHOD__ );
+ while( $row = $res->fetchObject() ) {
+ $emptyTags[] = $row->vt_tag;
+ }
+
+ wfRunHooks( 'ListDefinedTags', array(&$emptyTags) );
+
+ return array_filter( array_unique( $emptyTags ) );
+ }
+}
\ No newline at end of file
}
}
}
+
+ protected function insertTags( &$s, &$rc, &$classes ) {
+ list($tagSummary, $newClasses) = ChangeTags::formatSummaryRow( $rc->mAttribs['ts_tags'], 'changeslist' );
+ $classes = array_merge( $classes, $newClasses );
+ $s .= ' ' . $tagSummary;
+ }
}
$this->insertDateHeader( $dateheader, $rc->mAttribs['rc_timestamp'] );
$s = '';
+ $classes = array();
// Moved pages
if( $rc->mAttribs['rc_type'] == RC_MOVE || $rc->mAttribs['rc_type'] == RC_MOVE_OVER_REDIRECT ) {
$this->insertMove( $s, $rc );
$this->insertAction( $s, $rc );
# Edit or log comment
$this->insertComment( $s, $rc );
+ # Tags
+ $this->insertTags( $s, $rc, $classes );
# Rollback
$this->insertRollback( $s, $rc );
# Mark revision as deleted if so
wfRunHooks( 'OldChangesListRecentChangesLine', array(&$this, &$s, $rc) );
wfProfileOut( __METHOD__ );
- return "$dateheader<li>$s</li>\n";
+ return "$dateheader<li class=\"".implode( ' ', $classes )."\">$s</li>\n";
}
}
* to ensure that client-side caches don't keep obsolete copies of global
* styles.
*/
-$wgStyleVersion = '201';
+$wgStyleVersion = '202';
# Server-side caching:
* @param $filter Boolean
*/
public function showOptions( $type = '', $user = '', $page = '', $pattern = '', $year = '',
- $month = '', $filter = null )
+ $month = '', $filter = null, $tagFilter='' )
{
global $wgScript, $wgMiserMode;
$action = htmlspecialchars( $wgScript );
$this->getTitleInput( $page ) . "\n" .
( !$wgMiserMode ? ($this->getTitlePattern( $pattern )."\n") : "" ) .
"<p>" . $this->getDateMenu( $year, $month ) . "\n" .
- ( $filter ? "</p><p>".$this->getFilterLinks( $type, $filter )."\n" : "" ) .
+ Xml::tags( 'p', null, implode( ' ', ChangeTags::buildTagFilterSelector( $tagFilter ) ) ) . "\n" .
+ ( $filter ? "</p><p>".$this->getFilterLinks( $type, $filter )."\n" : "" ) . "\n" .
Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "</p>\n" .
"</fieldset></form>"
);
global $wgLang, $wgUser, $wgContLang;
$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 );
// User links
if( self::isDeleted($row,LogPage::DELETED_USER) ) {
$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>';
}
- return Xml::tags( 'li', array( "class" => "mw-logline-$row->log_type" ),
- $del . $time . ' ' . $userLink . ' ' . $action . ' ' . $comment . ' ' . $revert ) . "\n";
+ return Xml::tags( 'li', array( "class" => implode( ' ', $classes ) ),
+ $del . $time . ' ' . $userLink . ' ' . $action . ' ' . $comment . ' ' . $revert . " $tagDisplay" ) . "\n";
}
/**
* @param $month Integer
*/
public function __construct( $list, $type = '', $user = '', $title = '', $pattern = '',
- $conds = array(), $year = false, $month = false )
+ $conds = array(), $year = false, $month = false, $tagFilter = '' )
{
parent::__construct();
$this->mConds = $conds;
$this->limitUser( $user );
$this->limitTitle( $title, $pattern );
$this->getDateCond( $year, $month );
+ $this->mTagFilter = $tagFilter;
}
public function getDefaultQuery() {
} else {
$index = array( 'USE INDEX' => array( 'logging' => 'times' ) );
}
- return array(
+ $info = array(
'tables' => array( 'logging', 'user' ),
'fields' => array( 'log_type', 'log_action', 'log_user', 'log_namespace', 'log_title', 'log_params',
'log_comment', 'log_id', 'log_deleted', 'log_timestamp', 'user_name', 'user_editcount' ),
'conds' => $this->mConds,
- 'options' => $index
+ 'options' => $index,
+ 'join_conds' => array( 'user' => array( 'INNER JOIN', 'user_id=log_user' ) ),
);
+
+ ChangeTags::modifyDisplayQuery( $info['tables'], $info['fields'], $info['conds'], $info['join_conds'], $this->mTagFilter );
+
+ return $info;
}
function getIndexField() {
public function getMonth() {
return $this->mMonth;
}
+
+ public function getTagFilter() {
+ return $this->mTagFilter;
+ }
}
/**
$pattern = $request->getBool( 'pattern' );
$year = $request->getIntOrNull( 'year' );
$month = $request->getIntOrNull( 'month' );
+ $tagFilter = $request->getVal( 'tagfilter' );
# Don't let the user get stuck with a certain date
$skip = $request->getText( 'offset' ) || $request->getText( 'dir' ) == 'prev';
if( $skip ) {
}
# Use new list class to output results
$loglist = new LogEventsList( $wgUser->getSkin(), $wgOut, 0 );
- $this->pager = new LogPager( $loglist, $type, $user, $title, $pattern, $year, $month );
+ $this->pager = new LogPager( $loglist, $type, $user, $title, $pattern, $year, $month, $tagFilter );
}
/**
*/
$year = $wgRequest->getInt( 'year' );
$month = $wgRequest->getInt( 'month' );
+ $tagFilter = $wgRequest->getVal( 'tagfilter' );
$action = htmlspecialchars( $wgScript );
$wgOut->addHTML(
Xml::hidden( 'title', $this->mTitle->getPrefixedDBKey() ) . "\n" .
Xml::hidden( 'action', 'history' ) . "\n" .
$this->getDateMenu( $year, $month ) . ' ' .
+ implode( ' ', ChangeTags::buildTagFilterSelector( $tagFilter ) ) . ' ' .
Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
'</fieldset></form>'
);
/**
* Do the list
*/
- $pager = new PageHistoryPager( $this, $year, $month );
+ $pager = new PageHistoryPager( $this, $year, $month, $tagFilter );
$this->linesonpage = $pager->getNumRows();
$wgOut->addHTML(
$pager->getNavigationBar() .
$lastlink = $this->lastLink( $rev, $next, $counter );
$arbitrary = $this->diffButtons( $rev, $firstInList, $counter );
$link = $this->revLink( $rev );
+ $classes = array();
$s = "($curlink) ($lastlink) $arbitrary";
$s .= ' (' . implode( ' | ', $tools ) . ')';
}
+ # Tags
+ list($tagSummary, $newClasses) = ChangeTags::formatSummaryRow( $row->ts_tags, 'history' );
+ $classes = array_merge( $classes, $newClasses );
+ $s .= " $tagSummary";
+
wfRunHooks( 'PageHistoryLineEnding', array( $this, &$row , &$s ) );
- return "<li>$s</li>\n";
+ $classes = implode( ' ', $classes );
+
+ return "<li class=\"$classes\">$s</li>\n";
}
/**
class PageHistoryPager extends ReverseChronologicalPager {
public $mLastRow = false, $mPageHistory, $mTitle;
- function __construct( $pageHistory, $year='', $month='' ) {
+ function __construct( $pageHistory, $year='', $month='', $tagFilter = '' ) {
parent::__construct();
$this->mPageHistory = $pageHistory;
$this->mTitle =& $this->mPageHistory->mTitle;
+ $this->tagFilter = $tagFilter;
$this->getDateCond( $year, $month );
}
function getQueryInfo() {
$queryInfo = array(
'tables' => array('revision'),
- 'fields' => Revision::selectFields(),
+ 'fields' => array_merge( Revision::selectFields(), array('ts_tags') ),
'conds' => array('rev_page' => $this->mPageHistory->mTitle->getArticleID() ),
- 'options' => array( 'USE INDEX' => array('revision' => 'page_timestamp') )
+ 'options' => array( 'USE INDEX' => array('revision' => 'page_timestamp') ),
+ 'join_conds' => array( 'tag_summary' => array( 'LEFT JOIN', 'ts_rev_id=rev_id' ) ),
);
+ ChangeTags::modifyDisplayQuery( $queryInfo['tables'], $queryInfo['fields'], $queryInfo['conds'], $queryInfo['join_conds'], $this->tagFilter );
wfRunHooks( 'PageHistoryPager::getQueryInfo', array( &$this, &$queryInfo ) );
return $queryInfo;
}
'Randomredirect' => 'SpecialRandomredirect',
'Withoutinterwiki' => array( 'SpecialPage', 'Withoutinterwiki' ),
'Filepath' => array( 'SpecialPage', 'Filepath' ),
+ 'Tags' => 'SpecialTags',
'Mypage' => array( 'SpecialMypage' ),
'Mytalk' => array( 'SpecialMytalk' ),
} else {
$this->opts['namespace'] = '';
}
+
+ $this->opts['tagfilter'] = $wgRequest->getVal( 'tagfilter' );
// Allows reverts to have the bot flag in recent changes. It is just here to
// be passed in the form at the top of the page
Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' .
Xml::namespaceSelector( $this->opts['namespace'], '' ) .
'</span>' .
+ Xml::tags( 'p', null, implode( ' ', ChangeTags::buildTagFilterSelector( $this->opts['tagfilter'] ) ) ) .
Xml::openElement( 'p' ) .
'<span style="white-space: nowrap">' .
Xml::label( wfMsg( 'year' ), 'year' ) . ' '.
$target = $this->opts['target'] == 'newbies' ? 'newbies' : $nt->getText();
$pager = new ContribsPager( $target, $this->opts['namespace'],
- $this->opts['year'], $this->opts['month'] );
+ $this->opts['year'], $this->opts['month'], $this->opts['tagfilter'] );
$pager->mLimit = min( $this->opts['limit'], $wgFeedLimit );
var $messages, $target;
var $namespace = '', $mDb;
- function __construct( $target, $namespace = false, $year = false, $month = false ) {
+ function __construct( $target, $namespace = false, $year = false, $month = false, $tagFilter = false ) {
parent::__construct();
foreach( explode( ' ', 'uctop diff newarticle rollbacklink diff hist newpageletter minoreditletter' ) as $msg ) {
$this->messages[$msg] = wfMsgExt( $msg, array( 'escape') );
}
$this->target = $target;
$this->namespace = $namespace;
+ $this->tagFilter = $tagFilter;
$this->getDateCond( $year, $month );
function getQueryInfo() {
list( $tables, $index, $userCond, $join_cond ) = $this->getUserCond();
- $conds = array_merge( array('page_id=rev_page'), $userCond, $this->getNamespaceCond() );
+
+ $conds = array_merge( $userCond, $this->getNamespaceCond() );
+ $join_cond['page'] = array( 'INNER JOIN', 'page_id=rev_page' );
+
$queryInfo = array(
'tables' => $tables,
'fields' => array(
'options' => array( 'USE INDEX' => array('revision' => $index) ),
'join_conds' => $join_cond
);
+
+ ChangeTags::modifyDisplayQuery( $queryInfo['tables'], $queryInfo['fields'], $queryInfo['conds'], $queryInfo['join_conds'], $this->tagFilter );
+
wfRunHooks( 'ContribsPager::getQueryInfo', array( &$this, &$queryInfo ) );
return $queryInfo;
}
$sk = $this->getSkin();
$rev = new Revision( $row );
+ $classes = array();
$page = Title::newFromRow( $row );
$page->resetArticleId( $row->rev_page ); // use process cache
if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
$ret .= ' ' . wfMsgHtml( 'deletedrev' );
}
+
+ # Tags, if any.
+ list($tagSummary, $newClasses) = ChangeTags::formatSummaryRow( $row->ts_tags, 'contributions' );
+ $classes = array_merge( $classes, $newClasses );
+ $ret .= " $tagSummary";
+
// Let extensions add data
wfRunHooks( 'ContributionsLineEnding', array( &$this, &$ret, $row ) );
-
- $ret = "<li>$ret</li>\n";
+
+ $classes = implode( ' ', $classes );
+ $ret = "<li class=\"$classes\">$ret</li>\n";
wfProfileOut( __METHOD__ );
return $ret;
}
$pattern = $wgRequest->getBool( 'pattern' );
$y = $wgRequest->getIntOrNull( 'year' );
$m = $wgRequest->getIntOrNull( 'month' );
+ $tagFilter = $wgRequest->getVal( 'tagfilter' );
# Don't let the user get stuck with a certain date
$skip = $wgRequest->getText( 'offset' ) || $wgRequest->getText( 'dir' ) == 'prev';
if( $skip ) {
}
# Create a LogPager item to get the results and a LogEventsList item to format them...
$loglist = new LogEventsList( $wgUser->getSkin(), $wgOut, 0 );
- $pager = new LogPager( $loglist, $type, $user, $title, $pattern, array(), $y, $m );
+ $pager = new LogPager( $loglist, $type, $user, $title, $pattern, array(), $y, $m, $tagFilter );
# Set title and add header
$loglist->showHeader( $pager->getType() );
# Show form options
$loglist->showOptions( $pager->getType(), $pager->getUser(), $pager->getPage(), $pager->getPattern(),
- $pager->getYear(), $pager->getMonth(), $pager->getFilterParams() );
+ $pager->getYear(), $pager->getMonth(), $pager->getFilterParams(), $tagFilter );
# Insert list
$logBody = $pager->getBody();
if( $logBody ) {
$opts->add( 'namespace', '0' );
$opts->add( 'username', '' );
$opts->add( 'feed', '' );
+ $opts->add( 'tagfilter', '' );
// Set values
$opts->fetchValuesFromRequest( $wgRequest );
}
$hidden = implode( "\n", $hidden );
+ list( $tagFilterLabel, $tagFilterSelector ) = ChangeTags::buildTagFilterSelector( $this->opts['tagfilter'] );
+
$form = Xml::openElement( 'form', array( 'action' => $wgScript ) ) .
Xml::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) .
Xml::fieldset( wfMsg( 'newpages' ) ) .
Xml::namespaceSelector( $namespace, 'all' ) .
"</td>
</tr>" .
+ "<tr>
+ <td class='mw-label'>" .
+ $tagFilterLabel .
+ "</td>
+ <td class='mw-input'>" .
+ $tagFilterSelector .
+ "</td>
+ </tr>" .
($wgEnableNewpagesUserFilter ?
"<tr>
<td class='mw-label'>" .
*/
public function formatRow( $result ) {
global $wgLang, $wgContLang, $wgUser;
+
+ $classes = array();
+
$dm = $wgContLang->getDirMark();
$title = Title::makeTitleSafe( $result->rc_namespace, $result->rc_title );
$ulink = $this->skin->userLink( $result->rc_user, $result->rc_user_text ) . ' ' .
$this->skin->userToolLinks( $result->rc_user, $result->rc_user_text );
$comment = $this->skin->commentBlock( $result->rc_comment );
- $css = $this->patrollable( $result ) ? " class='not-patrolled'" : '';
+
+ if ( $this->patrollable( $result ) )
+ $classes[] = 'not-patrolled';
+
+ # Tags, if any.
+ list( $tagDisplay, $newClasses ) = ChangeTags::formatSummaryRow( $result->ts_tags, 'newpages' );
+ $classes = array_merge( $classes, $newClasses );
+
+ $css = count($classes) ? ' class="'.implode( " ", $classes).'"' : '';
- return "<li{$css}>{$time} {$dm}{$plink} ({$hist}) {$dm}[{$length}] {$dm}{$ulink} {$comment}</li>\n";
+ return "<li{$css}>{$time} {$dm}{$plink} ({$hist}) {$dm}[{$length}] {$dm}{$ulink} {$comment} {$tagDisplay}</li>\n";
}
/**
} else {
$rcIndexes = array( 'rc_timestamp' );
}
- $conds[] = 'page_id = rc_cur_id';
# $wgEnableNewpagesUserFilter - temp WMF hack
if( $wgEnableNewpagesUserFilter && $user ) {
$conds['page_is_redirect'] = 0;
}
- return array(
+ $info = array(
'tables' => array( 'recentchanges', 'page' ),
'fields' => 'rc_namespace,rc_title, rc_cur_id, rc_user,rc_user_text,rc_comment,
- rc_timestamp,rc_patrolled,rc_id,page_len as length, page_latest as rev_id',
+ rc_timestamp,rc_patrolled,rc_id,page_len as length, page_latest as rev_id, ts_tags',
'conds' => $conds,
- 'options' => array( 'USE INDEX' => array('recentchanges' => $rcIndexes) )
+ 'options' => array( 'USE INDEX' => array('recentchanges' => $rcIndexes) ),
+ 'join_conds' => array(
+ 'page' => array('INNER JOIN', 'page_id=rc_cur_id'),
+ ),
);
+
+ ## Empty array for fields, it'll be set by us anyway.
+ $fields = array();
+
+ ## Modify query for tags
+ ChangeTags::modifyDisplayQuery( $info['tables'], $fields, $info['conds'], $info['join_conds'], $this->opts['tagfilter'] );
+
+ return $info;
}
function getIndexField() {
$opts->add( 'categories', '' );
$opts->add( 'categories_any', false );
+ $opts->add( 'tagfilter', '' );
return $opts;
}
$namespace = $opts['namespace'];
$invert = $opts['invert'];
+ $join_conds = array();
+
// JOIN on watchlist for users
if( $uid ) {
$tables[] = 'watchlist';
- $join_conds = array( 'watchlist' => array('LEFT JOIN',
- "wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace") );
+ $join_conds['watchlist'] = array('LEFT JOIN',
+ "wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace");
}
+ // Tag stuff.
+ $fields = array(); // Fields are * in this case, so let the function modify an empty array to keep it happy.
+ ChangeTags::modifyDisplayQuery( &$tables, $fields, &$conds, &$join_conds, $opts['tagfilter'] );
+
wfRunHooks('SpecialRecentChangesQuery', array( &$conds, &$tables, &$join_conds, $opts ) );
// Is there either one namespace selected or excluded?
$extraOpts['category'] = $this->categoryFilterForm( $opts );
}
+ $extraOpts['tagfilter'] = ChangeTags::buildTagFilterSelector( $opts['tagfilter'] );
+
wfRunHooks( 'SpecialRecentChangesPanel', array( &$extraOpts, $opts ) );
return $extraOpts;
}
$opts = parent::getDefaultOptions();
$opts->add( 'target', '' );
$opts->add( 'showlinkedto', false );
+ $opts->add( 'tagfilter', '' );
return $opts;
}
$join_conds['watchlist'] = array( 'LEFT JOIN', "wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace" );
}
+ ChangeTags::modifyDisplayQuery( $tables, $select, $conds, $join_conds, $opts['tagfilter'] );
+
// XXX: parent class does this, should we too?
// wfRunHooks('SpecialRecentChangesQuery', array( &$conds, &$tables, &$join_conds, $opts ) );
Xml::input( 'target', 40, str_replace('_',' ',$opts['target']) ) .
Xml::check( 'showlinkedto', $opts['showlinkedto'], array('id' => 'showlinkedto') ) . ' ' .
Xml::label( wfMsg("recentchangeslinked-to"), 'showlinkedto' ) );
+ $extraOpts['tagfilter'] = ChangeTags::buildTagFilterSelector( $opts['tagfilter'] );
return $extraOpts;
}
--- /dev/null
+<?php
+
+if (!defined('MEDIAWIKI'))
+ die;
+
+class SpecialTags extends SpecialPage {
+
+ function __construct() {
+ parent::__construct( 'Tags' );
+ }
+
+ function execute() {
+ global $wgOut, $wgUser, $wgMessageCache;
+
+ $wgMessageCache->loadAllMessages();
+
+ $sk = $wgUser->getSkin();
+ $wgOut->setPageTitle( wfMsg( 'tags-title' ) );
+ $wgOut->addWikiMsg( 'tags-intro' );
+
+ // Write the headers
+ $html = '';
+ $html = Xml::tags( 'tr', null, Xml::tags( 'th', null, wfMsgExt( 'tags-tag', 'parseinline' ) ) .
+ Xml::tags( 'th', null, wfMsgExt( 'tags-display-header', 'parseinline' ) ) .
+ Xml::tags( 'th', null, wfMsgExt( 'tags-description-header', 'parseinline' ) ) .
+ Xml::tags( 'th', null, wfMsgExt( 'tags-hitcount-header', 'parseinline' ) )
+ );
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'change_tag', array( 'ct_tag', 'count(*) as hitcount' ), array(), __METHOD__, array( 'GROUP BY' => 'ct_tag', 'ORDER BY' => 'hitcount DESC' ) );
+
+ while ( $row = $res->fetchObject() ) {
+ $html .= $this->doTagRow( $row->ct_tag, $row->hitcount );
+ }
+
+ foreach( ChangeTags::listDefinedTags() as $tag ) {
+ $html .= $this->doTagRow( $tag, 0 );
+ }
+
+ $html = "<table style='width: 80%'><tbody>$html</tbody></table>";
+
+ $wgOut->addHTML( $html );
+ }
+
+ function doTagRow( $tag, $hitcount ) {
+ static $sk=null, $doneTags=array();
+ if (!$sk) {
+ global $wgUser;
+ $sk = $wgUser->getSkin();
+ }
+
+ if ( in_array( $tag, $doneTags ) ) {
+ return '';
+ }
+
+ $newRow = '';
+ $newRow .= Xml::tags( 'td', null, Xml::element( 'tt', null, $tag ) );
+
+ $disp = ChangeTags::tagDescription( $tag );
+ $disp .= ' (' . $sk->link( Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag" ), wfMsg( 'tag-edit' ) ) . ')';
+ $newRow .= Xml::tags( 'td', null, $disp );
+
+ $desc = wfMsgExt( "tag-$tag-description", 'parseinline' );
+ $desc = wfEmptyMsg( "tag-$tag-description", $desc ) ? '' : $desc;
+ $desc .= ' (' . $sk->link( Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag-description" ), wfMsg( 'tag-edit' ) ) . ')';
+ $newRow .= Xml::tags( 'td', null, $desc );
+
+ $hitcount = wfMsg( 'tags-hitcount', $hitcount );
+ $hitcount = $sk->link( SpecialPage::getTitleFor( 'RecentChanges' ), $hitcount, array(), array( 'tagfilter' => $tag ) );
+ $newRow .= Xml::tags( 'td', null, $hitcount );
+
+ $doneTags[] = $tag;
+
+ return Xml::tags( 'tr', null, $newRow ) . "\n";
+ }
+}
\ No newline at end of file
$tables[] = 'page';
$join_conds['page'] = array('LEFT JOIN','rc_cur_id=page_id');
}
-
+
+ ChangeTags::modifyDisplayQuery( $tables, $fields, $conds, $join_conds, '' );
wfRunHooks('SpecialWatchlistQuery', array(&$conds,&$tables,&$join_conds,&$fields) );
$res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $join_conds );
#Put all regex fragments above this line. Leave this line exactly as it is</pre>',
+## Taggng-related stuff
+'tag-filter' => '[[Special:Tags|Tag]] filter:',
+'tag-filter-submit' => 'Filter',
+'tags-title' => 'Tags',
+'tags-intro' => 'This page lists the tags that the software may mark an edit with, and their meaning.',
+'tags-tag' => 'Internal tag name',
+'tags-display-header' => 'Appearance on change lists',
+'tags-description-header' => 'Full description of meaning',
+'tags-hitcount-header' => 'Tagged edits',
+'tags-edit' => 'edit',
+'tags-hitcount' => '$1 changes',
);
--- /dev/null
+-- A table to track tags for revisions, logs and recent changes.
+-- Andrew Garrett, 2009-01
+CREATE TABLE /*_*/change_tag (
+ ct_rc_id int NULL,
+ ct_log_id int NULL,
+ ct_rev_id int NULL,
+ ct_tag varchar(255) NOT NULL,
+ ct_params BLOB NULL,
+
+ UNIQUE KEY (ct_rc_id,ct_tag),
+ UNIQUE KEY (ct_log_id,ct_tag),
+ UNIQUE KEY (ct_rev_id,ct_tag),
+ KEY (ct_tag,ct_rc_id,ct_rev_id,ct_log_id) -- Covering index, so we can pull all the info only out of the index.
+) /*$wgDBTableOptions*/;
+
+-- Rollup table to pull a LIST of tags simply without ugly GROUP_CONCAT that only works on MySQL 4.1+
+CREATE TABLE /*_*/tag_summary (
+ ts_rc_id int NULL,
+ ts_log_id int NULL,
+ ts_rev_id int NULL,
+ ts_tags BLOB NOT NULL,
+
+ UNIQUE KEY (ts_rc_id),
+ UNIQUE KEY (ts_log_id),
+ UNIQUE KEY (ts_rev_id)
+) /*$wgDBTableOptions*/;
+
+CREATE TABLE /*_*/valid_tag (
+ vt_tag varchar(255) NOT NULL,
+ PRIMARY KEY (vt_tag)
+) /*$wgDBTableOptions*/;
\ No newline at end of file
ul_key varchar(255) NOT NULL PRIMARY KEY
) /*$wgDBTableOptions*/;
+--- A table to track tags for revisions, logs and recent changes.
+REATE TABLE /*_*/change_tag (
+ ct_rc_id int NULL,
+ ct_log_id int NULL,
+ ct_rev_id int NULL,
+ ct_tag varchar(255) NOT NULL,
+ ct_params BLOB NULL,
+
+ UNIQUE KEY (ct_rc_id,ct_tag),
+ UNIQUE KEY (ct_log_id,ct_tag),
+ UNIQUE KEY (ct_rev_id,ct_tag),
+ KEY (ct_tag,ct_rc_id,ct_rev_id,ct_log_id) -- Covering index, so we can pull all the info only out of the index.
+) /*$wgDBTableOptions*/;
+
+-- Rollup table to pull a LIST of tags simply without ugly GROUP_CONCAT that only works on MySQL 4.1+
+CREATE TABLE /*_*/tag_summary (
+ ts_rc_id int NULL,
+ ts_log_id int NULL,
+ ts_rev_id int NULL,
+ ts_tags BLOB NOT NULL,
+
+ UNIQUE KEY (ts_rc_id),
+ UNIQUE KEY (ts_log_id),
+ UNIQUE KEY (ts_rev_id),
+) /*$wgDBTableOptions*/;
+
+CREATE TABLE /*_*/valid_tag (
+ vt_tag varchar(255) NOT NULL,
+ PRIMARY KEY (vt_tag)
+) /*$wgDBTableOptions*/;
+
+
-- vim: sw=2 sts=2 et
array( 'do_active_users_init' ),
array( 'add_field', 'ipblocks', 'ipb_allow_usertalk', 'patch-ipb_allow_usertalk.sql' ),
array( 'sqlite_initial_indexes' ),
+ array( 'add_table', 'change_tag', 'patch-change_tag.sql' ),
+ array( 'add_table', 'tag_summary', 'patch-change_tag.sql' ),
+ array( 'add_table', 'valid_tag', 'patch-change_tag.sql' ),
),
);
}
if (oli) { // it's the second checked radio
if (inputs[1].checked) {
- oli.className = "selected";
+ if ( (typeof oli.className) != 'undefined') {
+ oli.classNameOriginal = oli.className.replace( 'selected', '' );
+ } else {
+ oli.classNameOriginal = '';
+ }
+
+ oli.className = "selected "+oli.classNameOriginal;
return false;
}
} else if (inputs[0].checked) {
if (dli) {
inputs[1].style.visibility = 'hidden';
}
- lis[i].className = "selected";
+ if ( (typeof lis[i].className) != 'undefined') {
+ lis[i].classNameOriginal = lis[i].className.replace( 'selected', '' );
+ } else {
+ lis[i].classNameOriginal = '';
+ }
+
+ lis[i].className = "selected "+lis[i].classNameOriginal;
oli = lis[i];
} else { // no radio is checked in this row
if (!oli) {
} else {
inputs[1].style.visibility = 'visible';
}
- lis[i].className = "";
+ lis[i].className = lis[i].classNameOriginal;
}
}
}