(bug 19004) Added support for tags to the API. Patch by Matthew Britton.
authorBryan Tong Minh <btongminh@users.mediawiki.org>
Mon, 3 Aug 2009 17:48:01 +0000 (17:48 +0000)
committerBryan Tong Minh <btongminh@users.mediawiki.org>
Mon, 3 Aug 2009 17:48:01 +0000 (17:48 +0000)
CREDITS
RELEASE-NOTES
includes/AutoLoader.php
includes/ChangeTags.php
includes/api/ApiQuery.php
includes/api/ApiQueryLogEvents.php
includes/api/ApiQueryRecentChanges.php
includes/api/ApiQueryRevisions.php
includes/api/ApiQueryTags.php [new file with mode: 0644]
includes/api/ApiQueryUserContributions.php
includes/specials/SpecialTags.php

diff --git a/CREDITS b/CREDITS
index 6aa650b..603a770 100644 (file)
--- a/CREDITS
+++ b/CREDITS
@@ -83,6 +83,7 @@ following names for their contribution to the product.
 * Marcin Cieślak
 * Marcus Buck
 * Marooned
+* Matthew Britton
 * Max Semenik
 * Michael De La Rue
 * Michael Walsh
index de2d514..dab9166 100644 (file)
@@ -426,6 +426,7 @@ this. Was used when mwEmbed was going to be an extension.
 * (bug 19907) $wgCrossSiteAJAXdomains added to allow specified (or all)
   external domains to access api.php via AJAX, if the browser supports the 
   Access-Control-Allow-Origin HTTP header
+* (bug 19004) Added support for tags to the API.
 
 === Languages updated in 1.16 ===
 
index ef2bda1..0f7e6c7 100644 (file)
@@ -304,6 +304,7 @@ $wgAutoloadLocalClasses = array(
        'ApiQueryRevisions' => 'includes/api/ApiQueryRevisions.php',
        'ApiQuerySearch' => 'includes/api/ApiQuerySearch.php',
        'ApiQuerySiteinfo' => 'includes/api/ApiQuerySiteinfo.php',
+       'ApiQueryTags' => 'includes/api/ApiQueryTags.php',
        'ApiQueryUserInfo' => 'includes/api/ApiQueryUserInfo.php',
        'ApiQueryUsers' => 'includes/api/ApiQueryUsers.php',
        'ApiQueryWatchlist' => 'includes/api/ApiQueryWatchlist.php',
index ed7e29b..25c68df 100644 (file)
@@ -185,4 +185,35 @@ class ChangeTags {
                $wgMemc->set( $key, $emptyTags, 300 );
                return $emptyTags;
        }
+       
+       /** Returns associative array of tag names and hitcounts */
+       static function getHitCounts() {
+       
+               global $wgMemc;
+               $key = wfMemcKey( 'hitcounts' );
+               
+               if ($hitcounts = $wgMemc->get( $key ))
+                       return $hitcounts;
+               
+               $dbr = wfGetDB( DB_SLAVE );
+               $hitcounts = array();
+               
+               // Fetch defined tags
+               $res = $dbr->select( 'valid_tag', 'vt_tag', array(), __METHOD__ );
+               while( $row = $res->fetchObject() ) {
+                       $hitcounts[$row->vt_tag] = 0;
+               }
+               
+               // Fetch hit counts
+               $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() ) {
+                       $hitcounts[$row->ct_tag] = $row->hitcount;
+               }
+               
+               // Short-term caching
+               $wgMemc->set( $key, $hitcounts, 300 );
+               return $hitcounts;
+       }
+       
 }
index e3719e0..fca8e30 100644 (file)
@@ -74,6 +74,7 @@ class ApiQuery extends ApiBase {
                'logevents' => 'ApiQueryLogEvents',
                'recentchanges' => 'ApiQueryRecentChanges',
                'search' => 'ApiQuerySearch',
+               'tags' => 'ApiQueryTags',
                'usercontribs' => 'ApiQueryContributions',
                'watchlist' => 'ApiQueryWatchlist',
                'watchlistraw' => 'ApiQueryWatchlistRaw',
index 04fc366..ca91d92 100644 (file)
@@ -51,6 +51,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
                $this->fld_timestamp = in_array('timestamp', $prop);
                $this->fld_comment = in_array('comment', $prop);
                $this->fld_details = in_array('details', $prop);
+               $this->fld_tags = in_array('tags', $prop);
 
                list($tbl_logging, $tbl_page, $tbl_user) = $db->tableNamesN('logging', 'page', 'user');
 
@@ -85,6 +86,16 @@ class ApiQueryLogEvents extends ApiQueryBase {
                $this->addFieldsIf('log_comment', $this->fld_comment);
                $this->addFieldsIf('log_params', $this->fld_details);
                
+               if($this->fld_tags || !is_null($params['tag'])) {
+                       $this->addTables('tag_summary');
+                       $this->addJoinConds(array('tag_summary' => array('LEFT JOIN', 'log_id=ts_log_id')));
+                       $this->addFields('ts_tags');
+               }
+               
+               if( !is_null($params['tag']) ) {
+                       $this->addWhereFld('ts_tags', $params['tag']);
+               }
+               
                if( !is_null($params['type']) ) {
                        $this->addWhereFld('log_type', $params['type']);
                        $index = 'type_time';
@@ -247,6 +258,10 @@ class ApiQueryLogEvents extends ApiQueryBase {
                        }
                }
 
+               if ($this->fld_tags && isset($row->ts_tags)) {
+                       $vals['tags'] = $row->ts_tags;
+               }
+               
                return $vals;
        }
 
@@ -265,6 +280,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
                                        'timestamp',
                                        'comment',
                                        'details',
+                                       'tags'
                                )
                        ),
                        'type' => array (
@@ -285,6 +301,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
                        ),
                        'user' => null,
                        'title' => null,
+                       'tag' => null,
                        'limit' => array (
                                ApiBase :: PARAM_DFLT => 10,
                                ApiBase :: PARAM_TYPE => 'limit',
@@ -304,6 +321,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
                        'dir' => 'In which direction to enumerate.',
                        'user' => 'Filter entries to those made by the given user.',
                        'title' => 'Filter entries to those related to a page.',
+                       'tag' => 'Only list entries with this tag',
                        'limit' => 'How many total event entries to return.'
                );
        }
index 3b7ae2c..aa5a29b 100644 (file)
@@ -42,7 +42,7 @@ class ApiQueryRecentChanges extends ApiQueryBase {
 
        private $fld_comment = false, $fld_user = false, $fld_flags = false,
                        $fld_timestamp = false, $fld_title = false, $fld_ids = false,
-                       $fld_sizes = false;
+                       $fld_sizes = false, $fld_tags = false;
        /**
         * Get an array mapping token names to their handler functions.
         * The prototype for a token function is func($pageid, $title, $rc)
@@ -174,6 +174,7 @@ class ApiQueryRecentChanges extends ApiQueryBase {
                        $this->fld_redirect = isset($prop['redirect']);
                        $this->fld_patrolled = isset($prop['patrolled']);
                        $this->fld_loginfo = isset($prop['loginfo']);
+                       $this->fld_tags = isset($prop['tags']);
 
                        global $wgUser;
                        if($this->fld_patrolled && !$wgUser->useRCPatrol() && !$wgUser->useNPPatrol())
@@ -203,6 +204,17 @@ class ApiQueryRecentChanges extends ApiQueryBase {
                                $this->addFields('page_is_redirect');
                        }
                }
+               
+               if($this->fld_tags || !is_null($params['tag'])) {
+                       $this->addTables('tag_summary');
+                       $this->addJoinConds(array('tag_summary' => array('LEFT JOIN', array('rc_id=ts_rc_id'))));
+                       $this->addFields('ts_tags');
+               }
+                       
+               if(!is_null($params['tag'])) {
+                       $this->addWhereFld('ts_tags' , $params['tag']);
+               }
+               
                $this->token = $params['token'];
                $this->addOption('LIMIT', $params['limit'] +1);
                $this->addOption('USE INDEX', array('recentchanges' => $index));
@@ -335,6 +347,10 @@ class ApiQueryRecentChanges extends ApiQueryBase {
                                $row->rc_log_type, $row->rc_timestamp);
                }
                
+               if ($this->fld_tags && isset($row->ts_tags)) {
+                       $vals['tags'] = $row->ts_tags;
+               }
+               
                if(!is_null($this->token))
                {
                        $tokenFunctions = $this->getTokenFunctions();
@@ -408,6 +424,7 @@ class ApiQueryRecentChanges extends ApiQueryBase {
                                        'redirect',
                                        'patrolled',
                                        'loginfo',
+                                       'tags',
                                )
                        ),
                        'token' => array(
@@ -443,7 +460,8 @@ class ApiQueryRecentChanges extends ApiQueryBase {
                                        'new',
                                        'log'
                                )
-                       )
+                       ),
+                       'tag' => null,
                );
        }
 
@@ -462,6 +480,7 @@ class ApiQueryRecentChanges extends ApiQueryBase {
                                'For example, to see only minor edits done by logged-in users, set show=minor|!anon'
                        ),
                        'type' => 'Which types of changes to show.',
+                       'tag' => 'Only list changes with this tag',
                        'limit' => 'How many total changes to return.'
                );
        }
index 7e4f71f..6fbb038 100644 (file)
@@ -42,7 +42,7 @@ class ApiQueryRevisions extends ApiQueryBase {
        }
 
        private $fld_ids = false, $fld_flags = false, $fld_timestamp = false, $fld_size = false,
-                       $fld_comment = false, $fld_user = false, $fld_content = false;
+                       $fld_comment = false, $fld_user = false, $fld_content = false, $fld_tags = false;
 
        protected function getTokenFunctions() {
                // tokenname => function
@@ -121,9 +121,8 @@ class ApiQueryRevisions extends ApiQueryBase {
                }
 
                $db = $this->getDB();
-               $this->addTables('revision');
+               $this->addTables(array('page', 'revision'));
                $this->addFields(Revision::selectFields());
-               $this->addTables('page');
                $this->addWhere('page_id = rev_page');
 
                $prop = array_flip($params['prop']);
@@ -135,6 +134,7 @@ class ApiQueryRevisions extends ApiQueryBase {
                $this->fld_timestamp = isset ($prop['timestamp']);
                $this->fld_comment = isset ($prop['comment']);
                $this->fld_size = isset ($prop['size']);
+               $this->fld_tags = isset ($prop['tags']);
                $this->fld_user = isset ($prop['user']);
                $this->token = $params['token'];
                $this->diffto = $params['diffto'];
@@ -143,6 +143,16 @@ class ApiQueryRevisions extends ApiQueryBase {
                        $this->addFields( Revision::selectPageFields() );
                }
 
+               if ($this->fld_tags || !is_null($params['tag'])) {
+                       $this->addTables('tag_summary');
+                       $this->addJoinConds(array('tag_summary' => array('LEFT JOIN', array('rev_id=ts_rev_id'))));
+                       $this->addFields('ts_tags');
+               }
+               
+               if( !is_null($params['tag']) ) {
+                       $this->addWhereFld('ts_tags', $params['tag']);
+               }
+               
                if (isset ($prop['content'])) {
 
                        // For each page we will request, the user must have read rights for that page
@@ -293,9 +303,9 @@ class ApiQueryRevisions extends ApiQueryBase {
                                $this->setContinueEnumParameter('startid', intval($row->rev_id));
                                break;
                        }
-                       $revision = new Revision( $row );
+                       
                        //
-                       $fit = $this->addPageSubItem($revision->getPage(), $this->extractRowInfo($revision), 'rev');
+                       $fit = $this->addPageSubItem($row->rev_page, $this->extractRowInfo($row), 'rev');
                        if(!$fit)
                        {
                                if($enumRevMode)
@@ -311,7 +321,8 @@ class ApiQueryRevisions extends ApiQueryBase {
                $db->freeResult($res);
        }
 
-       private function extractRowInfo( $revision ) {
+       private function extractRowInfo( $row ) {
+               $revision = new Revision( $row );
                $title = $revision->getTitle();
                $vals = array ();
 
@@ -353,6 +364,9 @@ class ApiQueryRevisions extends ApiQueryBase {
                        }
                }       
 
+               if ($this->fld_tags && $row->ts_tags)
+                       $vals['tags'] = $row->ts_tags;
+               
                if(!is_null($this->token))
                {
                        $tokenFunctions = $this->getTokenFunctions();
@@ -427,6 +441,7 @@ class ApiQueryRevisions extends ApiQueryBase {
                                        'size',
                                        'comment',
                                        'content',
+                                       'tags'
                                )
                        ),
                        'limit' => array (
@@ -460,6 +475,7 @@ class ApiQueryRevisions extends ApiQueryBase {
                        'excludeuser' => array(
                                ApiBase :: PARAM_TYPE => 'user'
                        ),
+                       'tag' => null,
                        'expandtemplates' => false,
                        'generatexml' => false,
                        'section' => null,
@@ -483,6 +499,7 @@ class ApiQueryRevisions extends ApiQueryBase {
                        'dir' => 'direction of enumeration - towards "newer" or "older" revisions (enum)',
                        'user' => 'only include revisions made by user',
                        'excludeuser' => 'exclude revisions made by user',
+                       'tag' => 'only list revisions with this tag',
                        'expandtemplates' => 'expand templates in revision content',
                        'generatexml' => 'generate XML parse tree for revision content',
                        'section' => 'only retrieve the content of this section',
diff --git a/includes/api/ApiQueryTags.php b/includes/api/ApiQueryTags.php
new file mode 100644 (file)
index 0000000..9b937ff
--- /dev/null
@@ -0,0 +1,167 @@
+<?php
+
+/*
+ * Created on Jul 9, 2009
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2009 Matthew Britton <firstname>.<lastname>@btinternet.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if (!defined('MEDIAWIKI')) {
+       // Eclipse helper - will be ignored in production
+       require_once ('ApiQueryBase.php');
+}
+
+/**
+ * Query module to enumerate change tags.
+ *
+ * @ingroup API
+ */
+class ApiQueryTags extends ApiQueryBase {
+       
+       private $limit, $result;
+       private $fld_displayname = false, $fld_description = false,
+                       $fld_hitcount = false;
+       
+       public function __construct($query, $moduleName) {
+               parent :: __construct($query, $moduleName, 'tg');
+       }
+
+       public function execute() {
+               $params = $this->extractRequestParams();
+               
+               $prop = array_flip($params['prop']);
+               
+               $this->fld_displayname = isset($prop['displayname']);
+               $this->fld_description = isset($prop['description']);
+               $this->fld_hitcount = isset($prop['hitcount']);
+               
+               $this->limit = $params['limit'];
+               $this->result = $this->getResult();
+               
+               $pageSet = $this->getPageSet();
+               $titles = $pageSet->getTitles();
+               $data = array();
+               $ok = true;
+               
+               if($this->fld_hitcount) {
+                       foreach( ChangeTags::getHitCounts() as $tag => $count ) {
+                               if(!$ok) break;
+                               $ok = $this->doTag( $tag, $count );
+                       }
+               } else {
+                       foreach( ChangeTags::listDefinedTags() as $tag ) {
+                               if(!$ok) break;
+                               $ok = $this->doTag( $tag, 0 );
+                       }
+               }
+               
+               $this->result->setIndexedTagName_internal(array('query', $this->getModuleName()), 'tag');
+       }
+       
+       private function doTag( $tagName, $hitcount ) {
+               static $count = 0;
+               static $doneTags = array();
+               
+               if ( in_array( $tagName, $doneTags ) ) {
+                       return true;
+               }
+               
+               if(++$count > $this->limit)
+               {
+                       $this->setContinueEnumParameter('continue', $tagName);
+                       return false;
+               }
+               
+               $tag = array();
+               $tag['name'] = $tagName;
+               
+               if($this->fld_displayname)
+                       $tag['displayname'] = ChangeTags::tagDescription( $tagName );
+               
+               if($this->fld_description)
+               {
+                       $msg = wfMsg( "tag-$tagName-description" );
+                       $msg = wfEmptyMsg( "tag-$tagName-description", $msg ) ? '' : $msg;
+                       $tag['description'] = $msg;
+               }
+                       
+               if($this->fld_hitcount)
+                       $tag['hitcount'] = $hitcount;
+               
+               $doneTags[] = $tagName;
+               
+               $fit = $this->result->addValue(array('query', $this->getModuleName()), null, $tag);
+               if(!$fit)
+               {
+                       $this->setContinueEnumParameter('continue', $tagName);
+                       return false;
+               }
+                       
+               return true;
+       }
+       
+       public function getAllowedParams() {
+               return array (
+                       'continue' => array(
+                       ),
+                       'end' => array(
+                       ),
+                       'limit' => array(
+                               ApiBase :: PARAM_DFLT => 10,
+                               ApiBase :: PARAM_TYPE => 'limit',
+                               ApiBase :: PARAM_MIN => 1,
+                               ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
+                               ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
+                       ),
+                       'prop' => array(
+                               ApiBase :: PARAM_DFLT => 'name',
+                               ApiBase :: PARAM_TYPE => array(
+                                               'name',
+                                               'displayname',
+                                               'description',
+                                               'hitcount'
+                                       ),
+                               ApiBase :: PARAM_ISMULTI => true
+                       )
+               );
+       }
+
+       public function getParamDescription() {
+               return array (
+                       'continue' => 'When more results are available, use this to continue',
+                       'limit' => 'The maximum number of tags to list',
+                       'prop' => 'Which properties to get',
+               );
+       }
+
+       public function getDescription() {
+               return 'List change tags.';
+       }
+
+       protected function getExamples() {
+               return array (
+                       'api.php?action=query&list=tags&tgprop=displayname|description|hitcount'
+               );
+       }
+
+       public function getVersion() {
+               return __CLASS__ . ': $Id: ApiQueryTags.php';
+       }
+}
index b2cca4a..297e5c8 100644 (file)
@@ -42,7 +42,7 @@ class ApiQueryContributions extends ApiQueryBase {
        private $params, $username;
        private $fld_ids = false, $fld_title = false, $fld_timestamp = false,
                        $fld_comment = false, $fld_flags = false,
-                       $fld_patrolled = false;
+                       $fld_patrolled = false, $fld_tags = false;
 
        public function execute() {
 
@@ -57,6 +57,7 @@ class ApiQueryContributions extends ApiQueryBase {
                $this->fld_flags = isset($prop['flags']);
                $this->fld_timestamp = isset($prop['timestamp']);
                $this->fld_patrolled = isset($prop['patrolled']);
+               $this->fld_tags = isset($prop['tags']);
 
                // TODO: if the query is going only against the revision table, should this be done?
                $this->selectNamedDB('contributions', DB_SLAVE, 'contributions');
@@ -141,7 +142,7 @@ class ApiQueryContributions extends ApiQueryBase {
        private function prepareQuery() {
                // We're after the revision table, and the corresponding page
                // row for anything we retrieve. We may also need the
-               // recentchanges row.
+               // recentchanges row and/or tag summary row.
                global $wgUser;
                $tables = array('page', 'revision'); // Order may change
                $this->addWhere('page_id=rev_page');
@@ -245,6 +246,16 @@ class ApiQueryContributions extends ApiQueryBase {
                $this->addFieldsIf('rev_minor_edit', $this->fld_flags);
                $this->addFieldsIf('rev_parent_id', $this->fld_flags);
                $this->addFieldsIf('rc_patrolled', $this->fld_patrolled);
+               
+               if($this->fld_tags || !is_null($this->params['tag'])) {
+                       $this->addTables('tag_summary');
+                       $this->addJoinConds(array('tag_summary' => array('LEFT JOIN', array('rev_id=ts_rev_id'))));
+                       $this->addFields('ts_tags');
+               }
+               
+               if( !is_null($this->params['tag']) ) {
+                       $this->addWhereFld('ts_tags', $this->params['tag']);
+               }
        }
 
        /**
@@ -292,6 +303,9 @@ class ApiQueryContributions extends ApiQueryBase {
                if ($this->fld_size && !is_null($row->rev_len))
                        $vals['size'] = intval($row->rev_len);
 
+               if ($this->fld_tags && $row->ts_tags)
+                       $vals['tags'] = $row->ts_tags;
+                       
                return $vals;
        }
        
@@ -343,6 +357,7 @@ class ApiQueryContributions extends ApiQueryBase {
                                        'size',
                                        'flags',
                                        'patrolled',
+                                       'tags',
                                )
                        ),
                        'show' => array (
@@ -354,6 +369,7 @@ class ApiQueryContributions extends ApiQueryBase {
                                        '!patrolled',
                                )
                        ),
+                       'tag' => null,
                );
        }
 
@@ -370,6 +386,7 @@ class ApiQueryContributions extends ApiQueryBase {
                        'prop' => 'Include additional pieces of information',
                        'show' => array('Show only items that meet this criteria, e.g. non minor edits only: show=!minor',
                                        'NOTE: if show=patrolled or show=!patrolled is set, revisions older than $wgRCMaxAge won\'t be shown',),
+                       'tag' => 'Only list contributions with this tag',
                );
        }
 
index 57feeae..7003ec9 100644 (file)
@@ -25,15 +25,9 @@ class SpecialTags extends SpecialPage {
                                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 );
+               foreach( ChangeTags::getHitCounts() as $tag => $hitcount ) {
+                       $html .= $this->doTagRow( $tag, $hitcount );
                }
 
                $wgOut->addHTML( Xml::tags( 'table', array( 'class' => 'wikitable mw-tags-table' ), $html ) );