From: Andrew Garrett Date: Wed, 28 Jan 2009 19:08:18 +0000 (+0000) Subject: Branch merge of change-tagging branch with trunk X-Git-Tag: 1.31.0-rc.0~43173 X-Git-Url: https://git.cyclocoop.org/%7B%24www_url%7Dadmin/compta/comptes/?a=commitdiff_plain;h=9a3c1fcede0b3c8af6bf7ad56016ced03969b35d;p=lhc%2Fweb%2Fwiklou.git Branch merge of change-tagging branch with trunk -- 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). --- diff --git a/docs/hooks.txt b/docs/hooks.txt index 3fd131d9a5..2b34fc5122 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -846,6 +846,9 @@ $options: the options. Will always include either 'known' or 'broken', and may '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 diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index a349226aed..5d707f1056 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -28,6 +28,7 @@ $wgAutoloadLocalClasses = array( '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', @@ -495,6 +496,7 @@ $wgAutoloadLocalClasses = array( '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', diff --git a/includes/ChangeTags.php b/includes/ChangeTags.php new file mode 100644 index 0000000000..f83edbf3da --- /dev/null +++ b/includes/ChangeTags.php @@ -0,0 +1,163 @@ +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 diff --git a/includes/ChangesList.php b/includes/ChangesList.php index 538f188891..66e35cfad2 100644 --- a/includes/ChangesList.php +++ b/includes/ChangesList.php @@ -338,6 +338,12 @@ class ChangesList { } } } + + protected function insertTags( &$s, &$rc, &$classes ) { + list($tagSummary, $newClasses) = ChangeTags::formatSummaryRow( $rc->mAttribs['ts_tags'], 'changeslist' ); + $classes = array_merge( $classes, $newClasses ); + $s .= ' ' . $tagSummary; + } } @@ -358,6 +364,7 @@ class OldChangesList extends ChangesList { $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 ); @@ -394,6 +401,8 @@ class OldChangesList extends ChangesList { $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 @@ -409,7 +418,7 @@ class OldChangesList extends ChangesList { wfRunHooks( 'OldChangesListRecentChangesLine', array(&$this, &$s, $rc) ); wfProfileOut( __METHOD__ ); - return "$dateheader
  • $s
  • \n"; + return "$dateheader
  • $s
  • \n"; } } diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 2566a4ee77..09f6d504f9 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -1455,7 +1455,7 @@ $wgCacheEpoch = '20030516000000'; * to ensure that client-side caches don't keep obsolete copies of global * styles. */ -$wgStyleVersion = '201'; +$wgStyleVersion = '202'; # Server-side caching: diff --git a/includes/LogEventsList.php b/includes/LogEventsList.php index 129ed3471d..1cae3c7946 100644 --- a/includes/LogEventsList.php +++ b/includes/LogEventsList.php @@ -68,7 +68,7 @@ class LogEventsList { * @param $filter Boolean */ public function showOptions( $type = '', $user = '', $page = '', $pattern = '', $year = '', - $month = '', $filter = null ) + $month = '', $filter = null, $tagFilter='' ) { global $wgScript, $wgMiserMode; $action = htmlspecialchars( $wgScript ); @@ -83,7 +83,8 @@ class LogEventsList { $this->getTitleInput( $page ) . "\n" . ( !$wgMiserMode ? ($this->getTitlePattern( $pattern )."\n") : "" ) . "

    " . $this->getDateMenu( $year, $month ) . "\n" . - ( $filter ? "

    ".$this->getFilterLinks( $type, $filter )."\n" : "" ) . + Xml::tags( 'p', null, implode( ' ', ChangeTags::buildTagFilterSelector( $tagFilter ) ) ) . "\n" . + ( $filter ? "

    ".$this->getFilterLinks( $type, $filter )."\n" : "" ) . "\n" . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "

    \n" . "" ); @@ -230,6 +231,7 @@ class LogEventsList { 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) ) { @@ -357,12 +359,16 @@ class LogEventsList { $this->skin, $paramArray, true ); } + // Any tags... + list($tagDisplay, $newClasses) = ChangeTags::formatSummaryRow( $row->ts_tags, 'logevent' ); + $classes = array_merge( $classes, $newClasses ); + if( $revert != '' ) { $revert = '' . $revert . ''; } - 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"; } /** @@ -508,7 +514,7 @@ class LogPager extends ReverseChronologicalPager { * @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; @@ -519,6 +525,7 @@ class LogPager extends ReverseChronologicalPager { $this->limitUser( $user ); $this->limitTitle( $title, $pattern ); $this->getDateCond( $year, $month ); + $this->mTagFilter = $tagFilter; } public function getDefaultQuery() { @@ -643,13 +650,18 @@ class LogPager extends ReverseChronologicalPager { } 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() { @@ -700,6 +712,10 @@ class LogPager extends ReverseChronologicalPager { public function getMonth() { return $this->mMonth; } + + public function getTagFilter() { + return $this->mTagFilter; + } } /** @@ -721,6 +737,7 @@ class LogReader { $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 ) { @@ -729,7 +746,7 @@ class LogReader { } # 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 ); } /** diff --git a/includes/PageHistory.php b/includes/PageHistory.php index b3b9fc5dab..0441cf2d52 100644 --- a/includes/PageHistory.php +++ b/includes/PageHistory.php @@ -112,6 +112,7 @@ class PageHistory { */ $year = $wgRequest->getInt( 'year' ); $month = $wgRequest->getInt( 'month' ); + $tagFilter = $wgRequest->getVal( 'tagfilter' ); $action = htmlspecialchars( $wgScript ); $wgOut->addHTML( @@ -120,6 +121,7 @@ class PageHistory { 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" . '' ); @@ -129,7 +131,7 @@ class PageHistory { /** * Do the list */ - $pager = new PageHistoryPager( $this, $year, $month ); + $pager = new PageHistoryPager( $this, $year, $month, $tagFilter ); $this->linesonpage = $pager->getNumRows(); $wgOut->addHTML( $pager->getNavigationBar() . @@ -287,6 +289,7 @@ class PageHistory { $lastlink = $this->lastLink( $rev, $next, $counter ); $arbitrary = $this->diffButtons( $rev, $firstInList, $counter ); $link = $this->revLink( $rev ); + $classes = array(); $s = "($curlink) ($lastlink) $arbitrary"; @@ -355,9 +358,16 @@ class PageHistory { $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 "
  • $s
  • \n"; + $classes = implode( ' ', $classes ); + + return "
  • $s
  • \n"; } /** @@ -589,20 +599,23 @@ class PageHistory { 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; } diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php index 00eacd1ed1..1e1ced0277 100644 --- a/includes/SpecialPage.php +++ b/includes/SpecialPage.php @@ -159,6 +159,7 @@ class SpecialPage 'Randomredirect' => 'SpecialRandomredirect', 'Withoutinterwiki' => array( 'SpecialPage', 'Withoutinterwiki' ), 'Filepath' => array( 'SpecialPage', 'Filepath' ), + 'Tags' => 'SpecialTags', 'Mypage' => array( 'SpecialMypage' ), 'Mytalk' => array( 'SpecialMytalk' ), diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php index 477640be07..5723dafe3f 100644 --- a/includes/specials/SpecialContributions.php +++ b/includes/specials/SpecialContributions.php @@ -63,6 +63,8 @@ class SpecialContributions extends SpecialPage { } 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 @@ -256,6 +258,7 @@ class SpecialContributions extends SpecialPage { Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' . Xml::namespaceSelector( $this->opts['namespace'], '' ) . '' . + Xml::tags( 'p', null, implode( ' ', ChangeTags::buildTagFilterSelector( $this->opts['tagfilter'] ) ) ) . Xml::openElement( 'p' ) . '' . Xml::label( wfMsg( 'year' ), 'year' ) . ' '. @@ -307,7 +310,7 @@ class SpecialContributions extends SpecialPage { $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 ); @@ -371,13 +374,14 @@ class ContribsPager extends ReverseChronologicalPager { 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 ); @@ -392,7 +396,10 @@ class ContribsPager extends ReverseChronologicalPager { 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( @@ -404,6 +411,9 @@ class ContribsPager extends ReverseChronologicalPager { '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; } @@ -463,6 +473,7 @@ class ContribsPager extends ReverseChronologicalPager { $sk = $this->getSkin(); $rev = new Revision( $row ); + $classes = array(); $page = Title::newFromRow( $row ); $page->resetArticleId( $row->rev_page ); // use process cache @@ -521,10 +532,17 @@ class ContribsPager extends ReverseChronologicalPager { 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 = "
  • $ret
  • \n"; + + $classes = implode( ' ', $classes ); + $ret = "
  • $ret
  • \n"; wfProfileOut( __METHOD__ ); return $ret; } diff --git a/includes/specials/SpecialLog.php b/includes/specials/SpecialLog.php index 492c2608a7..2382344b8e 100644 --- a/includes/specials/SpecialLog.php +++ b/includes/specials/SpecialLog.php @@ -45,6 +45,7 @@ function wfSpecialLog( $par = '' ) { $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 ) { @@ -53,12 +54,12 @@ function wfSpecialLog( $par = '' ) { } # 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 ) { diff --git a/includes/specials/SpecialNewpages.php b/includes/specials/SpecialNewpages.php index 685e215e92..6f1a69c8ce 100644 --- a/includes/specials/SpecialNewpages.php +++ b/includes/specials/SpecialNewpages.php @@ -32,6 +32,7 @@ class SpecialNewpages extends SpecialPage { $opts->add( 'namespace', '0' ); $opts->add( 'username', '' ); $opts->add( 'feed', '' ); + $opts->add( 'tagfilter', '' ); // Set values $opts->fetchValuesFromRequest( $wgRequest ); @@ -176,6 +177,8 @@ class SpecialNewpages extends SpecialPage { } $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' ) ) . @@ -188,6 +191,14 @@ class SpecialNewpages extends SpecialPage { Xml::namespaceSelector( $namespace, 'all' ) . " " . + " + " . + $tagFilterLabel . + " + " . + $tagFilterSelector . + " + " . ($wgEnableNewpagesUserFilter ? " " . @@ -235,6 +246,9 @@ class SpecialNewpages extends SpecialPage { */ public function formatRow( $result ) { global $wgLang, $wgContLang, $wgUser; + + $classes = array(); + $dm = $wgContLang->getDirMark(); $title = Title::makeTitleSafe( $result->rc_namespace, $result->rc_title ); @@ -247,9 +261,17 @@ class SpecialNewpages extends SpecialPage { $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 "{$time} {$dm}{$plink} ({$hist}) {$dm}[{$length}] {$dm}{$ulink} {$comment}\n"; + return "{$time} {$dm}{$plink} ({$hist}) {$dm}[{$length}] {$dm}{$ulink} {$comment} {$tagDisplay}\n"; } /** @@ -378,7 +400,6 @@ class NewPagesPager extends ReverseChronologicalPager { } else { $rcIndexes = array( 'rc_timestamp' ); } - $conds[] = 'page_id = rc_cur_id'; # $wgEnableNewpagesUserFilter - temp WMF hack if( $wgEnableNewpagesUserFilter && $user ) { @@ -400,13 +421,24 @@ class NewPagesPager extends ReverseChronologicalPager { $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() { diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php index 4a77852b43..ea5166cfa4 100644 --- a/includes/specials/SpecialRecentchanges.php +++ b/includes/specials/SpecialRecentchanges.php @@ -35,6 +35,7 @@ class SpecialRecentChanges extends SpecialPage { $opts->add( 'categories', '' ); $opts->add( 'categories_any', false ); + $opts->add( 'tagfilter', '' ); return $opts; } @@ -275,13 +276,19 @@ class SpecialRecentChanges extends SpecialPage { $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? @@ -454,6 +461,8 @@ class SpecialRecentChanges extends SpecialPage { $extraOpts['category'] = $this->categoryFilterForm( $opts ); } + $extraOpts['tagfilter'] = ChangeTags::buildTagFilterSelector( $opts['tagfilter'] ); + wfRunHooks( 'SpecialRecentChangesPanel', array( &$extraOpts, $opts ) ); return $extraOpts; } diff --git a/includes/specials/SpecialRecentchangeslinked.php b/includes/specials/SpecialRecentchangeslinked.php index 1982f232d8..fb7eb3a641 100644 --- a/includes/specials/SpecialRecentchangeslinked.php +++ b/includes/specials/SpecialRecentchangeslinked.php @@ -15,6 +15,7 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges { $opts = parent::getDefaultOptions(); $opts->add( 'target', '' ); $opts->add( 'showlinkedto', false ); + $opts->add( 'tagfilter', '' ); return $opts; } @@ -83,6 +84,8 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges { $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 ) ); @@ -169,6 +172,7 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges { 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; } diff --git a/includes/specials/SpecialTags.php b/includes/specials/SpecialTags.php new file mode 100644 index 0000000000..8fc93c1af2 --- /dev/null +++ b/includes/specials/SpecialTags.php @@ -0,0 +1,75 @@ +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 = "$html
    "; + + $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 diff --git a/includes/specials/SpecialWatchlist.php b/includes/specials/SpecialWatchlist.php index 0283cf07ca..b905944084 100644 --- a/includes/specials/SpecialWatchlist.php +++ b/includes/specials/SpecialWatchlist.php @@ -218,7 +218,8 @@ function wfSpecialWatchlist( $par ) { $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 ); diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index 491c28ade4..e1982d9b3b 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -3801,4 +3801,15 @@ Enter the filename without the "{{ns:file}}:" prefix.', #Put all regex fragments above this line. Leave this line exactly as it is', +## 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', ); diff --git a/maintenance/archives/patch-change_tag.sql b/maintenance/archives/patch-change_tag.sql new file mode 100644 index 0000000000..b7bf0383c6 --- /dev/null +++ b/maintenance/archives/patch-change_tag.sql @@ -0,0 +1,31 @@ +-- 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 diff --git a/maintenance/tables.sql b/maintenance/tables.sql index f643d0874e..7329603768 100644 --- a/maintenance/tables.sql +++ b/maintenance/tables.sql @@ -1234,4 +1234,36 @@ CREATE TABLE /*_*/updatelog ( 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 diff --git a/maintenance/updaters.inc b/maintenance/updaters.inc index 2de6ac6741..9af5194fac 100644 --- a/maintenance/updaters.inc +++ b/maintenance/updaters.inc @@ -158,6 +158,9 @@ $wgUpdates = array( 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' ), ), ); diff --git a/skins/common/history.js b/skins/common/history.js index 57e6184918..6a84b99728 100644 --- a/skins/common/history.js +++ b/skins/common/history.js @@ -27,7 +27,13 @@ function diffcheck() { } 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) { @@ -42,7 +48,13 @@ function diffcheck() { 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) { @@ -55,7 +67,7 @@ function diffcheck() { } else { inputs[1].style.visibility = 'visible'; } - lis[i].className = ""; + lis[i].className = lis[i].classNameOriginal; } } }