Branch merge of change-tagging branch with trunk
authorAndrew Garrett <werdna@users.mediawiki.org>
Wed, 28 Jan 2009 19:08:18 +0000 (19:08 +0000)
committerAndrew Garrett <werdna@users.mediawiki.org>
Wed, 28 Jan 2009 19:08:18 +0000 (19:08 +0000)
-- 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).

20 files changed:
docs/hooks.txt
includes/AutoLoader.php
includes/ChangeTags.php [new file with mode: 0644]
includes/ChangesList.php
includes/DefaultSettings.php
includes/LogEventsList.php
includes/PageHistory.php
includes/SpecialPage.php
includes/specials/SpecialContributions.php
includes/specials/SpecialLog.php
includes/specials/SpecialNewpages.php
includes/specials/SpecialRecentchanges.php
includes/specials/SpecialRecentchangeslinked.php
includes/specials/SpecialTags.php [new file with mode: 0644]
includes/specials/SpecialWatchlist.php
languages/messages/MessagesEn.php
maintenance/archives/patch-change_tag.sql [new file with mode: 0644]
maintenance/tables.sql
maintenance/updaters.inc
skins/common/history.js

index 3fd131d..2b34fc5 100644 (file)
@@ -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
index a349226..5d707f1 100644 (file)
@@ -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 (file)
index 0000000..f83edbf
--- /dev/null
@@ -0,0 +1,163 @@
+<?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( '&nbsp;', $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
index 538f188..66e35cf 100644 (file)
@@ -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<li>$s</li>\n";
+               return "$dateheader<li class=\"".implode( ' ', $classes )."\">$s</li>\n";
        }
 }
 
index 2566a4e..09f6d50 100644 (file)
@@ -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:
index 129ed34..1cae3c7 100644 (file)
@@ -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") : "" ) .
                        "<p>" . $this->getDateMenu( $year, $month ) . "\n" .
-                       ( $filter ? "</p><p>".$this->getFilterLinks( $type, $filter )."\n" : "" ) .
+                       Xml::tags( 'p', null, implode( '&nbsp;', ChangeTags::buildTagFilterSelector( $tagFilter ) ) ) . "\n" .
+                       ( $filter ? "</p><p>".$this->getFilterLinks( $type, $filter )."\n" : "" ) . "\n" .
                        Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "</p>\n" .
                        "</fieldset></form>"
                );
@@ -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 = '<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";
        }
 
        /**
@@ -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 );
        }
 
        /**
index b3b9fc5..0441cf2 100644 (file)
@@ -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 ) . '&nbsp;' .
+                       implode( '&nbsp;', ChangeTags::buildTagFilterSelector( $tagFilter ) ) . '&nbsp;' .
                        Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
                        '</fieldset></form>'
                );
@@ -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 "<li>$s</li>\n";
+               $classes = implode( ' ', $classes );
+
+               return "<li class=\"$classes\">$s</li>\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;
        }
index 00eacd1..1e1ced0 100644 (file)
@@ -159,6 +159,7 @@ class SpecialPage
                'Randomredirect'            => 'SpecialRandomredirect',
                'Withoutinterwiki'          => array( 'SpecialPage', 'Withoutinterwiki' ),
                'Filepath'                  => array( 'SpecialPage', 'Filepath' ),
+               'Tags'                      => 'SpecialTags',
 
                'Mypage'                    => array( 'SpecialMypage' ),
                'Mytalk'                    => array( 'SpecialMytalk' ),
index 477640b..5723daf 100644 (file)
@@ -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'], '' ) .
                        '</span>' .
+                       Xml::tags( 'p', null, implode( '&nbsp;', ChangeTags::buildTagFilterSelector( $this->opts['tagfilter'] ) ) ) .
                        Xml::openElement( 'p' ) .
                        '<span style="white-space: nowrap">' .
                        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 = "<li>$ret</li>\n";
+
+               $classes = implode( ' ', $classes );
+               $ret = "<li class=\"$classes\">$ret</li>\n";
                wfProfileOut( __METHOD__ );
                return $ret;
        }
index 492c260..2382344 100644 (file)
@@ -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 ) {
index 685e215..6f1a69c 100644 (file)
@@ -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' ) .
                                "</td>
                        </tr>" .
+                       "<tr>
+                               <td class='mw-label'>" .
+                                       $tagFilterLabel .
+                               "</td>
+                               <td class='mw-input'>" .
+                                       $tagFilterSelector .
+                               "</td>
+                       </tr>" .
                        ($wgEnableNewpagesUserFilter ?
                        "<tr>
                                <td class='mw-label'>" .
@@ -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 "<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";
        }
 
        /**
@@ -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() {
index 4a77852..ea5166c 100644 (file)
@@ -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;
        }
index 1982f23..fb7eb3a 100644 (file)
@@ -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 (file)
index 0000000..8fc93c1
--- /dev/null
@@ -0,0 +1,75 @@
+<?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
index 0283cf0..b905944 100644 (file)
@@ -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 );
index 491c28a..e1982d9 100644 (file)
@@ -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</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',
 );
diff --git a/maintenance/archives/patch-change_tag.sql b/maintenance/archives/patch-change_tag.sql
new file mode 100644 (file)
index 0000000..b7bf038
--- /dev/null
@@ -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
index f643d08..7329603 100644 (file)
@@ -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
index 2de6ac6..9af5194 100644 (file)
@@ -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' ),
        ),
 );
 
index 57e6184..6a84b99 100644 (file)
@@ -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;
                        }
                }
        }