From 1ac223539ebe2ed16bece679c08b0cc69d0b8c3a Mon Sep 17 00:00:00 2001 From: Bryan Tong Minh Date: Mon, 3 Aug 2009 17:48:01 +0000 Subject: [PATCH] (bug 19004) Added support for tags to the API. Patch by Matthew Britton. --- CREDITS | 1 + RELEASE-NOTES | 1 + includes/AutoLoader.php | 1 + includes/ChangeTags.php | 31 ++++ includes/api/ApiQuery.php | 1 + includes/api/ApiQueryLogEvents.php | 18 +++ includes/api/ApiQueryRecentChanges.php | 23 ++- includes/api/ApiQueryRevisions.php | 29 +++- includes/api/ApiQueryTags.php | 167 +++++++++++++++++++++ includes/api/ApiQueryUserContributions.php | 21 ++- includes/specials/SpecialTags.php | 10 +- 11 files changed, 285 insertions(+), 18 deletions(-) create mode 100644 includes/api/ApiQueryTags.php diff --git a/CREDITS b/CREDITS index 6aa650b829..603a770fd8 100644 --- 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 diff --git a/RELEASE-NOTES b/RELEASE-NOTES index de2d514b09..dab9166b4f 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -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 === diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index ef2bda1e37..0f7e6c7bb4 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -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', diff --git a/includes/ChangeTags.php b/includes/ChangeTags.php index ed7e29b32b..25c68dfd9c 100644 --- a/includes/ChangeTags.php +++ b/includes/ChangeTags.php @@ -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; + } + } diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index e3719e0d50..fca8e30e9c 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -74,6 +74,7 @@ class ApiQuery extends ApiBase { 'logevents' => 'ApiQueryLogEvents', 'recentchanges' => 'ApiQueryRecentChanges', 'search' => 'ApiQuerySearch', + 'tags' => 'ApiQueryTags', 'usercontribs' => 'ApiQueryContributions', 'watchlist' => 'ApiQueryWatchlist', 'watchlistraw' => 'ApiQueryWatchlistRaw', diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php index 04fc366b8a..ca91d92c9e 100644 --- a/includes/api/ApiQueryLogEvents.php +++ b/includes/api/ApiQueryLogEvents.php @@ -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.' ); } diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index 3b7ae2cede..aa5a29b897 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -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.' ); } diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index 7e4f71f7d6..6fbb0384a0 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -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 index 0000000000..9b937ff372 --- /dev/null +++ b/includes/api/ApiQueryTags.php @@ -0,0 +1,167 @@ +.@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'; + } +} diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php index b2cca4aae4..297e5c85b8 100644 --- a/includes/api/ApiQueryUserContributions.php +++ b/includes/api/ApiQueryUserContributions.php @@ -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', ); } diff --git a/includes/specials/SpecialTags.php b/includes/specials/SpecialTags.php index 57feeae7a3..7003ec931e 100644 --- a/includes/specials/SpecialTags.php +++ b/includes/specials/SpecialTags.php @@ -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 ) ); -- 2.20.1