From ef219bae7bc12cc97b46a91d0414282dc69d7ba8 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sun, 20 May 2007 10:08:40 +0000 Subject: [PATCH] API: breaking change: Query watchlist shows flags only when explicitly requested with wlparam=flags, and rc_this_oldid (textid) is no longer accessible query watchlist cleanup bug in the integer parameter min/max validation bug in feed formatting in error handling some documentation --- RELEASE-NOTES | 2 + includes/api/ApiBase.php | 6 +-- includes/api/ApiFeedWatchlist.php | 48 +++++++++++-------- includes/api/ApiFormatBase.php | 21 +++++++- includes/api/ApiMain.php | 57 +++++++++++++++++++--- includes/api/ApiQueryWatchlist.php | 77 +++++++++++++++++++++++------- includes/api/ApiResult.php | 28 +++++++++-- 7 files changed, 185 insertions(+), 54 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index b2a2aa7c71..2933f90f8c 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -93,6 +93,8 @@ it from source control: http://www.mediawiki.org/wiki/Download_from_SVN * list=allpages now returns a list instead of a map in json format * Breaking Change: in json, revisions are now returned as a list, not as a map. * Add: prop=info can show page is new flag, current page length, and visit counter. +* Change: Query watchlist now shows flags only when explicitly requested with wlparam=flags + rc_this_oldid (textid) is no longer accessible from query watchlist == Maintenance script changes since 1.10 == diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index 34eeb8ab63..5ca43c3924 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -377,12 +377,12 @@ abstract class ApiBase { if ($checkMin || $checkMax) { $min = $checkMin ? $paramSettings[self :: PARAM_MIN] : false; $max = $checkMax ? $paramSettings[self :: PARAM_MAX] : false; - + $values = is_array($value) ? $value : array($value); foreach ($values as $v) { - if ($checkMin && $v < $checkMin) + if ($checkMin && $v < $min) $this->dieUsage("$paramName may not be less than $min (set to $v)", $paramName); - if ($checkMax && $v > $checkMax) + if ($checkMax && $v > $max) $this->dieUsage("$paramName may not be over $max (set to $v)", $paramName); } } diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php index 598e95de24..b989ab0525 100644 --- a/includes/api/ApiFeedWatchlist.php +++ b/includes/api/ApiFeedWatchlist.php @@ -29,6 +29,10 @@ if (!defined('MEDIAWIKI')) { } /** + * This action allows users to get their watchlist items in RSS/Atom formats. + * When executed, it performs a nested call to the API to get the needed data, + * and formats it in a proper format. + * * @addtogroup API */ class ApiFeedWatchlist extends ApiBase { @@ -37,34 +41,37 @@ class ApiFeedWatchlist extends ApiBase { parent :: __construct($main, $action); } + /** + * This module uses a custom feed wrapper printer. + */ public function getCustomPrinter() { return new ApiFormatFeedWrapper($this->getMain()); } public function execute() { - $params = $this->extractRequestParams(); - - // limit to the number of hours going from now back - $endTime = wfTimestamp(TS_MW, time() - intval($params['hours'] * 60 * 60)); - - // Prepare nested request - $fauxReq = new FauxRequest(array ( - 'action' => 'query', - 'meta' => 'siteinfo', - 'siprop' => 'general', - 'list' => 'watchlist', - 'wlprop' => 'user|comment|timestamp', - 'wldir' => 'older', // reverse order - from newest to oldest - 'wlend' => $endTime, // stop at this time - 'wllimit' => 50 - )); - - // Execute - $module = new ApiMain($fauxReq); global $wgFeedClasses, $wgSitename, $wgContLanguageCode; try { + $params = $this->extractRequestParams(); + + // limit to the number of hours going from now back + $endTime = wfTimestamp(TS_MW, time() - intval($params['hours'] * 60 * 60)); + + // Prepare nested request + $fauxReq = new FauxRequest(array ( + 'action' => 'query', + 'meta' => 'siteinfo', + 'siprop' => 'general', + 'list' => 'watchlist', + 'wlprop' => 'user|comment|timestamp', + 'wldir' => 'older', // reverse order - from newest to oldest + 'wlend' => $endTime, // stop at this time + 'wllimit' => 50 + )); + + // Execute + $module = new ApiMain($fauxReq); $module->execute(); // Get data array @@ -90,7 +97,8 @@ class ApiFeedWatchlist extends ApiBase { $feedTitle = $wgSitename . ' - Error - ' . wfMsgForContent('watchlist') . ' [' . $wgContLanguageCode . ']'; $feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullUrl(); - $feed = new $wgFeedClasses[$params['feedformat']] ($feedTitle, htmlspecialchars(wfMsgForContent('watchlist')), $feedUrl); + $feedFormat = isset($params['feedformat']) ? $params['feedformat'] : 'rss'; + $feed = new $wgFeedClasses[$feedFormat] ($feedTitle, htmlspecialchars(wfMsgForContent('watchlist')), $feedUrl); if ($e instanceof UsageException) { diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php index e420e204b9..ae3d8f34fb 100644 --- a/includes/api/ApiFormatBase.php +++ b/includes/api/ApiFormatBase.php @@ -29,6 +29,8 @@ if (!defined('MEDIAWIKI')) { } /** + * This is the abstract base class for API formatters. + * * @addtogroup API */ abstract class ApiFormatBase extends ApiBase { @@ -36,7 +38,8 @@ abstract class ApiFormatBase extends ApiBase { private $mIsHtml, $mFormat; /** - * Constructor + * Create a new instance of the formatter. + * If the format name ends with 'fm', wrap its output in the proper HTML. */ public function __construct($main, $format) { parent :: __construct($main, $format); @@ -56,6 +59,11 @@ abstract class ApiFormatBase extends ApiBase { */ public abstract function getMimeType(); + /** + * If formatter outputs data results as is, the results must first be sanitized. + * An XML formatter on the other hand uses special tags, such as "_element" for special handling, + * and thus needs to override this function to return true. + */ public function getNeedsRawData() { return false; } @@ -133,6 +141,10 @@ for more information. } } + /** + * The main format printing function. Call it to output the result string to the user. + * This function will automatically output HTML when format name ends in 'fm'. + */ public function printText($text) { if ($this->getIsHtml()) echo $this->formatHTML($text); @@ -187,7 +199,7 @@ class ApiFormatFeedWrapper extends ApiFormatBase { } /** - * Call this method to initialize output data + * Call this method to initialize output data. See self::execute() */ public static function setResult($result, $feed, $feedItems) { // Store output in the Result data. @@ -211,6 +223,11 @@ class ApiFormatFeedWrapper extends ApiFormatBase { return true; } + /** + * This class expects the result data to be in a custom format set by self::setResult() + * $result['_feed'] - an instance of one of the $wgFeedClasses classes + * $result['_feeditems'] - an array of FeedItem instances + */ public function execute() { $data = $this->getResultData(); if (isset ($data['_feed']) && isset ($data['_feeditems'])) { diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 234bc527cb..62140dfbcb 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -105,28 +105,46 @@ class ApiMain extends ApiBase { $this->mSquidMaxage = 0; } - public function & getRequest() { + /** + * Return the request object that contains client's request + */ + public function getRequest() { return $this->mRequest; } + /** + * Get the ApiResult object asscosiated with current request + */ public function getResult() { return $this->mResult; } + /** + * This method will simply cause an error if the write mode was disabled for this api. + */ public function requestWriteMode() { if (!$this->mEnableWrite) $this->dieUsage('Editing of this site is disabled. Make sure the $wgEnableWriteAPI=true; ' . 'statement is included in the site\'s LocalSettings.php file', 'readonly'); } + /** + * Set how long the response should be cached. + */ public function setCacheMaxAge($maxage) { $this->mSquidMaxage = $maxage; } + /** + * Create an instance of an output formatter by its name + */ public function createPrinterByName($format) { return new $this->mFormats[$format] ($this, $format); } + /** + * Execute api request. Any errors will be handled if the API was called by the remote client. + */ public function execute() { $this->profileIn(); if ($this->mInternalMode) @@ -136,10 +154,14 @@ class ApiMain extends ApiBase { $this->profileOut(); } + /** + * Execute an action, and in case of an error, erase whatever partial results + * have been accumulated, and replace it with an error message and a help screen. + */ protected function executeActionWithErrorHandling() { // In case an error occurs during data output, - // this clear the output buffer and print just the error information + // clear the output buffer and print just the error information ob_start(); try { @@ -202,9 +224,8 @@ class ApiMain extends ApiBase { header('Expires: ' . wfTimestamp(TS_RFC2822, $expires)); header('Cache-Control: s-maxage=' . $this->mSquidMaxage . ', must-revalidate, max-age=0'); - if($this->mPrinter->getIsHtml()) { + if($this->mPrinter->getIsHtml()) echo wfReportTime(); - } ob_end_flush(); } @@ -245,7 +266,7 @@ class ApiMain extends ApiBase { } /** - * Internal printer + * Print results using the current printer */ protected function printResult($isError) { $printer = $this->mPrinter; @@ -256,6 +277,9 @@ class ApiMain extends ApiBase { $printer->profileOut(); } + /** + * See ApiBase for description. + */ protected function getAllowedParams() { return array ( 'format' => array ( @@ -270,6 +294,9 @@ class ApiMain extends ApiBase { ); } + /** + * See ApiBase for description. + */ protected function getParamDescription() { return array ( 'format' => 'The format of the output', @@ -278,6 +305,9 @@ class ApiMain extends ApiBase { ); } + /** + * See ApiBase for description. + */ protected function getDescription() { return array ( '', @@ -292,10 +322,13 @@ class ApiMain extends ApiBase { ); } + /** + * Returns an array of strings with credits for the API + */ protected function getCredits() { return array( - 'This API is being implemented by Yuri Astrakhan [[User:Yurik]] / FirstnameLastname@gmail.com', - 'Please leave your comments and suggestions at http://meta.wikimedia.org/wiki/API' + 'This API is being implemented by Yuri Astrakhan [[User:Yurik]] / @gmail.com', + 'Please leave your comments and suggestions at http://www.mediawiki.org/wiki/API' ); } @@ -335,6 +368,10 @@ class ApiMain extends ApiBase { } private $mIsBot = null; + + /** + * Returns true if the currently logged in user is a bot, false otherwise + */ public function isBot() { if (!isset ($this->mIsBot)) { global $wgUser; @@ -347,6 +384,10 @@ class ApiMain extends ApiBase { return $this->mShowVersions; } + /** + * Returns the version information of this file, plus it includes + * the versions for all files that are not callable proper API modules + */ public function getVersion() { $vers = array (); $vers[] = __CLASS__ . ': $Id$'; @@ -360,6 +401,8 @@ class ApiMain extends ApiBase { /** * This exception will be thrown when dieUsage is called to stop module execution. + * The exception handling code will print a help screen explaining how this API may be used. + * * @addtogroup API */ class UsageException extends Exception { diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php index 66d53e7ad7..4ce7b6bbb4 100644 --- a/includes/api/ApiQueryWatchlist.php +++ b/includes/api/ApiQueryWatchlist.php @@ -45,6 +45,8 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $this->run($resultPageSet); } + private $fld_patrol = false, $fld_flags = false, $fld_timestamp = false, $fld_user = false, $fld_comment = false; + private function run($resultPageSet = null) { global $wgUser; @@ -56,17 +58,19 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $allrev = $start = $end = $namespace = $dir = $limit = $prop = null; extract($this->extractRequestParams()); - $patrol = $timestamp = $user = $comment = false; if (!is_null($prop)) { if (!is_null($resultPageSet)) $this->dieUsage('prop parameter may not be used in a generator', 'params'); - $user = (false !== array_search('user', $prop)); - $comment = (false !== array_search('comment', $prop)); - $timestamp = (false !== array_search('timestamp', $prop)); // TODO: $timestamp not currently being used. - $patrol = (false !== array_search('patrol', $prop)); + $prop = array_flip($prop); - if ($patrol) { + $this->fld_flags = isset($prop['flags']); + $this->fld_user = isset($prop['user']); + $this->fld_comment = isset($prop['comment']); + $this->fld_timestamp = isset($prop['timestamp']); + $this->fld_patrol = isset($prop['patrol']); + + if ($this->fld_patrol) { global $wgUseRCPatrol, $wgUser; if (!$wgUseRCPatrol || !$wgUser->isAllowed('patrol')) $this->dieUsage('patrol property is not available', 'patrol'); @@ -76,18 +80,18 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { if (is_null($resultPageSet)) { $this->addFields(array ( 'rc_cur_id', - 'rc_this_oldid', + // 'rc_this_oldid', // Should this field be exposed? 'rc_namespace', 'rc_title', - 'rc_new', - 'rc_minor', 'rc_timestamp' )); - $this->addFieldsIf('rc_user', $user); - $this->addFieldsIf('rc_user_text', $user); - $this->addFieldsIf('rc_comment', $comment); - $this->addFieldsIf('rc_patrolled', $patrol); + $this->addFieldsIf('rc_new', $this->fld_flags); + $this->addFieldsIf('rc_minor', $this->fld_flags); + $this->addFieldsIf('rc_user', $this->fld_user); + $this->addFieldsIf('rc_user_text', $this->fld_user); + $this->addFieldsIf('rc_comment', $this->fld_comment); + $this->addFieldsIf('rc_patrolled', $this->fld_patrol); } elseif ($allrev) { $this->addFields(array ( @@ -138,8 +142,8 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { } if (is_null($resultPageSet)) { - $vals = $this->addRowInfo('rc', $row); - if($vals) + $vals = $this->extractRowInfo($row); + if ($vals) $data[] = $vals; } else { $title = Title :: makeTitle($row->rc_namespace, $row->rc_title); @@ -167,6 +171,44 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { } } + private function extractRowInfo($row) { + + $title = Title :: makeTitle($row->rc_namespace, $row->rc_title); + if (!$title->userCanRead()) + return false; + + $vals = array (); + + $vals['pageid'] = intval($row->rc_cur_id); + // $vals['textid'] = intval($row->rc_this_oldid); // Should this field be exposed? + + ApiQueryBase :: addTitleInfo($vals, $title); + + if ($this->fld_user) { + $vals['user'] = $row->rc_user_text; + if (!$row->rc_user) + $vals['anon'] = ''; + } + + if ($this->fld_flags) { + if ($row->rc_new) + $vals['new'] = ''; + if ($row->rc_minor) + $vals['minor'] = ''; + } + + if ($this->fld_patrol && isset($row->rc_patrolled)) + $vals['patrolled'] = ''; + + if ($this->fld_timestamp) + $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rc_timestamp); + + if ($this->fld_comment && !empty ($row->rc_comment)) + $vals['comment'] = $row->rc_comment; + + return $vals; + } + protected function getAllowedParams() { return array ( 'allrev' => false, @@ -197,6 +239,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { 'prop' => array ( APIBase :: PARAM_ISMULTI => true, APIBase :: PARAM_TYPE => array ( + 'flags', 'user', 'comment', 'timestamp', @@ -224,8 +267,8 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { protected function getExamples() { return array ( - 'api.php?action=query&list=watchlist', - 'api.php?action=query&list=watchlist&wlallrev', + 'api.php?action=query&list=watchlist&wlprop=timestamp|user|comment', + 'api.php?action=query&list=watchlist&wlallrev&wlprop=timestamp|user|comment', 'api.php?action=query&generator=watchlist&prop=info', 'api.php?action=query&generator=watchlist&gwlallrev&prop=revisions&rvprop=timestamp|user' ); diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php index 8eaf87c581..82fc8e9101 100644 --- a/includes/api/ApiResult.php +++ b/includes/api/ApiResult.php @@ -29,6 +29,20 @@ if (!defined('MEDIAWIKI')) { } /** + * This class represents the result of the API operations. + * It simply wraps a nested array() structure, adding some functions to simplify array's modifications. + * As various modules execute, they add different pieces of information to this result, + * structuring it as it will be given to the client. + * + * Each subarray may either be a dictionary - key-value pairs with unique keys, + * or lists, where the items are added using $data[] = $value notation. + * + * There are two special key values that change how XML output is generated: + * '_element' This key sets the tag name for the rest of the elements in the current array. + * It is only inserted if the formatter returned true for getNeedsRawData() + * '*' This key has special meaning only to the XML formatter, and is outputed as is + * for all others. In XML it becomes the content of the current element. + * * @addtogroup API */ class ApiResult extends ApiBase { @@ -44,6 +58,9 @@ class ApiResult extends ApiBase { $this->reset(); } + /** + * Clear the current result data. + */ public function reset() { $this->mData = array (); } @@ -56,10 +73,16 @@ class ApiResult extends ApiBase { $this->mIsRawMode = true; } + /** + * Returns true if the result is being created for the formatter that requested raw data. + */ public function getIsRawMode() { return $this->mIsRawMode; } + /** + * Get result's internal data array + */ public function & getData() { return $this->mData; } @@ -103,11 +126,6 @@ class ApiResult extends ApiBase { } } - // public static function makeContentElement($tag, $value) { - // $result = array(); - // ApiResult::setContent($result, ) - // } - // /** * In case the array contains indexed values (in addition to named), * all indexed values will have the given tag name. -- 2.20.1