API:
authorYuri Astrakhan <yurik@users.mediawiki.org>
Sun, 20 May 2007 10:08:40 +0000 (10:08 +0000)
committerYuri Astrakhan <yurik@users.mediawiki.org>
Sun, 20 May 2007 10:08:40 +0000 (10:08 +0000)
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
includes/api/ApiBase.php
includes/api/ApiFeedWatchlist.php
includes/api/ApiFormatBase.php
includes/api/ApiMain.php
includes/api/ApiQueryWatchlist.php
includes/api/ApiResult.php

index b2a2aa7..2933f90 100644 (file)
@@ -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 ==
 
index 34eeb8a..5ca43c3 100644 (file)
@@ -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);
                                                        }
                                                }
index 598e95d..b989ab0 100644 (file)
@@ -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) {
index e420e20..ae3d8f3 100644 (file)
@@ -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'])) {
index 234bc52..62140df 100644 (file)
@@ -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]] / <FirstnameLastname>@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 {
index 66d53e7..4ce7b6b 100644 (file)
@@ -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'
                );
index 8eaf87c..82fc8e9 100644 (file)
@@ -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.