From 1d471caa467a23b7b8f4335d3f8e0a21297ef824 Mon Sep 17 00:00:00 2001 From: Roan Kattouw Date: Sun, 1 Nov 2009 10:42:41 +0000 Subject: [PATCH] API: (bug 19004) Add support for tags. Patch by Matthew Britton --- CREDITS | 1 + RELEASE-NOTES | 1 + includes/AutoLoader.php | 1 + includes/User.php | 97 +++++++---- includes/api/ApiQuery.php | 3 +- includes/api/ApiQueryLogEvents.php | 24 +++ includes/api/ApiQueryRecentChanges.php | 24 +++ includes/api/ApiQueryRevisions.php | 36 ++++- includes/api/ApiQueryTags.php | 177 +++++++++++++++++++++ includes/api/ApiQueryUserContributions.php | 29 +++- 10 files changed, 354 insertions(+), 39 deletions(-) create mode 100644 includes/api/ApiQueryTags.php diff --git a/CREDITS b/CREDITS index 86a1e3e98a..61dea69c81 100644 --- a/CREDITS +++ b/CREDITS @@ -86,6 +86,7 @@ following names for their contribution to the product. * Marcin Cieślak * Marcus Buck * Marooned +* Matthew Britton * Matěj Grabovský * mati * Max Sikström diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 9acda732eb..fb95ca64c0 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -674,6 +674,7 @@ it from source control: http://www.mediawiki.org/wiki/Download_from_SVN drcontinue param is passed * (bug 21106) Deprecated parameters now tagged in action=paraminfo * (bug 13453) rebuildrecentchanges maintenance script works on PG again +* (bug 19004) Added support for tags === Languages updated in 1.16 === diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index d79e87d23b..a28ffd67f2 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -320,6 +320,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/User.php b/includes/User.php index e577dde4b4..d64e3e8b53 100644 --- a/includes/User.php +++ b/includes/User.php @@ -3577,7 +3577,7 @@ class User { protected function loadOptions() { global $wgCookiePrefix; $this->load(); - if ( $this->mOptionsLoaded || !$this->getId() ) + if ( $this->mOptionsLoaded ) return; $this->mOptions = self::getDefaultOptions(); @@ -3589,20 +3589,11 @@ class User { $this->mOptions[$key] = $value; } } else { - wfDebug( "Loading options for user " . $this->getId() . " from database.\n" ); - // Load from database - $dbr = wfGetDB( DB_SLAVE ); - - $res = $dbr->select( - 'user_properties', - '*', - array( 'up_user' => $this->getId() ), - __METHOD__ - ); - - while( $row = $dbr->fetchObject( $res ) ) { - $this->mOptionOverrides[$row->up_property] = $row->up_value; - $this->mOptions[$row->up_property] = $row->up_value; + $this->mOptionOverrides = array(); + if ( $this->getId() ) { + $this->loadOptionsFromDatabase(); + } else { + $this->loadOptionsFromCookie(); } //null skin if User::mId is loaded out of session data without persistant credentials @@ -3616,23 +3607,73 @@ class User { wfRunHooks( 'UserLoadOptions', array( $this, &$this->mOptions ) ); } + protected function loadOptionsFromDatabase() { + wfDebug( "Loading options for user ".$this->getId()." from database.\n" ); + // Load from database + $dbr = wfGetDB( DB_SLAVE ); + + $res = $dbr->select( + 'user_properties', + '*', + array('up_user' => $this->getId()), + __METHOD__ + ); + + while( $row = $dbr->fetchObject( $res ) ) { + $this->mOptionOverrides[$row->up_property] = $row->up_value; + $this->mOptions[$row->up_property] = $row->up_value; + } + } + + protected function loadOptionsFromCookie() { + global $wgCookiePrefix; + $cookie = $_COOKIE[$wgCookiePrefix."Options"]; + + $overrides = json_decode($cookie, true); // Load assoc array from cookie + + foreach( $overrides as $key => $value ) { + $this->mOptions[$key] = $value; + $this->mOptionOverrides[$key] = $value; + } + } + protected function saveOptions() { global $wgAllowPrefChange; - $extuser = ExternalUser::newFromUser( $this ); - $this->loadOptions(); - $dbw = wfGetDB( DB_MASTER ); - - $insert_rows = array(); $saveOptions = $this->mOptions; + $extuser = ExternalUser::newFromUser( $this ); + foreach( $saveOptions as $key => $value ) { + if ( $extuser && isset( $wgAllowPrefChange[$key] ) ) { + switch ( $wgAllowPrefChange[$key] ) { + case 'local': + case 'message': + break; + case 'semiglobal': + case 'global': + $extuser->setPref( $key, $value ); + } + } + } + // Allow hooks to abort, for instance to save to a global profile. // Reset options to default state before saving. if( !wfRunHooks( 'UserSaveOptions', array( $this, &$saveOptions ) ) ) return; + if ( $this->getId() ) { + $this->saveOptionsToDatabase( $saveOptions ); + } else { + $this->saveOptionsToCookie( $saveOptions ); + } + } + + protected function saveOptionsToDatabase( $saveOptions ) { + $dbw = wfGetDB( DB_MASTER ); + $insert_rows = array(); + foreach( $saveOptions as $key => $value ) { # Don't bother storing default values if ( ( is_null( self::getDefaultOption( $key ) ) && @@ -3644,16 +3685,6 @@ class User { 'up_value' => $value, ); } - if ( $extuser && isset( $wgAllowPrefChange[$key] ) ) { - switch ( $wgAllowPrefChange[$key] ) { - case 'local': - case 'message': - break; - case 'semiglobal': - case 'global': - $extuser->setPref( $key, $value ); - } - } } $dbw->begin(); @@ -3662,6 +3693,12 @@ class User { $dbw->commit(); } + protected function saveOptionsToCookie( $saveOptions ) { + global $wgRequest; + + $data = json_encode( $saveOptions ); + $wgRequest->response()->setCookie( 'Options', $data, 86400 * 360 ); + } /** * Provide an array of HTML 5 attributes to put on an input element * intended for the user to enter a new password. This may include diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index e3719e0d50..e15edb68a1 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', @@ -584,4 +585,4 @@ class ApiQuery extends ApiBase { $vers[] = $psModule->getVersion(); return $vers; } -} \ No newline at end of file +} diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php index 04fc366b8a..3f83bdc7d4 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,18 @@ class ApiQueryLogEvents extends ApiQueryBase { $this->addFieldsIf('log_comment', $this->fld_comment); $this->addFieldsIf('log_params', $this->fld_details); + if($this->fld_tags) { + $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->addTables('change_tag'); + $this->addJoinConds(array('change_tag' => array('INNER JOIN', array('log_id=ct_log_id')))); + $this->addWhereFld('ct_tag', $params['tag']); + } + if( !is_null($params['type']) ) { $this->addWhereFld('log_type', $params['type']); $index = 'type_time'; @@ -247,6 +260,16 @@ class ApiQueryLogEvents extends ApiQueryBase { } } + if ($this->fld_tags) { + if ($row->ts_tags) { + $tags = explode(',', $row->ts_tags); + $this->getResult()->setIndexedTagName($tags, 'tag'); + $vals['tags'] = $tags; + } else { + $vals['tags'] = array(); + } + } + return $vals; } @@ -265,6 +288,7 @@ class ApiQueryLogEvents extends ApiQueryBase { 'timestamp', 'comment', 'details', + 'tags' ) ), 'type' => array ( diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index 5f7ac53771..8e0ad4e86d 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -96,6 +96,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']); } /** @@ -211,6 +212,18 @@ class ApiQueryRecentChanges extends ApiQueryBase { $this->addFields('page_is_redirect'); } } + + if($this->fld_tags) { + $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->addTables('change_tag'); + $this->addJoinConds(array('change_tag' => array('INNER JOIN', array('rc_id=ct_rc_id')))); + $this->addWhereFld('ct_tag' , $params['tag']); + } $this->token = $params['token']; $this->addOption('LIMIT', $params['limit'] +1); $this->addOption('USE INDEX', array('recentchanges' => $index)); @@ -343,6 +356,16 @@ class ApiQueryRecentChanges extends ApiQueryBase { $row->rc_log_type, $row->rc_timestamp); } + if ($this->fld_tags) { + if ($row->ts_tags) { + $tags = explode(',', $row->ts_tags); + $this->getResult()->setIndexedTagName($tags, 'tag'); + $vals['tags'] = $tags; + } else { + $vals['tags'] = array(); + } + } + if(!is_null($this->token)) { $tokenFunctions = $this->getTokenFunctions(); @@ -416,6 +439,7 @@ class ApiQueryRecentChanges extends ApiQueryBase { 'redirect', 'patrolled', 'loginfo', + 'tags' ) ), 'token' => array( diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index 7e4f71f7d6..5066b5c15e 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']); @@ -143,6 +142,19 @@ class ApiQueryRevisions extends ApiQueryBase { $this->addFields( Revision::selectPageFields() ); } + if (isset ($prop['tags'])) { + $this->fld_tags = true; + $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->addTables('change_tag'); + $this->addJoinConds(array('change_tag' => array('INNER JOIN', array('rev_id=ct_rev_id')))); + $this->addWhereFld('ct_tag' , $params['tag']); + } + if (isset ($prop['content'])) { // For each page we will request, the user must have read rights for that page @@ -293,9 +305,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 +323,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 +366,16 @@ class ApiQueryRevisions extends ApiQueryBase { } } + if ($this->fld_tags) { + if ($row->ts_tags) { + $tags = explode(',', $row->ts_tags); + $this->getResult()->setIndexedTagName($tags, 'tag'); + $vals['tags'] = $tags; + } else { + $vals['tags'] = array(); + } + } + if(!is_null($this->token)) { $tokenFunctions = $this->getTokenFunctions(); @@ -427,6 +450,7 @@ class ApiQueryRevisions extends ApiQueryBase { 'size', 'comment', 'content', + 'tags' ) ), 'limit' => array ( diff --git a/includes/api/ApiQueryTags.php b/includes/api/ApiQueryTags.php new file mode 100644 index 0000000000..8750b59ff7 --- /dev/null +++ b/includes/api/ApiQueryTags.php @@ -0,0 +1,177 @@ +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(); + + $this->addTables('change_tag'); + $this->addFields('ct_tag'); + + if($this->fld_hitcount) + $this->addFields('count(*) AS hitcount'); + + $this->addOption('LIMIT', $this->limit + 1); + $this->addOption('GROUP BY', 'ct_tag'); + $this->addWhereRange('ct_tag', 'newer', $params['continue'], null); + + $res = $this->select(__METHOD__); + + $ok = true; + + while ( $row = $res->fetchObject() ) { + if(!$ok) break; + $ok = $this->doTag( $row->ct_tag, $row->hitcount ); + } + + //include tags with no hits yet + 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( + ), + '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 756805c506..d230564b3b 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,19 @@ 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) + { + $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->addTables('change_tag'); + $this->addJoinConds(array('change_tag' => array('INNER JOIN', array('rev_id=ct_rev_id')))); + $this->addWhereFld('ct_tag', $this->params['tag']); + } } /** @@ -292,6 +306,16 @@ class ApiQueryContributions extends ApiQueryBase { if ($this->fld_size && !is_null($row->rev_len)) $vals['size'] = intval($row->rev_len); + if ($this->fld_tags) { + if ($row->ts_tags) { + $tags = explode(',', $row->ts_tags); + $this->getResult()->setIndexedTagName($tags, 'tag'); + $vals['tags'] = $tags; + } else { + $vals['tags'] = array(); + } + } + return $vals; } @@ -343,6 +367,7 @@ class ApiQueryContributions extends ApiQueryBase { 'size', 'flags', 'patrolled', + 'tags' ) ), 'show' => array ( -- 2.20.1