From 370e83bb0dfd0c70de268c93bf07ad5ee0897192 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Fri, 15 Aug 2008 01:29:47 +0200 Subject: Update auf 1.13.0 --- includes/api/ApiBase.php | 134 +++++++---- includes/api/ApiBlock.php | 10 +- includes/api/ApiDelete.php | 117 ++++++--- includes/api/ApiEditPage.php | 299 +++++++++++++++++++++++ includes/api/ApiEmailUser.php | 114 +++++++++ includes/api/ApiExpandTemplates.php | 38 ++- includes/api/ApiFeedWatchlist.php | 33 ++- includes/api/ApiFormatBase.php | 51 ++-- includes/api/ApiFormatDbg.php | 5 +- includes/api/ApiFormatJson.php | 7 +- includes/api/ApiFormatJson_json.php | 19 +- includes/api/ApiFormatPhp.php | 5 +- includes/api/ApiFormatTxt.php | 5 +- includes/api/ApiFormatWddx.php | 5 +- includes/api/ApiFormatXml.php | 34 ++- includes/api/ApiFormatYaml.php | 5 +- includes/api/ApiFormatYaml_spyc.php | 235 +++++++++--------- includes/api/ApiHelp.php | 7 +- includes/api/ApiLogin.php | 73 +++--- includes/api/ApiLogout.php | 10 +- includes/api/ApiMain.php | 150 ++++++++---- includes/api/ApiMove.php | 50 ++-- includes/api/ApiOpenSearch.php | 22 +- includes/api/ApiPageSet.php | 200 ++++++++-------- includes/api/ApiParamInfo.php | 11 +- includes/api/ApiParse.php | 83 ++++--- includes/api/ApiProtect.php | 15 +- includes/api/ApiQuery.php | 67 +++--- includes/api/ApiQueryAllCategories.php | 63 +++-- includes/api/ApiQueryAllLinks.php | 49 ++-- includes/api/ApiQueryAllUsers.php | 81 ++++--- includes/api/ApiQueryAllimages.php | 205 ++++++++++++++++ includes/api/ApiQueryAllmessages.php | 25 +- includes/api/ApiQueryAllpages.php | 48 ++-- includes/api/ApiQueryBacklinks.php | 371 +++++++++++++++-------------- includes/api/ApiQueryBase.php | 208 ++++++++++++---- includes/api/ApiQueryBlocks.php | 59 ++++- includes/api/ApiQueryCategories.php | 73 +++++- includes/api/ApiQueryCategoryInfo.php | 91 +++++++ includes/api/ApiQueryCategoryMembers.php | 75 +++--- includes/api/ApiQueryDeletedrevs.php | 30 ++- includes/api/ApiQueryExtLinksUsage.php | 65 ++--- includes/api/ApiQueryExternalLinks.php | 51 +++- includes/api/ApiQueryImageInfo.php | 162 +++++++------ includes/api/ApiQueryImages.php | 70 +++++- includes/api/ApiQueryInfo.php | 344 ++++++++++++++++++++++---- includes/api/ApiQueryLangLinks.php | 61 ++++- includes/api/ApiQueryLinks.php | 78 +++++- includes/api/ApiQueryLogEvents.php | 57 +++-- includes/api/ApiQueryRandom.php | 16 +- includes/api/ApiQueryRecentChanges.php | 120 +++++++--- includes/api/ApiQueryRevisions.php | 183 ++++++++------ includes/api/ApiQuerySearch.php | 19 +- includes/api/ApiQuerySiteinfo.php | 304 +++++++++++++---------- includes/api/ApiQueryUserContributions.php | 69 ++++-- includes/api/ApiQueryUserInfo.php | 18 +- includes/api/ApiQueryUsers.php | 47 ++-- includes/api/ApiQueryWatchlist.php | 29 ++- includes/api/ApiResult.php | 36 +-- includes/api/ApiRollback.php | 13 +- includes/api/ApiUnblock.php | 11 +- includes/api/ApiUndelete.php | 17 +- 62 files changed, 3423 insertions(+), 1529 deletions(-) create mode 100644 includes/api/ApiEditPage.php create mode 100644 includes/api/ApiEmailUser.php create mode 100644 includes/api/ApiQueryAllimages.php create mode 100644 includes/api/ApiQueryCategoryInfo.php (limited to 'includes/api') diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index 3a7b5099..732adae1 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -26,15 +26,15 @@ /** * This abstract class implements many basic API functions, and is the base of all API classes. * The class functions are divided into several areas of functionality: - * + * * Module parameters: Derived classes can define getAllowedParams() to specify which parameters to expect, * how to parse and validate them. - * + * * Profiling: various methods to allow keeping tabs on various tasks and their time costs - * + * * Self-documentation: code to allow api to document its own state. - * - * @addtogroup API + * + * @ingroup API */ abstract class ApiBase { @@ -68,18 +68,18 @@ abstract class ApiBase { *****************************************************************************/ /** - * Evaluates the parameters, performs the requested query, and sets up the - * result. Concrete implementations of ApiBase must override this method to + * Evaluates the parameters, performs the requested query, and sets up the + * result. Concrete implementations of ApiBase must override this method to * provide whatever functionality their module offers. Implementations must * not produce any output on their own and are not expected to handle any - * errors. + * errors. * * The execute method will be invoked directly by ApiMain immediately before * the result of the module is output. Aside from the constructor, implementations * should assume that no other methods will be called externally on the module * before the result is processed. * - * The result data should be stored in the result object referred to by + * The result data should be stored in the result object referred to by * "getResult()". Refer to ApiResult.php for details on populating a result * object. */ @@ -93,21 +93,21 @@ abstract class ApiBase { public abstract function getVersion(); /** - * Get the name of the module being executed by this instance + * Get the name of the module being executed by this instance */ public function getModuleName() { return $this->mModuleName; } /** - * Get parameter prefix (usually two letters or an empty string). + * Get parameter prefix (usually two letters or an empty string). */ public function getModulePrefix() { return $this->mModulePrefix; - } + } /** - * Get the name of the module as shown in the profiler log + * Get the name of the module as shown in the profiler log */ public function getModuleProfileName($db = false) { if ($db) @@ -124,7 +124,7 @@ abstract class ApiBase { } /** - * Returns true if this module is the main module ($this === $this->mMainModule), + * Returns true if this module is the main module ($this === $this->mMainModule), * false otherwise. */ public function isMain() { @@ -151,10 +151,17 @@ abstract class ApiBase { } /** - * Set warning section for this module. Users should monitor this section to + * Set warning section for this module. Users should monitor this section to * notice any changes in API. */ public function setWarning($warning) { + # If there is a warning already, append it to the existing one + $data =& $this->getResult()->getData(); + if(isset($data['warnings'][$this->getModuleName()])) + { + $warning = "{$data['warnings'][$this->getModuleName()]['*']}\n$warning"; + unset($data['warnings'][$this->getModuleName()]); + } $msg = array(); ApiResult :: setContent($msg, $warning); $this->getResult()->addValue('warnings', $this->getModuleName(), $msg); @@ -163,7 +170,7 @@ abstract class ApiBase { /** * If the module may only be used with a certain format module, * it should override this method to return an instance of that formatter. - * A value of null means the default format will be used. + * A value of null means the default format will be used. */ public function getCustomPrinter() { return null; @@ -186,6 +193,9 @@ abstract class ApiBase { ); $msg = $lnPrfx . implode($lnPrfx, $msg) . "\n"; + if ($this->mustBePosted()) + $msg .= "\nThis module only accepts POST requests.\n"; + // Parameters $paramsMsg = $this->makeHelpMsgParameters(); if ($paramsMsg !== false) { @@ -207,7 +217,7 @@ abstract class ApiBase { $versions = $this->getVersion(); $pattern = '(\$.*) ([0-9a-z_]+\.php) (.*\$)'; $replacement = '\\0' . "\n " . 'http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/api/\\2'; - + if (is_array($versions)) { foreach ($versions as &$v) $v = eregi_replace($pattern, $replacement, $v); @@ -223,7 +233,7 @@ abstract class ApiBase { return $msg; } - /** + /** * Generates the parameter descriptions for this module, to be displayed in the * module's help. */ @@ -239,7 +249,7 @@ abstract class ApiBase { if (is_array($desc)) $desc = implode($paramPrefix, $desc); - $type = $paramSettings[self :: PARAM_TYPE]; + $type = isset($paramSettings[self :: PARAM_TYPE])? $paramSettings[self :: PARAM_TYPE] : null; if (isset ($type)) { if (isset ($paramSettings[self :: PARAM_ISMULTI])) $prompt = 'Values (separate with \'|\'): '; @@ -274,7 +284,7 @@ abstract class ApiBase { $intRangeStr = "The value must be no more than {$paramSettings[self :: PARAM_MAX]}"; else $intRangeStr = "The value must be between {$paramSettings[self :: PARAM_MIN]} and {$paramSettings[self :: PARAM_MAX]}"; - + $desc .= $paramPrefix . $intRangeStr; } break; @@ -324,7 +334,7 @@ abstract class ApiBase { /** * This method mangles parameter name based on the prefix supplied to the constructor. - * Override this method to change parameter name during runtime + * Override this method to change parameter name during runtime */ public function encodeParamName($paramName) { return $this->mModulePrefix . $paramName; @@ -348,12 +358,12 @@ abstract class ApiBase { } /** - * Get a value for the given parameter + * Get a value for the given parameter */ - protected function getParameter($paramName) { + protected function getParameter($paramName, $parseMaxLimit = true) { $params = $this->getAllowedParams(); $paramSettings = $params[$paramName]; - return $this->getParameterFromSettings($paramName, $paramSettings); + return $this->getParameterFromSettings($paramName, $paramSettings, $parseMaxLimit); } /** @@ -435,7 +445,7 @@ abstract class ApiBase { $value = is_array($value) ? array_map('intval', $value) : intval($value); $min = isset ($paramSettings[self :: PARAM_MIN]) ? $paramSettings[self :: PARAM_MIN] : null; $max = isset ($paramSettings[self :: PARAM_MAX]) ? $paramSettings[self :: PARAM_MAX] : null; - + if (!is_null($min) || !is_null($max)) { $values = is_array($value) ? $value : array($value); foreach ($values as $v) { @@ -495,24 +505,41 @@ abstract class ApiBase { /** * Return an array of values that were given in a 'a|b|c' notation, * after it optionally validates them against the list allowed values. - * + * * @param valueName - The name of the parameter (for error reporting) * @param value - The value being parsed * @param allowMultiple - Can $value contain more than one value separated by '|'? * @param allowedValues - An array of values to check against. If null, all values are accepted. - * @return (allowMultiple ? an_array_of_values : a_single_value) + * @return (allowMultiple ? an_array_of_values : a_single_value) */ protected function parseMultiValue($valueName, $value, $allowMultiple, $allowedValues) { - $valuesList = explode('|', $value); + if( trim($value) === "" ) + return array(); + $sizeLimit = $this->mMainModule->canApiHighLimits() ? 501 : 51; + $valuesList = explode('|', $value,$sizeLimit); + if( count($valuesList) == $sizeLimit ) { + $junk = array_pop($valuesList); // kill last jumbled param + } if (!$allowMultiple && count($valuesList) != 1) { $possibleValues = is_array($allowedValues) ? "of '" . implode("', '", $allowedValues) . "'" : ''; $this->dieUsage("Only one $possibleValues is allowed for parameter '$valueName'", "multival_$valueName"); } if (is_array($allowedValues)) { - $unknownValues = array_diff($valuesList, $allowedValues); - if ($unknownValues) { - $this->dieUsage('Unrecognised value' . (count($unknownValues) > 1 ? "s" : "") . " for parameter '$valueName'", "unknown_$valueName"); + # Check for unknown values + $unknown = array_diff($valuesList, $allowedValues); + if(!empty($unknown)) + { + if($allowMultiple) + { + $s = count($unknown) > 1 ? "s" : ""; + $vals = implode(", ", $unknown); + $this->setWarning("Unrecognized value$s for parameter '$valueName': $vals"); + } + else + $this->dieUsage("Unrecognized value for parameter '$valueName': {$valuesList[0]}", "unknown_$valueName"); } + # Now throw them out + $valuesList = array_intersect($valuesList, $allowedValues); } return $allowMultiple ? $valuesList : $valuesList[0]; @@ -544,12 +571,12 @@ abstract class ApiBase { } /** - * Call main module's error handler + * Call main module's error handler */ public function dieUsage($description, $errorCode, $httpRespCode = 0) { throw new UsageException($description, $this->encodeParamName($errorCode), $httpRespCode); } - + /** * Array that maps message keys to error messages. $1 and friends are replaced. */ @@ -557,7 +584,7 @@ abstract class ApiBase { // This one MUST be present, or dieUsageMsg() will recurse infinitely 'unknownerror' => array('code' => 'unknownerror', 'info' => "Unknown error: ``\$1''"), 'unknownerror-nocode' => array('code' => 'unknownerror', 'info' => 'Unknown error'), - + // Messages from Title::getUserPermissionsErrors() 'ns-specialprotected' => array('code' => 'unsupportednamespace', 'info' => "Pages in the Special namespace can't be edited"), 'protectedinterface' => array('code' => 'protectednamespace-interface', 'info' => "You're not allowed to edit interface messages"), @@ -578,11 +605,11 @@ abstract class ApiBase { 'confirmedittext' => array('code' => 'confirmemail', 'info' => "You must confirm your e-mail address before you can edit"), 'blockedtext' => array('code' => 'blocked', 'info' => "You have been blocked from editing"), 'autoblockedtext' => array('code' => 'autoblocked', 'info' => "Your IP address has been blocked automatically, because it was used by a blocked user"), - + // Miscellaneous interface messages 'actionthrottledtext' => array('code' => 'ratelimited', 'info' => "You've exceeded your rate limit. Please wait some time and try again"), 'alreadyrolled' => array('code' => 'alreadyrolled', 'info' => "The page you tried to rollback was already rolled back"), - 'cantrollback' => array('code' => 'onlyauthor', 'info' => "The page you tried to rollback only has one author"), + 'cantrollback' => array('code' => 'onlyauthor', 'info' => "The page you tried to rollback only has one author"), 'readonlytext' => array('code' => 'readonly', 'info' => "The wiki is currently in read-only mode"), 'sessionfailure' => array('code' => 'badtoken', 'info' => "Invalid token"), 'cannotdelete' => array('code' => 'cantdelete', 'info' => "Couldn't delete ``\$1''. Maybe it was deleted already by someone else"), @@ -593,6 +620,8 @@ abstract class ApiBase { 'protectedpage' => array('code' => 'protectedpage', 'info' => "You don't have permission to perform this move"), 'hookaborted' => array('code' => 'hookaborted', 'info' => "The modification you tried to make was aborted by an extension hook"), 'cantmove-titleprotected' => array('code' => 'protectedtitle', 'info' => "The destination article has been protected from creation"), + 'imagenocrossnamespace' => array('code' => 'nonfilenamespace', 'info' => "Can't move a file to a non-file namespace"), + 'imagetypemismatch' => array('code' => 'filetypemismatch', 'info' => "The new file extension doesn't match its type"), // 'badarticleerror' => shouldn't happen // 'badtitletext' => shouldn't happen 'ip_range_invalid' => array('code' => 'invalidrange', 'info' => "Invalid IP range"), @@ -603,7 +632,7 @@ abstract class ApiBase { 'ipb_already_blocked' => array('code' => 'alreadyblocked', 'info' => "The user you tried to block was already blocked"), 'ipb_blocked_as_range' => array('code' => 'blockedasrange', 'info' => "IP address ``\$1'' was blocked as part of range ``\$2''. You can't unblock the IP invidually, but you can unblock the range as a whole."), 'ipb_cant_unblock' => array('code' => 'cantunblock', 'info' => "The block you specified was not found. It may have been unblocked already"), - + // API-specific messages 'missingparam' => array('code' => 'no$1', 'info' => "The \$1 parameter must be set"), 'invalidtitle' => array('code' => 'invalidtitle', 'info' => "Bad title ``\$1''"), @@ -616,12 +645,28 @@ abstract class ApiBase { 'canthide' => array('code' => 'canthide', 'info' => "You don't have permission to hide user names from the block log"), 'cantblock-email' => array('code' => 'cantblock-email', 'info' => "You don't have permission to block users from sending e-mail through the wiki"), 'unblock-notarget' => array('code' => 'notarget', 'info' => "Either the id or the user parameter must be set"), - 'unblock-idanduser' => array('code' => 'idanduser', 'info' => "The id and user parameters can\'t be used together"), + 'unblock-idanduser' => array('code' => 'idanduser', 'info' => "The id and user parameters can't be used together"), 'cantunblock' => array('code' => 'permissiondenied', 'info' => "You don't have permission to unblock users"), 'cannotundelete' => array('code' => 'cantundelete', 'info' => "Couldn't undelete: the requested revisions may not exist, or may have been undeleted already"), 'permdenied-undelete' => array('code' => 'permissiondenied', 'info' => "You don't have permission to restore deleted revisions"), + 'createonly-exists' => array('code' => 'articleexists', 'info' => "The article you tried to create has been created already"), + 'nocreate-missing' => array('code' => 'missingtitle', 'info' => "The article you tried to edit doesn't exist"), + + // ApiEditPage messages + 'noimageredirect-anon' => array('code' => 'noimageredirect-anon', 'info' => "Anonymous users can't create image redirects"), + 'noimageredirect-logged' => array('code' => 'noimageredirect', 'info' => "You don't have permission to create image redirects"), + 'spamdetected' => array('code' => 'spamdetected', 'info' => "Your edit was refused because it contained a spam fragment: ``\$1''"), + 'filtered' => array('code' => 'filtered', 'info' => "The filter callback function refused your edit"), + 'contenttoobig' => array('code' => 'contenttoobig', 'info' => "The content you supplied exceeds the article size limit of \$1 bytes"), + 'noedit-anon' => array('code' => 'noedit-anon', 'info' => "Anonymous users can't edit pages"), + 'noedit' => array('code' => 'noedit', 'info' => "You don't have permission to edit pages"), + 'wasdeleted' => array('code' => 'pagedeleted', 'info' => "The page has been deleted since you fetched its timestamp"), + 'blankpage' => array('code' => 'emptypage', 'info' => "Creating new, empty pages is not allowed"), + 'editconflict' => array('code' => 'editconflict', 'info' => "Edit conflict detected"), + 'hashcheckfailed' => array('code' => 'badmd5', 'info' => "The supplied MD5 hash was incorrect"), + 'missingtext' => array('code' => 'notext', 'info' => "One of the text, appendtext and prependtext parameters must be set"), ); - + /** * Output the error message related to a certain array * @param array $error Element of a getUserPermissionsErrors() @@ -654,7 +699,7 @@ abstract class ApiBase { public function isEditMode() { return false; } - + /** * Indicates whether this module must be called with a POST request */ @@ -694,7 +739,7 @@ abstract class ApiBase { /** * When modules crash, sometimes it is needed to do a profileOut() regardless - * of the profiling state the module was in. This method does such cleanup. + * of the profiling state the module was in. This method does such cleanup. */ public function safeProfileOut() { if ($this->mTimeIn !== 0) { @@ -755,7 +800,7 @@ abstract class ApiBase { ApiBase :: dieDebug(__METHOD__, 'called without calling profileDBOut() first'); return $this->mDBTime; } - + public static function debugPrint($value, $name = 'unknown', $backtrace = false) { print "\n\n
Debuging value '$name':\n\n";
 		var_export($value);
@@ -769,7 +814,6 @@ abstract class ApiBase {
 	 * Returns a String that identifies the version of this class.
 	 */
 	public static function getBaseVersion() {
-		return __CLASS__ . ': $Id: ApiBase.php 31259 2008-02-25 14:14:55Z catrope $';
-	} 
+		return __CLASS__ . ': $Id: ApiBase.php 36309 2008-06-15 20:37:28Z catrope $';
+	}
 }
-
diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php
index e5c238ae..34813bf7 100644
--- a/includes/api/ApiBlock.php
+++ b/includes/api/ApiBlock.php
@@ -31,7 +31,7 @@ if (!defined('MEDIAWIKI')) {
 * API module that facilitates the blocking of users. Requires API write mode
 * to be enabled.
 *
- * @addtogroup API
+ * @ingroup API
  */
 class ApiBlock extends ApiBase {
 
@@ -87,17 +87,15 @@ class ApiBlock extends ApiBase {
 		$form->BlockEmail = $params['noemail'];
 		$form->BlockHideName = $params['hidename'];
 
-		$dbw = wfGetDb(DB_MASTER);
-		$dbw->begin();
+		$userID = $expiry = null;
 		$retval = $form->doBlock($userID, $expiry);
 		if(!empty($retval))
 			// We don't care about multiple errors, just report one of them
 			$this->dieUsageMsg($retval);
 
-		$dbw->commit();
 		$res['user'] = $params['user'];
 		$res['userID'] = $userID;
-		$res['expiry'] = ($expiry == Block::infinity() ? 'infinite' : $expiry);
+		$res['expiry'] = ($expiry == Block::infinity() ? 'infinite' : wfTimestamp(TS_ISO_8601, $expiry));
 		$res['reason'] = $params['reason'];
 		if($params['anononly'])
 			$res['anononly'] = '';
@@ -159,6 +157,6 @@ class ApiBlock extends ApiBase {
 	}
 
 	public function getVersion() {
-		return __CLASS__ . ': $Id: ApiBlock.php 30222 2008-01-28 19:05:26Z catrope $';
+		return __CLASS__ . ': $Id: ApiBlock.php 35388 2008-05-27 10:18:28Z catrope $';
 	}
 }
diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php
index cd747e7e..06592d46 100644
--- a/includes/api/ApiDelete.php
+++ b/includes/api/ApiDelete.php
@@ -29,10 +29,10 @@ if (!defined('MEDIAWIKI')) {
 
 
 /**
- * API module that facilitates deleting pages. The API eqivalent of action=delete. 
+ * API module that facilitates deleting pages. The API eqivalent of action=delete.
  * Requires API write mode to be enabled.
  *
- * @addtogroup API
+ * @ingroup API
  */
 class ApiDelete extends ApiBase {
 
@@ -42,16 +42,16 @@ class ApiDelete extends ApiBase {
 
 	/**
 	 * Extracts the title, token, and reason from the request parameters and invokes
-	 * the local delete() function with these as arguments. It does not make use of 
-	 * the delete function specified by Article.php. If the deletion succeeds, the 
-	 * details of the article deleted and the reason for deletion are added to the 
+	 * the local delete() function with these as arguments. It does not make use of
+	 * the delete function specified by Article.php. If the deletion succeeds, the
+	 * details of the article deleted and the reason for deletion are added to the
 	 * result object.
 	 */
 	public function execute() {
 		global $wgUser;
 		$this->getMain()->requestWriteMode();
 		$params = $this->extractRequestParams();
-		
+
 		$titleObj = NULL;
 		if(!isset($params['title']))
 			$this->dieUsageMsg(array('missingparam', 'title'));
@@ -64,21 +64,45 @@ class ApiDelete extends ApiBase {
 		if(!$titleObj->exists())
 			$this->dieUsageMsg(array('notanarticle'));
 
-		$articleObj = new Article($titleObj);
 		$reason = (isset($params['reason']) ? $params['reason'] : NULL);
-		$dbw = wfGetDb(DB_MASTER);
-		$dbw->begin();
-		$retval = self::delete($articleObj, $params['token'], 	$reason);
-		
-		if(!empty($retval))
-			// We don't care about multiple errors, just report one of them
-			$this->dieUsageMsg(current($retval));
+		if ($titleObj->getNamespace() == NS_IMAGE) {
+			$retval = self::deletefile($params['token'], $titleObj, $params['oldimage'], $reason, false);
+			if(!empty($retval))
+				// We don't care about multiple errors, just report one of them
+				$this->dieUsageMsg(current($retval));
+		} else {
+			$articleObj = new Article($titleObj);
+			$retval = self::delete($articleObj, $params['token'], $reason);
+			
+			if(!empty($retval))
+				// We don't care about multiple errors, just report one of them
+				$this->dieUsageMsg(current($retval));
+			
+			if($params['watch'] || $wgUser->getOption('watchdeletion'))
+				$articleObj->doWatch();
+			else if($params['unwatch'])
+				$articleObj->doUnwatch();
+		}
 
-		$dbw->commit();
 		$r = array('title' => $titleObj->getPrefixedText(), 'reason' => $reason);
 		$this->getResult()->addValue(null, $this->getModuleName(), $r);
 	}
 
+	private static function getPermissionsError(&$title, $token) {
+		global $wgUser;
+		// Check wiki readonly
+		if (wfReadOnly()) return array(array('readonlytext'));
+		
+		// Check permissions
+		$errors = $title->getUserPermissionsErrors('delete', $wgUser);
+		if (count($errors) > 0) return $errors;
+		
+		// Check token
+		if(!$wgUser->matchEditToken($token))
+			return array(array('sessionfailure'));
+		return array();
+	}
+
 	/**
 	 * We have our own delete() function, since Article.php's implementation is split in two phases
 	 *
@@ -90,41 +114,67 @@ class ApiDelete extends ApiBase {
 	public static function delete(&$article, $token, &$reason = NULL)
 	{
 		global $wgUser;
-
-		// Check permissions
-		$errors = $article->mTitle->getUserPermissionsErrors('delete', $wgUser);
-		if(!empty($errors))
-			return $errors;
-		if(wfReadOnly())
-			return array(array('readonlytext'));
-		if($wgUser->isBlocked())
-			return array(array('blocked'));
-
-		// Check token
-		if(!$wgUser->matchEditToken($token))
-			return array(array('sessionfailure'));
+		
+		$errors = self::getPermissionsError($article->getTitle(), $token);
+		if (count($errors)) return $errors;
 
 		// Auto-generate a summary, if necessary
 		if(is_null($reason))
 		{
+			# Need to pass a throwaway variable because generateReason expects
+			# a reference
+			$hasHistory = false;
 			$reason = $article->generateReason($hasHistory);
 			if($reason === false)
 				return array(array('cannotdelete'));
 		}
+		
+		if (!wfRunHooks('ArticleDelete', array(&$article, &$wgUser, &$reason)))
+			$this->dieUsageMsg(array('hookaborted'));
 
 		// Luckily, Article.php provides a reusable delete function that does the hard work for us
-		if($article->doDeleteArticle($reason))
+		if($article->doDeleteArticle($reason)) {
+			wfRunHooks('ArticleDeleteComplete', array(&$article, &$wgUser, $reason, $article->getId()));
 			return array();
+		}
 		return array(array('cannotdelete', $article->mTitle->getPrefixedText()));
 	}
+
+	public static function deleteFile($token, &$title, $oldimage, &$reason = NULL, $suppress = false)
+	{
+		$errors = self::getPermissionsError($title, $token);
+		if (count($errors)) return $errors;
+
+		if( $oldimage && !FileDeleteForm::isValidOldSpec($oldimage) )
+			return array(array('invalidoldimage'));
+
+		$file = wfFindFile($title, false, FileRepo::FIND_IGNORE_REDIRECT);
+		$oldfile = false;
+		
+		if( $oldimage )
+			$oldfile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $title, $oldimage );
+			
+		if( !FileDeleteForm::haveDeletableFile($file, $oldfile, $oldimage) )
+			return array(array('nofile'));
+
+		$status = FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress );
+				
+		if( !$status->isGood() )
+			return array(array('cannotdelete', $title->getPrefixedText()));
+			
+		return array();
+	}
 	
 	public function mustBePosted() { return true; }
-	
+
 	public function getAllowedParams() {
 		return array (
 			'title' => null,
 			'token' => null,
 			'reason' => null,
+			'watch' => false,
+			'unwatch' => false,
+			'oldimage' => null
 		);
 	}
 
@@ -132,7 +182,10 @@ class ApiDelete extends ApiBase {
 		return array (
 			'title' => 'Title of the page you want to delete.',
 			'token' => 'A delete token previously retrieved through prop=info',
-			'reason' => 'Reason for the deletion. If not set, an automatically generated reason will be used.'
+			'reason' => 'Reason for the deletion. If not set, an automatically generated reason will be used.',
+			'watch' => 'Add the page to your watchlist',
+			'unwatch' => 'Remove the page from your watchlist',
+			'oldimage' => 'The name of the old image to delete as provided by iiprop=archivename'
 		);
 	}
 
@@ -150,6 +203,6 @@ class ApiDelete extends ApiBase {
 	}
 
 	public function getVersion() {
-		return __CLASS__ . ': $Id: ApiDelete.php 30222 2008-01-28 19:05:26Z catrope $';
+		return __CLASS__ . ': $Id: ApiDelete.php 35350 2008-05-26 12:15:21Z simetrical $';
 	}
 }
diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php
new file mode 100644
index 00000000..d10432f3
--- /dev/null
+++ b/includes/api/ApiEditPage.php
@@ -0,0 +1,299 @@
+@gmail.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 ("ApiBase.php");
+}
+
+/**
+ * A query module to list all external URLs found on a given set of pages.
+ *
+ * @ingroup API
+ */
+class ApiEditPage extends ApiBase {
+
+	public function __construct($query, $moduleName) {
+		parent :: __construct($query, $moduleName);
+	}
+
+	public function execute() {
+		global $wgUser;
+		$this->getMain()->requestWriteMode();
+
+		$params = $this->extractRequestParams();
+		if(is_null($params['title']))
+			$this->dieUsageMsg(array('missingparam', 'title'));
+		if(is_null($params['text']) && is_null($params['appendtext']) && is_null($params['prependtext']))
+			$this->dieUsageMsg(array('missingtext'));
+		if(is_null($params['token']))
+			$this->dieUsageMsg(array('missingparam', 'token'));
+		if(!$wgUser->matchEditToken($params['token']))
+			$this->dieUsageMsg(array('sessionfailure'));
+
+		$titleObj = Title::newFromText($params['title']);
+		if(!$titleObj)
+			$this->dieUsageMsg(array('invalidtitle', $params['title']));
+
+		if($params['createonly'] && $titleObj->exists())
+			$this->dieUsageMsg(array('createonly-exists'));
+		if($params['nocreate'] && !$titleObj->exists())
+			$this->dieUsageMsg(array('nocreate-missing'));
+
+		// Now let's check whether we're even allowed to do this
+		$errors = $titleObj->getUserPermissionsErrors('edit', $wgUser);
+		if(!$titleObj->exists())
+			$errors = array_merge($errors, $titleObj->getUserPermissionsErrors('create', $wgUser));
+		if(!empty($errors))
+			$this->dieUsageMsg($errors[0]);
+
+		$articleObj = new Article($titleObj);
+		$toMD5 = $params['text'];
+		if(!is_null($params['appendtext']) || !is_null($params['prependtext']))
+		{
+			$content = $articleObj->getContent();
+			$params['text'] = $params['prependtext'] . $content . $params['appendtext'];
+			$toMD5 = $params['prependtext'] . $params['appendtext'];
+		}
+
+		# See if the MD5 hash checks out
+		if(isset($params['md5']))
+			if(md5($toMD5) !== $params['md5'])
+				$this->dieUsageMsg(array('hashcheckfailed'));
+		
+		$ep = new EditPage($articleObj);
+		// EditPage wants to parse its stuff from a WebRequest
+		// That interface kind of sucks, but it's workable
+		$reqArr = array('wpTextbox1' => $params['text'],
+				'wpEdittoken' => $params['token'],
+				'wpIgnoreBlankSummary' => ''
+		);
+		if(!is_null($params['summary']))
+			$reqArr['wpSummary'] = $params['summary'];
+		# Watch out for basetimestamp == ''
+		# wfTimestamp() treats it as NOW, almost certainly causing an edit conflict
+		if(!is_null($params['basetimestamp']) && $params['basetimestamp'] != '')
+			$reqArr['wpEdittime'] = wfTimestamp(TS_MW, $params['basetimestamp']);
+		else
+			$reqArr['wpEdittime'] = $articleObj->getTimestamp();
+		# Fake wpStartime
+		$reqArr['wpStarttime'] = $reqArr['wpEdittime'];
+		if($params['minor'] || (!$params['notminor'] && $wgUser->getOption('minordefault')))
+			$reqArr['wpMinoredit'] = '';
+		if($params['recreate'])
+			$reqArr['wpRecreate'] = '';
+		if(!is_null($params['section']))
+		{
+			$section = intval($params['section']);
+			if($section == 0 && $params['section'] != '0' && $params['section'] != 'new')
+				$this->dieUsage("The section parameter must be set to an integer or 'new'", "invalidsection");
+			$reqArr['wpSection'] = $params['section'];
+		}
+
+		if($params['watch'])
+			$watch = true;
+		else if($params['unwatch'])
+			$watch = false;
+		else if($titleObj->userIsWatching())
+			$watch = true;
+		else if($wgUser->getOption('watchdefault'))
+			$watch = true;
+		else if($wgUser->getOption('watchcreations') && !$titleObj->exists())
+			$watch = true;
+		else
+			$watch = false;
+		if($watch)
+			$reqArr['wpWatchthis'] = '';
+
+		$req = new FauxRequest($reqArr, true);
+		$ep->importFormData($req);
+
+		# Run hooks
+		# Handle CAPTCHA parameters
+		global $wgRequest;
+		if(isset($params['captchaid']))
+			$wgRequest->data['wpCaptchaId'] = $params['captchaid'];
+		if(isset($params['captchaword']))
+			$wgRequest->data['wpCaptchaWord'] = $params['captchaword'];
+		$r = array();
+		if(!wfRunHooks('APIEditBeforeSave', array(&$ep, $ep->textbox1, &$r)))
+		{
+			if(!empty($r))
+			{
+				$r['result'] = "Failure";
+				$this->getResult()->addValue(null, $this->getModuleName(), $r);
+				return;
+			}
+			else
+				$this->dieUsageMsg(array('hookaborted'));
+		}
+
+		# Do the actual save
+		$oldRevId = $articleObj->getRevIdFetched();
+		$result = null;
+		# *Something* is setting $wgTitle to a title corresponding to "Msg",
+		# but that breaks API mode detection through is_null($wgTitle)
+		global $wgTitle;
+		$wgTitle = null;
+		# Fake $wgRequest for some hooks inside EditPage
+		# FIXME: This interface SUCKS
+		$oldRequest = $wgRequest;
+		$wgRequest = $req;
+
+		$retval = $ep->internalAttemptSave($result, $wgUser->isAllowed('bot') && $params['bot']);
+		$wgRequest = $oldRequest;
+		switch($retval)
+		{
+			case EditPage::AS_HOOK_ERROR:
+			case EditPage::AS_HOOK_ERROR_EXPECTED:
+				$this->dieUsageMsg(array('hookaborted'));
+			case EditPage::AS_IMAGE_REDIRECT_ANON:
+				$this->dieUsageMsg(array('noimageredirect-anon'));
+			case EditPage::AS_IMAGE_REDIRECT_LOGGED:
+				$this->dieUsageMsg(array('noimageredirect-logged'));
+			case EditPage::AS_SPAM_ERROR:
+				$this->dieUsageMsg(array('spamdetected', $result['spam']));
+			case EditPage::AS_FILTERING:
+				$this->dieUsageMsg(array('filtered'));
+			case EditPage::AS_BLOCKED_PAGE_FOR_USER:
+				$this->dieUsageMsg(array('blockedtext'));
+			case EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED:
+			case EditPage::AS_CONTENT_TOO_BIG:
+				global $wgMaxArticleSize;
+				$this->dieUsageMsg(array('contenttoobig', $wgMaxArticleSize));
+			case EditPage::AS_READ_ONLY_PAGE_ANON:
+				$this->dieUsageMsg(array('noedit-anon'));
+			case EditPage::AS_READ_ONLY_PAGE_LOGGED:
+				$this->dieUsageMsg(array('noedit'));
+			case EditPage::AS_READ_ONLY_PAGE:
+				$this->dieUsageMsg(array('readonlytext'));
+			case EditPage::AS_RATE_LIMITED:
+				$this->dieUsageMsg(array('actionthrottledtext'));
+			case EditPage::AS_ARTICLE_WAS_DELETED:
+				$this->dieUsageMsg(array('wasdeleted'));
+			case EditPage::AS_NO_CREATE_PERMISSION:
+				$this->dieUsageMsg(array('nocreate-loggedin'));
+			case EditPage::AS_BLANK_ARTICLE:
+				$this->dieUsageMsg(array('blankpage'));
+			case EditPage::AS_CONFLICT_DETECTED:
+				$this->dieUsageMsg(array('editconflict'));
+			#case EditPage::AS_SUMMARY_NEEDED: Can't happen since we set wpIgnoreBlankSummary
+			#case EditPage::AS_TEXTBOX_EMPTY: Can't happen since we don't do sections
+			case EditPage::AS_END:
+				# This usually means some kind of race condition
+				# or DB weirdness occurred. Throw an unknown error here.
+				$this->dieUsageMsg(array('unknownerror', 'AS_END'));
+			case EditPage::AS_SUCCESS_NEW_ARTICLE:
+				$r['new'] = '';
+			case EditPage::AS_SUCCESS_UPDATE:
+				$r['result'] = "Success";
+				$r['pageid'] = $titleObj->getArticleID();
+				$r['title'] = $titleObj->getPrefixedText();
+				$newRevId = $titleObj->getLatestRevId();
+				if($newRevId == $oldRevId)
+					$r['nochange'] = '';
+				else
+				{
+					$r['oldrevid'] = $oldRevId;
+					$r['newrevid'] = $newRevId;
+				}
+				break;
+			default:
+				$this->dieUsageMsg(array('unknownerror', $retval));
+		}
+		$this->getResult()->addValue(null, $this->getModuleName(), $r);
+	}
+
+	public function mustBePosted() {
+		return true;
+	}
+
+	protected function getDescription() {
+		return 'Create and edit pages.';
+	}
+
+	protected function getAllowedParams() {
+		return array (
+			'title' => null,
+			'section' => null,
+			'text' => null,
+			'token' => null,
+			'summary' => null,
+			'minor' => false,
+			'notminor' => false,
+			'bot' => false,
+			'basetimestamp' => null,
+			'recreate' => false,
+			'createonly' => false,
+			'nocreate' => false,
+			'captchaword' => null,
+			'captchaid' => null,
+			'watch' => false,
+			'unwatch' => false,
+			'md5' => null,
+			'prependtext' => null,
+			'appendtext' => null,
+		);
+	}
+
+	protected function getParamDescription() {
+		return array (
+			'title' => 'Page title',
+			'section' => 'Section number. 0 for the top section, \'new\' for a new section',
+			'text' => 'Page content',
+			'token' => 'Edit token. You can get one of these through prop=info',
+			'summary' => 'Edit summary. Also section title when section=new',
+			'minor' => 'Minor edit',
+			'notminor' => 'Non-minor edit',
+			'bot' => 'Mark this edit as bot',
+			'basetimestamp' => array('Timestamp of the base revision (gotten through prop=revisions&rvprop=timestamp).',
+						'Used to detect edit conflicts; leave unset to ignore conflicts.'
+			),
+			'recreate' => 'Override any errors about the article having been deleted in the meantime',
+			'createonly' => 'Don\'t edit the page if it exists already',
+			'nocreate' => 'Throw an error if the page doesn\'t exist',
+			'watch' => 'Add the page to your watchlist',
+			'unwatch' => 'Remove the page from your watchlist',
+			'captchaid' => 'CAPTCHA ID from previous request',
+			'captchaword' => 'Answer to the CAPTCHA',
+			'md5' => array(	'The MD5 hash of the text parameter, or the prependtext and appendtext parameters concatenated.',
+				 	'If set, the edit won\'t be done unless the hash is correct'),
+			'prependtext' => array( 'Add this text to the beginning of the page. Overrides text.',
+						'Don\'t use together with section: that won\'t do what you expect.'),
+			'appendtext' => 'Add this text to the end of the page. Overrides text',
+		);
+	}
+
+	protected function getExamples() {
+		return array (
+			"Edit a page (anonymous user):",
+			"    api.php?action=edit&title=Test&summary=test%20summary&text=article%20content&basetimestamp=20070824123454&token=%2B\\"
+		);
+	}
+
+	public function getVersion() {
+		return __CLASS__ . ': $Id: ApiEditPage.php 36309 2008-06-15 20:37:28Z catrope $';
+	}
+}
diff --git a/includes/api/ApiEmailUser.php b/includes/api/ApiEmailUser.php
new file mode 100644
index 00000000..7e083536
--- /dev/null
+++ b/includes/api/ApiEmailUser.php
@@ -0,0 +1,114 @@
+
+ *
+ * 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 ("ApiBase.php");
+}
+
+
+/**
+ * @ingroup API
+ */
+class ApiEmailUser extends ApiBase {
+
+	public function __construct($main, $action) {
+		parent :: __construct($main, $action);
+	}
+
+	public function execute() {
+		global $wgUser;
+		$this->getMain()->requestWriteMode();
+		$params = $this->extractRequestParams();
+		
+		// Check required parameters
+		if ( !isset( $params['target'] ) )
+			$this->dieUsageMsg( array( 'missingparam', 'target' ) );
+		if ( !isset( $params['text'] ) )
+			$this->dieUsageMsg( array( 'missingparam', 'text' ) );
+		if ( !isset( $params['token'] ) )
+			$this->dieUsageMsg( array( 'missingparam', 'token' ) );	
+		
+		// Validate target 
+		$targetUser = EmailUserForm::validateEmailTarget( $params['target'] );
+		if ( !( $targetUser instanceof User ) )
+			$this->dieUsageMsg( array( $targetUser[0] ) );
+		
+		// Check permissions
+		$error = EmailUserForm::getPermissionsError( $wgUser, $params['token'] );
+		if ( $error )
+			$this->dieUsageMsg( array( $error[0] ) );
+		
+			
+		$form = new EmailUserForm( $targetUser, $params['text'], $params['subject'], $params['ccme'] );
+		$retval = $form->doSubmit();
+		if ( is_null( $retval ) )
+			$result = array( 'result' => 'Success' );
+		else
+			$result = array( 'result' => 'Failure',
+				 'message' => $retval->getMessage() );
+		
+		$this->getResult()->addValue( null, $this->getModuleName(), $result );
+	}
+	
+	public function mustBePosted() { return true; }
+
+	public function getAllowedParams() {
+		return array (
+			'target' => null,
+			'subject' => null,
+			'text' => null,
+			'token' => null,
+			'ccme' => false,
+		);
+	}
+
+	public function getParamDescription() {
+		return array (
+			'target' => 'User to send email to',
+			'subject' => 'Subject header',
+			'text' => 'Mail body',
+			// FIXME: How to properly get a token?
+			'token' => 'A token previously acquired via prop=info',
+			'ccme' => 'Send a copy of this mail to me',
+		);
+	}
+
+	public function getDescription() {
+		return array(
+			'Emails a user.'
+		);
+	}
+
+	protected function getExamples() {
+		return array (
+			'api.php?action=emailuser&target=WikiSysop&text=Content'
+		);
+	}
+
+	public function getVersion() {
+		return __CLASS__ . ': $Id: $';
+	}
+}	
+	
\ No newline at end of file
diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php
index 278896fa..397aece3 100644
--- a/includes/api/ApiExpandTemplates.php
+++ b/includes/api/ApiExpandTemplates.php
@@ -33,7 +33,7 @@ if (!defined('MEDIAWIKI')) {
  * any templates in a provided string, and returns the result of this expansion
  * to the caller.
  *
- * @addtogroup API
+ * @ingroup API
  */
 class ApiExpandTemplates extends ApiBase {
 
@@ -43,22 +43,35 @@ class ApiExpandTemplates extends ApiBase {
 
 	public function execute() {
 		// Get parameters
-		$params = $this->extractRequestParams();
-		$text = $params['text'];
-		$title = $params['title'];
+		extract( $this->extractRequestParams() );
 		$retval = '';
 
 		//Create title for parser
-		$title_obj = Title :: newFromText($params['title']);
+		$title_obj = Title :: newFromText( $title );
 		if(!$title_obj)
-			$title_obj = Title :: newFromText("API");	//  Default title is "API". For example, ExpandTemplates uses "ExpendTemplates" for it
+			$title_obj = Title :: newFromText( "API" );	//  Default title is "API". For example, ExpandTemplates uses "ExpendTemplates" for it
+
+		$result = $this->getResult();
 
 		// Parse text
 		global $wgParser;
-		$retval = $wgParser->preprocess( $text, $title_obj, new ParserOptions() );
+		$options = new ParserOptions();
+		if ( $generatexml )
+		{
+			$wgParser->startExternalParse( $title_obj, $options, OT_PREPROCESS );
+			$dom = $wgParser->preprocessToDom( $text );
+			if ( is_callable( array( $dom, 'saveXML' ) ) ) {
+				$xml = $dom->saveXML();
+			} else {
+				$xml = $dom->__toString();
+			}
+			$xml_result = array();
+			$result->setContent( $xml_result, $xml );
+            $result->addValue( null, 'parsetree', $xml_result);
+		}
+		$retval = $wgParser->preprocess( $text, $title_obj, $options );
 
 		// Return result
-		$result = $this->getResult();
 		$retval_array = array();
 		$result->setContent( $retval_array, $retval );
 		$result->addValue( null, $this->getModuleName(), $retval_array );
@@ -66,10 +79,11 @@ class ApiExpandTemplates extends ApiBase {
 
 	public function getAllowedParams() {
 		return array (
-			'title' => array( 
+			'title' => array(
 				ApiBase :: PARAM_DFLT => 'API',
 			),
-			'text' => null
+			'text' => null,
+			'generatexml' => false,
 		);
 	}
 
@@ -77,6 +91,7 @@ class ApiExpandTemplates extends ApiBase {
 		return array (
 			'text' => 'Wikitext to convert',
 			'title' => 'Title of page',
+			'generatexml' => 'Generate XML parse tree',
 		);
 	}
 
@@ -91,7 +106,6 @@ class ApiExpandTemplates extends ApiBase {
 	}
 
 	public function getVersion() {
-		return __CLASS__ . ': $Id: ApiExpandTemplates.php 30222 2008-01-28 19:05:26Z catrope $';
+		return __CLASS__ . ': $Id: ApiExpandTemplates.php 35098 2008-05-20 17:13:28Z ialex $';
 	}
 }
-
diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php
index 9b17b9d3..109b6552 100644
--- a/includes/api/ApiFeedWatchlist.php
+++ b/includes/api/ApiFeedWatchlist.php
@@ -32,8 +32,8 @@ 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
+ *
+ * @ingroup API
  */
 class ApiFeedWatchlist extends ApiBase {
 
@@ -53,15 +53,15 @@ class ApiFeedWatchlist extends ApiBase {
 	 * Wrap the result as an RSS/Atom feed.
 	 */
 	public function execute() {
-		
-		global $wgFeedClasses, $wgSitename, $wgContLanguageCode;
+
+		global $wgFeedClasses, $wgFeedLimit, $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));
-	
+
 			$dbr = wfGetDB( DB_SLAVE );
 			// Prepare parameters for nested request
 			$fauxReqArr = array (
@@ -72,7 +72,7 @@ class ApiFeedWatchlist extends ApiBase {
 				'wlprop' => 'title|user|comment|timestamp',
 				'wldir' => 'older',		// reverse order - from newest to oldest
 				'wlend' => $dbr->timestamp($endTime),	// stop at this time
-				'wllimit' => 50
+				'wllimit' => (50 > $wgFeedLimit) ? $wgFeedLimit : 50
 			);
 
 			// Check for 'allrev' parameter, and if found, show all revisions to each page on wl.
@@ -80,35 +80,35 @@ class ApiFeedWatchlist extends ApiBase {
 
 			// Create the request
 			$fauxReq = new FauxRequest ( $fauxReqArr );
-	
+
 			// Execute
 			$module = new ApiMain($fauxReq);
 			$module->execute();
 
 			// Get data array
 			$data = $module->getResultData();
-	
+
 			$feedItems = array ();
 			foreach ($data['query']['watchlist'] as $info) {
 				$feedItems[] = $this->createFeedItem($info);
 			}
-	
+
 			$feedTitle = $wgSitename . ' - ' . wfMsgForContent('watchlist') . ' [' . $wgContLanguageCode . ']';
 			$feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullUrl();
-	
+
 			$feed = new $wgFeedClasses[$params['feedformat']] ($feedTitle, htmlspecialchars(wfMsgForContent('watchlist')), $feedUrl);
-	
+
 			ApiFormatFeedWrapper :: setResult($this->getResult(), $feed, $feedItems);
 
 		} catch (Exception $e) {
 
 			// Error results should not be cached
 			$this->getMain()->setCacheMaxAge(0);
-	
+
 			$feedTitle = $wgSitename . ' - Error - ' . wfMsgForContent('watchlist') . ' [' . $wgContLanguageCode . ']';
 			$feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullUrl();
-	
-			$feedFormat = isset($params['feedformat']) ? $params['feedformat'] : 'rss'; 
+
+			$feedFormat = isset($params['feedformat']) ? $params['feedformat'] : 'rss';
 			$feed = new $wgFeedClasses[$feedFormat] ($feedTitle, htmlspecialchars(wfMsgForContent('watchlist')), $feedUrl);
 
 
@@ -175,7 +175,6 @@ class ApiFeedWatchlist extends ApiBase {
 	}
 
 	public function getVersion() {
-		return __CLASS__ . ': $Id: ApiFeedWatchlist.php 30222 2008-01-28 19:05:26Z catrope $';
+		return __CLASS__ . ': $Id: ApiFeedWatchlist.php 35098 2008-05-20 17:13:28Z ialex $';
 	}
 }
-
diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php
index 768a18ac..db58fe52 100644
--- a/includes/api/ApiFormatBase.php
+++ b/includes/api/ApiFormatBase.php
@@ -30,12 +30,12 @@ if (!defined('MEDIAWIKI')) {
 
 /**
  * This is the abstract base class for API formatters.
- * 
- * @addtogroup API
+ *
+ * @ingroup API
  */
 abstract class ApiFormatBase extends ApiBase {
 
-	private $mIsHtml, $mFormat, $mUnescapeAmps, $mHelp;
+	private $mIsHtml, $mFormat, $mUnescapeAmps, $mHelp, $mCleared;
 
 	/**
 	* Create a new instance of the formatter.
@@ -50,6 +50,7 @@ abstract class ApiFormatBase extends ApiBase {
 		else
 			$this->mFormat = $format;
 		$this->mFormat = strtoupper($this->mFormat);
+		$this->mCleared = false;
 	}
 
 	/**
@@ -62,7 +63,7 @@ abstract class ApiFormatBase extends ApiBase {
 	/**
 	 * 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.  
+	 * and thus needs to override this function to return true.
 	 */
 	public function getNeedsRawData() {
 		return false;
@@ -82,8 +83,8 @@ abstract class ApiFormatBase extends ApiBase {
 
 	/**
 	 * Returns true when an HTML filtering printer should be used.
-	 * The default implementation assumes that formats ending with 'fm' 
-	 * should be formatted in HTML. 
+	 * The default implementation assumes that formats ending with 'fm'
+	 * should be formatted in HTML.
 	 */
 	public function getIsHtml() {
 		return $this->mIsHtml;
@@ -111,7 +112,7 @@ abstract class ApiFormatBase extends ApiBase {
 
 
 
-mUnescapeAmps) { 
+mUnescapeAmps) {
 ?>	MediaWiki API
 	MediaWiki API Result
@@ -127,7 +128,7 @@ abstract class ApiFormatBase extends ApiBase {
 
 You are looking at the HTML representation of the mFormat ); ?> format.
HTML is good for debugging, but probably is not suitable for your application.
-See complete documentation, or +See complete documentation, or API help for more information.
complete documentation, or if ($this->getIsHtml()) echo $this->formatHTML($text); else + { + // For non-HTML output, clear all errors that might have been + // displayed if display_errors=On + // Do this only once, of course + if(!$this->mCleared) + { + ob_clean(); + $this->mCleared = true; + } echo $text; + } } /** @@ -175,7 +186,7 @@ See complete documentation, or public function setHelp( $help = true ) { $this->mHelp = true; } - + /** * Prety-print various elements in HTML format, such as xml tags and URLs. * This method also replaces any '<' with < @@ -188,16 +199,17 @@ See complete documentation, or $text = preg_replace('/\<(!--.*?--|.*?)\>/', '<\1>', $text); // identify URLs $protos = "http|https|ftp|gopher"; - $text = ereg_replace("($protos)://[^ \\'\"()<\n]+", '\\0', $text); + # This regex hacks around bug 13218 (" included in the URL) + $text = preg_replace("#(($protos)://.*?)(")?([ \\'\"()<\n])#", '\\1\\3\\4', $text); // identify requests to api.php - $text = ereg_replace("api\\.php\\?[^ \\()<\n\t]+", '\\0', $text); + $text = preg_replace("#api\\.php\\?[^ \\()<\n\t]+#", '\\0', $text); if( $this->mHelp ) { // make strings inside * bold $text = ereg_replace("\\*[^<>\n]+\\*", '\\0', $text); // make strings inside $ italic $text = ereg_replace("\\$[^<>\n]+\\$", '\\0', $text); } - + /* Temporary fix for bad links in help messages. As a special case, * XML-escaped metachars are de-escaped one level in the help message * for legibility. Should be removed once we have completed a fully-html @@ -220,13 +232,13 @@ See complete documentation, or } public static function getBaseVersion() { - return __CLASS__ . ': $Id: ApiFormatBase.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiFormatBase.php 36153 2008-06-10 15:20:22Z tstarling $'; } } /** - * This printer is used to wrap an instance of the Feed class - * @addtogroup API + * This printer is used to wrap an instance of the Feed class + * @ingroup API */ class ApiFormatFeedWrapper extends ApiFormatBase { @@ -275,13 +287,12 @@ class ApiFormatFeedWrapper extends ApiFormatBase { $feed->outItem($item); $feed->outFooter(); } else { - // Error has occured, print something usefull - // TODO: make this error more informative using ApiBase :: dieDebug() or similar - wfHttpError(500, 'Internal Server Error', ''); + // Error has occured, print something useful + ApiBase::dieDebug( __METHOD__, 'Invalid feed class/item' ); } } - + public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatBase.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiFormatBase.php 36153 2008-06-10 15:20:22Z tstarling $'; } } diff --git a/includes/api/ApiFormatDbg.php b/includes/api/ApiFormatDbg.php index f0fc5e91..254c140b 100644 --- a/includes/api/ApiFormatDbg.php +++ b/includes/api/ApiFormatDbg.php @@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) { } /** - * @addtogroup API + * @ingroup API */ class ApiFormatDbg extends ApiFormatBase { @@ -53,7 +53,6 @@ class ApiFormatDbg extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatPhp.php 23531 2007-06-29 01:19:14Z simetrical $'; + return __CLASS__ . ': $Id: ApiFormatDbg.php 35098 2008-05-20 17:13:28Z ialex $'; } } - diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php index 852a64b6..42156849 100644 --- a/includes/api/ApiFormatJson.php +++ b/includes/api/ApiFormatJson.php @@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) { } /** - * @addtogroup API + * @ingroup API */ class ApiFormatJson extends ApiFormatBase { @@ -54,7 +54,7 @@ class ApiFormatJson extends ApiFormatBase { $params = $this->extractRequestParams(); $callback = $params['callback']; if(!is_null($callback)) { - $prefix = ereg_replace("[^_A-Za-z0-9]", "", $callback ) . "("; + $prefix = preg_replace("/[^][.\\'\\\"_A-Za-z0-9]/", "", $callback ) . "("; $suffix = ")"; } @@ -86,7 +86,6 @@ class ApiFormatJson extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatJson.php 31484 2008-03-03 05:46:20Z brion $'; + return __CLASS__ . ': $Id: ApiFormatJson.php 35098 2008-05-20 17:13:28Z ialex $'; } } - diff --git a/includes/api/ApiFormatJson_json.php b/includes/api/ApiFormatJson_json.php index a8c649c3..87d7086e 100644 --- a/includes/api/ApiFormatJson_json.php +++ b/includes/api/ApiFormatJson_json.php @@ -45,12 +45,12 @@ * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH * DAMAGE. * -* @addtogroup API +* @ingroup API * @author Michal Migurski * @author Matt Knapp * @author Brett Stimmerman * @copyright 2005 Michal Migurski -* @version CVS: $Id: JSON.php,v 1.30 2006/03/08 16:10:20 migurski Exp $ +* @version CVS: $Id: ApiFormatJson_json.php 35098 2008-05-20 17:13:28Z ialex $ * @license http://www.opensource.org/licenses/bsd-license.php * @see http://pear.php.net/pepr/pepr-proposal-show.php?id=198 */ @@ -111,7 +111,7 @@ define('SERVICES_JSON_SUPPRESS_ERRORS', 32); * $value = $json->decode($input); * * - * @addtogroup API + * @ingroup API */ class Services_JSON { @@ -257,7 +257,7 @@ class Services_JSON */ function encode2($var) { - if ($this->pretty) { + if ($this->pretty) { $close = "\n" . str_repeat("\t", $this->indent); $open = $close . "\t"; $mid = ',' . $open; @@ -426,7 +426,7 @@ class Services_JSON $this->indent++; $elements = array_map(array($this, 'encode2'), $var); $this->indent--; - + foreach($elements as $element) { if(Services_JSON::isError($element)) { return $element; @@ -703,7 +703,7 @@ class Services_JSON // element in an associative array, // for now $parts = array(); - + if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { // "name":value pair $key = $this->decode($parts[1]); @@ -815,7 +815,7 @@ class Services_JSON if (class_exists('PEAR_Error')) { /** - * @addtogroup API + * @ingroup API */ class Services_JSON_Error extends PEAR_Error { @@ -830,7 +830,7 @@ if (class_exists('PEAR_Error')) { /** * @todo Ultimately, this class shall be descended from PEAR_Error - * @addtogroup API + * @ingroup API */ class Services_JSON_Error { @@ -840,7 +840,4 @@ if (class_exists('PEAR_Error')) { } } - } - - diff --git a/includes/api/ApiFormatPhp.php b/includes/api/ApiFormatPhp.php index f830d8e1..163d3028 100644 --- a/includes/api/ApiFormatPhp.php +++ b/includes/api/ApiFormatPhp.php @@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) { } /** - * @addtogroup API + * @ingroup API */ class ApiFormatPhp extends ApiFormatBase { @@ -50,7 +50,6 @@ class ApiFormatPhp extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatPhp.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiFormatPhp.php 35098 2008-05-20 17:13:28Z ialex $'; } } - diff --git a/includes/api/ApiFormatTxt.php b/includes/api/ApiFormatTxt.php index c4c45f68..5f608d5c 100644 --- a/includes/api/ApiFormatTxt.php +++ b/includes/api/ApiFormatTxt.php @@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) { } /** - * @addtogroup API + * @ingroup API */ class ApiFormatTxt extends ApiFormatBase { @@ -53,7 +53,6 @@ class ApiFormatTxt extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatPhp.php 23531 2007-06-29 01:19:14Z simetrical $'; + return __CLASS__ . ': $Id: ApiFormatTxt.php 35098 2008-05-20 17:13:28Z ialex $'; } } - diff --git a/includes/api/ApiFormatWddx.php b/includes/api/ApiFormatWddx.php index 22a0e482..0909539e 100644 --- a/includes/api/ApiFormatWddx.php +++ b/includes/api/ApiFormatWddx.php @@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) { } /** - * @addtogroup API + * @ingroup API */ class ApiFormatWddx extends ApiFormatBase { @@ -85,7 +85,6 @@ class ApiFormatWddx extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatWddx.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiFormatWddx.php 35098 2008-05-20 17:13:28Z ialex $'; } } - diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php index d39e8049..d35eb3e9 100644 --- a/includes/api/ApiFormatXml.php +++ b/includes/api/ApiFormatXml.php @@ -29,11 +29,12 @@ if (!defined('MEDIAWIKI')) { } /** - * @addtogroup API + * @ingroup API */ class ApiFormatXml extends ApiFormatBase { private $mRootElemName = 'api'; + private $mDoubleQuote = false; public function __construct($main, $format) { parent :: __construct($main, $format); @@ -46,12 +47,15 @@ class ApiFormatXml extends ApiFormatBase { public function getNeedsRawData() { return true; } - + public function setRootElement($rootElemName) { $this->mRootElemName = $rootElemName; } public function execute() { + $params = $this->extractRequestParams(); + $this->mDoubleQuote = $params['xmldoublequote']; + $this->printText(''); $this->recXmlPrint($this->mRootElemName, $this->getResultData(), $this->getIsHtml() ? -2 : null); } @@ -79,9 +83,10 @@ class ApiFormatXml extends ApiFormatBase { switch (gettype($elemValue)) { case 'array' : - if (isset ($elemValue['*'])) { $subElemContent = $elemValue['*']; + if ($this->mDoubleQuote) + $subElemContent = $this->doubleQuote($subElemContent); unset ($elemValue['*']); } else { $subElemContent = null; @@ -97,6 +102,9 @@ class ApiFormatXml extends ApiFormatBase { $indElements = array (); $subElements = array (); foreach ($elemValue as $subElemId => & $subElemValue) { + if (is_string($subElemValue) && $this->mDoubleQuote) + $subElemValue = $this->doubleQuote($subElemValue); + if (gettype($subElemId) === 'integer') { $indElements[] = $subElemValue; unset ($elemValue[$subElemId]); @@ -136,12 +144,28 @@ class ApiFormatXml extends ApiFormatBase { break; } } + private function doubleQuote( $text ) { + return Sanitizer::encodeAttribute( $text ); + } + + public function getAllowedParams() { + return array ( + 'xmldoublequote' => false + ); + } + + public function getParamDescription() { + return array ( + 'xmldoublequote' => 'If specified, double quotes all attributes and content.', + ); + } + + public function getDescription() { return 'Output data in XML format' . parent :: getDescription(); } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatXml.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiFormatXml.php 37075 2008-07-04 22:44:57Z brion $'; } } - diff --git a/includes/api/ApiFormatYaml.php b/includes/api/ApiFormatYaml.php index 5e15aee6..cc255c63 100644 --- a/includes/api/ApiFormatYaml.php +++ b/includes/api/ApiFormatYaml.php @@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) { } /** - * @addtogroup API + * @ingroup API */ class ApiFormatYaml extends ApiFormatBase { @@ -50,7 +50,6 @@ class ApiFormatYaml extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatYaml.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiFormatYaml.php 35098 2008-05-20 17:13:28Z ialex $'; } } - diff --git a/includes/api/ApiFormatYaml_spyc.php b/includes/api/ApiFormatYaml_spyc.php index b2973b8c..c0d4093e 100644 --- a/includes/api/ApiFormatYaml_spyc.php +++ b/includes/api/ApiFormatYaml_spyc.php @@ -1,5 +1,5 @@ @@ -8,29 +8,29 @@ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ - /** + /** * A node, used by Spyc for parsing YAML. - * @addtogroup API + * @ingroup API */ class YAMLNode { /**#@+ * @access public * @var string - */ + */ var $parent; var $id; /**#@-*/ - /** + /** * @access public * @var mixed */ var $data; - /** + /** * @access public * @var int */ var $indent; - /** + /** * @access public * @var bool */ @@ -58,17 +58,17 @@ * $parser = new Spyc; * $array = $parser->load($file); * - * @addtogroup API + * @ingroup API */ class Spyc { - + /** * Load YAML into a PHP array statically * - * The load method, when supplied with a YAML stream (string or file), - * will do its best to convert YAML in a file into a PHP array. Pretty + * The load method, when supplied with a YAML stream (string or file), + * will do its best to convert YAML in a file into a PHP array. Pretty * simple. - * Usage: + * Usage: * * $array = Spyc::YAMLLoad('lucky.yml'); * print_r($array); @@ -81,7 +81,7 @@ $spyc = new Spyc; return $spyc->load($input); } - + /** * Dump YAML from PHP array statically * @@ -90,7 +90,7 @@ * save the returned string as nothing.yml and pass it around. * * Oh, and you can decide how big the indent is and what the wordwrap - * for folding is. Pretty cool -- just pass in 'false' for either if + * for folding is. Pretty cool -- just pass in 'false' for either if * you want to use the default. * * Indent's default is 2 spaces, wordwrap's default is 40 characters. And @@ -100,20 +100,20 @@ * @static * @return string * @param array $array PHP array - * @param int $indent Pass in false to use the default, which is 2 + * @param int $indent Pass in false to use the default, which is 2 * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) */ public static function YAMLDump($array,$indent = false,$wordwrap = false) { $spyc = new Spyc; return $spyc->dump($array,$indent,$wordwrap); } - + /** * Load YAML into a PHP array from an instantiated object * - * The load method, when supplied with a YAML stream (string or file path), + * The load method, when supplied with a YAML stream (string or file path), * will do its best to convert the YAML into a PHP array. Pretty simple. - * Usage: + * Usage: * * $parser = new Spyc; * $array = $parser->load('lucky.yml'); @@ -126,7 +126,7 @@ function load($input) { // See what type of input we're talking about // If it's not a file, assume it's a string - if (!empty($input) && (strpos($input, "\n") === false) + if (!empty($input) && (strpos($input, "\n") === false) && file_exists($input)) { $yaml = file($input); } else { @@ -139,7 +139,7 @@ $this->_lastNode = $base->id; $this->_inBlock = false; $this->_isInline = false; - + foreach ($yaml as $linenum => $line) { $ifchk = trim($line); @@ -149,7 +149,7 @@ ' with a tab. YAML only recognizes spaces. Please reformat.'; die($err); } - + if ($this->_inBlock === false && empty($ifchk)) { continue; } elseif ($this->_inBlock == true && empty($ifchk)) { @@ -159,7 +159,7 @@ // Create a new node and get its indent $node = new YAMLNode; $node->indent = $this->_getIndent($line); - + // Check where the node lies in the hierarchy if ($this->_lastIndent == $node->indent) { // If we're in a block, add the text to the parent's data @@ -172,16 +172,16 @@ $node->parent = $this->_allNodes[$this->_lastNode]->parent; } } - } elseif ($this->_lastIndent < $node->indent) { + } elseif ($this->_lastIndent < $node->indent) { if ($this->_inBlock === true) { $parent =& $this->_allNodes[$this->_lastNode]; $parent->data[key($parent->data)] .= trim($line).$this->_blockEnd; } elseif ($this->_inBlock === false) { // The current node's parent is the previous node $node->parent = $this->_lastNode; - - // If the value of the last node's data was > or | we need to - // start blocking i.e. taking in all lines as a text value until + + // If the value of the last node's data was > or | we need to + // start blocking i.e. taking in all lines as a text value until // we drop our indent. $parent =& $this->_allNodes[$node->parent]; $this->_allNodes[$node->parent]->children = true; @@ -190,7 +190,7 @@ if ($chk === '>') { $this->_inBlock = true; $this->_blockEnd = ' '; - $parent->data[key($parent->data)] = + $parent->data[key($parent->data)] = str_replace('>','',$parent->data[key($parent->data)]); $parent->data[key($parent->data)] .= trim($line).' '; $this->_allNodes[$node->parent]->children = false; @@ -198,7 +198,7 @@ } elseif ($chk === '|') { $this->_inBlock = true; $this->_blockEnd = "\n"; - $parent->data[key($parent->data)] = + $parent->data[key($parent->data)] = str_replace('|','',$parent->data[key($parent->data)]); $parent->data[key($parent->data)] .= trim($line)."\n"; $this->_allNodes[$node->parent]->children = false; @@ -212,11 +212,11 @@ $this->_inBlock = false; if ($this->_blockEnd = "\n") { $last =& $this->_allNodes[$this->_lastNode]; - $last->data[key($last->data)] = + $last->data[key($last->data)] = trim($last->data[key($last->data)]); } } - + // We don't know the parent of the node so we have to find it // foreach ($this->_allNodes as $n) { foreach ($this->_indentSort[$node->indent] as $n) { @@ -225,7 +225,7 @@ } } } - + if ($this->_inBlock === false) { // Set these properties with information from our current node $this->_lastIndent = $node->indent; @@ -239,13 +239,13 @@ $this->_indentSort[$node->indent][] =& $this->_allNodes[$node->id]; // Add a reference to the node in a References array if this node // has a YAML reference in it. - if ( + if ( ( (is_array($node->data)) && isset($node->data[key($node->data)]) && (!is_array($node->data[key($node->data)])) ) && - ( (preg_match('/^&([^ ]+)/',$node->data[key($node->data)])) - || + ( (preg_match('/^&([^ ]+)/',$node->data[key($node->data)])) + || (preg_match('/^\*([^ ]+)/',$node->data[key($node->data)])) ) ) { $this->_haveRefs[] =& $this->_allNodes[$node->id]; @@ -256,9 +256,9 @@ ) { // Incomplete reference making code. Ugly, needs cleaned up. foreach ($node->data[key($node->data)] as $d) { - if ( !is_array($d) && - ( (preg_match('/^&([^ ]+)/',$d)) - || + if ( !is_array($d) && + ( (preg_match('/^&([^ ]+)/',$d)) + || (preg_match('/^\*([^ ]+)/',$d)) ) ) { $this->_haveRefs[] =& $this->_allNodes[$node->id]; @@ -269,15 +269,15 @@ } } unset($node); - + // Here we travel through node-space and pick out references (& and *) $this->_linkReferences(); - + // Build the PHP array out of node-space $trunk = $this->_buildArray(); return $trunk; } - + /** * Dump PHP array to YAML * @@ -286,7 +286,7 @@ * save the returned string as tasteful.yml and pass it around. * * Oh, and you can decide how big the indent is and what the wordwrap - * for folding is. Pretty cool -- just pass in 'false' for either if + * for folding is. Pretty cool -- just pass in 'false' for either if * you want to use the default. * * Indent's default is 2 spaces, wordwrap's default is 40 characters. And @@ -295,7 +295,7 @@ * @access public * @return string * @param array $array PHP array - * @param int $indent Pass in false to use the default, which is 2 + * @param int $indent Pass in false to use the default, which is 2 * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) */ function dump($array,$indent = false,$wordwrap = false) { @@ -308,29 +308,29 @@ } else { $this->_dumpIndent = $indent; } - + if ($wordwrap === false or !is_numeric($wordwrap)) { $this->_dumpWordWrap = 40; } else { $this->_dumpWordWrap = $wordwrap; } - + // New YAML document $string = "---\n"; - + // Start at the base of the array and move through it. foreach ($array as $key => $value) { $string .= $this->_yamlize($key,$value,0); } return $string; } - + /**** Private Properties ****/ - + /**#@+ * @access private * @var mixed - */ + */ var $_haveRefs; var $_allNodes; var $_lastIndent; @@ -342,7 +342,7 @@ /**#@-*/ /**** Private Methods ****/ - + /** * Attempts to convert a key / value array item to YAML * @access private @@ -350,7 +350,7 @@ * @param $key The name of the key * @param $value The value of the item * @param $indent The indent of the current node - */ + */ function _yamlize($key,$value,$indent) { if (is_array($value)) { // It has children. What to do? @@ -366,14 +366,14 @@ } return $string; } - + /** * Attempts to convert an array to YAML * @access private * @return string * @param $array The array you want to convert * @param $indent The indent of the current level - */ + */ function _yamlizeArray($array,$indent) { if (is_array($array)) { $string = ''; @@ -395,9 +395,15 @@ function _needLiteral($value) { # Check whether the string contains # or : or begins with any of: # [ - ? , [ ] { } ! * & | > ' " % @ ` ] - return (bool)(preg_match("/[#:]/", $value) || preg_match("/^[-?,[\]{}!*&|>'\"%@`]/", $value)); + # or is a number or contains newlines + return (bool)(gettype($value) == "string" && + (is_numeric($value) || + strpos($value, "\n") || + preg_match("/[#:]/", $value) || + preg_match("/^[-?,[\]{}!*&|>'\"%@`]/", $value))); + } - + /** * Returns YAML from a key and a value * @access private @@ -405,23 +411,29 @@ * @param $key The name of the key * @param $value The value of the item * @param $indent The indent of the current node - */ + */ function _dumpNode($key,$value,$indent) { // do some folding here, for blocks - if (strpos($value,"\n") || $this->_needLiteral($value)) { + if ($this->_needLiteral($value)) { $value = $this->_doLiteralBlock($value,$indent); - } else { + } else { $value = $this->_doFolding($value,$indent); } - + $spaces = str_repeat(' ',$indent); if (is_int($key)) { // It's a sequence - $string = $spaces.'- '.$value."\n"; + if ($value) + $string = $spaces.'- '.$value."\n"; + else + $string = $spaces . "-\n"; } else { - // It's mapped - $string = $spaces.$key.': '.$value."\n"; + // It's mapped + if ($value) + $string = $spaces.$key.': '.$value."\n"; + else + $string = $spaces . $key . ":\n"; } return $string; } @@ -430,9 +442,9 @@ * Creates a literal block for dumping * @access private * @return string - * @param $value + * @param $value * @param $indent int The value of the indent - */ + */ function _doLiteralBlock($value,$indent) { $exploded = explode("\n",$value); $newValue = '|'; @@ -443,7 +455,7 @@ } return $newValue; } - + /** * Folds a string of text, if necessary * @access private @@ -455,7 +467,7 @@ if ($this->_dumpWordWrap === 0) { return $value; } - + if (strlen($value) > $this->_dumpWordWrap) { $indent += $this->_dumpIndent; $indent = str_repeat(' ',$indent); @@ -464,9 +476,9 @@ } return $value; } - + /* Methods used in loading */ - + /** * Finds and returns the indentation of a YAML line * @access private @@ -491,7 +503,7 @@ * @param string $line A line from the YAML file */ function _parseLine($line) { - $line = trim($line); + $line = trim($line); $array = array(); @@ -534,7 +546,7 @@ } return $array; } - + /** * Finds the type of the passed value, returns the value as the new type. * @access private @@ -543,7 +555,7 @@ */ function _toType($value) { $matches = array(); - if (preg_match('/^("(.*)"|\'(.*)\')/',$value,$matches)) { + if (preg_match('/^("(.*)"|\'(.*)\')/',$value,$matches)) { $value = (string)preg_replace('/(\'\'|\\\\\')/',"'",end($matches)); $value = preg_replace('/\\\\"/','"',$value); } elseif (preg_match('/^\\[(.+)\\]$/',$value,$matches)) { @@ -551,7 +563,7 @@ // Take out strings sequences and mappings $explode = $this->_inlineEscape($matches[1]); - + // Propogate value array $value = array(); foreach ($explode as $v) { @@ -581,10 +593,10 @@ $value = NULL; } elseif (ctype_digit($value)) { $value = (int)$value; - } elseif (in_array(strtolower($value), + } elseif (in_array(strtolower($value), array('true', 'on', '+', 'yes', 'y'))) { $value = TRUE; - } elseif (in_array(strtolower($value), + } elseif (in_array(strtolower($value), array('false', 'off', '-', 'no', 'n'))) { $value = FALSE; } elseif (is_numeric($value)) { @@ -593,10 +605,10 @@ // Just a normal string, right? $value = trim(preg_replace('/#(.+)$/','',$value)); } - + return $value; } - + /** * Used in inlines to check for more inlines or quoted strings * @access private @@ -607,13 +619,13 @@ // While pure sequences seem to be nesting just fine, // pure mappings and mappings with sequences inside can't go very // deep. This needs to be fixed. - - // Check for strings + + // Check for strings $regex = '/(?:(")|(?:\'))((?(1)[^"]+|[^\']+))(?(1)"|\')/'; $strings = array(); if (preg_match_all($regex,$inline,$strings)) { $saved_strings[] = $strings[0][0]; - $inline = preg_replace($regex,'YAMLString',$inline); + $inline = preg_replace($regex,'YAMLString',$inline); } unset($regex); @@ -623,14 +635,14 @@ $inline = preg_replace('/\[(.+)\]/U','YAMLSeq',$inline); $seqs = $seqs[0]; } - + // Check for mappings $maps = array(); if (preg_match_all('/{(.+)}/U',$inline,$maps)) { $inline = preg_replace('/{(.+)}/U','YAMLMap',$inline); $maps = $maps[0]; } - + $explode = explode(', ',$inline); // Re-add the strings @@ -654,7 +666,7 @@ } } } - + // Re-add the mappings if (!empty($maps)) { $i = 0; @@ -668,7 +680,7 @@ return $explode; } - + /** * Builds the PHP array from all the YAML nodes we've gathered * @access private @@ -690,10 +702,10 @@ $trunk = $this->_array_kmerge($trunk,$n->data); } } - + return $trunk; } - + /** * Traverses node-space and sets references (& and *) accordingly * @access private @@ -705,7 +717,7 @@ if (!empty($node->data)) { $key = key($node->data); // If it's an array, don't check. - if (is_array($node->data[$key])) { + if (is_array($node->data[$key])) { foreach ($node->data[$key] as $k => $v) { $this->_linkRef($node,$key,$k,$v); } @@ -713,11 +725,11 @@ $this->_linkRef($node,$key); } } - } + } } return true; } - + function _linkRef(&$n,$key,$k = NULL,$v = NULL) { if (empty($k) && empty($v)) { // Look for &refs @@ -725,7 +737,7 @@ if (preg_match('/^&([^ ]+)/',$n->data[$key],$matches)) { // Flag the node so we know it's a reference $this->_allNodes[$n->id]->ref = substr($matches[0],1); - $this->_allNodes[$n->id]->data[$key] = + $this->_allNodes[$n->id]->data[$key] = substr($n->data[$key],strlen($matches[0])+1); // Look for *refs } elseif (preg_match('/^\*([^ ]+)/',$n->data[$key],$matches)) { @@ -737,7 +749,7 @@ if (preg_match('/^&([^ ]+)/',$v,$matches)) { // Flag the node so we know it's a reference $this->_allNodes[$n->id]->ref = substr($matches[0],1); - $this->_allNodes[$n->id]->data[$key][$k] = + $this->_allNodes[$n->id]->data[$key][$k] = substr($v,strlen($matches[0])+1); // Look for *refs } elseif (preg_match('/^\*([^ ]+)/',$v,$matches)) { @@ -747,7 +759,7 @@ } } } - + /** * Finds the children of a node and aids in the building of the PHP array * @access private @@ -770,7 +782,7 @@ } return $return; } - + /** * Turns a node's data and its children's data into a PHP array * @@ -824,7 +836,7 @@ } return true; } - + /** * Merges arrays and maintains numeric keys. @@ -832,29 +844,29 @@ * An ever-so-slightly modified version of the array_kmerge() function posted * to php.net by mail at nospam dot iaindooley dot com on 2004-04-08. * - * http://us3.php.net/manual/en/function.array-merge.php#41394 + * http://www.php.net/manual/en/function.array-merge.php#41394 * * @access private * @param array $arr1 * @param array $arr2 * @return array */ - function _array_kmerge($arr1,$arr2) { - if(!is_array($arr1)) - $arr1 = array(); + function _array_kmerge($arr1,$arr2) { + if(!is_array($arr1)) + $arr1 = array(); if(!is_array($arr2)) - $arr2 = array(); - - $keys1 = array_keys($arr1); - $keys2 = array_keys($arr2); - $keys = array_merge($keys1,$keys2); - $vals1 = array_values($arr1); - $vals2 = array_values($arr2); - $vals = array_merge($vals1,$vals2); - $ret = array(); - - foreach($keys as $key) { + $arr2 = array(); + + $keys1 = array_keys($arr1); + $keys2 = array_keys($arr2); + $keys = array_merge($keys1,$keys2); + $vals1 = array_values($arr1); + $vals2 = array_values($arr2); + $vals = array_merge($vals1,$vals2); + $ret = array(); + + foreach($keys as $key) { list( /* unused */ ,$val) = each($vals); // This is the good part! If a key already exists, but it's part of a // sequence (an int), just keep addin numbers until we find a fresh one. @@ -862,11 +874,10 @@ while (array_key_exists($key, $ret)) { $key++; } - } - $ret[$key] = $val; - } + } + $ret[$key] = $val; + } - return $ret; + return $ret; } } - diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php index 47a45ea1..4ccb5acf 100644 --- a/includes/api/ApiHelp.php +++ b/includes/api/ApiHelp.php @@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) { /** * This is a simple class to handle action=help - * - * @addtogroup API + * + * @ingroup API */ class ApiHelp extends ApiBase { @@ -57,7 +57,6 @@ class ApiHelp extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiHelp.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiHelp.php 35098 2008-05-20 17:13:28Z ialex $'; } } - diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php index 3e66ed79..a45390c4 100644 --- a/includes/api/ApiLogin.php +++ b/includes/api/ApiLogin.php @@ -32,10 +32,10 @@ if (!defined('MEDIAWIKI')) { /** * Unit to authenticate log-in attempts to the current wiki. * - * @addtogroup API + * @ingroup API */ class ApiLogin extends ApiBase { - + /** * Time (in seconds) a user must wait after submitting * a bad login (will be multiplied by the THROTTLE_FACTOR for each bad attempt) @@ -47,12 +47,12 @@ class ApiLogin extends ApiBase { * attempts is increased every failed attempt. */ const THROTTLE_FACTOR = 2; - + /** - * The maximum number of failed logins after which the wait increase stops. + * The maximum number of failed logins after which the wait increase stops. */ const THOTTLE_MAX_COUNT = 10; - + public function __construct($main, $action) { parent :: __construct($main, $action, 'lg'); } @@ -104,10 +104,15 @@ class ApiLogin extends ApiBase { $wgUser->setOption('rememberpassword', 1); $wgUser->setCookies(); + // Run hooks. FIXME: split back and frontend from this hook. + // FIXME: This hook should be placed in the backend + $injected_html = ''; + wfRunHooks('UserLoginComplete', array(&$wgUser, &$injected_html)); + $result['result'] = 'Success'; - $result['lguserid'] = $_SESSION['wsUserID']; - $result['lgusername'] = $_SESSION['wsUserName']; - $result['lgtoken'] = $_SESSION['wsToken']; + $result['lguserid'] = $wgUser->getId(); + $result['lgusername'] = $wgUser->getName(); + $result['lgtoken'] = $wgUser->getToken(); $result['cookieprefix'] = $wgCookiePrefix; $result['sessionid'] = session_id(); break; @@ -130,34 +135,39 @@ class ApiLogin extends ApiBase { case LoginForm :: EMPTY_PASS : $result['result'] = 'EmptyPass'; break; + case LoginForm :: CREATE_BLOCKED : + $result['result'] = 'CreateBlocked'; + $result['details'] = 'Your IP address is blocked from account creation'; + break; default : ApiBase :: dieDebug(__METHOD__, 'Unhandled case value'); } - if ($result['result'] != 'Success') { - $result['wait'] = $this->cacheBadLogin(); - $result['details'] = "Please wait " . self::THROTTLE_TIME . " seconds before next log-in attempt"; + if ($result['result'] != 'Success' && !isset( $result['details'] ) ) { + $delay = $this->cacheBadLogin(); + $result['wait'] = $delay; + $result['details'] = "Please wait " . $delay . " seconds before next log-in attempt"; } // if we were allowed to try to login, memcache is fine - + $this->getResult()->addValue(null, 'login', $result); } - + /** - * Caches a bad-login attempt associated with the host and with an - * expiry of $this->mLoginThrottle. These are cached by a key + * Caches a bad-login attempt associated with the host and with an + * expiry of $this->mLoginThrottle. These are cached by a key * separate from that used by the captcha system--as such, logging * in through the standard interface will get you a legal session * and cookies to prove it, but will not remove this entry. * - * Returns the number of seconds until next login attempt will be allowed. + * Returns the number of seconds until next login attempt will be allowed. * * @access private */ private function cacheBadLogin() { global $wgMemc; - + $key = $this->getMemCacheKey(); $val = $wgMemc->get( $key ); @@ -167,24 +177,24 @@ class ApiLogin extends ApiBase { } else { $val['count'] = 1 + $val['count']; } - + $delay = ApiLogin::calculateDelay($val['count']); - + $wgMemc->delete($key); // Cache expiration should be the maximum timeout - to prevent a "try and wait" attack - $wgMemc->add( $key, $val, ApiLogin::calculateDelay(ApiLogin::THOTTLE_MAX_COUNT) ); - + $wgMemc->add( $key, $val, ApiLogin::calculateDelay(ApiLogin::THOTTLE_MAX_COUNT) ); + return $delay; } - + /** - * How much time the client must wait before it will be + * How much time the client must wait before it will be * allowed to try to log-in next. * The return value is 0 if no wait is required. */ private function getNextLoginTimeout() { global $wgMemc; - + $val = $wgMemc->get($this->getMemCacheKey()); $elapse = (time() - $val['lastReqTime']); // in seconds @@ -192,7 +202,7 @@ class ApiLogin extends ApiBase { return $canRetryIn < 0 ? 0 : $canRetryIn; } - + /** * Based on the number of previously attempted logins, returns * the delay (in seconds) when the next login attempt will be allowed. @@ -204,10 +214,10 @@ class ApiLogin extends ApiBase { $count = $count > self::THOTTLE_MAX_COUNT ? self::THOTTLE_MAX_COUNT : $count; return self::THROTTLE_TIME + self::THROTTLE_TIME * ($count - 1) * self::THROTTLE_FACTOR; - } + } /** - * Internal cache key for badlogin checks. Robbed from the + * Internal cache key for badlogin checks. Robbed from the * ConfirmEdit extension and modified to use a key unique to the * API login.3 * @@ -217,7 +227,7 @@ class ApiLogin extends ApiBase { private function getMemCacheKey() { return wfMemcKey( 'apilogin', 'badlogin', 'ip', wfGetIP() ); } - + public function mustBePosted() { return true; } public function getAllowedParams() { @@ -241,11 +251,11 @@ class ApiLogin extends ApiBase { 'This module is used to login and get the authentication tokens. ', 'In the event of a successful log-in, a cookie will be attached', 'to your session. In the event of a failed log-in, you will not ', - 'be able to attempt another log-in through this method for 60 seconds.', + 'be able to attempt another log-in through this method for 5 seconds.', 'This is to prevent password guessing by automated password crackers.' ); } - + protected function getExamples() { return array( 'api.php?action=login&lgname=user&lgpassword=password' @@ -253,7 +263,6 @@ class ApiLogin extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiLogin.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiLogin.php 35565 2008-05-29 19:23:37Z btongminh $'; } } - diff --git a/includes/api/ApiLogout.php b/includes/api/ApiLogout.php index d578acf3..694c9e3c 100644 --- a/includes/api/ApiLogout.php +++ b/includes/api/ApiLogout.php @@ -29,10 +29,10 @@ if (!defined('MEDIAWIKI')) { } /** - * API module to allow users to log out of the wiki. API equivalent of + * API module to allow users to log out of the wiki. API equivalent of * Special:Userlogout. * - * @addtogroup API + * @ingroup API */ class ApiLogout extends ApiBase { @@ -43,6 +43,10 @@ class ApiLogout extends ApiBase { public function execute() { global $wgUser; $wgUser->logout(); + + // Give extensions to do something after user logout + $injected_html = ''; + wfRunHooks( 'UserLogoutComplete', array(&$wgUser, &$injected_html) ); } public function getAllowedParams() { @@ -66,6 +70,6 @@ class ApiLogout extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id$'; + return __CLASS__ . ': $Id: ApiLogout.php 35294 2008-05-24 20:44:49Z btongminh $'; } } diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 874e531c..cce4c3e7 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -28,6 +28,10 @@ if (!defined('MEDIAWIKI')) { require_once ('ApiBase.php'); } +/** + * @defgroup API API + */ + /** * This is the main API class, used for both external and internal processing. * When executed, it will create the requested formatter object, @@ -37,9 +41,9 @@ if (!defined('MEDIAWIKI')) { * * To use API from another application, run it using FauxRequest object, in which * case any internal exceptions will not be handled but passed up to the caller. - * After successful execution, use getResult() for the resulting data. - * - * @addtogroup API + * After successful execution, use getResult() for the resulting data. + * + * @ingroup API */ class ApiMain extends ApiBase { @@ -62,7 +66,7 @@ class ApiMain extends ApiBase { 'help' => 'ApiHelp', 'paraminfo' => 'ApiParamInfo', ); - + private static $WriteModules = array ( 'rollback' => 'ApiRollback', 'delete' => 'ApiDelete', @@ -71,8 +75,8 @@ class ApiMain extends ApiBase { 'block' => 'ApiBlock', 'unblock' => 'ApiUnblock', 'move' => 'ApiMove', - #'changerights' => 'ApiChangeRights' - # Disabled for now + 'edit' => 'ApiEditPage', + 'emailuser' => 'ApiEmailUser', ); /** @@ -113,25 +117,25 @@ class ApiMain extends ApiBase { parent :: __construct($this, $this->mInternalMode ? 'main_int' : 'main'); if (!$this->mInternalMode) { - + // Impose module restrictions. - // If the current user cannot read, + // If the current user cannot read, // Remove all modules other than login global $wgUser; - + if( $request->getVal( 'callback' ) !== null ) { // JSON callback allows cross-site reads. // For safety, strip user credentials. wfDebug( "API: stripping user credentials for JSON callback\n" ); $wgUser = new User(); } - + if (!$wgUser->isAllowed('read')) { self::$Modules = array( 'login' => self::$Modules['login'], 'logout' => self::$Modules['logout'], 'help' => self::$Modules['help'], - ); + ); } } @@ -150,7 +154,8 @@ class ApiMain extends ApiBase { $this->mRequest = & $request; - $this->mSquidMaxage = 0; + $this->mSquidMaxage = -1; // flag for executeActionWithErrorHandling() + $this->mCommit = false; } /** @@ -175,12 +180,19 @@ class ApiMain extends ApiBase { } /** - * This method will simply cause an error if the write mode was disabled for this api. + * This method will simply cause an error if the write mode was disabled + * or if the current user doesn't have the right to use it */ public function requestWriteMode() { + global $wgUser; 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', 'noapiwrite'); + $this->dieUsage('Editing of this wiki through the API' . + ' is disabled. Make sure the $wgEnableWriteAPI=true; ' . + 'statement is included in the wiki\'s ' . + 'LocalSettings.php file', 'noapiwrite'); + if (!$wgUser->isAllowed('writeapi')) + $this->dieUsage('You\'re not allowed to edit this ' . + 'wiki through the API', 'writeapidenied'); } /** @@ -198,7 +210,7 @@ class ApiMain extends ApiBase { } /** - * Execute api request. Any errors will be handled if the API was called by the remote client. + * Execute api request. Any errors will be handled if the API was called by the remote client. */ public function execute() { $this->profileIn(); @@ -206,6 +218,7 @@ class ApiMain extends ApiBase { $this->executeAction(); else $this->executeActionWithErrorHandling(); + $this->profileOut(); } @@ -247,11 +260,22 @@ class ApiMain extends ApiBase { $this->printResult(true); } + global $wgRequest; + if($this->mSquidMaxage == -1) + { + # Nobody called setCacheMaxAge(), use the (s)maxage parameters + $smaxage = $wgRequest->getVal('smaxage', 0); + $maxage = $wgRequest->getVal('maxage', 0); + } + else + $smaxage = $maxage = $this->mSquidMaxage; + // Set the cache expiration at the last moment, as any errors may change the expiration. // if $this->mSquidMaxage == 0, the expiry time is set to the first second of unix epoch - $expires = $this->mSquidMaxage == 0 ? 1 : time() + $this->mSquidMaxage; + $exp = min($smaxage, $maxage); + $expires = ($exp == 0 ? 1 : time() + $exp); header('Expires: ' . wfTimestamp(TS_RFC2822, $expires)); - header('Cache-Control: s-maxage=' . $this->mSquidMaxage . ', must-revalidate, max-age=0'); + header('Cache-Control: s-maxage=' . $smaxage . ', must-revalidate, max-age=' . $maxage); if($this->mPrinter->getIsHtml()) echo wfReportTime(); @@ -261,10 +285,10 @@ class ApiMain extends ApiBase { /** * Replace the result data with the information about an exception. - * Returns the error code + * Returns the error code */ protected function substituteResultWithError($e) { - + // Printer may not be initialized if the extractRequestParams() fails for the main module if (!isset ($this->mPrinter)) { // The printer has not been created yet. Try to manually get formatter value. @@ -284,20 +308,27 @@ class ApiMain extends ApiBase { $errMessage = array ( 'code' => $e->getCodeString(), 'info' => $e->getMessage()); - + // Only print the help message when this is for the developer, not runtime if ($this->mPrinter->getIsHtml() || $this->mAction == 'help') ApiResult :: setContent($errMessage, $this->makeHelpMsg()); } else { + global $wgShowSQLErrors, $wgShowExceptionDetails; // // Something is seriously wrong // + if ( ( $e instanceof DBQueryError ) && !$wgShowSQLErrors ) { + $info = "Database query error"; + } else { + $info = "Exception Caught: {$e->getMessage()}"; + } + $errMessage = array ( 'code' => 'internal_api_error_'. get_class($e), - 'info' => "Exception Caught: {$e->getMessage()}" + 'info' => $info, ); - ApiResult :: setContent($errMessage, "\n\n{$e->getTraceAsString()}\n\n"); + ApiResult :: setContent($errMessage, $wgShowExceptionDetails ? "\n\n{$e->getTraceAsString()}\n\n" : "" ); } $this->getResult()->reset(); @@ -318,12 +349,12 @@ class ApiMain extends ApiBase { // Instantiate the module requested by the user $module = new $this->mModules[$this->mAction] ($this, $this->mAction); - + if( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) { // Check for maxlag - global $wgLoadBalancer, $wgShowHostnames; + global $wgShowHostnames; $maxLag = $params['maxlag']; - list( $host, $lag ) = $wgLoadBalancer->getMaxLag(); + list( $host, $lag ) = wfGetLB()->getMaxLag(); if ( $lag > $maxLag ) { if( $wgShowHostnames ) { ApiBase :: dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' ); @@ -333,7 +364,7 @@ class ApiMain extends ApiBase { return; } } - + if (!$this->mInternalMode) { // Ignore mustBePosted() for internal calls if($module->mustBePosted() && !$this->mRequest->wasPosted()) @@ -367,13 +398,12 @@ class ApiMain extends ApiBase { protected function printResult($isError) { $printer = $this->mPrinter; $printer->profileIn(); - + /* If the help message is requested in the default (xmlfm) format, * tell the printer not to escape ampersands so that our links do * not break. */ - $params = $this->extractRequestParams(); - $printer->setUnescapeAmps ( ( $this->mAction == 'help' || $isError ) - && $params['format'] == ApiMain::API_DEFAULT_FORMAT ); + $printer->setUnescapeAmps ( ( $this->mAction == 'help' || $isError ) + && $this->getParameter('format') == ApiMain::API_DEFAULT_FORMAT ); $printer->initPrinter($isError); @@ -399,6 +429,14 @@ class ApiMain extends ApiBase { 'maxlag' => array ( ApiBase :: PARAM_TYPE => 'integer' ), + 'smaxage' => array ( + ApiBase :: PARAM_TYPE => 'integer', + ApiBase :: PARAM_DFLT => 0 + ), + 'maxage' => array ( + ApiBase :: PARAM_TYPE => 'integer', + ApiBase :: PARAM_DFLT => 0 + ), ); } @@ -410,7 +448,9 @@ class ApiMain extends ApiBase { 'format' => 'The format of the output', 'action' => 'What action you would like to perform', 'version' => 'When showing help, include version for each module', - 'maxlag' => 'Maximum lag' + 'maxlag' => 'Maximum lag', + 'smaxage' => 'Set the s-maxage header to this many seconds. Errors are never cached', + 'maxage' => 'Set the max-age header to this many seconds. Errors are never cached', ); } @@ -444,13 +484,17 @@ 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 Roan Kattouw .@home.nl', + 'API developers:', + ' Roan Kattouw .@home.nl (lead developer Sep 2007-present)', + ' Victor Vasiliev - vasilvv at gee mail dot com', + ' Yuri Astrakhan @gmail.com (creator, lead developer Sep 2006-Sep 2007)', + '', 'Please send your comments, suggestions and questions to mediawiki-api@lists.wikimedia.org', 'or file a bug report at http://bugzilla.wikimedia.org/' ); @@ -460,7 +504,7 @@ class ApiMain extends ApiBase { * Override the parent to generate help messages for all available modules. */ public function makeHelpMsg() { - + $this->mPrinter->setHelp(); // Use parent to make default message for the main module @@ -486,9 +530,9 @@ class ApiMain extends ApiBase { $msg .= $msg2; $msg .= "\n"; } - + $msg .= "\n*** Credits: ***\n " . implode("\n ", $this->getCredits()) . "\n"; - + return $msg; } @@ -496,15 +540,15 @@ class ApiMain extends ApiBase { public static function makeHelpMsgHeader($module, $paramName) { $modulePrefix = $module->getModulePrefix(); if (!empty($modulePrefix)) - $modulePrefix = "($modulePrefix) "; - + $modulePrefix = "($modulePrefix) "; + return "* $paramName={$module->getModuleName()} $modulePrefix*"; - } + } private $mIsBot = null; private $mIsSysop = null; private $mCanApiHighLimits = null; - + /** * Returns true if the currently logged in user is a bot, false otherwise * OBSOLETE, use canApiHighLimits() instead @@ -516,7 +560,7 @@ class ApiMain extends ApiBase { } return $this->mIsBot; } - + /** * Similar to isBot(), this method returns true if the logged in user is * a sysop, and false if not. @@ -530,7 +574,11 @@ class ApiMain extends ApiBase { return $this->mIsSysop; } - + + /** + * Check whether the current user is allowed to use high limits + * @return bool + */ public function canApiHighLimits() { if (!isset($this->mCanApiHighLimits)) { global $wgUser; @@ -540,6 +588,10 @@ class ApiMain extends ApiBase { return $this->mCanApiHighLimits; } + /** + * Check whether the user wants us to show version information in the API help + * @return bool + */ public function getShowVersions() { return $this->mShowVersions; } @@ -551,7 +603,7 @@ class ApiMain extends ApiBase { public function getVersion() { $vers = array (); $vers[] = 'MediaWiki ' . SpecialVersion::getVersion(); - $vers[] = __CLASS__ . ': $Id: ApiMain.php 31484 2008-03-03 05:46:20Z brion $'; + $vers[] = __CLASS__ . ': $Id: ApiMain.php 37349 2008-07-08 20:53:41Z catrope $'; $vers[] = ApiBase :: getBaseVersion(); $vers[] = ApiFormatBase :: getBaseVersion(); $vers[] = ApiQueryBase :: getBaseVersion(); @@ -561,7 +613,7 @@ class ApiMain extends ApiBase { /** * Add or overwrite a module in this ApiMain instance. Intended for use by extending - * classes who wish to add their own modules to their lexicon or override the + * classes who wish to add their own modules to their lexicon or override the * behavior of inherent ones. * * @access protected @@ -583,7 +635,7 @@ class ApiMain extends ApiBase { protected function addFormat( $fmtName, $fmtClass ) { $this->mFormats[$fmtName] = $fmtClass; } - + /** * Get the array mapping module names to class names */ @@ -595,8 +647,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 + * + * @ingroup API */ class UsageException extends Exception { @@ -613,5 +665,3 @@ class UsageException extends Exception { return "{$this->getCodeString()}: {$this->getMessage()}"; } } - - diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php index a8c39c9a..8687bdcd 100644 --- a/includes/api/ApiMove.php +++ b/includes/api/ApiMove.php @@ -29,21 +29,21 @@ if (!defined('MEDIAWIKI')) { /** - * @addtogroup API + * @ingroup API */ class ApiMove extends ApiBase { public function __construct($main, $action) { parent :: __construct($main, $action); } - + public function execute() { global $wgUser; $this->getMain()->requestWriteMode(); $params = $this->extractRequestParams(); if(is_null($params['reason'])) $params['reason'] = ''; - + $titleObj = NULL; if(!isset($params['from'])) $this->dieUsageMsg(array('missingparam', 'from')); @@ -69,7 +69,7 @@ class ApiMove extends ApiBase { // Run getUserPermissionsErrors() here so we get message arguments too, // rather than just a message key. The latter is troublesome for messages // that use arguments. - // FIXME: moveTo() should really return an array, requires some + // FIXME: moveTo() should really return an array, requires some // refactoring of other code, though (mainly SpecialMovepage.php) $errors = array_merge($fromTitle->getUserPermissionsErrors('move', $wgUser), $fromTitle->getUserPermissionsErrors('edit', $wgUser), @@ -79,16 +79,19 @@ class ApiMove extends ApiBase { // We don't care about multiple errors, just report one of them $this->dieUsageMsg(current($errors)); - $dbw = wfGetDB(DB_MASTER); - $dbw->begin(); + $hookErr = null; + $retval = $fromTitle->moveTo($toTitle, true, $params['reason'], !$params['noredirect']); if($retval !== true) - $this->dieUsageMsg(array($retval)); + { + # FIXME: Title::moveTo() sometimes returns a string + $this->dieUsageMsg(reset($retval)); + } $r = array('from' => $fromTitle->getPrefixedText(), 'to' => $toTitle->getPrefixedText(), 'reason' => $params['reason']); - if(!$params['noredirect']) + if(!$params['noredirect'] || !$wgUser->isAllowed('suppressredirect')) $r['redirectcreated'] = ''; - + if($params['movetalk'] && $fromTalk->exists() && !$fromTitle->isTalkPage()) { // We need to move the talk page as well @@ -104,14 +107,25 @@ class ApiMove extends ApiBase { { $r['talkmove-error-code'] = ApiBase::$messageMap[$retval]['code']; $r['talkmove-error-info'] = ApiBase::$messageMap[$retval]['info']; - } + } + } + + # Watch pages + if($params['watch'] || $wgUser->getOption('watchmoves')) + { + $wgUser->addWatch($fromTitle); + $wgUser->addWatch($toTitle); + } + else if($params['unwatch']) + { + $wgUser->removeWatch($fromTitle); + $wgUser->removeWatch($toTitle); } - $dbw->commit(); // Make sure all changes are really written to the DB $this->getResult()->addValue(null, $this->getModuleName(), $r); } - + public function mustBePosted() { return true; } - + public function getAllowedParams() { return array ( 'from' => null, @@ -119,7 +133,9 @@ class ApiMove extends ApiBase { 'token' => null, 'reason' => null, 'movetalk' => false, - 'noredirect' => false + 'noredirect' => false, + 'watch' => false, + 'unwatch' => false ); } @@ -130,7 +146,9 @@ class ApiMove extends ApiBase { 'token' => 'A move token previously retrieved through prop=info', 'reason' => 'Reason for the move (optional).', 'movetalk' => 'Move the talk page, if it exists.', - 'noredirect' => 'Don\'t create a redirect' + 'noredirect' => 'Don\'t create a redirect', + 'watch' => 'Add the page and the redirect to your watchlist', + 'unwatch' => 'Remove the page and the redirect from your watchlist' ); } @@ -147,6 +165,6 @@ class ApiMove extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiMove.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiMove.php 35619 2008-05-30 19:59:47Z btongminh $'; } } diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php index f4b600fe..2da92059 100644 --- a/includes/api/ApiOpenSearch.php +++ b/includes/api/ApiOpenSearch.php @@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) { } /** - * @addtogroup API + * @ingroup API */ class ApiOpenSearch extends ApiBase { @@ -45,11 +45,12 @@ class ApiOpenSearch extends ApiBase { $params = $this->extractRequestParams(); $search = $params['search']; $limit = $params['limit']; - + $namespaces = $params['namespace']; + // Open search results may be stored for a very long time $this->getMain()->setCacheMaxAge(1200); - - $srchres = PrefixSearch::titleSearch( $search, $limit ); + + $srchres = PrefixSearch::titleSearch( $search, $limit, $namespaces ); // Set top level elements $result = $this->getResult(); @@ -66,14 +67,20 @@ class ApiOpenSearch extends ApiBase { ApiBase :: PARAM_MIN => 1, ApiBase :: PARAM_MAX => 100, ApiBase :: PARAM_MAX2 => 100 - ) + ), + 'namespace' => array( + ApiBase :: PARAM_DFLT => NS_MAIN, + ApiBase :: PARAM_TYPE => 'namespace', + ApiBase :: PARAM_ISMULTI => true + ), ); } public function getParamDescription() { return array ( 'search' => 'Search string', - 'limit' => 'Maximum amount of results to return' + 'limit' => 'Maximum amount of results to return', + 'namespace' => 'Namespaces to search', ); } @@ -88,7 +95,6 @@ class ApiOpenSearch extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiOpenSearch.php 30275 2008-01-30 01:07:49Z brion $'; + return __CLASS__ . ': $Id: ApiOpenSearch.php 35098 2008-05-20 17:13:28Z ialex $'; } } - diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php index 185c0c59..e09cb285 100644 --- a/includes/api/ApiPageSet.php +++ b/includes/api/ApiPageSet.php @@ -33,17 +33,18 @@ if (!defined('MEDIAWIKI')) { * Initially, when the client passes in titles=, pageids=, or revisions= parameter, * an instance of the ApiPageSet class will normalize titles, * determine if the pages/revisions exist, and prefetch any additional data page data requested. - * + * * When generator is used, the result of the generator will become the input for the * second instance of this class, and all subsequent actions will go use the second instance - * for all their work. - * - * @addtogroup API + * for all their work. + * + * @ingroup API */ class ApiPageSet extends ApiQueryBase { - private $mAllPages; // [ns][dbkey] => page_id or 0 when missing - private $mTitles, $mGoodTitles, $mMissingTitles, $mMissingPageIDs, $mRedirectTitles; + private $mAllPages; // [ns][dbkey] => page_id or negative when missing + private $mTitles, $mGoodTitles, $mMissingTitles, $mInvalidTitles; + private $mMissingPageIDs, $mRedirectTitles; private $mNormalizedTitles, $mInterwikiTitles; private $mResolveRedirects, $mPendingRedirectIDs; private $mGoodRevIDs, $mMissingRevIDs; @@ -58,6 +59,7 @@ class ApiPageSet extends ApiQueryBase { $this->mTitles = array(); $this->mGoodTitles = array (); $this->mMissingTitles = array (); + $this->mInvalidTitles = array (); $this->mMissingPageIDs = array (); $this->mRedirectTitles = array (); $this->mNormalizedTitles = array (); @@ -69,7 +71,7 @@ class ApiPageSet extends ApiQueryBase { $this->mResolveRedirects = $resolveRedirects; if($resolveRedirects) $this->mPendingRedirectIDs = array(); - + $this->mFakePageId = -1; } @@ -107,8 +109,9 @@ class ApiPageSet extends ApiQueryBase { } /** - * Returns an array [ns][dbkey] => page_id for all requested titles - * page_id is a unique negative number in case title was not found + * Returns an array [ns][dbkey] => page_id for all requested titles. + * page_id is a unique negative number in case title was not found. + * Invalid titles will also have negative page IDs and will be in namespace 0 */ public function getAllTitlesByNamespace() { return $this->mAllPages; @@ -153,6 +156,15 @@ class ApiPageSet extends ApiQueryBase { return $this->mMissingTitles; } + /** + * Titles that were deemed invalid by Title::newFromText() + * The array's index will be unique and negative for each item + * @return array of strings (not Title objects) + */ + public function getInvalidTitles() { + return $this->mInvalidTitles; + } + /** * Page IDs that were not found in the database * @return array of page IDs @@ -170,18 +182,18 @@ class ApiPageSet extends ApiQueryBase { } /** - * Get a list of title normalizations - maps the title given + * Get a list of title normalizations - maps the title given * with its normalized version. - * @return array raw_prefixed_title (string) => prefixed_title (string) + * @return array raw_prefixed_title (string) => prefixed_title (string) */ public function getNormalizedTitles() { return $this->mNormalizedTitles; } /** - * Get a list of interwiki titles - maps the title given + * Get a list of interwiki titles - maps the title given * with to the interwiki prefix. - * @return array raw_prefixed_title (string) => interwiki_prefix (string) + * @return array raw_prefixed_title (string) => interwiki_prefix (string) */ public function getInterwikiTitles() { return $this->mInterwikiTitles; @@ -293,11 +305,11 @@ class ApiPageSet extends ApiQueryBase { * Extract all requested fields from the row received from the database */ public function processDbRow($row) { - + // Store Title object in various data structures $title = Title :: makeTitle($row->page_namespace, $row->page_title); - - $pageId = intval($row->page_id); + + $pageId = intval($row->page_id); $this->mAllPages[$row->page_namespace][$row->page_title] = $pageId; $this->mTitles[] = $title; @@ -310,26 +322,26 @@ class ApiPageSet extends ApiQueryBase { foreach ($this->mRequestedPageFields as $fieldName => & $fieldValues) $fieldValues[$pageId] = $row-> $fieldName; } - + public function finishPageSetGeneration() { $this->profileIn(); $this->resolvePendingRedirects(); $this->profileOut(); } - + /** * This method populates internal variables with page information * based on the given array of title strings. - * + * * Steps: * #1 For each title, get data from `page` table * #2 If page was not found in the DB, store it as missing - * + * * Additionally, when resolving redirects: * #3 If no more redirects left, stop. * #4 For each redirect, get its links from `pagelinks` table. * #5 Substitute the original LinkBatch object with the new list - * #6 Repeat from step #1 + * #6 Repeat from step #1 */ private function initFromTitles($titles) { @@ -337,7 +349,7 @@ class ApiPageSet extends ApiQueryBase { $linkBatch = $this->processTitlesArray($titles); if($linkBatch->isEmpty()) return; - + $db = $this->getDB(); $set = $linkBatch->constructSet('page', $db); @@ -368,13 +380,14 @@ class ApiPageSet extends ApiQueryBase { $this->profileDBIn(); $res = $db->select('page', $this->getPageTableFields(), $set, __METHOD__); $this->profileDBOut(); - - $this->initFromQueryResult($db, $res, array_flip($pageids), false); // process PageIDs + + $remaining = array_flip($pageids); + $this->initFromQueryResult($db, $res, $remaining, false); // process PageIDs // Resolve any found redirects $this->resolvePendingRedirects(); } - + /** * Iterate through the result of the query on 'page' table, * and for each row create and store title object and save any extra fields requested. @@ -390,7 +403,7 @@ class ApiPageSet extends ApiQueryBase { private function initFromQueryResult($db, $res, &$remaining = null, $processTitles = null) { if (!is_null($remaining) && is_null($processTitles)) ApiBase :: dieDebug(__METHOD__, 'Missing $processTitles parameter when $remaining is provided'); - + while ($row = $db->fetchObject($res)) { $pageId = intval($row->page_id); @@ -402,12 +415,12 @@ class ApiPageSet extends ApiQueryBase { else unset ($remaining[$pageId]); } - + // Store any extra fields requested by modules $this->processDbRow($row); } $db->freeResult($res); - + if(isset($remaining)) { // Any items left in the $remaining list are added as missing if($processTitles) { @@ -437,15 +450,15 @@ class ApiPageSet extends ApiQueryBase { if(empty($revids)) return; - + $db = $this->getDB(); $pageids = array(); $remaining = array_flip($revids); - + $tables = array('revision'); $fields = array('rev_id','rev_page'); $where = array('rev_deleted' => 0, 'rev_id' => $revids); - + // Get pageIDs data from the `page` table $this->profileDBIn(); $res = $db->select( $tables, $fields, $where, __METHOD__ ); @@ -472,27 +485,27 @@ class ApiPageSet extends ApiQueryBase { if($this->mResolveRedirects) { $db = $this->getDB(); $pageFlds = $this->getPageTableFields(); - + // Repeat until all redirects have been resolved // The infinite loop is prevented by keeping all known pages in $this->mAllPages - while (!empty ($this->mPendingRedirectIDs)) { - + while (!empty ($this->mPendingRedirectIDs)) { + // Resolve redirects by querying the pagelinks table, and repeat the process // Create a new linkBatch object for the next pass $linkBatch = $this->getRedirectTargets(); - + if ($linkBatch->isEmpty()) break; - + $set = $linkBatch->constructSet('page', $db); if(false === $set) break; - + // Get pageIDs data from the `page` table $this->profileDBIn(); $res = $db->select('page', $pageFlds, $set, __METHOD__); $this->profileDBOut(); - + // Hack: get the ns:titles stored in array(ns => array(titles)) format $this->initFromQueryResult($db, $res, $linkBatch->data, true); } @@ -500,76 +513,55 @@ class ApiPageSet extends ApiQueryBase { } private function getRedirectTargets() { - - $linkBatch = new LinkBatch(); + $lb = new LinkBatch(); $db = $this->getDB(); - // find redirect targets for all redirect pages $this->profileDBIn(); - $res = $db->select('pagelinks', array ( - 'pl_from', - 'pl_namespace', - 'pl_title' - ), array ( - 'pl_from' => array_keys($this->mPendingRedirectIDs - )), __METHOD__); + $res = $db->select('redirect', array( + 'rd_from', + 'rd_namespace', + 'rd_title' + ), array('rd_from' => array_keys($this->mPendingRedirectIDs)), + __METHOD__ + ); $this->profileDBOut(); - while ($row = $db->fetchObject($res)) { - - $plfrom = intval($row->pl_from); - - // Bug 7304 workaround - // ( http://bugzilla.wikipedia.org/show_bug.cgi?id=7304 ) - // A redirect page may have more than one link. - // This code will only use the first link returned. - if (isset ($this->mPendingRedirectIDs[$plfrom])) { // remove line when bug 7304 is fixed - - $titleStrFrom = $this->mPendingRedirectIDs[$plfrom]->getPrefixedText(); - $titleStrTo = Title :: makeTitle($row->pl_namespace, $row->pl_title)->getPrefixedText(); - unset ($this->mPendingRedirectIDs[$plfrom]); // remove line when bug 7304 is fixed - - // Avoid an infinite loop by checking if we have already processed this target - if (!isset ($this->mAllPages[$row->pl_namespace][$row->pl_title])) { - $linkBatch->add($row->pl_namespace, $row->pl_title); - } - } else { - // This redirect page has more than one link. - // This is very slow, but safer until bug 7304 is resolved - $title = Title :: newFromID($plfrom); - $titleStrFrom = $title->getPrefixedText(); - + while($row = $db->fetchObject($res)) + { + $rdfrom = intval($row->rd_from); + $from = $this->mPendingRedirectIDs[$rdfrom]->getPrefixedText(); + $to = Title::makeTitle($row->rd_namespace, $row->rd_title)->getPrefixedText(); + unset($this->mPendingRedirectIDs[$rdfrom]); + if(!isset($this->mAllPages[$row->rd_namespace][$row->rd_title])) + $lb->add($row->rd_namespace, $row->rd_title); + $this->mRedirectTitles[$from] = $to; + } + $db->freeResult($res); + if(!empty($this->mPendingRedirectIDs)) + { + # We found pages that aren't in the redirect table + # Add them + foreach($this->mPendingRedirectIDs as $id => $title) + { $article = new Article($title); - $text = $article->getContent(); - $titleTo = Title :: newFromRedirect($text); - $titleStrTo = $titleTo->getPrefixedText(); - - if (is_null($titleStrTo)) - ApiBase :: dieDebug(__METHOD__, 'Bug7304 workaround: redir target from {$title->getPrefixedText()} not found'); - - // Avoid an infinite loop by checking if we have already processed this target - if (!isset ($this->mAllPages[$titleTo->getNamespace()][$titleTo->getDBkey()])) { - $linkBatch->addObj($titleTo); - } + $rt = $article->insertRedirect(); + if(!$rt) + # What the hell. Let's just ignore this + continue; + $lb->addObj($rt); + $this->mRedirectTitles[$title->getPrefixedText()] = $rt->getPrefixedText(); + unset($this->mPendingRedirectIDs[$id]); } - - $this->mRedirectTitles[$titleStrFrom] = $titleStrTo; } - $db->freeResult($res); - - // All IDs must exist in the page table - if (!empty($this->mPendingRedirectIDs[$plfrom])) - ApiBase :: dieDebug(__METHOD__, 'Invalid redirect IDs were found'); - - return $linkBatch; + return $lb; } /** * Given an array of title strings, convert them into Title objects. * Alternativelly, an array of Title objects may be given. - * This method validates access rights for the title, + * This method validates access rights for the title, * and appends normalization values to the output. - * + * * @return LinkBatch of title objects. */ private function processTitlesArray($titles) { @@ -577,16 +569,21 @@ class ApiPageSet extends ApiQueryBase { $linkBatch = new LinkBatch(); foreach ($titles as $title) { - + $titleObj = is_string($title) ? Title :: newFromText($title) : $title; if (!$titleObj) - $this->dieUsage("bad title", 'invalidtitle'); - + { + # Handle invalid titles gracefully + $this->mAllpages[0][$title] = $this->mFakePageId; + $this->mInvalidTitles[$this->mFakePageId] = $title; + $this->mFakePageId--; + continue; // There's nothing else we can do + } $iw = $titleObj->getInterwiki(); if (!empty($iw)) { // This title is an interwiki link. $this->mInterwikiTitles[$titleObj->getPrefixedText()] = $iw; - } else { + } else { // Validation if ($titleObj->getNamespace() < 0) @@ -594,7 +591,7 @@ class ApiPageSet extends ApiQueryBase { $linkBatch->addObj($titleObj); } - + // Make sure we remember the original title that was given to us // This way the caller can correlate new titles with the originally requested, // i.e. namespace is localized or capitalization is different @@ -605,7 +602,7 @@ class ApiPageSet extends ApiQueryBase { return $linkBatch; } - + protected function getAllowedParams() { return array ( 'titles' => array ( @@ -631,7 +628,6 @@ class ApiPageSet extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiPageSet.php 24935 2007-08-20 08:13:16Z nickj $'; + return __CLASS__ . ': $Id: ApiPageSet.php 35098 2008-05-20 17:13:28Z ialex $'; } } - diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php index 7de22252..77ce514f 100644 --- a/includes/api/ApiParamInfo.php +++ b/includes/api/ApiParamInfo.php @@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) { } /** - * @addtogroup API + * @ingroup API */ class ApiParamInfo extends ApiBase { @@ -55,7 +55,7 @@ class ApiParamInfo extends ApiBase { $obj = new $modArr[$m]($this->getMain(), $m); $a = $this->getClassInfo($obj); $a['name'] = $m; - $r['modules'][] = $a; + $r['modules'][] = $a; } $result->setIndexedTagName($r['modules'], 'module'); } @@ -106,7 +106,7 @@ class ApiParamInfo extends ApiBase { $retval['parameters'][] = $a; continue; } - + if(isset($p[ApiBase::PARAM_DFLT])) $a['default'] = $p[ApiBase::PARAM_DFLT]; if(isset($p[ApiBase::PARAM_ISMULTI])) @@ -131,7 +131,7 @@ class ApiParamInfo extends ApiBase { $result->setIndexedTagName($retval['parameters'], 'param'); return $retval; } - + public function getAllowedParams() { return array ( 'modules' => array( @@ -161,7 +161,6 @@ class ApiParamInfo extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiParse.php 29810 2008-01-15 21:33:08Z catrope $'; + return __CLASS__ . ': $Id: ApiParamInfo.php 35098 2008-05-20 17:13:28Z ialex $'; } } - diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php index 21a21e8d..4dcc94b6 100644 --- a/includes/api/ApiParse.php +++ b/includes/api/ApiParse.php @@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) { } /** - * @addtogroup API + * @ingroup API */ class ApiParse extends ApiBase { @@ -43,31 +43,56 @@ class ApiParse extends ApiBase { $text = $params['text']; $title = $params['title']; $page = $params['page']; + $oldid = $params['oldid']; if(!is_null($page) && (!is_null($text) || $title != "API")) $this->dieUsage("The page parameter cannot be used together with the text and title parameters", 'params'); $prop = array_flip($params['prop']); - + $revid = false; + global $wgParser, $wgUser; - if(!is_null($page)) { - $titleObj = Title::newFromText($page); - if(!$titleObj) - $this->dieUsageMsg(array('missingtitle', $page)); - - // Try the parser cache first - $articleObj = new Article($titleObj); - $pcache =& ParserCache::singleton(); - $p_result = $pcache->get($articleObj, $wgUser); - if(!$p_result) { - $p_result = $wgParser->parse($articleObj->getContent(), $titleObj, new ParserOptions()); - global $wgUseParserCache; - if($wgUseParserCache) - $pcache->save($p_result, $articleObj, $wgUser); + $popts = new ParserOptions(); + $popts->setTidy(true); + $popts->enableLimitReport(); + if(!is_null($oldid) || !is_null($page)) + { + if(!is_null($oldid)) + { + # Don't use the parser cache + $rev = Revision::newFromID($oldid); + if(!$rev) + $this->dieUsage("There is no revision ID $oldid", 'missingrev'); + if(!$rev->userCan(Revision::DELETED_TEXT)) + $this->dieUsage("You don't have permission to view deleted revisions", 'permissiondenied'); + $text = $rev->getRawText(); + $titleObj = $rev->getTitle(); + $p_result = $wgParser->parse($text, $titleObj, $popts); } - } else { + else + { + $titleObj = Title::newFromText($page); + if(!$titleObj) + $this->dieUsage("The page you specified doesn't exist", 'missingtitle'); + + // Try the parser cache first + $articleObj = new Article($titleObj); + if(isset($prop['revid'])) + $oldid = $articleObj->getRevIdFetched(); + $pcache = ParserCache::singleton(); + $p_result = $pcache->get($articleObj, $wgUser); + if(!$p_result) { + $p_result = $wgParser->parse($articleObj->getContent(), $titleObj, $popts); + global $wgUseParserCache; + if($wgUseParserCache) + $pcache->save($p_result, $articleObj, $wgUser); + } + } + } + else + { $titleObj = Title::newFromText($title); if(!$titleObj) $titleObj = Title::newFromText("API"); - $p_result = $wgParser->parse($text, $titleObj, new ParserOptions()); + $p_result = $wgParser->parse($text, $titleObj, $popts); } // Return result @@ -91,6 +116,8 @@ class ApiParse extends ApiBase { $result_array['externallinks'] = array_keys($p_result->getExternalLinks()); if(isset($prop['sections'])) $result_array['sections'] = $p_result->getSections(); + if(!is_null($oldid)) + $result_array['revid'] = $oldid; $result_mapping = array( 'langlinks' => 'll', @@ -104,7 +131,7 @@ class ApiParse extends ApiBase { $this->setIndexedTagNames( $result_array, $result_mapping ); $result->addValue( null, $this->getModuleName(), $result_array ); } - + private function formatLangLinks( $links ) { $result = array(); foreach( $links as $link ) { @@ -116,7 +143,7 @@ class ApiParse extends ApiBase { } return $result; } - + private function formatCategoryLinks( $links ) { $result = array(); foreach( $links as $link => $sortkey ) { @@ -127,7 +154,7 @@ class ApiParse extends ApiBase { } return $result; } - + private function formatLinks( $links ) { $result = array(); foreach( $links as $ns => $nslinks ) { @@ -142,7 +169,7 @@ class ApiParse extends ApiBase { } return $result; } - + private function setIndexedTagNames( &$array, $mapping ) { foreach( $mapping as $key => $name ) { if( isset( $array[$key] ) ) @@ -152,13 +179,14 @@ class ApiParse extends ApiBase { public function getAllowedParams() { return array ( - 'title' => array( + 'title' => array( ApiBase :: PARAM_DFLT => 'API', ), 'text' => null, 'page' => null, + 'oldid' => null, 'prop' => array( - ApiBase :: PARAM_DFLT => 'text|langlinks|categories|links|templates|images|externallinks|sections', + ApiBase :: PARAM_DFLT => 'text|langlinks|categories|links|templates|images|externallinks|sections|revid', ApiBase :: PARAM_ISMULTI => true, ApiBase :: PARAM_TYPE => array( 'text', @@ -168,7 +196,8 @@ class ApiParse extends ApiBase { 'templates', 'images', 'externallinks', - 'sections' + 'sections', + 'revid' ) ) ); @@ -179,6 +208,7 @@ class ApiParse extends ApiBase { 'text' => 'Wikitext to parse', 'title' => 'Title of page the text belongs to', 'page' => 'Parse the content of this page. Cannot be used together with text and title', + 'oldid' => 'Parse the content of this revision. Overrides page', 'prop' => array('Which pieces of information to get.', 'NOTE: Section tree is only generated if there are more than 4 sections, or if the __TOC__ keyword is present' ), @@ -196,7 +226,6 @@ class ApiParse extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiParse.php 30262 2008-01-29 14:47:27Z catrope $'; + return __CLASS__ . ': $Id: ApiParse.php 36983 2008-07-03 15:01:50Z catrope $'; } } - diff --git a/includes/api/ApiProtect.php b/includes/api/ApiProtect.php index 40a4b73d..30bcfdbc 100644 --- a/includes/api/ApiProtect.php +++ b/includes/api/ApiProtect.php @@ -28,7 +28,7 @@ if (!defined('MEDIAWIKI')) { } /** - * @addtogroup API + * @ingroup API */ class ApiProtect extends ApiBase { @@ -40,7 +40,7 @@ class ApiProtect extends ApiBase { global $wgUser; $this->getMain()->requestWriteMode(); $params = $this->extractRequestParams(); - + $titleObj = NULL; if(!isset($params['title'])) $this->dieUsageMsg(array('missingparam', 'title')); @@ -55,12 +55,12 @@ class ApiProtect extends ApiBase { $titleObj = Title::newFromText($params['title']); if(!$titleObj) $this->dieUsageMsg(array('invalidtitle', $params['title'])); - + $errors = $titleObj->getUserPermissionsErrors('protect', $wgUser); if(!empty($errors)) // We don't care about multiple errors, just report one of them $this->dieUsageMsg(current($errors)); - + if(in_array($params['expiry'], array('infinite', 'indefinite', 'never'))) $expiry = Block::infinity(); else @@ -68,7 +68,7 @@ class ApiProtect extends ApiBase { $expiry = strtotime($params['expiry']); if($expiry < 0 || $expiry == false) $this->dieUsageMsg(array('invalidexpiry')); - + $expiry = wfTimestamp(TS_MW, $expiry); if($expiry < wfTimestampNow()) $this->dieUsageMsg(array('pastexpiry')); @@ -85,8 +85,6 @@ class ApiProtect extends ApiBase { $this->dieUsageMsg(array('missingtitles-createonly')); } - $dbw = wfGetDb(DB_MASTER); - $dbw->begin(); if($titleObj->exists()) { $articleObj = new Article($titleObj); $ok = $articleObj->updateRestrictions($protections, $params['reason'], $params['cascade'], $expiry); @@ -96,7 +94,6 @@ class ApiProtect extends ApiBase { // This is very weird. Maybe the article was deleted or the user was blocked/desysopped in the meantime? // Just throw an unknown error in this case, as it's very likely to be a race condition $this->dieUsageMsg(array()); - $dbw->commit(); $res = array('title' => $titleObj->getPrefixedText(), 'reason' => $params['reason']); if($expiry == Block::infinity()) $res['expiry'] = 'infinity'; @@ -149,6 +146,6 @@ class ApiProtect extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiProtect.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiProtect.php 35098 2008-05-20 17:13:28Z ialex $'; } } diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index 29abd859..f4a2402f 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -33,11 +33,11 @@ if (!defined('MEDIAWIKI')) { * it will create a list of titles to work on (an instance of the ApiPageSet object) * instantiate and execute various property/list/meta modules, * and assemble all resulting data into a single ApiResult object. - * + * * In the generator mode, a generator will be first executed to populate a second ApiPageSet object, * and that object will be used for all subsequent modules. - * - * @addtogroup API + * + * @ingroup API */ class ApiQuery extends ApiBase { @@ -55,9 +55,11 @@ class ApiQuery extends ApiBase { 'templates' => 'ApiQueryLinks', 'categories' => 'ApiQueryCategories', 'extlinks' => 'ApiQueryExternalLinks', + 'categoryinfo' => 'ApiQueryCategoryInfo', ); private $mQueryListModules = array ( + 'allimages' => 'ApiQueryAllimages', 'allpages' => 'ApiQueryAllpages', 'alllinks' => 'ApiQueryAllLinks', 'allcategories' => 'ApiQueryAllCategories', @@ -90,7 +92,7 @@ class ApiQuery extends ApiBase { public function __construct($main, $action) { parent :: __construct($main, $action); - // Allow custom modules to be added in LocalSettings.php + // Allow custom modules to be added in LocalSettings.php global $wgApiQueryPropModules, $wgApiQueryListModules, $wgApiQueryMetaModules; self :: appendUserModules($this->mQueryPropModules, $wgApiQueryPropModules); self :: appendUserModules($this->mQueryListModules, $wgApiQueryListModules); @@ -122,7 +124,7 @@ class ApiQuery extends ApiBase { public function getDB() { if (!isset ($this->mSlaveDB)) { $this->profileDBIn(); - $this->mSlaveDB = wfGetDB(DB_SLAVE); + $this->mSlaveDB = wfGetDB(DB_SLAVE,'api'); $this->profileDBOut(); } return $this->mSlaveDB; @@ -130,9 +132,9 @@ class ApiQuery extends ApiBase { /** * Get the query database connection with the given name. - * If no such connection has been requested before, it will be created. - * Subsequent calls with the same $name will return the same connection - * as the first, regardless of $db or $groups new values. + * If no such connection has been requested before, it will be created. + * Subsequent calls with the same $name will return the same connection + * as the first, regardless of $db or $groups new values. */ public function getNamedDB($name, $db, $groups) { if (!array_key_exists($name, $this->mNamedDB)) { @@ -149,7 +151,7 @@ class ApiQuery extends ApiBase { public function getPageSet() { return $this->mPageSet; } - + /** * Get the array mapping module names to class names */ @@ -161,17 +163,17 @@ class ApiQuery extends ApiBase { * Query execution happens in the following steps: * #1 Create a PageSet object with any pages requested by the user * #2 If using generator, execute it to get a new PageSet object - * #3 Instantiate all requested modules. + * #3 Instantiate all requested modules. * This way the PageSet object will know what shared data is required, - * and minimize DB calls. + * and minimize DB calls. * #4 Output all normalization and redirect resolution information * #5 Execute all requested modules */ public function execute() { - + $this->params = $this->extractRequestParams(); $this->redirects = $this->params['redirects']; - + // // Create PageSet // @@ -186,7 +188,7 @@ class ApiQuery extends ApiBase { $this->InstantiateModules($modules, 'meta', $this->mQueryMetaModules); // - // If given, execute generator to substitute user supplied data with generated data. + // If given, execute generator to substitute user supplied data with generated data. // if (isset ($this->params['generator'])) { $this->executeGeneratorModule($this->params['generator'], $modules); @@ -210,21 +212,21 @@ class ApiQuery extends ApiBase { $module->profileOut(); } } - + /** * Query modules may optimize data requests through the $this->getPageSet() object * by adding extra fields from the page table. - * This function will gather all the extra request fields from the modules. + * This function will gather all the extra request fields from the modules. */ private function addCustomFldsToPageSet($modules, $pageSet) { - // Query all requested modules. + // Query all requested modules. foreach ($modules as $module) { $module->requestExtraData($pageSet); } } /** - * Create instances of all modules requested by the client + * Create instances of all modules requested by the client */ private function InstantiateModules(&$modules, $param, $moduleList) { $list = $this->params[$param]; @@ -235,7 +237,7 @@ class ApiQuery extends ApiBase { /** * Appends an element for each page in the current pageSet with the most general - * information (id, title), plus any title normalizations and missing title/pageids/revids. + * information (id, title), plus any title normalizations and missing or invalid title/pageids/revids. */ private function outputGeneralPageInfo() { @@ -255,7 +257,7 @@ class ApiQuery extends ApiBase { $result->setIndexedTagName($normValues, 'n'); $result->addValue('query', 'normalized', $normValues); } - + // Interwiki titles $intrwValues = array (); foreach ($pageSet->getInterwikiTitles() as $rawTitleStr => $interwikiStr) { @@ -269,12 +271,12 @@ class ApiQuery extends ApiBase { $result->setIndexedTagName($intrwValues, 'i'); $result->addValue('query', 'interwiki', $intrwValues); } - + // Show redirect information $redirValues = array (); foreach ($pageSet->getRedirectTitles() as $titleStrFrom => $titleStrTo) { $redirValues[] = array ( - 'from' => $titleStrFrom, + 'from' => strval($titleStrFrom), 'to' => $titleStrTo ); } @@ -311,7 +313,9 @@ class ApiQuery extends ApiBase { $vals['missing'] = ''; $pages[$fakeId] = $vals; } - + // Report any invalid titles + foreach ($pageSet->getInvalidTitles() as $fakeId => $title) + $pages[$fakeId] = array('title' => $title, 'invalid' => ''); // Report any missing page ids foreach ($pageSet->getMissingPageIDs() as $pageid) { $pages[$pageid] = array ( @@ -329,7 +333,7 @@ class ApiQuery extends ApiBase { } if (!empty ($pages)) { - + if ($this->params['indexpageids']) { $pageIDs = array_keys($pages); // json treats all map keys as strings - converting to match @@ -337,14 +341,14 @@ class ApiQuery extends ApiBase { $result->setIndexedTagName($pageIDs, 'id'); $result->addValue('query', 'pageids', $pageIDs); } - + $result->setIndexedTagName($pages, 'page'); $result->addValue('query', 'pages', $pages); } } /** - * For generator mode, execute generator, and use its output as new pageSet + * For generator mode, execute generator, and use its output as new pageSet */ protected function executeGeneratorModule($generatorName, $modules) { @@ -357,7 +361,7 @@ class ApiQuery extends ApiBase { ApiBase :: dieDebug(__METHOD__, "Unknown generator=$generatorName"); } - // Generator results + // Generator results $resultPageSet = new ApiPageSet($this, $this->redirects); // Create and execute the generator @@ -386,7 +390,7 @@ class ApiQuery extends ApiBase { /** * Returns the list of allowed parameters for this module. - * Qurey module also lists all ApiPageSet parameters as its own. + * Qurey module also lists all ApiPageSet parameters as its own. */ public function getAllowedParams() { return array ( @@ -423,12 +427,14 @@ class ApiQuery extends ApiBase { $this->mAllowedGenerators = array(); // Will be repopulated $astriks = str_repeat('--- ', 8); + $astriks2 = str_repeat('*** ', 10); $msg .= "\n$astriks Query: Prop $astriks\n\n"; $msg .= $this->makeHelpMsgHelper($this->mQueryPropModules, 'prop'); $msg .= "\n$astriks Query: List $astriks\n\n"; $msg .= $this->makeHelpMsgHelper($this->mQueryListModules, 'list'); $msg .= "\n$astriks Query: Meta $astriks\n\n"; $msg .= $this->makeHelpMsgHelper($this->mQueryMetaModules, 'meta'); + $msg .= "\n\n$astriks2 Modules: continuation $astriks2\n\n"; // Perform the base call last because the $this->mAllowedGenerators // will be updated inside makeHelpMsgHelper() @@ -469,7 +475,7 @@ class ApiQuery extends ApiBase { $psModule = new ApiPageSet($this); return $psModule->makeHelpMsgParameters() . parent :: makeHelpMsgParameters(); } - + // @todo should work correctly public function shouldCheckMaxlag() { return true; @@ -503,9 +509,8 @@ class ApiQuery extends ApiBase { public function getVersion() { $psModule = new ApiPageSet($this); $vers = array (); - $vers[] = __CLASS__ . ': $Id: ApiQuery.php 30222 2008-01-28 19:05:26Z catrope $'; + $vers[] = __CLASS__ . ': $Id: ApiQuery.php 35098 2008-05-20 17:13:28Z ialex $'; $vers[] = $psModule->getVersion(); return $vers; } } - diff --git a/includes/api/ApiQueryAllCategories.php b/includes/api/ApiQueryAllCategories.php index 84494876..3ff42c88 100644 --- a/includes/api/ApiQueryAllCategories.php +++ b/includes/api/ApiQueryAllCategories.php @@ -31,8 +31,8 @@ if (!defined('MEDIAWIKI')) { /** * Query module to enumerate all categories, even the ones that don't have * category pages. - * - * @addtogroup API + * + * @ingroup API */ class ApiQueryAllCategories extends ApiQueryGeneratorBase { @@ -53,44 +53,58 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { $db = $this->getDB(); $params = $this->extractRequestParams(); - $this->addTables('categorylinks'); - $this->addFields('cl_to'); - + $this->addTables('category'); + $this->addFields('cat_title'); + if (!is_null($params['from'])) - $this->addWhere('cl_to>=' . $db->addQuotes(ApiQueryBase :: titleToKey($params['from']))); + $this->addWhere('cat_title>=' . $db->addQuotes($this->titleToKey($params['from']))); if (isset ($params['prefix'])) - $this->addWhere("cl_to LIKE '" . $db->escapeLike(ApiQueryBase :: titleToKey($params['prefix'])) . "%'"); + $this->addWhere("cat_title LIKE '" . $db->escapeLike($this->titleToKey($params['prefix'])) . "%'"); $this->addOption('LIMIT', $params['limit']+1); - $this->addOption('ORDER BY', 'cl_to' . ($params['dir'] == 'descending' ? ' DESC' : '')); - $this->addOption('DISTINCT'); + $this->addOption('ORDER BY', 'cat_title' . ($params['dir'] == 'descending' ? ' DESC' : '')); + + $prop = array_flip($params['prop']); + $this->addFieldsIf( array( 'cat_pages', 'cat_subcats', 'cat_files' ), isset($prop['size']) ); + $this->addFieldsIf( 'cat_hidden', isset($prop['hidden']) ); $res = $this->select(__METHOD__); $pages = array(); + $categories = array(); + $result = $this->getResult(); $count = 0; while ($row = $db->fetchObject($res)) { if (++ $count > $params['limit']) { // We've reached the one extra which shows that there are additional cats to be had. Stop here... // TODO: Security issue - if the user has no right to view next title, it will still be shown - $this->setContinueEnumParameter('from', ApiQueryBase :: keyToTitle($row->cl_to)); + $this->setContinueEnumParameter('from', $this->keyToTitle($row->cat_title)); break; } - + // Normalize titles - $titleObj = Title::makeTitle(NS_CATEGORY, $row->cl_to); + $titleObj = Title::makeTitle(NS_CATEGORY, $row->cat_title); if(!is_null($resultPageSet)) $pages[] = $titleObj->getPrefixedText(); - else - // Don't show "Category:" everywhere in non-generator mode - $pages[] = $titleObj->getText(); + else { + $item = array(); + $result->setContent( $item, $titleObj->getText() ); + if( isset( $prop['size'] ) ) { + $item['size'] = $row->cat_pages; + $item['pages'] = $row->cat_pages - $row->cat_subcats - $row->cat_files; + $item['files'] = $row->cat_files; + $item['subcats'] = $row->cat_subcats; + } + if( isset( $prop['hidden'] ) && $row->cat_hidden ) + $item['hidden'] = ''; + $categories[] = $item; + } } $db->freeResult($res); if (is_null($resultPageSet)) { - $result = $this->getResult(); - $result->setIndexedTagName($pages, 'c'); - $result->addValue('query', $this->getModuleName(), $pages); + $result->setIndexedTagName($categories, 'c'); + $result->addValue('query', $this->getModuleName(), $categories); } else { $resultPageSet->populateFromTitles($pages); } @@ -113,7 +127,12 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { ApiBase :: PARAM_MIN => 1, ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 - ) + ), + 'prop' => array ( + ApiBase :: PARAM_TYPE => array( 'size', 'hidden' ), + ApiBase :: PARAM_DFLT => '', + ApiBase :: PARAM_ISMULTI => true + ), ); } @@ -122,7 +141,8 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { 'from' => 'The category to start enumerating from.', 'prefix' => 'Search for all category titles that begin with this value.', 'dir' => 'Direction to sort in.', - 'limit' => 'How many categories to return.' + 'limit' => 'How many categories to return.', + 'prop' => 'Which properties to get', ); } @@ -132,11 +152,12 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { protected function getExamples() { return array ( + 'api.php?action=query&list=allcategories&acprop=size', 'api.php?action=query&generator=allcategories&gacprefix=List&prop=info', ); } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllLinks.php 28216 2007-12-06 18:33:18Z vasilievvv $'; + return __CLASS__ . ': $Id: ApiQueryAllCategories.php 36790 2008-06-29 22:26:23Z catrope $'; } } diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php index d5b80644..aefbb725 100644 --- a/includes/api/ApiQueryAllLinks.php +++ b/includes/api/ApiQueryAllLinks.php @@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) { /** * Query module to enumerate links from all pages together. - * - * @addtogroup API + * + * @ingroup API */ class ApiQueryAllLinks extends ApiQueryGeneratorBase { @@ -67,26 +67,37 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $this->addTables('pagelinks'); $this->addWhereFld('pl_namespace', $params['namespace']); + if (!is_null($params['from']) && !is_null($params['continue'])) + $this->dieUsage('alcontinue and alfrom cannot be used together', 'params'); + if (!is_null($params['continue'])) + { + $arr = explode('|', $params['continue']); + if(count($arr) != 2) + $this->dieUsage("Invalid continue parameter", 'badcontinue'); + $params['from'] = $arr[0]; // Handled later + $id = intval($arr[1]); + $this->addWhere("pl_from >= $id"); + } + if (!is_null($params['from'])) - $this->addWhere('pl_title>=' . $db->addQuotes(ApiQueryBase :: titleToKey($params['from']))); + $this->addWhere('pl_title>=' . $db->addQuotes($this->titleToKey($params['from']))); if (isset ($params['prefix'])) - $this->addWhere("pl_title LIKE '" . $db->escapeLike(ApiQueryBase :: titleToKey($params['prefix'])) . "%'"); + $this->addWhere("pl_title LIKE '" . $db->escapeLike($this->titleToKey($params['prefix'])) . "%'"); - if (is_null($resultPageSet)) { - $this->addFields(array ( - 'pl_namespace', - 'pl_title' - )); - $this->addFieldsIf('pl_from', $fld_ids); - } else { - $this->addFields('pl_from'); - $pageids = array(); - } + $this->addFields(array ( + 'pl_namespace', + 'pl_title', + 'pl_from' + )); $this->addOption('USE INDEX', 'pl_namespace'); $limit = $params['limit']; $this->addOption('LIMIT', $limit+1); - $this->addOption('ORDER BY', 'pl_namespace, pl_title'); + # Only order by pl_namespace if it isn't constant in the WHERE clause + if(count($params['namespace']) != 1) + $this->addOption('ORDER BY', 'pl_namespace, pl_title'); + else + $this->addOption('ORDER BY', 'pl_title'); $res = $this->select(__METHOD__); @@ -96,7 +107,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { if (++ $count > $limit) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... // TODO: Security issue - if the user has no right to view next title, it will still be shown - $this->setContinueEnumParameter('from', ApiQueryBase :: keyToTitle($row->pl_title)); + $this->setContinueEnumParameter('continue', $this->keyToTitle($row->pl_title) . "|" . $row->pl_from); break; } @@ -127,6 +138,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { public function getAllowedParams() { return array ( + 'continue' => null, 'from' => null, 'prefix' => null, 'unique' => false, @@ -159,7 +171,8 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { 'unique' => 'Only show unique links. Cannot be used with generator or prop=ids', 'prop' => 'What pieces of information to include', 'namespace' => 'The namespace to enumerate.', - 'limit' => 'How many total links to return.' + 'limit' => 'How many total links to return.', + 'continue' => 'When more results are available, use this to continue.', ); } @@ -174,6 +187,6 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllLinks.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryAllLinks.php 37258 2008-07-07 14:48:40Z catrope $'; } } diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php index e055b3c5..dd0e98a8 100644 --- a/includes/api/ApiQueryAllUsers.php +++ b/includes/api/ApiQueryAllUsers.php @@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) { /** * Query module to enumerate all registered users. - * - * @addtogroup API + * + * @ingroup API */ class ApiQueryAllUsers extends ApiQueryBase { @@ -46,26 +46,27 @@ class ApiQueryAllUsers extends ApiQueryBase { $prop = $params['prop']; if (!is_null($prop)) { $prop = array_flip($prop); + $fld_blockinfo = isset($prop['blockinfo']); $fld_editcount = isset($prop['editcount']); $fld_groups = isset($prop['groups']); $fld_registration = isset($prop['registration']); - } else { - $fld_editcount = $fld_groups = $fld_registration = false; + } else { + $fld_blockinfo = $fld_editcount = $fld_groups = $fld_registration = false; } $limit = $params['limit']; - $tables = $db->tableName('user'); - + $this->addTables('user', 'u1'); + if( !is_null( $params['from'] ) ) - $this->addWhere( 'user_name >= ' . $db->addQuotes( self::keyToTitle( $params['from'] ) ) ); - + $this->addWhere( 'u1.user_name >= ' . $db->addQuotes( $this->keyToTitle( $params['from'] ) ) ); + if( isset( $params['prefix'] ) ) - $this->addWhere( 'user_name LIKE "' . $db->escapeLike( self::keyToTitle( $params['prefix'] ) ) . '%"' ); + $this->addWhere( 'u1.user_name LIKE "' . $db->escapeLike( $this->keyToTitle( $params['prefix'] ) ) . '%"' ); if (!is_null($params['group'])) { // Filter only users that belong to a given group - $tblName = $db->tableName('user_groups'); - $tables = "$tables INNER JOIN $tblName ug1 ON ug1.ug_user=user_id"; + $this->addTables('user_groups', 'ug1'); + $this->addWhere('ug1.ug_user=u1.user_id'); $this->addWhereFld('ug1.ug_group', $params['group']); } @@ -75,23 +76,30 @@ class ApiQueryAllUsers extends ApiQueryBase { $groupCount = count(User::getAllGroups()); $sqlLimit = $limit+$groupCount+1; - $tblName = $db->tableName('user_groups'); - $tables = "$tables LEFT JOIN $tblName ug2 ON ug2.ug_user=user_id"; + $this->addTables('user_groups', 'ug2'); + $tname = $this->getAliasedName('user_groups', 'ug2'); + $this->addJoinConds(array($tname => array('LEFT JOIN', 'ug2.ug_user=u1.user_id'))); $this->addFields('ug2.ug_group ug_group2'); } else { $sqlLimit = $limit+1; } - - if ($fld_registration) - $this->addFields('user_registration'); + if ($fld_blockinfo) { + $this->addTables('ipblocks'); + $this->addTables('user', 'u2'); + $u2 = $this->getAliasedName('user', 'u2'); + $this->addJoinConds(array( + 'ipblocks' => array('LEFT JOIN', 'ipb_user=u1.user_id'), + $u2 => array('LEFT JOIN', 'ipb_by=u2.user_id'))); + $this->addFields(array('ipb_reason', 'u2.user_name blocker_name')); + } $this->addOption('LIMIT', $sqlLimit); - $this->addTables($tables); - $this->addFields('user_name'); - $this->addFieldsIf('user_editcount', $fld_editcount); + $this->addFields('u1.user_name'); + $this->addFieldsIf('u1.user_editcount', $fld_editcount); + $this->addFieldsIf('u1.user_registration', $fld_registration); - $this->addOption('ORDER BY', 'user_name'); + $this->addOption('ORDER BY', 'u1.user_name'); $res = $this->select(__METHOD__); @@ -100,7 +108,7 @@ class ApiQueryAllUsers extends ApiQueryBase { $lastUserData = false; $lastUser = false; $result = $this->getResult(); - + // // This loop keeps track of the last entry. // For each new row, if the new row is for different user then the last, the last entry is added to results. @@ -109,49 +117,53 @@ class ApiQueryAllUsers extends ApiQueryBase { // to make sure all rows that belong to the same user are received. // while (true) { - + $row = $db->fetchObject($res); $count++; - + if (!$row || $lastUser != $row->user_name) { // Save the last pass's user data if (is_array($lastUserData)) $data[] = $lastUserData; - + // No more rows left if (!$row) break; if ($count > $limit) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... - $this->setContinueEnumParameter('from', ApiQueryBase :: keyToTitle($row->user_name)); + $this->setContinueEnumParameter('from', $this->keyToTitle($row->user_name)); break; } // Record new user's data $lastUser = $row->user_name; $lastUserData = array( 'name' => $lastUser ); + if ($fld_blockinfo) { + $lastUserData['blockedby'] = $row->blocker_name; + $lastUserData['blockreason'] = $row->ipb_reason; + } if ($fld_editcount) $lastUserData['editcount'] = intval($row->user_editcount); if ($fld_registration) $lastUserData['registration'] = wfTimestamp(TS_ISO_8601, $row->user_registration); - + } - + if ($sqlLimit == $count) { // BUG! database contains group name that User::getAllGroups() does not return // TODO: should handle this more gracefully - ApiBase :: dieDebug(__METHOD__, + ApiBase :: dieDebug(__METHOD__, 'MediaWiki configuration error: the database contains more user groups than known to User::getAllGroups() function'); } - + // Add user's group info if ($fld_groups && !is_null($row->ug_group2)) { $lastUserData['groups'][] = $row->ug_group2; $result->setIndexedTagName($lastUserData['groups'], 'g'); } } - + $db->freeResult($res); $result->setIndexedTagName($data, 'u'); @@ -166,11 +178,12 @@ class ApiQueryAllUsers extends ApiQueryBase { ApiBase :: PARAM_TYPE => User::getAllGroups() ), 'prop' => array ( - ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_ISMULTI => true, ApiBase :: PARAM_TYPE => array ( - 'editcount', + 'blockinfo', 'groups', - 'registration', + 'editcount', + 'registration' ) ), 'limit' => array ( @@ -206,6 +219,6 @@ class ApiQueryAllUsers extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllUsers.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryAllUsers.php 36790 2008-06-29 22:26:23Z catrope $'; } } diff --git a/includes/api/ApiQueryAllimages.php b/includes/api/ApiQueryAllimages.php new file mode 100644 index 00000000..26cbc368 --- /dev/null +++ b/includes/api/ApiQueryAllimages.php @@ -0,0 +1,205 @@ +run(); + } + + public function executeGenerator($resultPageSet) { + if ($resultPageSet->isResolvingRedirects()) + $this->dieUsage('Use "gaifilterredir=nonredirects" option instead of "redirects" when using allimages as a generator', 'params'); + + $this->run($resultPageSet); + } + + private function run($resultPageSet = null) { + $repo = RepoGroup::singleton()->getLocalRepo(); + if ( !$repo instanceof LocalRepo ) + $this->dieUsage('Local file repository does not support querying all images', 'unsupportedrepo'); + + $db = $this->getDB(); + + $params = $this->extractRequestParams(); + + // Image filters + if (!is_null($params['from'])) + $this->addWhere('img_name>=' . $db->addQuotes($this->titleToKey($params['from']))); + if (isset ($params['prefix'])) + $this->addWhere("img_name LIKE '" . $db->escapeLike($this->titleToKey($params['prefix'])) . "%'"); + + if (isset ($params['minsize'])) { + $this->addWhere('img_size>=' . intval($params['minsize'])); + } + + if (isset ($params['maxsize'])) { + $this->addWhere('img_size<=' . intval($params['maxsize'])); + } + + $sha1 = false; + if( isset( $params['sha1'] ) ) { + $sha1 = wfBaseConvert( $params['sha1'], 16, 36, 31 ); + } elseif( isset( $params['sha1base36'] ) ) { + $sha1 = $params['sha1base36']; + } + if( $sha1 ) { + $this->addWhere( 'img_sha1=' . $db->addQuotes( $sha1 ) ); + } + + $this->addTables('image'); + + $prop = array_flip($params['prop']); + $this->addFields( LocalFile::selectFields() ); + + $limit = $params['limit']; + $this->addOption('LIMIT', $limit+1); + $this->addOption('ORDER BY', 'img_name' . + ($params['dir'] == 'descending' ? ' DESC' : '')); + + $res = $this->select(__METHOD__); + + $data = array (); + $count = 0; + $result = $this->getResult(); + while ($row = $db->fetchObject($res)) { + if (++ $count > $limit) { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + // TODO: Security issue - if the user has no right to view next title, it will still be shown + $this->setContinueEnumParameter('from', $this->keyToTitle($row->img_name)); + break; + } + + if (is_null($resultPageSet)) { + $file = $repo->newFileFromRow( $row ); + + $data[] = ApiQueryImageInfo::getInfo( $file, $prop, $result ); + } else { + $data[] = Title::makeTitle( NS_IMAGE, $row->img_name ); + } + } + $db->freeResult($res); + + if (is_null($resultPageSet)) { + $result = $this->getResult(); + $result->setIndexedTagName($data, 'img'); + $result->addValue('query', $this->getModuleName(), $data); + } else { + $resultPageSet->populateFromTitles( $data ); + } + } + + public function getAllowedParams() { + return array ( + 'from' => null, + 'prefix' => null, + 'minsize' => array ( + ApiBase :: PARAM_TYPE => 'integer', + ), + 'maxsize' => array ( + ApiBase :: PARAM_TYPE => 'integer', + ), + '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 + ), + 'dir' => array ( + ApiBase :: PARAM_DFLT => 'ascending', + ApiBase :: PARAM_TYPE => array ( + 'ascending', + 'descending' + ) + ), + 'sha1' => null, + 'sha1base36' => null, + 'prop' => array ( + ApiBase :: PARAM_TYPE => array( + 'timestamp', + 'user', + 'comment', + 'url', + 'size', + 'dimensions', // Obsolete + 'mime', + 'sha1', + 'metadata' + ), + ApiBase :: PARAM_DFLT => 'timestamp|url', + ApiBase :: PARAM_ISMULTI => true + ) + ); + } + + public function getParamDescription() { + return array ( + 'from' => 'The image title to start enumerating from.', + 'prefix' => 'Search for all image titles that begin with this value.', + 'dir' => 'The direction in which to list', + 'minsize' => 'Limit to images with at least this many bytes', + 'maxsize' => 'Limit to images with at most this many bytes', + 'limit' => 'How many total images to return.', + 'sha1' => 'SHA1 hash of image', + 'sha1base36' => 'SHA1 hash of image in base 36 (used in MediaWiki)', + 'prop' => 'Which properties to get', + ); + } + + public function getDescription() { + return 'Enumerate all images sequentially'; + } + + protected function getExamples() { + return array ( + 'Simple Use', + ' Show a list of images starting at the letter "B"', + ' api.php?action=query&list=allimages&aifrom=B', + 'Using as Generator', + ' Show info about 4 images starting at the letter "T"', + ' api.php?action=query&generator=allimages&gailimit=4&gaifrom=T&prop=imageinfo', + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryAllimages.php 37909 2008-07-22 13:26:15Z catrope $'; + } +} diff --git a/includes/api/ApiQueryAllmessages.php b/includes/api/ApiQueryAllmessages.php index b7c86a91..06683379 100644 --- a/includes/api/ApiQueryAllmessages.php +++ b/includes/api/ApiQueryAllmessages.php @@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) { /** * A query action to return messages from site message cache - * - * @addtogroup API + * + * @ingroup API */ class ApiQueryAllmessages extends ApiQueryBase { @@ -42,13 +42,13 @@ class ApiQueryAllmessages extends ApiQueryBase { public function execute() { global $wgMessageCache; $params = $this->extractRequestParams(); - + if(!is_null($params['lang'])) { global $wgLang; $wgLang = Language::factory($params['lang']); } - + //Determine which messages should we print $messages_target = array(); @@ -60,7 +60,7 @@ class ApiQueryAllmessages extends ApiQueryBase { } else { $messages_target = explode( '|', $params['messages'] ); } - + //Filter messages if( isset( $params['filter'] ) ) { $messages_filtered = array(); @@ -72,12 +72,9 @@ class ApiQueryAllmessages extends ApiQueryBase { $messages_target = $messages_filtered; } - $wgMessageCache->disableTransform(); - //Get all requested messages $messages = array(); foreach( $messages_target as $message ) { - $message = trim( $message ); //Message list can be formatted like "msg1 | msg2 | msg3", so let's trim() it $messages[$message] = wfMsg( $message ); } @@ -87,7 +84,11 @@ class ApiQueryAllmessages extends ApiQueryBase { foreach( $messages as $name => $value ) { $message = array(); $message['name'] = $name; - $result->setContent( $message, $value ); + if( wfEmptyMsg( $name, $value ) ) { + $message['missing'] = ''; + } else { + $result->setContent( $message, $value ); + } $messages_out[] = $message; } $result->setIndexedTagName( $messages_out, 'message' ); @@ -107,8 +108,8 @@ class ApiQueryAllmessages extends ApiQueryBase { public function getParamDescription() { return array ( 'messages' => 'Which messages to output. "*" means all messages', - 'filter' => 'Return only messages that contains specified string', - 'lang' => 'Language code', + 'filter' => 'Return only messages that contain this string', + 'lang' => 'Return messages in this language', ); } @@ -124,6 +125,6 @@ class ApiQueryAllmessages extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllmessages.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryAllmessages.php 37504 2008-07-10 14:28:09Z catrope $'; } } diff --git a/includes/api/ApiQueryAllpages.php b/includes/api/ApiQueryAllpages.php index 280d1de2..39490fe7 100644 --- a/includes/api/ApiQueryAllpages.php +++ b/includes/api/ApiQueryAllpages.php @@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) { /** * Query module to enumerate all available pages. - * - * @addtogroup API + * + * @ingroup API */ class ApiQueryAllpages extends ApiQueryGeneratorBase { @@ -55,27 +55,29 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { $db = $this->getDB(); $params = $this->extractRequestParams(); - + // Page filters + $this->addTables('page'); if (!$this->addWhereIf('page_is_redirect = 1', $params['filterredir'] === 'redirects')) $this->addWhereIf('page_is_redirect = 0', $params['filterredir'] === 'nonredirects'); $this->addWhereFld('page_namespace', $params['namespace']); - if (!is_null($params['from'])) - $this->addWhere('page_title>=' . $db->addQuotes(ApiQueryBase :: titleToKey($params['from']))); + $dir = ($params['dir'] == 'descending' ? 'older' : 'newer'); + $from = (is_null($params['from']) ? null : $this->titleToKey($params['from'])); + $this->addWhereRange('page_title', $dir, $from, null); if (isset ($params['prefix'])) - $this->addWhere("page_title LIKE '" . $db->escapeLike(ApiQueryBase :: titleToKey($params['prefix'])) . "%'"); + $this->addWhere("page_title LIKE '" . $db->escapeLike($this->titleToKey($params['prefix'])) . "%'"); $forceNameTitleIndex = true; if (isset ($params['minsize'])) { $this->addWhere('page_len>=' . intval($params['minsize'])); $forceNameTitleIndex = false; } - + if (isset ($params['maxsize'])) { $this->addWhere('page_len<=' . intval($params['maxsize'])); $forceNameTitleIndex = false; } - + // Page protection filtering if (isset ($params['prtype'])) { $this->addTables('page_restrictions'); @@ -86,7 +88,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { $prlevel = $params['prlevel']; if (!is_null($prlevel) && $prlevel != '' && $prlevel != '*') $this->addWhereFld('pr_level', $prlevel); - + $this->addOption('DISTINCT'); $forceNameTitleIndex = false; @@ -94,20 +96,16 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { } else if (isset ($params['prlevel'])) { $this->dieUsage('prlevel may not be used without prtype', 'params'); } - + if($params['filterlanglinks'] == 'withoutlanglinks') { - $pageName = $this->getDB()->tableName('page'); - $llName = $this->getDB()->tableName('langlinks'); - $tables = "$pageName LEFT JOIN $llName ON page_id=ll_from"; + $this->addTables('langlinks'); + $this->addJoinConds(array('langlinks' => array('LEFT JOIN', 'page_id=ll_from'))); $this->addWhere('ll_from IS NULL'); - $this->addTables($tables); $forceNameTitleIndex = false; } else if($params['filterlanglinks'] == 'withlanglinks') { - $this->addTables(array('page', 'langlinks')); + $this->addTables('langlinks'); $this->addWhere('page_id=ll_from'); - $forceNameTitleIndex = false; - } else { - $this->addTables('page'); + $forceNameTitleIndex = false; } if ($forceNameTitleIndex) $this->addOption('USE INDEX', 'name_title'); @@ -124,9 +122,6 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { $limit = $params['limit']; $this->addOption('LIMIT', $limit+1); - $this->addOption('ORDER BY', 'page_namespace, page_title' . - ($params['dir'] == 'descending' ? ' DESC' : '')); - $res = $this->select(__METHOD__); $data = array (); @@ -135,7 +130,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { if (++ $count > $limit) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... // TODO: Security issue - if the user has no right to view next title, it will still be shown - $this->setContinueEnumParameter('from', ApiQueryBase :: keyToTitle($row->page_title)); + $this->setContinueEnumParameter('from', $this->keyToTitle($row->page_title)); break; } @@ -160,7 +155,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { public function getAllowedParams() { global $wgRestrictionTypes, $wgRestrictionLevels; - + return array ( 'from' => null, 'prefix' => null, @@ -178,10 +173,10 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { ), 'minsize' => array ( ApiBase :: PARAM_TYPE => 'integer', - ), + ), 'maxsize' => array ( ApiBase :: PARAM_TYPE => 'integer', - ), + ), 'prtype' => array ( ApiBase :: PARAM_TYPE => $wgRestrictionTypes, ApiBase :: PARAM_ISMULTI => true @@ -249,7 +244,6 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllpages.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryAllpages.php 37775 2008-07-17 09:26:01Z brion $'; } } - diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php index 1ca5c33a..fea058f3 100644 --- a/includes/api/ApiQueryBacklinks.php +++ b/includes/api/ApiQueryBacklinks.php @@ -29,18 +29,18 @@ if (!defined('MEDIAWIKI')) { } /** - * This is three-in-one module to query: + * This is a three-in-one module to query: * * backlinks - links pointing to the given page, * * embeddedin - what pages transclude the given page within themselves, * * imageusage - what pages use the given image - * - * @addtogroup API + * + * @ingroup API */ class ApiQueryBacklinks extends ApiQueryGeneratorBase { - private $params, $rootTitle, $contRedirs, $contLevel, $contTitle, $contID; + private $params, $rootTitle, $contRedirs, $contLevel, $contTitle, $contID, $redirID; - // output element name, database column field prefix, database table + // output element name, database column field prefix, database table private $backlinksSettings = array ( 'backlinks' => array ( 'code' => 'bl', @@ -66,10 +66,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { parent :: __construct($query, $moduleName, $code); $this->bl_ns = $prefix . '_namespace'; $this->bl_from = $prefix . '_from'; - $this->bl_tables = array ( - $linktbl, - 'page' - ); + $this->bl_table = $linktbl; $this->bl_code = $code; $this->hasNS = $moduleName !== 'imageusage'; @@ -97,207 +94,219 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $this->run($resultPageSet); } - private function run($resultPageSet = null) { - $this->params = $this->extractRequestParams(); - - $redirect = $this->params['redirect']; - if ($redirect) - $this->dieDebug('Redirect has not been implemented', 'notimplemented'); - - $this->processContinue(); - - $this->addFields($this->bl_fields); - if (is_null($resultPageSet)) - $this->addFields(array ( - 'page_id', - 'page_namespace', - 'page_title' - )); + private function prepareFirstQuery($resultPageSet = null) { + /* SELECT page_id, page_title, page_namespace, page_is_redirect + * FROM pagelinks, page WHERE pl_from=page_id + * AND pl_title='Foo' AND pl_namespace=0 + * LIMIT 11 ORDER BY pl_from + */ + $db = $this->getDb(); + $this->addTables(array('page', $this->bl_table)); + $this->addWhere("{$this->bl_from}=page_id"); + if(is_null($resultPageSet)) + $this->addFields(array('page_id', 'page_title', 'page_namespace')); else - $this->addFields($resultPageSet->getPageTableFields()); // will include page_id - - $this->addTables($this->bl_tables); - $this->addWhere($this->bl_from . '=page_id'); - - if ($this->hasNS) + $this->addFields($resultPageSet->getPageTableFields()); + $this->addFields('page_is_redirect'); + $this->addWhereFld($this->bl_title, $this->rootTitle->getDbKey()); + if($this->hasNS) $this->addWhereFld($this->bl_ns, $this->rootTitle->getNamespace()); - $this->addWhereFld($this->bl_title, $this->rootTitle->getDBkey()); $this->addWhereFld('page_namespace', $this->params['namespace']); - + if(!is_null($this->contID)) + $this->addWhere("page_id>={$this->contID}"); if($this->params['filterredir'] == 'redirects') $this->addWhereFld('page_is_redirect', 1); if($this->params['filterredir'] == 'nonredirects') $this->addWhereFld('page_is_redirect', 0); + $this->addOption('LIMIT', $this->params['limit'] + 1); + $this->addOption('ORDER BY', $this->bl_from); + } - $limit = $this->params['limit']; - $this->addOption('LIMIT', $limit +1); + private function prepareSecondQuery($resultPageSet = null) { + /* SELECT page_id, page_title, page_namespace, page_is_redirect, pl_title, pl_namespace + * FROM pagelinks, page WHERE pl_from=page_id + * AND (pl_title='Foo' AND pl_namespace=0) OR (pl_title='Bar' AND pl_namespace=1) + * LIMIT 11 ORDER BY pl_namespace, pl_title, pl_from + */ + $db = $this->getDb(); + $this->addTables(array('page', $this->bl_table)); + $this->addWhere("{$this->bl_from}=page_id"); + if(is_null($resultPageSet)) + $this->addFields(array('page_id', 'page_title', 'page_namespace', 'page_is_redirect')); + else + $this->addFields($resultPageSet->getPageTableFields()); + $this->addFields($this->bl_title); + if($this->hasNS) + $this->addFields($this->bl_ns); + $titleWhere = ''; + foreach($this->redirTitles as $t) + $titleWhere .= ($titleWhere != '' ? " OR " : '') . + "({$this->bl_title} = ".$db->addQuotes($t->getDBKey()). + ($this->hasNS ? " AND {$this->bl_ns} = '{$t->getNamespace()}'" : "") . + ")"; + $this->addWhere($titleWhere); + $this->addWhereFld('page_namespace', $this->params['namespace']); + if(!is_null($this->redirID)) + $this->addWhere("page_id>={$this->redirID}"); + if($this->params['filterredir'] == 'redirects') + $this->addWhereFld('page_is_redirect', 1); + if($this->params['filterredir'] == 'nonredirects') + $this->addWhereFld('page_is_redirect', 0); + $this->addOption('LIMIT', $this->params['limit'] + 1); $this->addOption('ORDER BY', $this->bl_sort); + } - $db = $this->getDB(); - if (!is_null($this->params['continue'])) { - $plfrm = intval($this->contID); - if ($this->contLevel == 0) { - // For the first level, there is only one target title, so no need for complex filtering - $this->addWhere($this->bl_from . '>=' . $plfrm); - } else { - $ns = $this->contTitle->getNamespace(); - $t = $db->addQuotes($this->contTitle->getDBkey()); - $whereWithoutNS = "{$this->bl_title}>$t OR ({$this->bl_title}=$t AND {$this->bl_from}>=$plfrm))"; - - if ($this->hasNS) - $this->addWhere("{$this->bl_ns}>$ns OR ({$this->bl_ns}=$ns AND ($whereWithoutNS)"); - else - $this->addWhere($whereWithoutNS); - } + private function run($resultPageSet = null) { + $this->params = $this->extractRequestParams(false); + $this->redirect = isset($this->params['redirect']) && $this->params['redirect']; + $userMax = ( $this->redirect ? ApiBase::LIMIT_BIG1/2 : ApiBase::LIMIT_BIG1 ); + $botMax = ( $this->redirect ? ApiBase::LIMIT_BIG2/2 : ApiBase::LIMIT_BIG2 ); + if( $this->params['limit'] == 'max' ) { + $this->params['limit'] = $this->getMain()->canApiHighLimits() ? $botMax : $userMax; + $this->getResult()->addValue( 'limits', $this->getModuleName(), $this->params['limit'] ); } + $this->processContinue(); + $this->prepareFirstQuery($resultPageSet); + + $db = $this->getDB(); $res = $this->select(__METHOD__); $count = 0; - $data = array (); + $this->data = array (); + $this->continueStr = null; + $this->redirTitles = array(); while ($row = $db->fetchObject($res)) { - if (++ $count > $limit) { + if (++ $count > $this->params['limit']) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... - if ($redirect) { - $ns = $row-> { $this->bl_ns }; - $t = $row-> { $this->bl_title }; - $continue = $this->getContinueRedirStr(false, 0, $ns, $t, $row->page_id); - } else - $continue = $this->getContinueStr($row->page_id); - // TODO: Security issue - if the user has no right to view next title, it will still be shown - $this->setContinueEnumParameter('continue', $continue); + // Continue string preserved in case the redirect query doesn't pass the limit + $this->continueStr = $this->getContinueStr($row->page_id); break; } - if (is_null($resultPageSet)) { - $vals = $this->extractRowInfo($row); - if ($vals) - $data[] = $vals; - } else { + if (is_null($resultPageSet)) + $this->extractRowInfo($row); + else + { + if($row->page_is_redirect) + $this->redirTitles[] = Title::makeTitle($row->page_namespace, $row->page_title); $resultPageSet->processDbRow($row); } } $db->freeResult($res); + if($this->redirect && !empty($this->redirTitles)) + { + $this->resetQueryParams(); + $this->prepareSecondQuery($resultPageSet); + $res = $this->select(__METHOD__); + $count = 0; + while($row = $db->fetchObject($res)) + { + if(++$count > $this->params['limit']) + { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + // We need to keep the parent page of this redir in + if($this->hasNS) + $contTitle = Title::makeTitle($row->{$this->bl_ns}, $row->{$this->bl_title}); + else + $contTitle = Title::makeTitle(NS_IMAGE, $row->{$this->bl_title}); + $this->continueStr = $this->getContinueRedirStr($contTitle->getArticleID(), $row->page_id); + break; + } + + if(is_null($resultPageSet)) + $this->extractRedirRowInfo($row); + else + $resultPageSet->processDbRow($row); + } + $db->freeResult($res); + } + if(!is_null($this->continueStr)) + $this->setContinueEnumParameter('continue', $this->continueStr); + if (is_null($resultPageSet)) { + $resultData = array(); + foreach($this->data as $ns => $a) + foreach($a as $title => $arr) + $resultData[$arr['pageid']] = $arr; $result = $this->getResult(); - $result->setIndexedTagName($data, $this->bl_code); - $result->addValue('query', $this->getModuleName(), $data); + $result->setIndexedTagName($resultData, $this->bl_code); + $result->addValue('query', $this->getModuleName(), $resultData); } } private function extractRowInfo($row) { + if(!isset($this->data[$row->page_namespace][$row->page_title])) { + $this->data[$row->page_namespace][$row->page_title]['pageid'] = $row->page_id; + ApiQueryBase::addTitleInfo($this->data[$row->page_namespace][$row->page_title], Title::makeTitle($row->page_namespace, $row->page_title)); + if($row->page_is_redirect) + { + $this->data[$row->page_namespace][$row->page_title]['redirect'] = ''; + $this->redirTitles[] = Title::makeTitle($row->page_namespace, $row->page_title); + } + } + } - $vals = array(); - $vals['pageid'] = intval($row->page_id); - ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle($row->page_namespace, $row->page_title)); - - return $vals; + private function extractRedirRowInfo($row) + { + $a['pageid'] = $row->page_id; + ApiQueryBase::addTitleInfo($a, Title::makeTitle($row->page_namespace, $row->page_title)); + if($row->page_is_redirect) + $a['redirect'] = ''; + $ns = $this->hasNS ? $row->{$this->bl_ns} : NS_IMAGE; + $this->data[$ns][$row->{$this->bl_title}]['redirlinks'][] = $a; + $this->getResult()->setIndexedTagName($this->data[$ns][$row->{$this->bl_title}]['redirlinks'], $this->bl_code); } protected function processContinue() { - $pageSet = $this->getPageSet(); - $count = $pageSet->getTitleCount(); - - if (!is_null($this->params['continue'])) { + if (!is_null($this->params['continue'])) $this->parseContinueParam(); - - // Skip all completed links - - } else { - $title = $this->params['title']; - if (!is_null($title)) { - $this->rootTitle = Title :: newFromText($title); - } else { // This case is obsolete. Will support this for a while - if ($count !== 1) - $this->dieUsage("The {$this->getModuleName()} query requires one title to start", 'bad_title_count'); - $this->rootTitle = current($pageSet->getTitles()); // only one title there - $this->setWarning('Using titles parameter is obsolete for this list. Use ' . $this->encodeParamName('title') . ' instead.'); + else { + if ( $this->params['title'] !== "" ) { + $title = Title::newFromText( $this->params['title'] ); + if ( !$title ) { + $this->dieUsageMsg(array('invalidtitle', $this->params['title'])); + } else { + $this->rootTitle = $title; + } + } else { + $this->dieUsageMsg(array('missingparam', 'title')); } } - // only image titles are allowed for the root + // only image titles are allowed for the root in imageinfo mode if (!$this->hasNS && $this->rootTitle->getNamespace() !== NS_IMAGE) $this->dieUsage("The title for {$this->getModuleName()} query must be an image", 'bad_image_title'); } protected function parseContinueParam() { $continueList = explode('|', $this->params['continue']); - if ($this->params['redirect']) { - // - // expected redirect-mode parameter: - // ns|db_key|step|level|ns|db_key|id - // ns+db_key -- the root title - // step = 1 or 2 - which step to continue from - 1-titles, 2-redirects - // level -- how many levels to follow before starting enumerating. - // if level > 0 -- ns+title to continue from, otherwise skip these - // id = last page_id to continue from - // - if (count($continueList) > 4) { - $rootNs = intval($continueList[0]); - if (($rootNs !== 0 || $continueList[0] === '0') && !empty ($continueList[1])) { - $this->rootTitle = Title :: makeTitleSafe($rootNs, $continueList[1]); - if ($this->rootTitle) { - - $step = intval($continueList[2]); - if ($step === 1 || $step === 2) { - $this->contRedirs = ($step === 2); - - $level = intval($continueList[3]); - if ($level !== 0 || $continueList[3] === '0') { - $this->contLevel = $level; - - if ($level === 0) { - if (count($continueList) === 5) { - $contID = intval($continueList[4]); - if ($contID !== 0 || $continueList[4] === '0') { - $this->contID = $contID; - return; // done - } - } - } else { - if (count($continueList) === 7) { - $contNs = intval($continueList[4]); - if (($contNs !== 0 || $continueList[4] === '0') && !empty ($continueList[5])) { - $this->contTitle = Title :: makeTitleSafe($contNs, $continueList[5]); - - $contID = intval($continueList[6]); - if ($contID !== 0 || $continueList[6] === '0') { - $this->contID = $contID; - return; // done - } - } - } - } - } - } - } - } - } - } else { - // - // expected non-redirect-mode parameter: - // ns|db_key|id - // ns+db_key -- the root title - // id = last page_id to continue from - // - if (count($continueList) === 3) { - $rootNs = intval($continueList[0]); - if (($rootNs !== 0 || $continueList[0] === '0') && !empty ($continueList[1])) { - $this->rootTitle = Title :: makeTitleSafe($rootNs, $continueList[1]); - if ($this->rootTitle) { - - $contID = intval($continueList[2]); - if ($contID !== 0) { - $this->contID = $contID; - return; // done - } - } - } - } - } + // expected format: + // ns | key | id1 [| id2] + // ns+key: root title + // id1: first-level page ID to continue from + // id2: second-level page ID to continue from + + // null stuff out now so we know what's set and what isn't + $this->rootTitle = $this->contID = $this->redirID = null; + $rootNs = intval($continueList[0]); + if($rootNs === 0 && $continueList[0] !== '0') + // Illegal continue parameter + $this->dieUsage("Invalid continue param. You should pass the original value returned by the previous query", "_badcontinue"); + $this->rootTitle = Title::makeTitleSafe($rootNs, $continueList[1]); + if(!$this->rootTitle) + $this->dieUsage("Invalid continue param. You should pass the original value returned by the previous query", "_badcontinue"); + $contID = intval($continueList[2]); + if($contID === 0 && $continueList[2] !== '0') + $this->dieUsage("Invalid continue param. You should pass the original value returned by the previous query", "_badcontinue"); + $this->contID = $contID; + $redirID = intval(@$continueList[3]); + if($redirID === 0 && @$continueList[3] !== '0') + // This one isn't required + return; + $this->redirID = $redirID; - $this->dieUsage("Invalid continue param. You should pass the original value returned by the previous query", "_badcontinue"); } protected function getContinueStr($lastPageID) { @@ -306,18 +315,12 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { '|' . $lastPageID; } - protected function getContinueRedirStr($isRedirPhase, $level, $ns, $title, $lastPageID) { - return $this->rootTitle->getNamespace() . - '|' . $this->rootTitle->getDBkey() . - '|' . ($isRedirPhase ? 1 : 2) . - '|' . $level . - ($level > 0 ? ('|' . $ns . '|' . $title) : '') . - '|' . $lastPageID; + protected function getContinueRedirStr($lastPageID, $lastRedirID) { + return $this->getContinueStr($lastPageID) . '|' . $lastRedirID; } public function getAllowedParams() { - - return array ( + $retval = array ( 'title' => null, 'continue' => null, 'namespace' => array ( @@ -332,7 +335,6 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { 'nonredirects' ) ), - 'redirect' => false, 'limit' => array ( ApiBase :: PARAM_DFLT => 10, ApiBase :: PARAM_TYPE => 'limit', @@ -341,17 +343,27 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 ) ); + if($this->getModuleName() == 'embeddedin') + return $retval; + $retval['redirect'] = false; + return $retval; } public function getParamDescription() { - return array ( + $retval = array ( 'title' => 'Title to search. If null, titles= parameter will be used instead, but will be obsolete soon.', 'continue' => 'When more results are available, use this to continue.', 'namespace' => 'The namespace to enumerate.', - 'filterredir' => 'How to filter for redirects', - 'redirect' => 'If linking page is a redirect, find all pages that link to that redirect (not implemented)', - 'limit' => 'How many total pages to return.' + 'filterredir' => 'How to filter for redirects' ); + if($this->getModuleName() != 'embeddedin') + return array_merge($retval, array( + 'redirect' => 'If linking page is a redirect, find all pages that link to that redirect as well. Maximum limit is halved.', + 'limit' => "How many total pages to return. If {$this->bl_code}redirect is enabled, limit applies to each level separately." + )); + return array_merge($retval, array( + 'limit' => "How many total pages to return." + )); } public function getDescription() { @@ -387,7 +399,6 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryBacklinks.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryBacklinks.php 37504 2008-07-10 14:28:09Z catrope $'; } } - diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php index 031e3c02..f392186b 100644 --- a/includes/api/ApiQueryBase.php +++ b/includes/api/ApiQueryBase.php @@ -31,12 +31,12 @@ if (!defined('MEDIAWIKI')) { /** * This is a base class for all Query modules. * It provides some common functionality such as constructing various SQL queries. - * - * @addtogroup API + * + * @ingroup API */ abstract class ApiQueryBase extends ApiBase { - private $mQueryModule, $mDb, $tables, $where, $fields, $options; + private $mQueryModule, $mDb, $tables, $where, $fields, $options, $join_conds; public function __construct($query, $moduleName, $paramPrefix = '') { parent :: __construct($query->getMain(), $moduleName, $paramPrefix); @@ -45,13 +45,22 @@ abstract class ApiQueryBase extends ApiBase { $this->resetQueryParams(); } + /** + * Blank the internal arrays with query parameters + */ protected function resetQueryParams() { $this->tables = array (); $this->where = array (); $this->fields = array (); $this->options = array (); + $this->join_conds = array (); } + /** + * Add a set of tables to the internal array + * @param mixed $tables Table name or array of table names + * @param mixed $alias Table alias, or null for no alias. Cannot be used with multiple tables + */ protected function addTables($tables, $alias = null) { if (is_array($tables)) { if (!is_null($alias)) @@ -59,11 +68,38 @@ abstract class ApiQueryBase extends ApiBase { $this->tables = array_merge($this->tables, $tables); } else { if (!is_null($alias)) - $tables = $this->getDB()->tableName($tables) . ' ' . $alias; + $tables = $this->getAliasedName($tables, $alias); $this->tables[] = $tables; } } + + /** + * Get the SQL for a table name with alias + * @param string $table Table name + * @param string $alias Alias + * @return string SQL + */ + protected function getAliasedName($table, $alias) { + return $this->getDB()->tableName($table) . ' ' . $alias; + } + + /** + * Add a set of JOIN conditions to the internal array + * + * JOIN conditions are formatted as array( tablename => array(jointype, conditions) + * e.g. array('page' => array('LEFT JOIN', 'page_id=rev_page')) + * @param array $join_conds JOIN conditions + */ + protected function addJoinConds($join_conds) { + if(!is_array($join_conds)) + ApiBase::dieDebug(__METHOD__, 'Join conditions have to be arrays'); + $this->join_conds = array_merge($this->join_conds, $join_conds); + } + /** + * Add a set of fields to select to the internal array + * @param mixed $value Field name or array of field names + */ protected function addFields($value) { if (is_array($value)) $this->fields = array_merge($this->fields, $value); @@ -71,6 +107,12 @@ abstract class ApiQueryBase extends ApiBase { $this->fields[] = $value; } + /** + * Same as addFields(), but add the fields only if a condition is met + * @param mixed $value See addFields() + * @param bool $condition If false, do nothing + * @return bool $condition + */ protected function addFieldsIf($value, $condition) { if ($condition) { $this->addFields($value); @@ -79,6 +121,15 @@ abstract class ApiQueryBase extends ApiBase { return false; } + /** + * Add a set of WHERE clauses to the internal array. + * Clauses can be formatted as 'foo=bar' or array('foo' => 'bar'), + * the latter only works if the value is a constant (i.e. not another field) + * + * For example, array('foo=bar', 'baz' => 3, 'bla' => 'foo') translates + * to "foo=bar AND baz='3' AND bla='foo'" + * @param mixed $value String or array + */ protected function addWhere($value) { if (is_array($value)) $this->where = array_merge($this->where, $value); @@ -86,6 +137,12 @@ abstract class ApiQueryBase extends ApiBase { $this->where[] = $value; } + /** + * Same as addWhere(), but add the WHERE clauses only if a condition is met + * @param mixed $value See addWhere() + * @param bool $condition If false, do nothing + * @return bool $condition + */ protected function addWhereIf($value, $condition) { if ($condition) { $this->addWhere($value); @@ -94,11 +151,24 @@ abstract class ApiQueryBase extends ApiBase { return false; } + /** + * Equivalent to addWhere(array($field => $value)) + * @param string $field Field name + * @param string $value Value; ignored if nul; + */ protected function addWhereFld($field, $value) { if (!is_null($value)) $this->where[$field] = $value; } + /** + * Add a WHERE clause corresponding to a range, and an ORDER BY + * clause to sort in the right direction + * @param string $field Field name + * @param string $dir If 'newer', sort in ascending order, otherwise sort in descending order + * @param string $start Value to start the list at. If $dir == 'newer' this is the lower boundary, otherwise it's the upper boundary + * @param string $end Value to end the list at. If $dir == 'newer' this is the upper boundary, otherwise it's the lower boundary + */ protected function addWhereRange($field, $dir, $start, $end) { $isDirNewer = ($dir === 'newer'); $after = ($isDirNewer ? '>=' : '<='); @@ -110,11 +180,19 @@ abstract class ApiQueryBase extends ApiBase { if (!is_null($end)) $this->addWhere($field . $before . $db->addQuotes($end)); - + + $order = $field . ($isDirNewer ? '' : ' DESC'); if (!isset($this->options['ORDER BY'])) - $this->addOption('ORDER BY', $field . ($isDirNewer ? '' : ' DESC')); + $this->addOption('ORDER BY', $order); + else + $this->addOption('ORDER BY', $this->options['ORDER BY'] . ', ' . $order); } + /** + * Add an option such as LIMIT or USE INDEX + * @param string $name Option name + * @param string $value Option value + */ protected function addOption($name, $value = null) { if (is_null($value)) $this->options[] = $name; @@ -122,39 +200,71 @@ abstract class ApiQueryBase extends ApiBase { $this->options[$name] = $value; } + /** + * Execute a SELECT query based on the values in the internal arrays + * @param string $method Function the query should be attributed to. You should usually use __METHOD__ here + * @return ResultWrapper + */ protected function select($method) { // getDB has its own profileDBIn/Out calls $db = $this->getDB(); $this->profileDBIn(); - $res = $db->select($this->tables, $this->fields, $this->where, $method, $this->options); + $res = $db->select($this->tables, $this->fields, $this->where, $method, $this->options, $this->join_conds); $this->profileDBOut(); return $res; } + /** + * Estimate the row count for the SELECT query that would be run if we + * called select() right now, and check if it's acceptable. + * @return bool true if acceptable, false otherwise + */ + protected function checkRowCount() { + $db = $this->getDB(); + $this->profileDBIn(); + $rowcount = $db->estimateRowCount($this->tables, $this->fields, $this->where, __METHOD__, $this->options); + $this->profileDBOut(); + + global $wgAPIMaxDBRows; + if($rowcount > $wgAPIMaxDBRows) + return false; + return true; + } + + /** + * Add information (title and namespace) about a Title object to a result array + * @param array $arr Result array à la ApiResult + * @param Title $title Title object + * @param string $prefix Module prefix + */ public static function addTitleInfo(&$arr, $title, $prefix='') { $arr[$prefix . 'ns'] = intval($title->getNamespace()); $arr[$prefix . 'title'] = $title->getPrefixedText(); } - + /** * Override this method to request extra fields from the pageSet * using $pageSet->requestField('fieldName') + * @param ApiPageSet $pageSet */ public function requestExtraData($pageSet) { } /** * Get the main Query module + * @return ApiQuery */ public function getQuery() { return $this->mQueryModule; } /** - * Add sub-element under the page element with the given pageId. + * Add a sub-element under the page element with the given page ID + * @param int $pageId Page ID + * @param array $data Data array à la ApiResult */ protected function addPageSubItems($pageId, $data) { $result = $this->getResult(); @@ -164,19 +274,21 @@ abstract class ApiQueryBase extends ApiBase { $data); } + /** + * Set a query-continue value + * @param $paramName Parameter name + * @param $paramValue Parameter value + */ protected function setContinueEnumParameter($paramName, $paramValue) { - + $paramName = $this->encodeParamName($paramName); $msg = array( $paramName => $paramValue ); - -// This is an alternative continue format as a part of the URL string -// ApiResult :: setContent($msg, $paramName . '=' . urlencode($paramValue)); - $this->getResult()->addValue('query-continue', $this->getModuleName(), $msg); } /** * Get the Query database connection (readonly) + * @return Database */ protected function getDB() { if (is_null($this->mDb)) @@ -186,57 +298,62 @@ abstract class ApiQueryBase extends ApiBase { /** * Selects the query database connection with the given name. - * If no such connection has been requested before, it will be created. - * Subsequent calls with the same $name will return the same connection - * as the first, regardless of $db or $groups new values. + * If no such connection has been requested before, it will be created. + * Subsequent calls with the same $name will return the same connection + * as the first, regardless of $db or $groups new values. + * @param string $name Name to assign to the database connection + * @param int $db One of the DB_* constants + * @param array $groups Query groups + * @return Database */ public function selectNamedDB($name, $db, $groups) { - $this->mDb = $this->getQuery()->getNamedDB($name, $db, $groups); + $this->mDb = $this->getQuery()->getNamedDB($name, $db, $groups); } /** * Get the PageSet object to work on - * @return ApiPageSet data + * @return ApiPageSet */ protected function getPageSet() { return $this->getQuery()->getPageSet(); } /** - * This is a very simplistic utility function - * to convert a non-namespaced title string to a db key. - * It will replace all ' ' with '_' + * Convert a title to a DB key + * @param string $title Page title with spaces + * @return string Page title with underscores */ - public static function titleToKey($title) { - return str_replace(' ', '_', $title); + public function titleToKey($title) { + $t = Title::newFromText($title); + if(!$t) + $this->dieUsageMsg(array('invalidtitle', $title)); + return $t->getDbKey(); } - public static function keyToTitle($key) { - return str_replace('_', ' ', $key); + /** + * The inverse of titleToKey() + * @param string $key Page title with underscores + * @return string Page title with spaces + */ + public function keyToTitle($key) { + $t = Title::newFromDbKey($key); + # This really shouldn't happen but we gotta check anyway + if(!$t) + $this->dieUsageMsg(array('invalidtitle', $key)); + return $t->getPrefixedText(); } - public function getTokenFlag($tokenArr, $action) { - if ($this->getMain()->getRequest()->getVal('callback') !== null) { - // Don't do any session-specific data. - return false; - } - if (in_array($action, $tokenArr)) { - global $wgUser; - if ($wgUser->isAllowed($action)) - return true; - else - $this->dieUsage("Action '$action' is not allowed for the current user", 'permissiondenied'); - } - return false; - } - + /** + * Get version string for use in the API help output + * @return string + */ public static function getBaseVersion() { - return __CLASS__ . ': $Id: ApiQueryBase.php 31484 2008-03-03 05:46:20Z brion $'; + return __CLASS__ . ': $Id: ApiQueryBase.php 37083 2008-07-05 11:18:50Z catrope $'; } } /** - * @addtogroup API + * @ingroup API */ abstract class ApiQueryGeneratorBase extends ApiQueryBase { @@ -247,6 +364,10 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase { $this->mIsGenerator = false; } + /** + * Switch this module to generator mode. By default, generator mode is + * switched off and the module acts like a normal query module. + */ public function setGeneratorMode() { $this->mIsGenerator = true; } @@ -267,4 +388,3 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase { */ public abstract function executeGenerator($resultPageSet); } - diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php index 165792b5..ebe87908 100644 --- a/includes/api/ApiQueryBlocks.php +++ b/includes/api/ApiQueryBlocks.php @@ -30,10 +30,12 @@ if (!defined('MEDIAWIKI')) { /** * Query module to enumerate all available pages. - * - * @addtogroup API + * + * @ingroup API */ class ApiQueryBlocks extends ApiQueryBase { + + var $users; public function __construct($query, $moduleName) { parent :: __construct($query, $moduleName, 'bk'); @@ -47,6 +49,9 @@ class ApiQueryBlocks extends ApiQueryBase { global $wgUser; $params = $this->extractRequestParams(); + if(isset($params['users']) && isset($params['ip'])) + $this->dieUsage('bkusers and bkip cannot be used together', 'usersandip'); + $prop = array_flip($params['prop']); $fld_id = isset($prop['id']); $fld_user = isset($prop['user']); @@ -66,7 +71,7 @@ class ApiQueryBlocks extends ApiQueryBase { if($fld_id) $this->addFields('ipb_id'); if($fld_user) - $this->addFields(array('ipb_address', 'ipb_user')); + $this->addFields(array('ipb_address', 'ipb_user', 'ipb_auto')); if($fld_by) { $this->addTables('user'); @@ -89,8 +94,32 @@ class ApiQueryBlocks extends ApiQueryBase { if(isset($params['ids'])) $this->addWhere(array('ipb_id' => $params['ids'])); if(isset($params['users'])) - $this->addWhere(array('ipb_address' => $params['users'])); - if(!$wgUser->isAllowed('oversight')) + { + foreach((array)$params['users'] as $u) + $this->prepareUsername($u); + $this->addWhere(array('ipb_address' => $this->usernames)); + } + if(isset($params['ip'])) + { + list($ip, $range) = IP::parseCIDR($params['ip']); + if($ip && $range) + { + # We got a CIDR range + if($range < 16) + $this->dieUsage('CIDR ranges broader than /16 are not accepted', 'cidrtoobroad'); + $lower = wfBaseConvert($ip, 10, 16, 8, false); + $upper = wfBaseConvert($ip + pow(2, 32 - $range) - 1, 10, 16, 8, false); + } + else + $lower = $upper = IP::toHex($params['ip']); + $prefix = substr($lower, 0, 4); + $this->addWhere(array( + "ipb_range_start LIKE '$prefix%'", + "ipb_range_start <= '$lower'", + "ipb_range_end >= '$upper'" + )); + } + if(!$wgUser->isAllowed('suppress')) $this->addWhere(array('ipb_deleted' => 0)); // Purge expired entries on one in every 10 queries @@ -152,6 +181,18 @@ class ApiQueryBlocks extends ApiQueryBase { $result->setIndexedTagName($data, 'block'); $result->addValue('query', $this->getModuleName(), $data); } + + protected function prepareUsername($user) + { + if(!$user) + $this->dieUsage('User parameter may not be empty', 'param_user'); + $name = User::isIP($user) + ? $user + : User::getCanonicalName($user, 'valid'); + if($name === false) + $this->dieUsage("User name {$user} is not valid", 'param_user'); + $this->usernames[] = $name; + } protected function convertHexIP($ip) { @@ -188,6 +229,7 @@ class ApiQueryBlocks extends ApiQueryBase { 'users' => array( ApiBase :: PARAM_ISMULTI => true ), + 'ip' => null, 'limit' => array( ApiBase :: PARAM_DFLT => 10, ApiBase :: PARAM_TYPE => 'limit', @@ -219,6 +261,8 @@ class ApiQueryBlocks extends ApiQueryBase { 'dir' => 'The direction in which to enumerate', 'ids' => 'Pipe-separated list of block IDs to list (optional)', 'users' => 'Pipe-separated list of users to search for (optional)', + 'ip' => array( 'Get all blocks applying to this IP or CIDR range, including range blocks.', + 'Cannot be used together with bkusers. CIDR ranges broader than /16 are not accepted.'), 'limit' => 'The maximum amount of blocks to list', 'prop' => 'Which properties to get', ); @@ -229,11 +273,12 @@ class ApiQueryBlocks extends ApiQueryBase { } protected function getExamples() { - return array ( + return array ( 'api.php?action=query&list=blocks', + 'api.php?action=query&list=blocks&bkusers=Alice|Bob' ); } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryBlocks.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryBlocks.php 37892 2008-07-21 21:37:11Z catrope $'; } } diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php index 63d42bfa..51492d63 100644 --- a/includes/api/ApiQueryCategories.php +++ b/includes/api/ApiQueryCategories.php @@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) { /** * A query module to enumerate categories the set of pages belong to. - * - * @addtogroup API + * + * @ingroup API */ class ApiQueryCategories extends ApiQueryGeneratorBase { @@ -59,8 +59,8 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { 'cl_from', 'cl_to' )); - - $fld_sortkey = false; + + $fld_sortkey = $fld_timestamp = false; if (!is_null($prop)) { foreach($prop as $p) { switch ($p) { @@ -68,24 +68,51 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { $this->addFields('cl_sortkey'); $fld_sortkey = true; break; + case 'timestamp': + $this->addFields('cl_timestamp'); + $fld_timestamp = true; + break; default : ApiBase :: dieDebug(__METHOD__, "Unknown prop=$p"); } } } - + $this->addTables('categorylinks'); $this->addWhereFld('cl_from', array_keys($this->getPageSet()->getGoodTitles())); - $this->addOption('ORDER BY', "cl_from, cl_to"); + if(!is_null($params['continue'])) { + $cont = explode('|', $params['continue']); + if(count($cont) != 2) + $this->dieUsage("Invalid continue param. You should pass the " . + "original value returned by the previous query", "_badcontinue"); + $clfrom = intval($cont[0]); + $clto = $this->getDb()->strencode($this->titleToKey($cont[1])); + $this->addWhere("cl_from > $clfrom OR ". + "(cl_from = $clfrom AND ". + "cl_to >= '$clto')"); + } + # Don't order by cl_from if it's constant in the WHERE clause + if(count($this->getPageSet()->getGoodTitles()) == 1) + $this->addOption('ORDER BY', 'cl_to'); + else + $this->addOption('ORDER BY', "cl_from, cl_to"); $db = $this->getDB(); $res = $this->select(__METHOD__); if (is_null($resultPageSet)) { - + $data = array(); - $lastId = 0; // database has no ID 0 + $lastId = 0; // database has no ID 0 + $count = 0; while ($row = $db->fetchObject($res)) { + if (++$count > $params['limit']) { + // We've reached the one extra which shows that + // there are additional pages to be had. Stop here... + $this->setContinueEnumParameter('continue', $row->cl_from . + '|' . $this->keyToTitle($row->cl_to)); + break; + } if ($lastId != $row->cl_from) { if($lastId != 0) { $this->addPageSubItems($lastId, $data); @@ -93,13 +120,15 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { } $lastId = $row->cl_from; } - + $title = Title :: makeTitle(NS_CATEGORY, $row->cl_to); - + $vals = array(); ApiQueryBase :: addTitleInfo($vals, $title); if ($fld_sortkey) $vals['sortkey'] = $row->cl_sortkey; + if ($fld_timestamp) + $vals['timestamp'] = $row->cl_timestamp; $data[] = $vals; } @@ -112,6 +141,14 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { $titles = array(); while ($row = $db->fetchObject($res)) { + if (++$count > $params['limit']) { + // We've reached the one extra which shows that + // there are additional pages to be had. Stop here... + $this->setContinueEnumParameter('continue', $row->cl_from . + '|' . $this->keyToTitle($row->cl_to)); + break; + } + $titles[] = Title :: makeTitle(NS_CATEGORY, $row->cl_to); } $resultPageSet->populateFromTitles($titles); @@ -126,14 +163,25 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { ApiBase :: PARAM_ISMULTI => true, ApiBase :: PARAM_TYPE => array ( 'sortkey', + 'timestamp', ) - ) + ), + '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 + ), + 'continue' => null, ); } public function getParamDescription() { return array ( 'prop' => 'Which additional properties to get for each category.', + 'limit' => 'How many categories to return', + 'continue' => 'When more results are available, use this to continue', ); } @@ -151,7 +199,6 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryCategories.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryCategories.php 37909 2008-07-22 13:26:15Z catrope $'; } } - diff --git a/includes/api/ApiQueryCategoryInfo.php b/includes/api/ApiQueryCategoryInfo.php new file mode 100644 index 00000000..f809bb15 --- /dev/null +++ b/includes/api/ApiQueryCategoryInfo.php @@ -0,0 +1,91 @@ +@gmail.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"); +} + +/** + * This query adds subelement to all pages with the list of images embedded into those pages. + * + * @ingroup API + */ +class ApiQueryCategoryInfo extends ApiQueryBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'ci'); + } + + public function execute() { + $alltitles = $this->getPageSet()->getAllTitlesByNamespace(); + $categories = $alltitles[NS_CATEGORY]; + if(empty($categories)) + return; + + $titles = $this->getPageSet()->getGoodTitles() + + $this->getPageSet()->getMissingTitles(); + $cattitles = array(); + foreach($categories as $c) + { + $t = $titles[$c]; + $cattitles[$c] = $t->getDbKey(); + } + + $this->addTables('category'); + $this->addFields(array('cat_title', 'cat_pages', 'cat_subcats', 'cat_files', 'cat_hidden')); + $this->addWhere(array('cat_title' => $cattitles)); + + $db = $this->getDB(); + $res = $this->select(__METHOD__); + + $data = array(); + $catids = array_flip($cattitles); + while($row = $db->fetchObject($res)) + { + $vals = array(); + $vals['size'] = $row->cat_pages; + $vals['pages'] = $row->cat_pages - $row->cat_subcats - $row->cat_files; + $vals['files'] = $row->cat_files; + $vals['subcats'] = $row->cat_subcats; + if($row->cat_hidden) + $vals['hidden'] = ''; + $this->addPageSubItems($catids[$row->cat_title], $vals); + } + $db->freeResult($res); + } + + public function getDescription() { + return 'Returns information about the given categories'; + } + + protected function getExamples() { + return "api.php?action=query&prop=categoryinfo&titles=Category:Foo|Category:Bar"; + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryCategoryInfo.php 37504 2008-07-10 14:28:09Z catrope $'; + } +} diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php index e831f291..3909b213 100644 --- a/includes/api/ApiQueryCategoryMembers.php +++ b/includes/api/ApiQueryCategoryMembers.php @@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) { /** * A query module to enumerate pages that belong to a category. - * - * @addtogroup API + * + * @ingroup API */ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { @@ -51,19 +51,13 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $params = $this->extractRequestParams(); - if (is_null($params['category'])) { - if (is_null($params['title'])) - $this->dieUsage("Either the cmcategory or the cmtitle parameter is required", 'notitle'); - else - $categoryTitle = Title::newFromText($params['title']); - } else if(is_null($params['title'])) - $categoryTitle = Title::makeTitleSafe(NS_CATEGORY, $params['category']); - else - $this->dieUsage("The cmcategory and cmtitle parameters can't be used together", 'titleandcategory'); + if ( !isset($params['title']) || is_null($params['title']) ) + $this->dieUsage("The cmtitle parameter is required", 'notitle'); + $categoryTitle = Title::newFromText($params['title']); if ( is_null( $categoryTitle ) || $categoryTitle->getNamespace() != NS_CATEGORY ) $this->dieUsage("The category name you entered is not valid", 'invalidcategory'); - + $prop = array_flip($params['prop']); $fld_ids = isset($prop['ids']); $fld_title = isset($prop['title']); @@ -78,26 +72,29 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $this->addFields(array('cl_from', 'cl_sortkey')); } - $this->addFieldsIf('cl_timestamp', $fld_timestamp); - $this->addTables(array('page','categorylinks')); // must be in this order for 'USE INDEX' + $this->addFieldsIf('cl_timestamp', $fld_timestamp || $params['sort'] == 'timestamp'); + $this->addTables(array('page','categorylinks')); // must be in this order for 'USE INDEX' // Not needed after bug 10280 is applied to servers if($params['sort'] == 'timestamp') { $this->addOption('USE INDEX', 'cl_timestamp'); - $this->addOption('ORDER BY', 'cl_to, cl_timestamp' . ($params['dir'] == 'desc' ? ' DESC' : '')); + // cl_timestamp will be added by addWhereRange() later + $this->addOption('ORDER BY', 'cl_to'); } else { + $dir = ($params['dir'] == 'desc' ? ' DESC' : ''); $this->addOption('USE INDEX', 'cl_sortkey'); - $this->addOption('ORDER BY', 'cl_to, cl_sortkey' . ($params['dir'] == 'desc' ? ' DESC' : '') . ', cl_from'); + $this->addOption('ORDER BY', 'cl_to, cl_sortkey' . $dir . ', cl_from' . $dir); } $this->addWhere('cl_from=page_id'); - $this->setContinuation($params['continue']); + $this->setContinuation($params['continue'], $params['dir']); $this->addWhereFld('cl_to', $categoryTitle->getDBkey()); $this->addWhereFld('page_namespace', $params['namespace']); - $this->addWhereRange('cl_timestamp', ($params['dir'] == 'asc' ? 'newer' : 'older'), $params['start'], $params['end']); - + if($params['sort'] == 'timestamp') + $this->addWhereRange('cl_timestamp', ($params['dir'] == 'asc' ? 'newer' : 'older'), $params['start'], $params['end']); + $limit = $params['limit']; $this->addOption('LIMIT', $limit +1); @@ -111,16 +108,19 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { if (++ $count > $limit) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... // TODO: Security issue - if the user has no right to view next title, it will still be shown - $this->setContinueEnumParameter('continue', $this->getContinueStr($row, $lastSortKey)); + if ($params['sort'] == 'timestamp') + $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->cl_timestamp)); + else + $this->setContinueEnumParameter('continue', $this->getContinueStr($row, $lastSortKey)); break; } - $lastSortKey = $row->cl_sortkey; // detect duplicate sortkeys - + $lastSortKey = $row->cl_sortkey; // detect duplicate sortkeys + if (is_null($resultPageSet)) { $vals = array(); if ($fld_ids) - $vals['pageid'] = intval($row->page_id); + $vals['pageid'] = intval($row->page_id); if ($fld_title) { $title = Title :: makeTitle($row->page_namespace, $row->page_title); $vals['ns'] = intval($title->getNamespace()); @@ -142,47 +142,48 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $this->getResult()->addValue('query', $this->getModuleName(), $data); } } - + private function getContinueStr($row, $lastSortKey) { $ret = $row->cl_sortkey . '|'; if ($row->cl_sortkey == $lastSortKey) // duplicate sort key, add cl_from $ret .= $row->cl_from; return $ret; } - + /** - * Add DB WHERE clause to continue previous query based on 'continue' parameter + * Add DB WHERE clause to continue previous query based on 'continue' parameter */ - private function setContinuation($continue) { + private function setContinuation($continue, $dir) { if (is_null($continue)) return; // This is not a continuation request - + $continueList = explode('|', $continue); $hasError = count($continueList) != 2; $from = 0; if (!$hasError && strlen($continueList[1]) > 0) { $from = intval($continueList[1]); - $hasError = ($from == 0); + $hasError = ($from == 0); } - + if ($hasError) $this->dieUsage("Invalid continue param. You should pass the original value returned by the previous query", "badcontinue"); $encSortKey = $this->getDB()->addQuotes($continueList[0]); $encFrom = $this->getDB()->addQuotes($from); + + $op = ($dir == 'desc' ? '<' : '>'); if ($from != 0) { // Duplicate sort key continue - $this->addWhere( "cl_sortkey>$encSortKey OR (cl_sortkey=$encSortKey AND cl_from>=$encFrom)" ); + $this->addWhere( "cl_sortkey$op$encSortKey OR (cl_sortkey=$encSortKey AND cl_from$op=$encFrom)" ); } else { - $this->addWhere( "cl_sortkey>=$encSortKey" ); + $this->addWhere( "cl_sortkey$op=$encSortKey" ); } } public function getAllowedParams() { return array ( 'title' => null, - 'category' => null, // DEPRECATED, will be removed in early March 'prop' => array ( ApiBase :: PARAM_DFLT => 'ids|title', ApiBase :: PARAM_ISMULTI => true, @@ -235,11 +236,10 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { 'namespace' => 'Only include pages in these namespaces', 'sort' => 'Property to sort by', 'dir' => 'In which direction to sort', - 'start' => 'Timestamp to start listing from', - 'end' => 'Timestamp to end listing at', + 'start' => 'Timestamp to start listing from. Can only be used with cmsort=timestamp', + 'end' => 'Timestamp to end listing at. Can only be used with cmsort=timestamp', 'continue' => 'For large categories, give the value retured from previous query', 'limit' => 'The maximum number of pages to return.', - 'category' => 'DEPRECATED. Like title, but without the Category: prefix.', ); } @@ -257,7 +257,6 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryCategoryMembers.php 30670 2008-02-07 15:17:42Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryCategoryMembers.php 35098 2008-05-20 17:13:28Z ialex $'; } } - diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php index 1b7fbdb0..8368896d 100644 --- a/includes/api/ApiQueryDeletedrevs.php +++ b/includes/api/ApiQueryDeletedrevs.php @@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) { /** * Query module to enumerate all available pages. - * - * @addtogroup API + * + * @ingroup API */ class ApiQueryDeletedrevs extends ApiQueryBase { @@ -87,11 +87,16 @@ class ApiQueryDeletedrevs extends ApiQueryBase { // Check limits $userMax = $fld_content ? ApiBase :: LIMIT_SML1 : ApiBase :: LIMIT_BIG1; $botMax = $fld_content ? ApiBase :: LIMIT_SML2 : ApiBase :: LIMIT_BIG2; + + $limit = $params['limit']; + if( $limit == 'max' ) { $limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax; - $this->getResult()->addValue( 'limits', 'limit', $limit ); + $this->getResult()->addValue( 'limits', $this->getModuleName(), $limit ); } - $this->validateLimit('limit', $params['limit'], 1, $userMax, $botMax); + + $this->validateLimit('limit', $limit, 1, $userMax, $botMax); + if($fld_token) // Undelete tokens are identical for all pages, so we cache one here $token = $wgUser->editToken(); @@ -104,17 +109,15 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $this->addWhere($where); } - $this->addOption('LIMIT', $params['limit'] + 1); + $this->addOption('LIMIT', $limit + 1); $this->addWhereRange('ar_timestamp', $params['dir'], $params['start'], $params['end']); - if(isset($params['namespace'])) - $this->addWhereFld('ar_namespace', $params['namespace']); $res = $this->select(__METHOD__); $pages = array(); $count = 0; // First populate the $pages array while($row = $db->fetchObject($res)) { - if($count++ == $params['limit']) + if(++$count > $limit) { // We've had enough $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->ar_timestamp)); @@ -178,10 +181,6 @@ class ApiQueryDeletedrevs extends ApiQueryBase { ), ApiBase :: PARAM_DFLT => 'older' ), - 'namespace' => array( - ApiBase :: PARAM_ISMULTI => true, - ApiBase :: PARAM_TYPE => 'namespace' - ), 'limit' => array( ApiBase :: PARAM_DFLT => 10, ApiBase :: PARAM_TYPE => 'limit', @@ -210,7 +209,6 @@ class ApiQueryDeletedrevs extends ApiQueryBase { 'start' => 'The timestamp to start enumerating from', 'end' => 'The timestamp to stop enumerating at', 'dir' => 'The direction in which to enumerate', - 'namespace' => 'The namespaces to search in', 'limit' => 'The maximum amount of revisions to list', 'prop' => 'Which properties to get' ); @@ -222,14 +220,14 @@ class ApiQueryDeletedrevs extends ApiQueryBase { protected function getExamples() { return array ( - 'List the first 50 deleted revisions in the Category and Category talk namespaces', - ' api.php?action=query&list=deletedrevs&drdir=newer&drlimit=50&drnamespace=14|15', + 'List the first 50 deleted revisions', + ' api.php?action=query&list=deletedrevs&drdir=newer&drlimit=50', 'List the last deleted revisions of Main Page and Talk:Main Page, with content:', ' api.php?action=query&list=deletedrevs&titles=Main%20Page|Talk:Main%20Page&drprop=user|comment|content' ); } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryDeletedrevs.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryDeletedrevs.php 37502 2008-07-10 14:13:11Z catrope $'; } } diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php index 896a0171..8ffb7246 100644 --- a/includes/api/ApiQueryExtLinksUsage.php +++ b/includes/api/ApiQueryExtLinksUsage.php @@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) { } /** - * @addtogroup API + * @ingroup API */ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { @@ -51,43 +51,53 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { $protocol = $params['protocol']; $query = $params['query']; - if (is_null($query)) - $this->dieUsage('Missing required query parameter', 'params'); - + // Find the right prefix global $wgUrlProtocols; - foreach ($wgUrlProtocols as $p) { - if( substr( $p, 0, strlen( $protocol ) ) === $protocol ) { - $protocol = $p; - break; + if(!is_null($protocol) && !empty($protocol) && !in_array($protocol, $wgUrlProtocols)) + { + foreach ($wgUrlProtocols as $p) { + if( substr( $p, 0, strlen( $protocol ) ) === $protocol ) { + $protocol = $p; + break; + } } } - - $likeQuery = LinkFilter::makeLike($query , $protocol); - if (!$likeQuery) - $this->dieUsage('Invalid query', 'bad_query'); - $likeQuery = substr($likeQuery, 0, strpos($likeQuery,'%')+1); + else + $protocol = null; - $this->addTables(array('page','externallinks')); // must be in this order for 'USE INDEX' + $db = $this->getDb(); + $this->addTables(array('page','externallinks')); // must be in this order for 'USE INDEX' $this->addOption('USE INDEX', 'el_index'); - - $db = $this->getDB(); $this->addWhere('page_id=el_from'); - $this->addWhere('el_index LIKE ' . $db->addQuotes( $likeQuery )); $this->addWhereFld('page_namespace', $params['namespace']); + if(!is_null($query) || $query != '') + { + if(is_null($protocol)) + $protocol = 'http://'; + + $likeQuery = LinkFilter::makeLike($query, $protocol); + if (!$likeQuery) + $this->dieUsage('Invalid query', 'bad_query'); + $likeQuery = substr($likeQuery, 0, strpos($likeQuery,'%')+1); + $this->addWhere('el_index LIKE ' . $db->addQuotes( $likeQuery )); + } + else if(!is_null($protocol)) + $this->addWhere('el_index LIKE ' . $db->addQuotes( "$protocol%" )); + $prop = array_flip($params['prop']); $fld_ids = isset($prop['ids']); $fld_title = isset($prop['title']); $fld_url = isset($prop['url']); - + if (is_null($resultPageSet)) { $this->addFields(array ( 'page_id', 'page_namespace', 'page_title' )); - $this->addFieldsIf('el_to', $fld_url); + $this->addFieldsIf('el_to', $fld_url); } else { $this->addFields($resultPageSet->getPageTableFields()); } @@ -105,7 +115,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { while ($row = $db->fetchObject($res)) { if (++ $count > $limit) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... - $this->setContinueEnumParameter('offset', $offset+$limit+1); + $this->setContinueEnumParameter('offset', $offset+$limit); break; } @@ -136,11 +146,11 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { public function getAllowedParams() { global $wgUrlProtocols; - $protocols = array(); + $protocols = array(''); foreach ($wgUrlProtocols as $p) { $protocols[] = substr($p, 0, strpos($p,':')); } - + return array ( 'prop' => array ( ApiBase :: PARAM_ISMULTI => true, @@ -156,7 +166,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { ), 'protocol' => array ( ApiBase :: PARAM_TYPE => $protocols, - ApiBase :: PARAM_DFLT => 'http', + ApiBase :: PARAM_DFLT => '', ), 'query' => null, 'namespace' => array ( @@ -177,10 +187,11 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { return array ( 'prop' => 'What pieces of information to include', 'offset' => 'Used for paging. Use the value returned for "continue"', - 'protocol' => 'Protocol of the url', - 'query' => 'Search string without protocol. See [[Special:LinkSearch]]', + 'protocol' => array( 'Protocol of the url. If empty and euquery set, the protocol is http.', + 'Leave both this and euquery empty to list all external links'), + 'query' => 'Search string without protocol. See [[Special:LinkSearch]]. Leave empty to list all external links', 'namespace' => 'The page namespace(s) to enumerate.', - 'limit' => 'How many entries to return.' + 'limit' => 'How many pages to return.' ); } @@ -195,6 +206,6 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryExtLinksUsage.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryExtLinksUsage.php 37909 2008-07-22 13:26:15Z catrope $'; } } diff --git a/includes/api/ApiQueryExternalLinks.php b/includes/api/ApiQueryExternalLinks.php index 07183910..a24f15d8 100644 --- a/includes/api/ApiQueryExternalLinks.php +++ b/includes/api/ApiQueryExternalLinks.php @@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) { /** * A query module to list all external URLs found on a given set of pages. - * - * @addtogroup API + * + * @ingroup API */ class ApiQueryExternalLinks extends ApiQueryBase { @@ -40,21 +40,37 @@ class ApiQueryExternalLinks extends ApiQueryBase { } public function execute() { + if ( $this->getPageSet()->getGoodTitleCount() == 0 ) + return; + $params = $this->extractRequestParams(); $this->addFields(array ( 'el_from', 'el_to' )); - + $this->addTables('externallinks'); $this->addWhereFld('el_from', array_keys($this->getPageSet()->getGoodTitles())); + # Don't order by el_from if it's constant in the WHERE clause + if(count($this->getPageSet()->getGoodTitles()) != 1) + $this->addOption('ORDER BY', 'el_from'); + $this->addOption('LIMIT', $params['limit'] + 1); + if(!is_null($params['offset'])) + $this->addOption('OFFSET', $params['offset']); $db = $this->getDB(); $res = $this->select(__METHOD__); - + $data = array(); - $lastId = 0; // database has no ID 0 + $lastId = 0; // database has no ID 0 + $count = 0; while ($row = $db->fetchObject($res)) { + if (++$count > $params['limit']) { + // We've reached the one extra which shows that + // there are additional pages to be had. Stop here... + $this->setContinueEnumParameter('offset', @$params['offset'] + $params['limit']); + break; + } if ($lastId != $row->el_from) { if($lastId != 0) { $this->addPageSubItems($lastId, $data); @@ -62,7 +78,7 @@ class ApiQueryExternalLinks extends ApiQueryBase { } $lastId = $row->el_from; } - + $entry = array(); ApiResult :: setContent($entry, $row->el_to); $data[] = $entry; @@ -75,6 +91,26 @@ class ApiQueryExternalLinks extends ApiQueryBase { $db->freeResult($res); } + public function getAllowedParams() { + return 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 + ), + 'offset' => null, + ); + } + + public function getParamDescription () { + return array( + 'limit' => 'How many links to return', + 'offset' => 'When more results are available, use this to continue', + ); + } + public function getDescription() { return 'Returns all external urls (not interwikies) from the given page(s)'; } @@ -87,7 +123,6 @@ class ApiQueryExternalLinks extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryExternalLinks.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryExternalLinks.php 37270 2008-07-07 17:32:22Z catrope $'; } } - diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php index 3714ccf6..33ff1d3f 100644 --- a/includes/api/ApiQueryImageInfo.php +++ b/includes/api/ApiQueryImageInfo.php @@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) { /** * A query action to get image information and upload history. - * - * @addtogroup API + * + * @ingroup API */ class ApiQueryImageInfo extends ApiQueryBase { @@ -42,65 +42,63 @@ class ApiQueryImageInfo extends ApiQueryBase { public function execute() { $params = $this->extractRequestParams(); - $prop = array_flip($params['prop']); - $this->fld_timestamp = isset($prop['timestamp']); - $this->fld_user = isset($prop['user']); - $this->fld_comment = isset($prop['comment']); - $this->fld_url = isset($prop['url']); - $this->fld_size = isset($prop['size']); - $this->fld_sha1 = isset($prop['sha1']); - $this->fld_metadata = isset($prop['metadata']); - + $prop = array_flip($params['prop']); + if($params['urlheight'] != -1 && $params['urlwidth'] == -1) $this->dieUsage("iiurlheight cannot be used without iiurlwidth", 'iiurlwidth'); - $this->scale = ($params['urlwidth'] != -1); - $this->urlwidth = $params['urlwidth']; - $this->urlheight = $params['urlheight']; + + if ( $params['urlwidth'] != -1 ) { + $scale = array(); + $scale['width'] = $params['urlwidth']; + $scale['height'] = $params['urlheight']; + } else { + $scale = null; + } $pageIds = $this->getPageSet()->getAllTitlesByNamespace(); if (!empty($pageIds[NS_IMAGE])) { - foreach ($pageIds[NS_IMAGE] as $dbKey => $pageId) { - - $title = Title :: makeTitle(NS_IMAGE, $dbKey); - $img = wfFindFile($title); - + + $result = $this->getResult(); + $images = RepoGroup::singleton()->findFiles( array_keys( $pageIds[NS_IMAGE] ) ); + foreach ( $images as $img ) { $data = array(); - if ( !$img ) { - $repository = ''; - } else { - - $repository = $img->getRepoName(); - - // Get information about the current version first - // Check that the current version is within the start-end boundaries - if((is_null($params['start']) || $img->getTimestamp() <= $params['start']) && - (is_null($params['end']) || $img->getTimestamp() >= $params['end'])) { - $data[] = $this->getInfo($img); - } - - // Now get the old revisions - // Get one more to facilitate query-continue functionality - $count = count($data); - $oldies = $img->getHistory($params['limit'] - $count + 1, $params['start'], $params['end']); - foreach($oldies as $oldie) { - if(++$count > $params['limit']) { - // We've reached the extra one which shows that there are additional pages to be had. Stop here... - // Only set a query-continue if there was only one title - if(count($pageIds[NS_IMAGE]) == 1) - $this->setContinueEnumParameter('start', $oldie->getTimestamp()); - break; - } - $data[] = $this->getInfo($oldie); + + // Get information about the current version first + // Check that the current version is within the start-end boundaries + if((is_null($params['start']) || $img->getTimestamp() <= $params['start']) && + (is_null($params['end']) || $img->getTimestamp() >= $params['end'])) { + $data[] = self::getInfo( $img, $prop, $result, $scale ); + } + + // Now get the old revisions + // Get one more to facilitate query-continue functionality + $count = count($data); + $oldies = $img->getHistory($params['limit'] - $count + 1, $params['start'], $params['end']); + foreach($oldies as $oldie) { + if(++$count > $params['limit']) { + // We've reached the extra one which shows that there are additional pages to be had. Stop here... + // Only set a query-continue if there was only one title + if(count($pageIds[NS_IMAGE]) == 1) + $this->setContinueEnumParameter('start', $oldie->getTimestamp()); + break; } + $data[] = self::getInfo( $oldie, $prop, $result ); } - $this->getResult()->addValue(array( - 'query', 'pages', intval($pageId)), - 'imagerepository', $repository + $pageId = $pageIds[NS_IMAGE][ $img->getOriginalTitle()->getDBkey() ]; + $result->addValue( + array( 'query', 'pages', intval( $pageId ) ), + 'imagerepository', $img->getRepoName() ); - if (!empty($data)) - $this->addPageSubItems($pageId, $data); + $this->addPageSubItems($pageId, $data); } + + $missing = array_diff( array_keys( $pageIds[NS_IMAGE] ), array_keys( $images ) ); + foreach ( $missing as $title ) + $result->addValue( + array( 'query', 'pages', intval( $pageIds[NS_IMAGE][$title] ) ), + 'imagerepository', '' + ); } } @@ -109,41 +107,48 @@ class ApiQueryImageInfo extends ApiQueryBase { * @param File f The image * @return array Result array */ - protected function getInfo($f) { + static function getInfo($file, $prop, $result, $scale = null) { $vals = array(); - if($this->fld_timestamp) - $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $f->getTimestamp()); - if($this->fld_user) { - $vals['user'] = $f->getUser(); - if(!$f->getUser('id')) + if( isset( $prop['timestamp'] ) ) + $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $file->getTimestamp()); + if( isset( $prop['user'] ) ) { + $vals['user'] = $file->getUser(); + if( !$file->getUser( 'id' ) ) $vals['anon'] = ''; } - if($this->fld_size) { - $vals['size'] = intval($f->getSize()); - $vals['width'] = intval($f->getWidth()); - $vals['height'] = intval($f->getHeight()); + if( isset( $prop['size'] ) || isset( $prop['dimensions'] ) ) { + $vals['size'] = intval( $file->getSize() ); + $vals['width'] = intval( $file->getWidth() ); + $vals['height'] = intval( $file->getHeight() ); } - if($this->fld_url) { - if($this->scale && !$f->isOld()) { - $thumb = $f->getThumbnail($this->urlwidth, $this->urlheight); - if($thumb) + if( isset( $prop['url'] ) ) { + if( !is_null( $scale ) && !$file->isOld() ) { + $thumb = $file->getThumbnail( $scale['width'], $scale['height'] ); + if( $thumb ) { - $vals['thumburl'] = $thumb->getURL(); + $vals['thumburl'] = wfExpandUrl( $thumb->getURL() ); $vals['thumbwidth'] = $thumb->getWidth(); $vals['thumbheight'] = $thumb->getHeight(); } } - $vals['url'] = $f->getURL(); + $vals['url'] = $file->getFullURL(); + $vals['descriptionurl'] = wfExpandUrl( $file->getDescriptionUrl() ); } - if($this->fld_comment) - $vals['comment'] = $f->getDescription(); - if($this->fld_sha1) - $vals['sha1'] = wfBaseConvert($f->getSha1(), 36, 16, 40); - if($this->fld_metadata) { - $metadata = unserialize($f->getMetadata()); - $vals['metadata'] = $metadata ? $metadata : null; - $this->getResult()->setIndexedTagName_recursive($vals['metadata'], 'meta'); + if( isset( $prop['comment'] ) ) + $vals['comment'] = $file->getDescription(); + if( isset( $prop['sha1'] ) ) + $vals['sha1'] = wfBaseConvert( $file->getSha1(), 36, 16, 40 ); + if( isset( $prop['metadata'] ) ) { + $metadata = $file->getMetadata(); + $vals['metadata'] = $metadata ? unserialize( $metadata ) : null; + $result->setIndexedTagName_recursive( $vals['metadata'], 'meta' ); } + if( isset( $prop['mime'] ) ) + $vals['mime'] = $file->getMimeType(); + + if( isset( $prop['archivename'] ) && $file->isOld() ) + $vals['archivename'] = $file->getArchiveName(); + return $vals; } @@ -159,7 +164,9 @@ class ApiQueryImageInfo extends ApiQueryBase { 'url', 'size', 'sha1', - 'metadata' + 'mime', + 'metadata', + 'archivename' ) ), 'limit' => array( @@ -192,7 +199,8 @@ class ApiQueryImageInfo extends ApiQueryBase { 'limit' => 'How many image revisions to return', 'start' => 'Timestamp to start listing from', 'end' => 'Timestamp to stop listing at', - 'urlwidth' => 'If iiprop=url is set, a URL to an image scaled to this width will be returned. Only the current version of the image can be scaled.', + 'urlwidth' => array('If iiprop=url is set, a URL to an image scaled to this width will be returned.', + 'Only the current version of the image can be scaled.'), 'urlheight' => 'Similar to iiurlwidth. Cannot be used without iiurlwidth', ); } @@ -211,6 +219,6 @@ class ApiQueryImageInfo extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryImageInfo.php 30665 2008-02-07 12:21:48Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryImageInfo.php 37504 2008-07-10 14:28:09Z catrope $'; } } diff --git a/includes/api/ApiQueryImages.php b/includes/api/ApiQueryImages.php index f7405374..32c4e1b0 100644 --- a/includes/api/ApiQueryImages.php +++ b/includes/api/ApiQueryImages.php @@ -29,9 +29,9 @@ if (!defined('MEDIAWIKI')) { } /** - * This query adds subelement to all pages with the list of images embedded into those pages. - * - * @addtogroup API + * This query adds an subelement to all pages with the list of images embedded into those pages. + * + * @ingroup API */ class ApiQueryImages extends ApiQueryGeneratorBase { @@ -52,6 +52,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase { if ($this->getPageSet()->getGoodTitleCount() == 0) return; // nothing to do + $params = $this->extractRequestParams(); $this->addFields(array ( 'il_from', 'il_to' @@ -59,16 +60,40 @@ class ApiQueryImages extends ApiQueryGeneratorBase { $this->addTables('imagelinks'); $this->addWhereFld('il_from', array_keys($this->getPageSet()->getGoodTitles())); - $this->addOption('ORDER BY', "il_from, il_to"); + if(!is_null($params['continue'])) { + $cont = explode('|', $params['continue']); + if(count($cont) != 2) + $this->dieUsage("Invalid continue param. You should pass the " . + "original value returned by the previous query", "_badcontinue"); + $ilfrom = intval($cont[0]); + $ilto = $this->getDb()->strencode($this->titleToKey($cont[1])); + $this->addWhere("il_from > $ilfrom OR ". + "(il_from = $ilfrom AND ". + "il_to >= '$ilto')"); + } + # Don't order by il_from if it's constant in the WHERE clause + if(count($this->getPageSet()->getGoodTitles()) == 1) + $this->addOption('ORDER BY', 'il_to'); + else + $this->addOption('ORDER BY', 'il_from, il_to'); + $this->addOption('LIMIT', $params['limit'] + 1); $db = $this->getDB(); $res = $this->select(__METHOD__); if (is_null($resultPageSet)) { - + $data = array(); - $lastId = 0; // database has no ID 0 + $lastId = 0; // database has no ID 0 + $count = 0; while ($row = $db->fetchObject($res)) { + if (++$count > $params['limit']) { + // We've reached the one extra which shows that + // there are additional pages to be had. Stop here... + $this->setContinueEnumParameter('continue', $row->il_from . + '|' . $this->keyToTitle($row->il_to)); + break; + } if ($lastId != $row->il_from) { if($lastId != 0) { $this->addPageSubItems($lastId, $data); @@ -76,7 +101,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase { } $lastId = $row->il_from; } - + $vals = array(); ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle(NS_IMAGE, $row->il_to)); $data[] = $vals; @@ -89,7 +114,15 @@ class ApiQueryImages extends ApiQueryGeneratorBase { } else { $titles = array(); + $count = 0; while ($row = $db->fetchObject($res)) { + if (++$count > $params['limit']) { + // We've reached the one extra which shows that + // there are additional pages to be had. Stop here... + $this->setContinueEnumParameter('continue', $row->il_from . + '|' . $this->keyToTitle($row->il_to)); + break; + } $titles[] = Title :: makeTitle(NS_IMAGE, $row->il_to); } $resultPageSet->populateFromTitles($titles); @@ -98,6 +131,26 @@ class ApiQueryImages extends ApiQueryGeneratorBase { $db->freeResult($res); } + public function getAllowedParams() { + return 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 + ), + 'continue' => null, + ); + } + + public function getParamDescription () { + return array( + 'limit' => 'How many images to return', + 'continue' => 'When more results are available, use this to continue', + ); + } + public function getDescription() { return 'Returns all images contained on the given page(s)'; } @@ -112,7 +165,6 @@ class ApiQueryImages extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryImages.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryImages.php 37535 2008-07-10 21:20:43Z catrope $'; } } - diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php index 2dee22b0..9c6487b3 100644 --- a/includes/api/ApiQueryInfo.php +++ b/includes/api/ApiQueryInfo.php @@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) { /** * A query module to show basic page information. - * - * @addtogroup API + * + * @ingroup API */ class ApiQueryInfo extends ApiQueryBase { @@ -49,27 +49,123 @@ class ApiQueryInfo extends ApiQueryBase { $pageSet->requestField('page_len'); } + protected function getTokenFunctions() { + // tokenname => function + // function prototype is func($pageid, $title) + // should return token or false + + // Don't call the hooks twice + if(isset($this->tokenFunctions)) + return $this->tokenFunctions; + + // If we're in JSON callback mode, no tokens can be obtained + if(!is_null($this->getMain()->getRequest()->getVal('callback'))) + return array(); + + $this->tokenFunctions = array( + 'edit' => array( 'ApiQueryInfo', 'getEditToken' ), + 'delete' => array( 'ApiQueryInfo', 'getDeleteToken' ), + 'protect' => array( 'ApiQueryInfo', 'getProtectToken' ), + 'move' => array( 'ApiQueryInfo', 'getMoveToken' ), + 'block' => array( 'ApiQueryInfo', 'getBlockToken' ), + 'unblock' => array( 'ApiQueryInfo', 'getUnblockToken' ) + ); + wfRunHooks('APIQueryInfoTokens', array(&$this->tokenFunctions)); + return $this->tokenFunctions; + } + + public static function getEditToken($pageid, $title) + { + // We could check for $title->userCan('edit') here, + // but that's too expensive for this purpose + global $wgUser; + if(!$wgUser->isAllowed('edit')) + return false; + + // The edit token is always the same, let's exploit that + static $cachedEditToken = null; + if(!is_null($cachedEditToken)) + return $cachedEditToken; + + $cachedEditToken = $wgUser->editToken(); + return $cachedEditToken; + } + + public static function getDeleteToken($pageid, $title) + { + global $wgUser; + if(!$wgUser->isAllowed('delete')) + return false; + + static $cachedDeleteToken = null; + if(!is_null($cachedDeleteToken)) + return $cachedDeleteToken; + + $cachedDeleteToken = $wgUser->editToken(); + return $cachedDeleteToken; + } + + public static function getProtectToken($pageid, $title) + { + global $wgUser; + if(!$wgUser->isAllowed('protect')) + return false; + + static $cachedProtectToken = null; + if(!is_null($cachedProtectToken)) + return $cachedProtectToken; + + $cachedProtectToken = $wgUser->editToken(); + return $cachedProtectToken; + } + + public static function getMoveToken($pageid, $title) + { + global $wgUser; + if(!$wgUser->isAllowed('move')) + return false; + + static $cachedMoveToken = null; + if(!is_null($cachedMoveToken)) + return $cachedMoveToken; + + $cachedMoveToken = $wgUser->editToken(); + return $cachedMoveToken; + } + + public static function getBlockToken($pageid, $title) + { + global $wgUser; + if(!$wgUser->isAllowed('block')) + return false; + + static $cachedBlockToken = null; + if(!is_null($cachedBlockToken)) + return $cachedBlockToken; + + $cachedBlockToken = $wgUser->editToken(); + return $cachedBlockToken; + } + + public static function getUnblockToken($pageid, $title) + { + // Currently, this is exactly the same as the block token + return self::getBlockToken($pageid, $title); + } + public function execute() { global $wgUser; $params = $this->extractRequestParams(); - $fld_protection = false; + $fld_protection = $fld_talkid = $fld_subjectid = false; if(!is_null($params['prop'])) { $prop = array_flip($params['prop']); $fld_protection = isset($prop['protection']); + $fld_talkid = isset($prop['talkid']); + $fld_subjectid = isset($prop['subjectid']); } - if(!is_null($params['token'])) { - $token = $params['token']; - $tok_edit = $this->getTokenFlag($token, 'edit'); - $tok_delete = $this->getTokenFlag($token, 'delete'); - $tok_protect = $this->getTokenFlag($token, 'protect'); - $tok_move = $this->getTokenFlag($token, 'move'); - } - else - // Fix E_NOTICEs about unset variables - $token = $tok_edit = $tok_delete = $tok_protect = $tok_move = null; - + $pageSet = $this->getPageSet(); $titles = $pageSet->getGoodTitles(); $missing = $pageSet->getMissingTitles(); @@ -101,7 +197,64 @@ class ApiQueryInfo extends ApiQueryBase { $protections[$row->pr_page][] = $a; } $db->freeResult($res); + + $imageIds = array(); + foreach ($titles as $id => $title) + if ($title->getNamespace() == NS_IMAGE) + $imageIds[] = $id; + // To avoid code duplication + $cascadeTypes = array( + array( + 'prefix' => 'tl', + 'table' => 'templatelinks', + 'ns' => 'tl_namespace', + 'title' => 'tl_title', + 'ids' => array_diff(array_keys($titles), $imageIds) + ), + array( + 'prefix' => 'il', + 'table' => 'imagelinks', + 'ns' => NS_IMAGE, + 'title' => 'il_to', + 'ids' => $imageIds + ) + ); + + foreach ($cascadeTypes as $type) + { + if (count($type['ids']) != 0) { + $this->resetQueryParams(); + $this->addTables(array('page_restrictions', $type['table'])); + $this->addTables('page', 'page_source'); + $this->addTables('page', 'page_target'); + $this->addFields(array('pr_type', 'pr_level', 'pr_expiry', + 'page_target.page_id AS page_target_id', + 'page_source.page_namespace AS page_source_namespace', + 'page_source.page_title AS page_source_title')); + $this->addWhere(array("{$type['prefix']}_from = pr_page", + 'page_target.page_namespace = '.$type['ns'], + 'page_target.page_title = '.$type['title'], + 'page_source.page_id = pr_page' + )); + $this->addWhereFld('pr_cascade', 1); + $this->addWhereFld('page_target.page_id', $type['ids']); + + $res = $this->select(__METHOD__); + while($row = $db->fetchObject($res)) { + $source = Title::makeTitle($row->page_source_namespace, $row->page_source_title); + $a = array( + 'type' => $row->pr_type, + 'level' => $row->pr_level, + 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ), + 'source' => $source->getPrefixedText() + ); + $protections[$row->page_target_id][] = $a; + } + $db->freeResult($res); + } + } } + // We don't need to check for pt stuff if there are no nonexistent titles if($fld_protection && !empty($missing)) { @@ -114,13 +267,107 @@ class ApiQueryInfo extends ApiQueryBase { $res = $this->select(__METHOD__); $prottitles = array(); while($row = $db->fetchObject($res)) { - $prottitles[$row->pt_namespace][$row->pt_title] = array( + $prottitles[$row->pt_namespace][$row->pt_title][] = array( 'type' => 'create', 'level' => $row->pt_create_perm, 'expiry' => Block::decodeExpiry($row->pt_expiry, TS_ISO_8601) ); } $db->freeResult($res); + + $images = array(); + $others = array(); + foreach ($missing as $title) + if ($title->getNamespace() == NS_IMAGE) + $images[] = $title->getDbKey(); + else + $others[] = $title; + + if (count($others) != 0) { + $lb = new LinkBatch($others); + $this->resetQueryParams(); + $this->addTables(array('page_restrictions', 'page', 'templatelinks')); + $this->addFields(array('pr_type', 'pr_level', 'pr_expiry', + 'page_title', 'page_namespace', + 'tl_title', 'tl_namespace')); + $this->addWhere($lb->constructSet('tl', $db)); + $this->addWhere('pr_page = page_id'); + $this->addWhere('pr_page = tl_from'); + $this->addWhereFld('pr_cascade', 1); + + $res = $this->select(__METHOD__); + while($row = $db->fetchObject($res)) { + $source = Title::makeTitle($row->page_namespace, $row->page_title); + $a = array( + 'type' => $row->pr_type, + 'level' => $row->pr_level, + 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ), + 'source' => $source->getPrefixedText() + ); + $prottitles[$row->tl_namespace][$row->tl_title][] = $a; + } + $db->freeResult($res); + } + + if (count($images) != 0) { + $this->resetQueryParams(); + $this->addTables(array('page_restrictions', 'page', 'imagelinks')); + $this->addFields(array('pr_type', 'pr_level', 'pr_expiry', + 'page_title', 'page_namespace', 'il_to')); + $this->addWhere('pr_page = page_id'); + $this->addWhere('pr_page = il_from'); + $this->addWhereFld('pr_cascade', 1); + $this->addWhereFld('il_to', $images); + + $res = $this->select(__METHOD__); + while($row = $db->fetchObject($res)) { + $source = Title::makeTitle($row->page_namespace, $row->page_title); + $a = array( + 'type' => $row->pr_type, + 'level' => $row->pr_level, + 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ), + 'source' => $source->getPrefixedText() + ); + $prottitles[NS_IMAGE][$row->il_to][] = $a; + } + $db->freeResult($res); + } + } + + // Run the talkid/subjectid query + if($fld_talkid || $fld_subjectid) + { + $talktitles = $subjecttitles = + $talkids = $subjectids = array(); + $everything = array_merge($titles, $missing); + foreach($everything as $t) + { + if(MWNamespace::isTalk($t->getNamespace())) + { + if($fld_subjectid) + $subjecttitles[] = $t->getSubjectPage(); + } + else if($fld_talkid) + $talktitles[] = $t->getTalkPage(); + } + if(!empty($talktitles) || !empty($subjecttitles)) + { + // Construct a custom WHERE clause that matches + // all titles in $talktitles and $subjecttitles + $lb = new LinkBatch(array_merge($talktitles, $subjecttitles)); + $this->resetQueryParams(); + $this->addTables('page'); + $this->addFields(array('page_title', 'page_namespace', 'page_id')); + $this->addWhere($lb->constructSet('page', $db)); + $res = $this->select(__METHOD__); + while($row = $db->fetchObject($res)) + { + if(MWNamespace::isTalk($row->page_namespace)) + $talkids[MWNamespace::getSubject($row->page_namespace)][$row->page_title] = $row->page_id; + else + $subjectids[MWNamespace::getTalk($row->page_namespace)][$row->page_title] = $row->page_id; + } + } } foreach ( $titles as $pageid => $title ) { @@ -137,18 +384,18 @@ class ApiQueryInfo extends ApiQueryBase { if ($pageIsNew[$pageid]) $pageInfo['new'] = ''; - if (!is_null($token)) { - // Currently all tokens are generated the same way, but it might change - if ($tok_edit) - $pageInfo['edittoken'] = $wgUser->editToken(); - if ($tok_delete) - $pageInfo['deletetoken'] = $wgUser->editToken(); - if ($tok_protect) - $pageInfo['protecttoken'] = $wgUser->editToken(); - if ($tok_move) - $pageInfo['movetoken'] = $wgUser->editToken(); + if (!is_null($params['token'])) { + $tokenFunctions = $this->getTokenFunctions(); + foreach($params['token'] as $t) + { + $val = call_user_func($tokenFunctions[$t], $pageid, $title); + if($val === false) + $this->setWarning("Action '$t' is not allowed for the current user"); + else + $pageInfo[$t . 'token'] = $val; + } } - + if($fld_protection) { if (isset($protections[$pageid])) { $pageInfo['protection'] = $protections[$pageid]; @@ -186,6 +433,10 @@ class ApiQueryInfo extends ApiQueryBase { } } } + if($fld_talkid && isset($talkids[$title->getNamespace()][$title->getDbKey()])) + $pageInfo['talkid'] = $talkids[$title->getNamespace()][$title->getDbKey()]; + if($fld_subjectid && isset($subjectids[$title->getNamespace()][$title->getDbKey()])) + $pageInfo['subjectid'] = $subjectids[$title->getNamespace()][$title->getDbKey()]; $result->addValue(array ( 'query', @@ -195,24 +446,34 @@ class ApiQueryInfo extends ApiQueryBase { // Get edit/protect tokens and protection data for missing titles if requested // Delete and move tokens are N/A for missing titles anyway - if($tok_edit || $tok_protect || $fld_protection) + if(!is_null($params['token']) || $fld_protection || $fld_talkid || $fld_subjectid) { $res = &$result->getData(); foreach($missing as $pageid => $title) { - if($tok_edit) - $res['query']['pages'][$pageid]['edittoken'] = $wgUser->editToken(); - if($tok_protect) - $res['query']['pages'][$pageid]['protecttoken'] = $wgUser->editToken(); + if(!is_null($params['token'])) + { + $tokenFunctions = $this->getTokenFunctions(); + foreach($params['token'] as $t) + { + $val = call_user_func($tokenFunctions[$t], $pageid, $title); + if($val !== false) + $res['query']['pages'][$pageid][$t . 'token'] = $val; + } + } if($fld_protection) { // Apparently the XML formatting code doesn't like array(null) // This is painful to fix, so we'll just work around it if(isset($prottitles[$title->getNamespace()][$title->getDBkey()])) - $res['query']['pages'][$pageid]['protection'][] = $prottitles[$title->getNamespace()][$title->getDBkey()]; + $res['query']['pages'][$pageid]['protection'] = $prottitles[$title->getNamespace()][$title->getDBkey()]; else $res['query']['pages'][$pageid]['protection'] = array(); $result->setIndexedTagName($res['query']['pages'][$pageid]['protection'], 'pr'); } + if($fld_talkid && isset($talkids[$title->getNamespace()][$title->getDbKey()])) + $res['query']['pages'][$pageid]['talkid'] = $talkids[$title->getNamespace()][$title->getDbKey()]; + if($fld_subjectid && isset($subjectids[$title->getNamespace()][$title->getDbKey()])) + $res['query']['pages'][$pageid]['subjectid'] = $subjectids[$title->getNamespace()][$title->getDbKey()]; } } } @@ -223,17 +484,15 @@ class ApiQueryInfo extends ApiQueryBase { ApiBase :: PARAM_DFLT => NULL, ApiBase :: PARAM_ISMULTI => true, ApiBase :: PARAM_TYPE => array ( - 'protection' + 'protection', + 'talkid', + 'subjectid' )), 'token' => array ( ApiBase :: PARAM_DFLT => NULL, ApiBase :: PARAM_ISMULTI => true, - ApiBase :: PARAM_TYPE => array ( - 'edit', - 'delete', - 'protect', - 'move', - )), + ApiBase :: PARAM_TYPE => array_keys($this->getTokenFunctions()) + ) ); } @@ -241,7 +500,9 @@ class ApiQueryInfo extends ApiQueryBase { return array ( 'prop' => array ( 'Which additional properties to get:', - ' "protection" - List the protection level of each page' + ' "protection" - List the protection level of each page', + ' "talkid" - The page ID of the talk page for each non-talk page', + ' "subjectid" - The page ID of the parent page for each talk page' ), 'token' => 'Request a token to perform a data-modifying action on a page', ); @@ -260,7 +521,6 @@ class ApiQueryInfo extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryInfo.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryInfo.php 37191 2008-07-06 18:43:06Z brion $'; } } - diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php index 04a930db..e7d84fc3 100644 --- a/includes/api/ApiQueryLangLinks.php +++ b/includes/api/ApiQueryLangLinks.php @@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) { /** * A query module to list all langlinks (links to correspanding foreign language pages). - * - * @addtogroup API + * + * @ingroup API */ class ApiQueryLangLinks extends ApiQueryBase { @@ -40,6 +40,10 @@ class ApiQueryLangLinks extends ApiQueryBase { } public function execute() { + if ( $this->getPageSet()->getGoodTitleCount() == 0 ) + return; + + $params = $this->extractRequestParams(); $this->addFields(array ( 'll_from', 'll_lang', @@ -48,14 +52,36 @@ class ApiQueryLangLinks extends ApiQueryBase { $this->addTables('langlinks'); $this->addWhereFld('ll_from', array_keys($this->getPageSet()->getGoodTitles())); - $this->addOption('ORDER BY', "ll_from, ll_lang"); + if(!is_null($params['continue'])) { + $cont = explode('|', $params['continue']); + if(count($cont) != 2) + $this->dieUsage("Invalid continue param. You should pass the " . + "original value returned by the previous query", "_badcontinue"); + $llfrom = intval($cont[0]); + $lllang = $this->getDb()->strencode($cont[1]); + $this->addWhere("ll_from > $llfrom OR ". + "(ll_from = $llfrom AND ". + "ll_lang >= '$lllang')"); + } + # Don't order by ll_from if it's constant in the WHERE clause + if(count($this->getPageSet()->getGoodTitles()) == 1) + $this->addOption('ORDER BY', 'll_lang'); + else + $this->addOption('ORDER BY', 'll_from, ll_lang'); + $this->addOption('LIMIT', $params['limit'] + 1); $res = $this->select(__METHOD__); $data = array(); - $lastId = 0; // database has no ID 0 + $lastId = 0; // database has no ID 0 + $count = 0; $db = $this->getDB(); while ($row = $db->fetchObject($res)) { - + if (++$count > $params['limit']) { + // We've reached the one extra which shows that + // there are additional pages to be had. Stop here... + $this->setContinueEnumParameter('continue', "{$row->ll_from}|{$row->ll_lang}"); + break; + } if ($lastId != $row->ll_from) { if($lastId != 0) { $this->addPageSubItems($lastId, $data); @@ -64,7 +90,7 @@ class ApiQueryLangLinks extends ApiQueryBase { $lastId = $row->ll_from; } - $entry = array('lang'=>$row->ll_lang); + $entry = array('lang' => $row->ll_lang); ApiResult :: setContent($entry, $row->ll_title); $data[] = $entry; } @@ -76,6 +102,26 @@ class ApiQueryLangLinks extends ApiQueryBase { $db->freeResult($res); } + public function getAllowedParams() { + return 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 + ), + 'continue' => null, + ); + } + + public function getParamDescription () { + return array( + 'limit' => 'How many langlinks to return', + 'continue' => 'When more results are available, use this to continue', + ); + } + public function getDescription() { return 'Returns all interlanguage links from the given page(s)'; } @@ -88,7 +134,6 @@ class ApiQueryLangLinks extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryLangLinks.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryLangLinks.php 37534 2008-07-10 21:08:37Z brion $'; } } - diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php index d77e627a..546a599d 100644 --- a/includes/api/ApiQueryLinks.php +++ b/includes/api/ApiQueryLinks.php @@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) { /** * A query module to list all wiki links on a given set of pages. - * - * @addtogroup API + * + * @ingroup API */ class ApiQueryLinks extends ApiQueryGeneratorBase { @@ -41,7 +41,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { private $table, $prefix, $description; public function __construct($query, $moduleName) { - + switch ($moduleName) { case self::LINKS : $this->table = 'pagelinks'; @@ -84,16 +84,54 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { $this->addTables($this->table); $this->addWhereFld($this->prefix . '_from', array_keys($this->getPageSet()->getGoodTitles())); $this->addWhereFld($this->prefix . '_namespace', $params['namespace']); - $this->addOption('ORDER BY', str_replace('pl_', $this->prefix . '_', 'pl_from, pl_namespace, pl_title')); + + if(!is_null($params['continue'])) { + $cont = explode('|', $params['continue']); + if(count($cont) != 3) + $this->dieUsage("Invalid continue param. You should pass the " . + "original value returned by the previous query", "_badcontinue"); + $plfrom = intval($cont[0]); + $plns = intval($cont[1]); + $pltitle = $this->getDb()->strencode($this->titleToKey($cont[2])); + $this->addWhere("{$this->prefix}_from > $plfrom OR ". + "({$this->prefix}_from = $plfrom AND ". + "({$this->prefix}_namespace > $plns OR ". + "({$this->prefix}_namespace = $plns AND ". + "{$this->prefix}_title >= '$pltitle')))"); + } + + # Here's some MySQL craziness going on: if you use WHERE foo='bar' + # and later ORDER BY foo MySQL doesn't notice the ORDER BY is pointless + # but instead goes and filesorts, because the index for foo was used + # already. To work around this, we drop constant fields in the WHERE + # clause from the ORDER BY clause + $order = array(); + if(count($this->getPageSet()->getGoodTitles()) != 1) + $order[] = "{$this->prefix}_from"; + if(count($params['namespace']) != 1) + $order[] = "{$this->prefix}_namespace"; + $order[] = "{$this->prefix}_title"; + $this->addOption('ORDER BY', implode(", ", $order)); + $this->addOption('USE INDEX', "{$this->prefix}_from"); + $this->addOption('LIMIT', $params['limit'] + 1); $db = $this->getDB(); $res = $this->select(__METHOD__); if (is_null($resultPageSet)) { - + $data = array(); - $lastId = 0; // database has no ID 0 + $lastId = 0; // database has no ID 0 + $count = 0; while ($row = $db->fetchObject($res)) { + if(++$count > $params['limit']) { + // We've reached the one extra which shows that + // there are additional pages to be had. Stop here... + $this->setContinueEnumParameter('continue', + "{$row->pl_from}|{$row->pl_namespace}|" . + $this->keyToTitle($row->pl_title)); + break; + } if ($lastId != $row->pl_from) { if($lastId != 0) { $this->addPageSubItems($lastId, $data); @@ -114,7 +152,16 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { } else { $titles = array(); + $count = 0; while ($row = $db->fetchObject($res)) { + if(++$count > $params['limit']) { + // We've reached the one extra which shows that + // there are additional pages to be had. Stop here... + $this->setContinueEnumParameter('continue', + "{$row->pl_from}|{$row->pl_namespace}|" . + $this->keyToTitle($row->pl_title)); + break; + } $titles[] = Title :: makeTitle($row->pl_namespace, $row->pl_title); } $resultPageSet->populateFromTitles($titles); @@ -129,15 +176,25 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { 'namespace' => array( ApiBase :: PARAM_TYPE => 'namespace', ApiBase :: PARAM_ISMULTI => true - ) + ), + '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 + ), + 'continue' => null, ); } public function getParamDescription() { return array( - 'namespace' => "Show {$this->description}s in this namespace(s) only" - ); + 'namespace' => "Show {$this->description}s in this namespace(s) only", + 'limit' => "How many {$this->description}s to return", + 'continue' => 'When more results are available, use this to continue', + ); } public function getDescription() { @@ -156,7 +213,6 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryLinks.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryLinks.php 37909 2008-07-22 13:26:15Z catrope $'; } } - diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php index e25e5275..47a526bb 100644 --- a/includes/api/ApiQueryLogEvents.php +++ b/includes/api/ApiQueryLogEvents.php @@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) { /** * Query action to List the log events, with optional filtering by various parameters. - * - * @addtogroup API + * + * @ingroup API */ class ApiQueryLogEvents extends ApiQueryBase { @@ -40,7 +40,7 @@ class ApiQueryLogEvents extends ApiQueryBase { } public function execute() { - $params = $this->extractRequestParams(); + $params = $this->extractRequestParams(); $db = $this->getDB(); $prop = $params['prop']; @@ -54,19 +54,26 @@ class ApiQueryLogEvents extends ApiQueryBase { list($tbl_logging, $tbl_page, $tbl_user) = $db->tableNamesN('logging', 'page', 'user'); - $this->addOption('STRAIGHT_JOIN'); - $this->addTables("$tbl_logging LEFT OUTER JOIN $tbl_page ON " . - "log_namespace=page_namespace AND log_title=page_title " . - "INNER JOIN $tbl_user ON user_id=log_user"); + $hideLogs = LogEventsList::getExcludeClause($db); + if($hideLogs !== false) + $this->addWhere($hideLogs); + + // Order is significant here + $this->addTables(array('user', 'page', 'logging')); + $this->addJoinConds(array( + 'page' => array('LEFT JOIN', + array( 'log_namespace=page_namespace', + 'log_title=page_title')))); + $this->addWhere('user_id=log_user'); + $this->addOption('USE INDEX', array('logging' => 'times')); // default, may change $this->addFields(array ( 'log_type', 'log_action', 'log_timestamp', )); - - // FIXME: Fake out log_id for now until the column is live on Wikimedia - // $this->addFieldsIf('log_id', $this->fld_ids); + + $this->addFieldsIf('log_id', $this->fld_ids); $this->addFieldsIf('page_id', $this->fld_ids); $this->addFieldsIf('log_user', $this->fld_user); $this->addFieldsIf('user_name', $this->fld_user); @@ -74,10 +81,14 @@ class ApiQueryLogEvents extends ApiQueryBase { $this->addFieldsIf('log_title', $this->fld_title); $this->addFieldsIf('log_comment', $this->fld_comment); $this->addFieldsIf('log_params', $this->fld_details); - $this->addWhereFld('log_deleted', 0); - $this->addWhereFld('log_type', $params['type']); + + if( !is_null($params['type']) ) { + $this->addWhereFld('log_type', $params['type']); + $this->addOption('USE INDEX', array('logging' => array('type_time'))); + } + $this->addWhereRange('log_timestamp', $params['dir'], $params['start'], $params['end']); $limit = $params['limit']; @@ -91,6 +102,7 @@ class ApiQueryLogEvents extends ApiQueryBase { if (!$userid) $this->dieUsage("User name $user not found", 'param_user'); $this->addWhereFld('log_user', $userid); + $this->addOption('USE INDEX', array('logging' => array('user_time','page_time'))); } $title = $params['title']; @@ -100,6 +112,7 @@ class ApiQueryLogEvents extends ApiQueryBase { $this->dieUsage("Bad title value '$title'", 'param_title'); $this->addWhereFld('log_namespace', $titleObj->getNamespace()); $this->addWhereFld('log_title', $titleObj->getDBkey()); + $this->addOption('USE INDEX', array('logging' => array('user_time','page_time'))); } $data = array (); @@ -126,26 +139,24 @@ class ApiQueryLogEvents extends ApiQueryBase { $vals = array(); if ($this->fld_ids) { - // FIXME: Fake out log_id for now until the column is live on Wikimedia - // $vals['logid'] = intval($row->log_id); - $vals['logid'] = 0; + $vals['logid'] = intval($row->log_id); $vals['pageid'] = intval($row->page_id); } - + if ($this->fld_title) { $title = Title :: makeTitle($row->log_namespace, $row->log_title); ApiQueryBase :: addTitleInfo($vals, $title); } - + if ($this->fld_type) { $vals['type'] = $row->log_type; $vals['action'] = $row->log_action; } - + if ($this->fld_details && $row->log_params !== '') { $params = explode("\n", $row->log_params); switch ($row->log_type) { - case 'move': + case 'move': if (isset ($params[0])) { $title = Title :: newFromText($params[0]); if ($title) { @@ -175,7 +186,7 @@ class ApiQueryLogEvents extends ApiQueryBase { $params = null; break; } - + if (isset($params)) { $this->getResult()->setIndexedTagName($params, 'param'); $vals = array_merge($vals, $params); @@ -193,7 +204,7 @@ class ApiQueryLogEvents extends ApiQueryBase { if ($this->fld_comment && !empty ($row->log_comment)) { $vals['comment'] = $row->log_comment; } - + return $vals; } @@ -215,7 +226,6 @@ class ApiQueryLogEvents extends ApiQueryBase { ) ), 'type' => array ( - ApiBase :: PARAM_ISMULTI => true, ApiBase :: PARAM_TYPE => $wgLogTypes ), 'start' => array ( @@ -267,7 +277,6 @@ class ApiQueryLogEvents extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryLogEvents.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryLogEvents.php 35098 2008-05-20 17:13:28Z ialex $'; } } - diff --git a/includes/api/ApiQueryRandom.php b/includes/api/ApiQueryRandom.php index b8282098..046157a6 100644 --- a/includes/api/ApiQueryRandom.php +++ b/includes/api/ApiQueryRandom.php @@ -30,24 +30,24 @@ if (!defined('MEDIAWIKI')) { /** * Query module to get list of random pages - * - * @addtogroup API + * + * @ingroup API */ - + class ApiQueryRandom extends ApiQueryGeneratorBase { public function __construct($query, $moduleName) { parent :: __construct($query, $moduleName, 'rn'); } - + public function execute() { $this->run(); } - + public function executeGenerator($resultPageSet) { $this->run($resultPageSet); } - + protected function prepareQuery($randstr, $limit, $namespace, &$resultPageSet) { $this->resetQueryParams(); $this->addTables('page'); @@ -104,7 +104,7 @@ if (!defined('MEDIAWIKI')) { if(is_null($resultPageSet)) { $result->setIndexedTagName($data, 'page'); $result->addValue('query', $this->getModuleName(), $data); - } + } } private function extractRowInfo($row) { @@ -115,7 +115,7 @@ if (!defined('MEDIAWIKI')) { $vals['id'] = $row->page_id; return $vals; } - + public function getAllowedParams() { return array ( 'namespace' => array( diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index 44093854..2b8c6a92 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -31,8 +31,8 @@ if (!defined('MEDIAWIKI')) { /** * A query action to enumerate the recent changes that were done to the wiki. * Various filters are supported. - * - * @addtogroup API + * + * @ingroup API */ class ApiQueryRecentChanges extends ApiQueryBase { @@ -43,26 +43,48 @@ 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; - + /** * Generates and outputs the result of this query based upon the provided parameters. */ public function execute() { /* Initialize vars */ - $limit = $prop = $namespace = $show = $type = $dir = $start = $end = null; - + $limit = $prop = $namespace = $titles = $show = $type = $dir = $start = $end = null; + /* Get the parameters of the request. */ extract($this->extractRequestParams()); /* Build our basic query. Namely, something along the lines of: - * SELECT * from recentchanges WHERE rc_timestamp > $start - * AND rc_timestamp < $end AND rc_namespace = $namespace + * SELECT * FROM recentchanges WHERE rc_timestamp > $start + * AND rc_timestamp < $end AND rc_namespace = $namespace * AND rc_deleted = '0' */ + $db = $this->getDB(); $this->addTables('recentchanges'); + $this->addOption('USE INDEX', array('recentchanges' => 'rc_timestamp')); $this->addWhereRange('rc_timestamp', $dir, $start, $end); $this->addWhereFld('rc_namespace', $namespace); $this->addWhereFld('rc_deleted', 0); + if(!empty($titles)) + { + $lb = new LinkBatch; + foreach($titles as $t) + { + $obj = Title::newFromText($t); + $lb->addObj($obj); + if($obj->getNamespace() < 0) + { + // LinkBatch refuses these, but we need them anyway + if(!array_key_exists($obj->getNamespace(), $lb->data)) + $lb->data[$obj->getNamespace()] = array(); + $lb->data[$obj->getNamespace()][$obj->getDbKey()] = 1; + } + } + $where = $lb->constructSet('rc', $this->getDb()); + if($where != '') + $this->addWhere($where); + } + if(!is_null($type)) $this->addWhereFld('rc_type', $this->parseRCType($type)); @@ -70,12 +92,19 @@ class ApiQueryRecentChanges extends ApiQueryBase { $show = array_flip($show); /* Check for conflicting parameters. */ - if ((isset ($show['minor']) && isset ($show['!minor'])) - || (isset ($show['bot']) && isset ($show['!bot'])) - || (isset ($show['anon']) && isset ($show['!anon']))) { - + if ((isset ($show['minor']) && isset ($show['!minor'])) + || (isset ($show['bot']) && isset ($show['!bot'])) + || (isset ($show['anon']) && isset ($show['!anon'])) + || (isset ($show['redirect']) && isset ($show['!redirect'])) + || (isset ($show['patrolled']) && isset ($show['!patrolled']))) { + $this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show'); } + + // Check permissions + global $wgUser; + if((isset($show['patrolled']) || isset($show['!patrolled'])) && !$wgUser->isAllowed('patrol')) + $this->dieUsage("You need the patrol right to request the patrolled flag", 'permissiondenied'); /* Add additional conditions to query depending upon parameters. */ $this->addWhereIf('rc_minor = 0', isset ($show['!minor'])); @@ -84,6 +113,11 @@ class ApiQueryRecentChanges extends ApiQueryBase { $this->addWhereIf('rc_bot != 0', isset ($show['bot'])); $this->addWhereIf('rc_user = 0', isset ($show['anon'])); $this->addWhereIf('rc_user != 0', isset ($show['!anon'])); + $this->addWhereIf('rc_patrolled = 0', isset($show['!patrolled'])); + $this->addWhereIf('rc_patrolled != 0', isset($show['patrolled'])); + $this->addWhereIf('page_is_redirect = 1', isset ($show['redirect'])); + // Don't throw log entries out the window here + $this->addWhereIf('page_is_redirect = 0 OR page_is_redirect IS NULL', isset ($show['!redirect'])); } /* Add the fields we're concerned with to out query. */ @@ -108,13 +142,19 @@ class ApiQueryRecentChanges extends ApiQueryBase { $this->fld_title = isset ($prop['title']); $this->fld_ids = isset ($prop['ids']); $this->fld_sizes = isset ($prop['sizes']); + $this->fld_redirect = isset($prop['redirect']); + $this->fld_patrolled = isset($prop['patrolled']); + + global $wgUser; + if($this->fld_patrolled && !$wgUser->isAllowed('patrol')) + $this->dieUsage("You need the patrol right to request the patrolled flag", 'permissiondenied'); /* Add fields to our query if they are specified as a needed parameter. */ - $this->addFieldsIf('rc_id', $this->fld_ids); - $this->addFieldsIf('rc_cur_id', $this->fld_ids); - $this->addFieldsIf('rc_this_oldid', $this->fld_ids); - $this->addFieldsIf('rc_last_oldid', $this->fld_ids); - $this->addFieldsIf('rc_comment', $this->fld_comment); + $this->addFieldsIf('rc_id', $this->fld_ids); + $this->addFieldsIf('rc_cur_id', $this->fld_ids); + $this->addFieldsIf('rc_this_oldid', $this->fld_ids); + $this->addFieldsIf('rc_last_oldid', $this->fld_ids); + $this->addFieldsIf('rc_comment', $this->fld_comment); $this->addFieldsIf('rc_user', $this->fld_user); $this->addFieldsIf('rc_user_text', $this->fld_user); $this->addFieldsIf('rc_minor', $this->fld_flags); @@ -122,15 +162,18 @@ class ApiQueryRecentChanges extends ApiQueryBase { $this->addFieldsIf('rc_new', $this->fld_flags); $this->addFieldsIf('rc_old_len', $this->fld_sizes); $this->addFieldsIf('rc_new_len', $this->fld_sizes); + $this->addFieldsIf('rc_patrolled', $this->fld_patrolled); + if($this->fld_redirect || isset($show['redirect']) || isset($show['!redirect'])) + { + $this->addTables('page'); + $this->addJoinConds(array('page' => array('LEFT JOIN', array('rc_namespace=page_namespace', 'rc_title=page_title')))); + $this->addFields('page_is_redirect'); + } } - - /* Specify the limit for our query. It's $limit+1 because we (possibly) need to + /* Specify the limit for our query. It's $limit+1 because we (possibly) need to * generate a "continue" parameter, to allow paging. */ $this->addOption('LIMIT', $limit +1); - /* Specify the index to use in the query as rc_timestamp, instead of rc_revid (default). */ - $this->addOption('USE INDEX', 'rc_timestamp'); - $data = array (); $count = 0; @@ -148,7 +191,7 @@ class ApiQueryRecentChanges extends ApiQueryBase { /* Extract the data from a single row. */ $vals = $this->extractRowInfo($row); - + /* Add that row's data to our final output. */ if($vals) $data[] = $vals; @@ -240,9 +283,17 @@ class ApiQueryRecentChanges extends ApiQueryBase { $vals['comment'] = $row->rc_comment; } + if ($this->fld_redirect) + if($row->page_is_redirect) + $vals['redirect'] = ''; + + /* Add the patrolled flag */ + if ($this->fld_patrolled && $row->rc_patrolled == 1) + $vals['patrolled'] = ''; + return $vals; } - + private function parseRCType($type) { if(is_array($type)) @@ -258,7 +309,7 @@ class ApiQueryRecentChanges extends ApiQueryBase { case 'new': return RC_NEW; case 'log': return RC_LOG; } - } + } public function getAllowedParams() { return array ( @@ -279,6 +330,9 @@ class ApiQueryRecentChanges extends ApiQueryBase { ApiBase :: PARAM_ISMULTI => true, ApiBase :: PARAM_TYPE => 'namespace' ), + 'titles' => array( + ApiBase :: PARAM_ISMULTI => true + ), 'prop' => array ( ApiBase :: PARAM_ISMULTI => true, ApiBase :: PARAM_DFLT => 'title|timestamp|ids', @@ -289,7 +343,9 @@ class ApiQueryRecentChanges extends ApiQueryBase { 'timestamp', 'title', 'ids', - 'sizes' + 'sizes', + 'redirect', + 'patrolled' ) ), 'show' => array ( @@ -300,7 +356,11 @@ class ApiQueryRecentChanges extends ApiQueryBase { 'bot', '!bot', 'anon', - '!anon' + '!anon', + 'redirect', + '!redirect', + 'patrolled', + '!patrolled' ) ), 'limit' => array ( @@ -314,7 +374,7 @@ class ApiQueryRecentChanges extends ApiQueryBase { ApiBase :: PARAM_ISMULTI => true, ApiBase :: PARAM_TYPE => array ( 'edit', - 'new', + 'new', 'log' ) ) @@ -327,13 +387,14 @@ class ApiQueryRecentChanges extends ApiQueryBase { 'end' => 'The timestamp to end enumerating.', 'dir' => 'In which direction to enumerate.', 'namespace' => 'Filter log entries to only this namespace(s)', + 'titles' => 'Filter log entries to only these page titles', 'prop' => 'Include additional pieces of information', 'show' => array ( 'Show only items that meet this criteria.', 'For example, to see only minor edits done by logged-in users, set show=minor|!anon' ), 'type' => 'Which types of changes to show.', - 'limit' => 'How many total pages to return.' + 'limit' => 'How many total changes to return.' ); } @@ -348,7 +409,6 @@ class ApiQueryRecentChanges extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 37909 2008-07-22 13:26:15Z catrope $'; } } - diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index e22d3b30..1fd2d7c6 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -30,10 +30,10 @@ if (!defined('MEDIAWIKI')) { /** * A query action to enumerate revisions of a given page, or show top revisions of multiple pages. - * Various pieces of information may be shown - flags, comments, and the actual wiki markup of the rev. - * In the enumeration mode, ranges of revisions may be requested and filtered. - * - * @addtogroup API + * Various pieces of information may be shown - flags, comments, and the actual wiki markup of the rev. + * In the enumeration mode, ranges of revisions may be requested and filtered. + * + * @ingroup API */ class ApiQueryRevisions extends ApiQueryBase { @@ -44,16 +44,45 @@ 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; + protected function getTokenFunctions() { + // tokenname => function + // function prototype is func($pageid, $title, $rev) + // should return token or false + + // Don't call the hooks twice + if(isset($this->tokenFunctions)) + return $this->tokenFunctions; + + // If we're in JSON callback mode, no tokens can be obtained + if(!is_null($this->getMain()->getRequest()->getVal('callback'))) + return array(); + + $this->tokenFunctions = array( + 'rollback' => array( 'ApiQueryRevisions','getRollbackToken' ) + ); + wfRunHooks('APIQueryRevisionsTokens', array(&$this->tokenFunctions)); + return $this->tokenFunctions; + } + + public static function getRollbackToken($pageid, $title, $rev) + { + global $wgUser; + if(!$wgUser->isAllowed('rollback')) + return false; + return $wgUser->editToken(array($title->getPrefixedText(), + $rev->getUserText())); + } + public function execute() { - $limit = $startid = $endid = $start = $end = $dir = $prop = $user = $excludeuser = $token = null; + $limit = $startid = $endid = $start = $end = $dir = $prop = $user = $excludeuser = $expandtemplates = $section = $token = null; extract($this->extractRequestParams(false)); // If any of those parameters are used, work in 'enumeration' mode. // Enum mode can only be used when exactly one page is provided. - // Enumerating revisions on multiple pages make it extremely - // difficult to manage continuations and require additional SQL indexes + // Enumerating revisions on multiple pages make it extremely + // difficult to manage continuations and require additional SQL indexes $enumRevMode = (!is_null($user) || !is_null($excludeuser) || !is_null($limit) || !is_null($startid) || !is_null($endid) || $dir === 'newer' || !is_null($start) || !is_null($end)); - + $pageSet = $this->getPageSet(); $pageCount = $pageSet->getGoodTitleCount(); @@ -70,35 +99,26 @@ class ApiQueryRevisions extends ApiQueryBase { $this->dieUsage('titles, pageids or a generator was used to supply multiple pages, but the limit, startid, endid, dirNewer, user, excludeuser, start and end parameters may only be used on a single page.', 'multpages'); $this->addTables('revision'); - $this->addWhere('rev_deleted=0'); + $this->addFields( Revision::selectFields() ); $prop = array_flip($prop); - // These field are needed regardless of the client requesting them - $this->addFields('rev_id'); - $this->addFields('rev_page'); - // Optional fields $this->fld_ids = isset ($prop['ids']); // $this->addFieldsIf('rev_text_id', $this->fld_ids); // should this be exposed? - $this->fld_flags = $this->addFieldsIf('rev_minor_edit', isset ($prop['flags'])); - $this->fld_timestamp = $this->addFieldsIf('rev_timestamp', isset ($prop['timestamp'])); - $this->fld_comment = $this->addFieldsIf('rev_comment', isset ($prop['comment'])); - $this->fld_size = $this->addFieldsIf('rev_len', isset ($prop['size'])); - $this->tok_rollback = false; // Prevent PHP undefined property notice - if(!is_null($token)) - { - $this->tok_rollback = $this->getTokenFlag($token, 'rollback'); + $this->fld_flags = isset ($prop['flags']); + $this->fld_timestamp = isset ($prop['timestamp']); + $this->fld_comment = isset ($prop['comment']); + $this->fld_size = isset ($prop['size']); + $this->fld_user = isset ($prop['user']); + $this->token = $token; + + if ( !is_null($this->token) || ( $this->fld_content && $this->expandTemplates ) || $pageCount > 0) { + $this->addTables( 'page' ); + $this->addWhere('page_id=rev_page'); + $this->addFields( Revision::selectPageFields() ); } - if (isset ($prop['user'])) { - $this->addFields('rev_user'); - $this->addFields('rev_user_text'); - $this->fld_user = true; - } - else if($this->tok_rollback) - $this->addFields('rev_user_text'); - if (isset ($prop['content'])) { // For each page we will request, the user must have read rights for that page @@ -112,12 +132,15 @@ class ApiQueryRevisions extends ApiQueryBase { $this->addTables('text'); $this->addWhere('rev_text_id=old_id'); $this->addFields('old_id'); - $this->addFields('old_text'); - $this->addFields('old_flags'); + $this->addFields( Revision::selectTextFields() ); $this->fld_content = true; - + $this->expandTemplates = $expandtemplates; + if(isset($section)) + $this->section = $section; + else + $this->section = false; } $userMax = ( $this->fld_content ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1 ); @@ -137,13 +160,13 @@ class ApiQueryRevisions extends ApiQueryBase { $this->dieUsage('end and endid cannot be used together', 'badparams'); if(!is_null($user) && !is_null( $excludeuser)) - $this->dieUsage('user and excludeuser cannot be used together', 'badparams'); - + $this->dieUsage('user and excludeuser cannot be used together', 'badparams'); + // This code makes an assumption that sorting by rev_id and rev_timestamp produces // the same result. This way users may request revisions starting at a given time, // but to page through results use the rev_id returned after each page. - // Switching to rev_id removes the potential problem of having more than - // one row with the same timestamp for the same page. + // Switching to rev_id removes the potential problem of having more than + // one row with the same timestamp for the same page. // The order needs to be the same as start parameter to avoid SQL filesort. if (is_null($startid) && is_null($endid)) @@ -158,7 +181,7 @@ class ApiQueryRevisions extends ApiQueryBase { // There is only one ID, use it $this->addWhereFld('rev_page', current(array_keys($pageSet->getGoodTitles()))); - + if(!is_null($user)) { $this->addWhereFld('rev_user_text', $user); } elseif (!is_null( $excludeuser)) { @@ -177,7 +200,6 @@ class ApiQueryRevisions extends ApiQueryBase { elseif ($pageCount > 0) { // When working in multi-page non-enumeration mode, // limit to the latest revision only - $this->addTables('page'); $this->addWhere('page_id=rev_page'); $this->addWhere('page_latest=rev_id'); $this->validateLimit('page_count', $pageCount, 1, $userMax, $botMax); @@ -207,17 +229,18 @@ class ApiQueryRevisions extends ApiQueryBase { break; } + $revision = new Revision( $row ); $this->getResult()->addValue( array ( 'query', 'pages', - intval($row->rev_page), + $revision->getPage(), 'revisions'), null, - $this->extractRowInfo($row)); + $this->extractRowInfo( $revision )); } $db->freeResult($res); - + // Ensure that all revisions are shown as '' elements $result = $this->getResult(); if ($result->getIsRawMode()) { @@ -230,53 +253,70 @@ class ApiQueryRevisions extends ApiQueryBase { } } - private function extractRowInfo($row) { + private function extractRowInfo( $revision ) { $vals = array (); if ($this->fld_ids) { - $vals['revid'] = intval($row->rev_id); + $vals['revid'] = $revision->getId(); // $vals['oldid'] = intval($row->rev_text_id); // todo: should this be exposed? } - - if ($this->fld_flags && $row->rev_minor_edit) + + if ($this->fld_flags && $revision->isMinor()) $vals['minor'] = ''; if ($this->fld_user) { - $vals['user'] = $row->rev_user_text; - if (!$row->rev_user) + $vals['user'] = $revision->getUserText(); + if (!$revision->getUser()) $vals['anon'] = ''; } if ($this->fld_timestamp) { - $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rev_timestamp); + $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $revision->getTimestamp()); } - - if ($this->fld_size && !is_null($row->rev_len)) { - $vals['size'] = intval($row->rev_len); + + if ($this->fld_size && !is_null($revision->getSize())) { + $vals['size'] = $revision->getSize(); } - if ($this->fld_comment && !empty ($row->rev_comment)) { - $vals['comment'] = $row->rev_comment; + if ($this->fld_comment) { + $comment = $revision->getComment(); + if (!empty($comment)) + $vals['comment'] = $comment; } - - if($this->tok_rollback || ($this->fld_content && $this->expandTemplates)) - $title = Title::newFromID($row->rev_page); - - if($this->tok_rollback) { - global $wgUser; - $vals['rollbacktoken'] = $wgUser->editToken(array($title->getPrefixedText(), $row->rev_user_text)); + + if(!is_null($this->token) || ($this->fld_content && $this->expandTemplates)) + $title = $revision->getTitle(); + + if(!is_null($this->token)) + { + $tokenFunctions = $this->getTokenFunctions(); + foreach($this->token as $t) + { + $val = call_user_func($tokenFunctions[$t], $title->getArticleID(), $title, $revision); + if($val === false) + $this->setWarning("Action '$t' is not allowed for the current user"); + else + $vals[$t . 'token'] = $val; + } } - - + if ($this->fld_content) { - $text = Revision :: getRevisionText($row); + global $wgParser; + $text = $revision->getText(); + # Expand templates after getting section content because + # template-added sections don't count and Parser::preprocess() + # will have less input + if ($this->section !== false) { + $text = $wgParser->getSection( $text, $this->section, false); + if($text === false) + $this->dieUsage("There is no section {$this->section} in r".$revision->getId(), 'nosuchsection'); + } if ($this->expandTemplates) { - global $wgParser; $text = $wgParser->preprocess( $text, $title, new ParserOptions() ); } ApiResult :: setContent($vals, $text); - } + } return $vals; } @@ -326,12 +366,13 @@ class ApiQueryRevisions extends ApiQueryBase { 'excludeuser' => array( ApiBase :: PARAM_TYPE => 'user' ), - + 'expandtemplates' => false, + 'section' => array( + ApiBase :: PARAM_TYPE => 'integer' + ), 'token' => array( - ApiBase :: PARAM_TYPE => array( - 'rollback' - ), + ApiBase :: PARAM_TYPE => array_keys($this->getTokenFunctions()), ApiBase :: PARAM_ISMULTI => true ), ); @@ -349,6 +390,7 @@ class ApiQueryRevisions extends ApiQueryBase { 'user' => 'only include revisions made by user', 'excludeuser' => 'exclude revisions made by user', 'expandtemplates' => 'expand templates in revision content', + 'section' => 'only retrieve the content of this section', 'token' => 'Which tokens to obtain for each revision', ); } @@ -382,7 +424,6 @@ class ApiQueryRevisions extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryRevisions.php 31259 2008-02-25 14:14:55Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryRevisions.php 37300 2008-07-08 08:42:27Z btongminh $'; } } - diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php index b15f36ce..84a2ec63 100644 --- a/includes/api/ApiQuerySearch.php +++ b/includes/api/ApiQuerySearch.php @@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) { /** * Query module to perform full text search within wiki titles and content - * - * @addtogroup API + * + * @ingroup API */ class ApiQuerySearch extends ApiQueryGeneratorBase { @@ -52,7 +52,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $params = $this->extractRequestParams(); $limit = $params['limit']; - $query = $params['search']; + $query = $params['search']; if (is_null($query) || empty($query)) $this->dieUsage("empty search string is not allowed", 'param-search'); @@ -60,11 +60,14 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $search->setLimitOffset( $limit+1, $params['offset'] ); $search->setNamespaces( $params['namespace'] ); $search->showRedirects = $params['redirects']; - + if ($params['what'] == 'text') $matches = $search->searchText( $query ); else $matches = $search->searchTitle( $query ); + if (is_null($matches)) + $this->dieUsage("{$params['what']} search is disabled", + "search-{$params['what']}-disabled"); $data = array (); $count = 0; @@ -75,6 +78,9 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { break; } + // Silently skip broken titles + if ($result->isBrokenTitle()) continue; + $title = $result->getTitle(); if (is_null($resultPageSet)) { $data[] = array( @@ -100,7 +106,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { 'namespace' => array ( ApiBase :: PARAM_DFLT => 0, ApiBase :: PARAM_TYPE => 'namespace', - ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_ISMULTI => true, ), 'what' => array ( ApiBase :: PARAM_DFLT => 'title', @@ -145,7 +151,6 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQuerySearch.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiQuerySearch.php 35098 2008-05-20 17:13:28Z ialex $'; } } - diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php index 81af7997..1fd3b888 100644 --- a/includes/api/ApiQuerySiteinfo.php +++ b/includes/api/ApiQuerySiteinfo.php @@ -23,218 +23,278 @@ * http://www.gnu.org/copyleft/gpl.html */ -if (!defined('MEDIAWIKI')) { +if( !defined('MEDIAWIKI') ) { // Eclipse helper - will be ignored in production - require_once ('ApiQueryBase.php'); + require_once( 'ApiQueryBase.php' ); } /** * A query action to return meta information about the wiki site. - * - * @addtogroup API + * + * @ingroup API */ class ApiQuerySiteinfo extends ApiQueryBase { - public function __construct($query, $moduleName) { - parent :: __construct($query, $moduleName, 'si'); + public function __construct( $query, $moduleName ) { + parent :: __construct( $query, $moduleName, 'si' ); } public function execute() { - $params = $this->extractRequestParams(); - - foreach ($params['prop'] as $p) { - switch ($p) { - default : - ApiBase :: dieDebug(__METHOD__, "Unknown prop=$p"); - case 'general' : - $this->appendGeneralInfo($p); + foreach( $params['prop'] as $p ) + { + switch ( $p ) + { + case 'general': + $this->appendGeneralInfo( $p ); + break; + case 'namespaces': + $this->appendNamespaces( $p ); break; - case 'namespaces' : - $this->appendNamespaces($p); + case 'namespacealiases': + $this->appendNamespaceAliases( $p ); break; - case 'namespacealiases' : - $this->appendNamespaceAliases($p); + case 'specialpagealiases': + $this->appendSpecialPageAliases( $p ); break; - case 'interwikimap' : - $filteriw = isset($params['filteriw']) ? $params['filteriw'] : false; - $this->appendInterwikiMap($p, $filteriw); + case 'interwikimap': + $filteriw = isset( $params['filteriw'] ) ? $params['filteriw'] : false; + $this->appendInterwikiMap( $p, $filteriw ); break; - case 'dbrepllag' : - $this->appendDbReplLagInfo($p, $params['showalldb']); + case 'dbrepllag': + $this->appendDbReplLagInfo( $p, $params['showalldb'] ); break; - case 'statistics' : - $this->appendStatistics($p); + case 'statistics': + $this->appendStatistics( $p ); break; + case 'usergroups': + $this->appendUserGroups( $p ); + break; + default : + ApiBase :: dieDebug( __METHOD__, "Unknown prop=$p" ); } } } - protected function appendGeneralInfo($property) { - global $wgSitename, $wgVersion, $wgCapitalLinks, $wgRightsCode, $wgRightsText, $wgLanguageCode, $IP; - - $data = array (); + protected function appendGeneralInfo( $property ) { + global $wgSitename, $wgVersion, $wgCapitalLinks, $wgRightsCode, $wgRightsText, $wgContLang; + global $wgLanguageCode, $IP, $wgEnableWriteAPI, $wgLang, $wgLocaltimezone, $wgLocalTZoffset; + + $data = array(); $mainPage = Title :: newFromText(wfMsgForContent('mainpage')); - $data['mainpage'] = $mainPage->getText(); + $data['mainpage'] = $mainPage->getPrefixedText(); $data['base'] = $mainPage->getFullUrl(); $data['sitename'] = $wgSitename; $data['generator'] = "MediaWiki $wgVersion"; - $svn = SpecialVersion::getSvnRevision ( $IP ); - if ( $svn ) $data['rev'] = $svn; + $svn = SpecialVersion::getSvnRevision( $IP ); + if( $svn ) + $data['rev'] = $svn; $data['case'] = $wgCapitalLinks ? 'first-letter' : 'case-sensitive'; // 'case-insensitive' option is reserved for future - if (isset($wgRightsCode)) + + if( isset( $wgRightsCode ) ) $data['rightscode'] = $wgRightsCode; $data['rights'] = $wgRightsText; $data['lang'] = $wgLanguageCode; + if( $wgContLang->isRTL() ) + $data['rtl'] = ''; + $data['fallback8bitEncoding'] = $wgLang->fallback8bitEncoding(); - $this->getResult()->addValue('query', $property, $data); + if( wfReadOnly() ) + $data['readonly'] = ''; + if( $wgEnableWriteAPI ) + $data['writeapi'] = ''; + + $tz = $wgLocaltimezone; + $offset = $wgLocalTZoffset; + if( is_null( $tz ) ) { + $tz = 'UTC'; + $offset = 0; + } elseif( is_null( $offset ) ) { + $offset = 0; + } + $data['timezone'] = $tz; + $data['timeoffset'] = $offset; + + $this->getResult()->addValue( 'query', $property, $data ); } - - protected function appendNamespaces($property) { + + protected function appendNamespaces( $property ) { global $wgContLang; - - $data = array (); - foreach ($wgContLang->getFormattedNamespaces() as $ns => $title) { - $data[$ns] = array ( + $data = array(); + foreach( $wgContLang->getFormattedNamespaces() as $ns => $title ) + { + $data[$ns] = array( 'id' => $ns ); - ApiResult :: setContent($data[$ns], $title); + ApiResult :: setContent( $data[$ns], $title ); + if( MWNamespace::hasSubpages($ns) ) + $data[$ns]['subpages'] = ''; } - - $this->getResult()->setIndexedTagName($data, 'ns'); - $this->getResult()->addValue('query', $property, $data); + + $this->getResult()->setIndexedTagName( $data, 'ns' ); + $this->getResult()->addValue( 'query', $property, $data ); } - - protected function appendNamespaceAliases($property) { + + protected function appendNamespaceAliases( $property ) { global $wgNamespaceAliases; - - $data = array (); - foreach ($wgNamespaceAliases as $title => $ns) { - $item = array ( + $data = array(); + foreach( $wgNamespaceAliases as $title => $ns ) { + $item = array( 'id' => $ns ); - ApiResult :: setContent($item, strtr($title, '_', ' ')); + ApiResult :: setContent( $item, strtr( $title, '_', ' ' ) ); $data[] = $item; } - - $this->getResult()->setIndexedTagName($data, 'ns'); - $this->getResult()->addValue('query', $property, $data); + + $this->getResult()->setIndexedTagName( $data, 'ns' ); + $this->getResult()->addValue( 'query', $property, $data ); } - - protected function appendInterwikiMap($property, $filter) { - $this->resetQueryParams(); - $this->addTables('interwiki'); - $this->addFields(array('iw_prefix', 'iw_local', 'iw_url')); - - if($filter === 'local') { - $this->addWhere('iw_local = 1'); - } elseif($filter === '!local') { - $this->addWhere('iw_local = 0'); - } elseif($filter !== false) { - ApiBase :: dieDebug(__METHOD__, "Unknown filter=$filter"); + protected function appendSpecialPageAliases( $property ) { + global $wgLang; + $data = array(); + foreach( $wgLang->getSpecialPageAliases() as $specialpage => $aliases ) + { + $arr = array( 'realname' => $specialpage, 'aliases' => $aliases ); + $this->getResult()->setIndexedTagName( $arr['aliases'], 'alias' ); + $data[] = $arr; } + $this->getResult()->setIndexedTagName( $data, 'specialpage' ); + $this->getResult()->addValue( 'query', $property, $data ); + } + + protected function appendInterwikiMap( $property, $filter ) { + $this->resetQueryParams(); + $this->addTables( 'interwiki' ); + $this->addFields( array( 'iw_prefix', 'iw_local', 'iw_url' ) ); + + if( $filter === 'local' ) + $this->addWhere( 'iw_local = 1' ); + elseif( $filter === '!local' ) + $this->addWhere( 'iw_local = 0' ); + elseif( $filter !== false ) + ApiBase :: dieDebug( __METHOD__, "Unknown filter=$filter" ); + + $this->addOption( 'ORDER BY', 'iw_prefix' ); - $this->addOption('ORDER BY', 'iw_prefix'); - $db = $this->getDB(); - $res = $this->select(__METHOD__); + $res = $this->select( __METHOD__ ); $data = array(); - while($row = $db->fetchObject($res)) + $langNames = Language::getLanguageNames(); + while( $row = $db->fetchObject($res) ) { + $val = array(); $val['prefix'] = $row->iw_prefix; - if ($row->iw_local == '1') + if( $row->iw_local == '1' ) $val['local'] = ''; // $val['trans'] = intval($row->iw_trans); // should this be exposed? + if( isset( $langNames[$row->iw_prefix] ) ) + $val['language'] = $langNames[$row->iw_prefix]; $val['url'] = $row->iw_url; - + $data[] = $val; } - $db->freeResult($res); - - $this->getResult()->setIndexedTagName($data, 'iw'); - $this->getResult()->addValue('query', $property, $data); + $db->freeResult( $res ); + + $this->getResult()->setIndexedTagName( $data, 'iw' ); + $this->getResult()->addValue( 'query', $property, $data ); } - - protected function appendDbReplLagInfo($property, $includeAll) { - global $wgLoadBalancer, $wgShowHostnames; + protected function appendDbReplLagInfo( $property, $includeAll ) { + global $wgShowHostnames; $data = array(); - - if ($includeAll) { - if (!$wgShowHostnames) + if( $includeAll ) { + if ( !$wgShowHostnames ) $this->dieUsage('Cannot view all servers info unless $wgShowHostnames is true', 'includeAllDenied'); - - global $wgDBservers; - $lags = $wgLoadBalancer->getLagTimes(); + + $lb = wfGetLB(); + $lags = $lb->getLagTimes(); foreach( $lags as $i => $lag ) { - $data[] = array ( - 'host' => $wgDBservers[$i]['host'], - 'lag' => $lag); + $data[] = array( + 'host' => $lb->getServerName( $i ), + 'lag' => $lag + ); } } else { - list( $host, $lag ) = $wgLoadBalancer->getMaxLag(); - $data[] = array ( + list( $host, $lag ) = wfGetLB()->getMaxLag(); + $data[] = array( 'host' => $wgShowHostnames ? $host : '', - 'lag' => $lag); - } + 'lag' => $lag + ); + } $result = $this->getResult(); - $result->setIndexedTagName($data, 'db'); - $result->addValue('query', $property, $data); - } - - protected function appendStatistics($property) { - $data = array (); - $data['pages'] = intval(SiteStats::pages()); - $data['articles'] = intval(SiteStats::articles()); - $data['views'] = intval(SiteStats::views()); - $data['edits'] = intval(SiteStats::edits()); - $data['images'] = intval(SiteStats::images()); - $data['users'] = intval(SiteStats::users()); - $data['admins'] = intval(SiteStats::admins()); - $data['jobs'] = intval(SiteStats::jobs()); - $this->getResult()->addValue('query', $property, $data); - } - + $result->setIndexedTagName( $data, 'db' ); + $result->addValue( 'query', $property, $data ); + } + + protected function appendStatistics( $property ) { + $data = array(); + $data['pages'] = intval( SiteStats::pages() ); + $data['articles'] = intval( SiteStats::articles() ); + $data['views'] = intval( SiteStats::views() ); + $data['edits'] = intval( SiteStats::edits() ); + $data['images'] = intval( SiteStats::images() ); + $data['users'] = intval( SiteStats::users() ); + $data['admins'] = intval( SiteStats::admins() ); + $data['jobs'] = intval( SiteStats::jobs() ); + $this->getResult()->addValue( 'query', $property, $data ); + } + + protected function appendUserGroups( $property ) { + global $wgGroupPermissions; + $data = array(); + foreach( $wgGroupPermissions as $group => $permissions ) { + $arr = array( 'name' => $group, 'rights' => array_keys( $permissions, true ) ); + $this->getResult()->setIndexedTagName( $arr['rights'], 'permission' ); + $data[] = $arr; + } + + $this->getResult()->setIndexedTagName( $data, 'group' ); + $this->getResult()->addValue( 'query', $property, $data ); + } + public function getAllowedParams() { - return array ( - - 'prop' => array ( + return array( + 'prop' => array( ApiBase :: PARAM_DFLT => 'general', ApiBase :: PARAM_ISMULTI => true, - ApiBase :: PARAM_TYPE => array ( + ApiBase :: PARAM_TYPE => array( 'general', 'namespaces', 'namespacealiases', + 'specialpagealiases', 'interwikimap', 'dbrepllag', 'statistics', - )), - - 'filteriw' => array ( - ApiBase :: PARAM_TYPE => array ( + 'usergroups', + ) + ), + 'filteriw' => array( + ApiBase :: PARAM_TYPE => array( 'local', '!local', - )), - + ) + ), 'showalldb' => false, ); } public function getParamDescription() { - return array ( - 'prop' => array ( + return array( + 'prop' => array( 'Which sysinfo properties to get:', ' "general" - Overall system information', ' "namespaces" - List of registered namespaces (localized)', ' "namespacealiases" - List of registered namespace aliases', + ' "specialpagealiases" - List of special page aliases', ' "statistics" - Returns site statistics', ' "interwikimap" - Returns interwiki map (optionally filtered)', ' "dbrepllag" - Returns database server with the highest replication lag', + ' "usergroups" - Returns user groups and the associated permissions', ), 'filteriw' => 'Return only local or only nonlocal entries of the interwiki map', 'showalldb' => 'List all database servers, not just the one lagging the most', @@ -254,6 +314,6 @@ class ApiQuerySiteinfo extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 30484 2008-02-03 19:29:59Z btongminh $'; + return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 37034 2008-07-04 09:21:11Z vasilievvv $'; } } diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php index 57d51cdb..c477acdb 100644 --- a/includes/api/ApiQueryUserContributions.php +++ b/includes/api/ApiQueryUserContributions.php @@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) { /** * This query action adds a list of a specified user's contributions to the output. - * - * @addtogroup API + * + * @ingroup API */ class ApiQueryContributions extends ApiQueryBase { @@ -48,7 +48,7 @@ class ApiQueryContributions extends ApiQueryBase { // Parse some parameters $this->params = $this->extractRequestParams(); - $prop = array_flip($this->params['prop']); + $prop = array_flip($this->params['prop']); $this->fld_ids = isset($prop['ids']); $this->fld_title = isset($prop['title']); $this->fld_comment = isset($prop['comment']); @@ -59,12 +59,20 @@ class ApiQueryContributions extends ApiQueryBase { $this->selectNamedDB('contributions', DB_SLAVE, 'contributions'); $db = $this->getDB(); - // Prepare query - $this->usernames = array(); - if(!is_array($this->params['user'])) - $this->params['user'] = array($this->params['user']); - foreach($this->params['user'] as $u) - $this->prepareUsername($u); + if(isset($this->params['userprefix'])) + { + $this->prefixMode = true; + $this->userprefix = $this->params['userprefix']; + } + else + { + $this->usernames = array(); + if(!is_array($this->params['user'])) + $this->params['user'] = array($this->params['user']); + foreach($this->params['user'] as $u) + $this->prepareUsername($u); + $this->prefixMode = false; + } $this->prepareQuery(); //Do the actual query. @@ -114,7 +122,7 @@ class ApiQueryContributions extends ApiQueryBase { $this->dieUsage( 'User parameter may not be empty', 'param_user' ); } } - + /** * Prepares the query and returns the limit of rows requested */ @@ -122,14 +130,20 @@ class ApiQueryContributions extends ApiQueryBase { //We're after the revision table, and the corresponding page row for //anything we retrieve. - list ($tbl_page, $tbl_revision) = $this->getDB()->tableNamesN('page', 'revision'); - $this->addTables("$tbl_revision LEFT OUTER JOIN $tbl_page ON page_id=rev_page"); - + $this->addTables(array('revision', 'page')); + $this->addWhere('page_id=rev_page'); + $this->addWhereFld('rev_deleted', 0); // We only want pages by the specified users. - $this->addWhereFld( 'rev_user_text', $this->usernames ); + if($this->prefixMode) + $this->addWhere("rev_user_text LIKE '" . $this->getDb()->escapeLike($this->userprefix) . "%'"); + else + $this->addWhereFld( 'rev_user_text', $this->usernames ); // ... and in the specified timeframe. - $this->addWhereRange('rev_timestamp', + // Ensure the same sort order for rev_user_text and rev_timestamp + // so our query is indexed + $this->addWhereRange('rev_user_text', $this->params['dir'], null, null); + $this->addWhereRange('rev_timestamp', $this->params['dir'], $this->params['start'], $this->params['end'] ); $this->addWhereFld('page_namespace', $this->params['namespace']); @@ -146,22 +160,23 @@ class ApiQueryContributions extends ApiQueryBase { // Mandatory fields: timestamp allows request continuation // ns+title checks if the user has access rights for this page - // user_text is necessary if multiple users were specified + // user_text is necessary if multiple users were specified $this->addFields(array( 'rev_timestamp', 'page_namespace', 'page_title', 'rev_user_text', )); - + $this->addFieldsIf('rev_page', $this->fld_ids); - $this->addFieldsIf('rev_id', $this->fld_ids); + $this->addFieldsIf('rev_id', $this->fld_ids || $this->fld_flags); + $this->addFieldsIf('page_latest', $this->fld_flags); // $this->addFieldsIf('rev_text_id', $this->fld_ids); // Should this field be exposed? $this->addFieldsIf('rev_comment', $this->fld_comment); $this->addFieldsIf('rev_minor_edit', $this->fld_flags); $this->addFieldsIf('page_is_new', $this->fld_flags); } - + /** * Extract fields from the database row and append them to a result array */ @@ -172,12 +187,12 @@ class ApiQueryContributions extends ApiQueryBase { $vals['user'] = $row->rev_user_text; if ($this->fld_ids) { $vals['pageid'] = intval($row->rev_page); - $vals['revid'] = intval($row->rev_id); + $vals['revid'] = intval($row->rev_id); // $vals['textid'] = intval($row->rev_text_id); // todo: Should this field be exposed? } - + if ($this->fld_title) - ApiQueryBase :: addTitleInfo($vals, + ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle($row->page_namespace, $row->page_title)); if ($this->fld_timestamp) @@ -188,6 +203,8 @@ class ApiQueryContributions extends ApiQueryBase { $vals['new'] = ''; if ($row->rev_minor_edit) $vals['minor'] = ''; + if ($row->page_latest == $row->rev_id) + $vals['top'] = ''; } if ($this->fld_comment && !empty ($row->rev_comment)) @@ -214,6 +231,7 @@ class ApiQueryContributions extends ApiQueryBase { 'user' => array ( ApiBase :: PARAM_ISMULTI => true ), + 'userprefix' => null, 'dir' => array ( ApiBase :: PARAM_DFLT => 'older', ApiBase :: PARAM_TYPE => array ( @@ -252,6 +270,7 @@ class ApiQueryContributions extends ApiQueryBase { 'start' => 'The start timestamp to return from.', 'end' => 'The end timestamp to return to.', 'user' => 'The user to retrieve contributions for.', + 'userprefix' => 'Retrieve contibutions for all users whose names begin with this value. Overrides ucuser.', 'dir' => 'The direction to search (older or newer).', 'namespace' => 'Only list contributions in these namespaces', 'prop' => 'Include additional pieces of information', @@ -265,12 +284,12 @@ class ApiQueryContributions extends ApiQueryBase { protected function getExamples() { return array ( - 'api.php?action=query&list=usercontribs&ucuser=YurikBot' + 'api.php?action=query&list=usercontribs&ucuser=YurikBot', + 'api.php?action=query&list=usercontribs&ucuserprefix=217.121.114.', ); } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryUserContributions.php 30578 2008-02-05 15:40:58Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryUserContributions.php 37383 2008-07-09 11:44:49Z btongminh $'; } } - diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php index 010d9f4f..2d55a352 100644 --- a/includes/api/ApiQueryUserInfo.php +++ b/includes/api/ApiQueryUserInfo.php @@ -30,8 +30,8 @@ if (!defined('MEDIAWIKI')) { /** * Query module to get information about the currently logged-in user - * - * @addtogroup API + * + * @ingroup API */ class ApiQueryUserInfo extends ApiQueryBase { @@ -52,7 +52,7 @@ class ApiQueryUserInfo extends ApiQueryBase { $r = $this->getCurrentUserInfo(); $result->addValue("query", $this->getModuleName(), $r); } - + protected function getCurrentUserInfo() { global $wgUser; $result = $this->getResult(); @@ -67,7 +67,7 @@ class ApiQueryUserInfo extends ApiQueryBase { $vals['blockedby'] = User::whoIs($wgUser->blockedBy()); $vals['blockreason'] = $wgUser->blockedFor(); } - } + } if (isset($this->prop['hasmsg']) && $wgUser->getNewtalk()) { $vals['messages'] = ''; } @@ -90,13 +90,13 @@ class ApiQueryUserInfo extends ApiQueryBase { } return $vals; } - + protected function getRateLimits() { global $wgUser, $wgRateLimits; if(!$wgUser->isPingLimitable()) return array(); // No limits - + // Find out which categories we belong to $categories = array(); if($wgUser->isAnon()) @@ -110,7 +110,7 @@ class ApiQueryUserInfo extends ApiQueryBase { if(!$wgUser->isAnon()) $categories[] = 'newbie'; } - + // Now get the actual limits $retval = array(); foreach($wgRateLimits as $action => $limits) @@ -121,7 +121,7 @@ class ApiQueryUserInfo extends ApiQueryBase { $retval[$action][$cat]['seconds'] = $limits[$cat][1]; } return $retval; - } + } public function getAllowedParams() { return array ( @@ -168,6 +168,6 @@ class ApiQueryUserInfo extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryUserInfo.php 30395 2008-02-01 14:46:46Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryUserInfo.php 35186 2008-05-22 16:39:43Z brion $'; } } diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php index 144bfba2..a8147567 100644 --- a/includes/api/ApiQueryUsers.php +++ b/includes/api/ApiQueryUsers.php @@ -30,10 +30,10 @@ if (!defined('MEDIAWIKI')) { /** * Query module to get information about a list of users - * - * @addtogroup API + * + * @ingroup API */ - + class ApiQueryUsers extends ApiQueryBase { public function __construct($query, $moduleName) { @@ -50,7 +50,7 @@ if (!defined('MEDIAWIKI')) { } else { $this->prop = array(); } - + if(is_array($params['users'])) { $r = $this->getOtherUsersInfo($params['users']); $result->setIndexedTagName($r, 'user'); @@ -63,38 +63,44 @@ if (!defined('MEDIAWIKI')) { // Canonicalize user names foreach($users as $u) { $n = User::getCanonicalName($u); - if($n === false) + if($n === false || $n === '') $retval[] = array('name' => $u, 'invalid' => ''); else $goodNames[] = $n; } + if(empty($goodNames)) + return $retval; $db = $this->getDb(); - $userTable = $db->tableName('user'); - $tables = "$userTable AS u1"; + $this->addTables('user', 'u1'); $this->addFields('u1.user_name'); $this->addWhereFld('u1.user_name', $goodNames); $this->addFieldsIf('u1.user_editcount', isset($this->prop['editcount'])); - + $this->addFieldsIf('u1.user_registration', isset($this->prop['registration'])); + if(isset($this->prop['groups'])) { - $ug = $db->tableName('user_groups'); - $tables = "$tables LEFT JOIN $ug ON ug_user=u1.user_id"; + $this->addTables('user_groups'); + $this->addJoinConds(array('user_groups' => array('LEFT JOIN', 'ug_user=u1.user_id'))); $this->addFields('ug_group'); } if(isset($this->prop['blockinfo'])) { - $ipb = $db->tableName('ipblocks'); - $tables = "$tables LEFT JOIN $ipb ON ipb_user=u1.user_id"; - $tables = "$tables LEFT JOIN $userTable AS u2 ON ipb_by=u2.user_id"; - $this->addFields(array('ipb_reason', 'u2.user_name AS blocker_name')); + $this->addTables('ipblocks'); + $this->addTables('user', 'u2'); + $u2 = $this->getAliasedName('user', 'u2'); + $this->addJoinConds(array( + 'ipblocks' => array('LEFT JOIN', 'ipb_user=u1.user_id'), + $u2 => array('LEFT JOIN', 'ipb_by=u2.user_id'))); + $this->addFields(array('ipb_reason', 'u2.user_name blocker_name')); } - $this->addTables($tables); - + $data = array(); $res = $this->select(__METHOD__); while(($r = $db->fetchObject($res))) { $data[$r->user_name]['name'] = $r->user_name; if(isset($this->prop['editcount'])) $data[$r->user_name]['editcount'] = $r->user_editcount; + if(isset($this->prop['registration'])) + $data[$r->user_name]['registration'] = wfTimestampOrNull(TS_ISO_8601, $r->user_registration); if(isset($this->prop['groups'])) // This row contains only one group, others will be added from other rows if(!is_null($r->ug_group)) @@ -105,7 +111,7 @@ if (!defined('MEDIAWIKI')) { $data[$r->user_name]['blockreason'] = $r->ipb_reason; } } - + // Second pass: add result data to $retval foreach($goodNames as $u) { if(!isset($data[$u])) @@ -116,7 +122,7 @@ if (!defined('MEDIAWIKI')) { $retval[] = $data[$u]; } } - return $retval; + return $retval; } public function getAllowedParams() { @@ -127,7 +133,8 @@ if (!defined('MEDIAWIKI')) { ApiBase :: PARAM_TYPE => array ( 'blockinfo', 'groups', - 'editcount' + 'editcount', + 'registration' ) ), 'users' => array( @@ -157,6 +164,6 @@ if (!defined('MEDIAWIKI')) { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryUserInfo.php 30128 2008-01-24 17:59:07Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryUsers.php 38183 2008-07-29 12:58:04Z rotem $'; } } diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php index 91a0c951..d17e83f6 100644 --- a/includes/api/ApiQueryWatchlist.php +++ b/includes/api/ApiQueryWatchlist.php @@ -31,8 +31,8 @@ if (!defined('MEDIAWIKI')) { /** * This query action allows clients to retrieve a list of recently modified pages * that are part of the logged-in user's watchlist. - * - * @addtogroup API + * + * @ingroup API */ class ApiQueryWatchlist extends ApiQueryGeneratorBase { @@ -50,7 +50,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { private $fld_ids = false, $fld_title = false, $fld_patrol = false, $fld_flags = false, $fld_timestamp = false, $fld_user = false, $fld_comment = false, $fld_sizes = false; - + private function run($resultPageSet = null) { global $wgUser, $wgDBtype; @@ -122,7 +122,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { 'recentchanges' )); - $userId = $wgUser->getID(); + $userId = $wgUser->getId(); $this->addWhere(array ( 'wl_namespace = rc_namespace', 'wl_title = rc_title', @@ -134,15 +134,15 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $this->addWhereRange('rc_timestamp', $dir, $start, $end); $this->addWhereFld('wl_namespace', $namespace); $this->addWhereIf('rc_this_oldid=page_latest', !$allrev); - + if (!is_null($show)) { $show = array_flip($show); /* Check for conflicting parameters. */ - if ((isset ($show['minor']) && isset ($show['!minor'])) - || (isset ($show['bot']) && isset ($show['!bot'])) + if ((isset ($show['minor']) && isset ($show['!minor'])) + || (isset ($show['bot']) && isset ($show['!bot'])) || (isset ($show['anon']) && isset ($show['!anon']))) { - + $this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show'); } @@ -155,7 +155,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $this->addWhereIf('rc_user != 0', isset ($show['!anon'])); } - + # This is an index optimization for mysql, as done in the Special:Watchlist page $this->addWhereIf("rc_timestamp > ''", !isset ($start) && !isset ($end) && $wgDBtype == 'mysql'); @@ -205,9 +205,9 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { if ($this->fld_ids) { $vals['pageid'] = intval($row->rc_cur_id); - $vals['revid'] = intval($row->rc_this_oldid); + $vals['revid'] = intval($row->rc_this_oldid); } - + if ($this->fld_title) ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle($row->rc_namespace, $row->rc_title)); @@ -305,7 +305,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { 'end' => 'The timestamp to end enumerating.', 'namespace' => 'Filter changes to only the given namespace(s).', 'dir' => 'In which direction to enumerate pages.', - 'limit' => 'How many total pages to return per request.', + 'limit' => 'How many total results to return per request.', 'prop' => 'Which additional items to get (non-generator mode only).', 'show' => array ( 'Show only items that meet this criteria.', @@ -315,7 +315,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { } public function getDescription() { - return ''; + return "Get all recent changes to pages in the logged in user's watchlist"; } protected function getExamples() { @@ -329,7 +329,6 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryWatchlist.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryWatchlist.php 37909 2008-07-22 13:26:15Z catrope $'; } } - diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php index ffab51ef..9e798d35 100644 --- a/includes/api/ApiResult.php +++ b/includes/api/ApiResult.php @@ -33,17 +33,17 @@ if (!defined('MEDIAWIKI')) { * 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 + * for all others. In XML it becomes the content of the current element. + * + * @ingroup API */ class ApiResult extends ApiBase { @@ -64,15 +64,15 @@ class ApiResult extends ApiBase { public function reset() { $this->mData = array (); } - + /** - * Call this function when special elements such as '_element' - * are needed by the formatter, for example in XML printing. + * Call this function when special elements such as '_element' + * are needed by the formatter, for example in XML printing. */ public function setRawMode() { $this->mIsRawMode = true; } - + /** * Returns true if the result is being created for the formatter that requested raw data. */ @@ -139,7 +139,7 @@ class ApiResult extends ApiBase { // Do not use setElement() as it is ok to call this more than once $arr['_element'] = $tag; } - + /** * Calls setIndexedTagName() on $arr and each sub-array */ @@ -147,7 +147,7 @@ class ApiResult extends ApiBase { { if(!is_array($arr)) return; - foreach($arr as $a) + foreach($arr as &$a) { if(!is_array($a)) continue; @@ -160,7 +160,7 @@ class ApiResult extends ApiBase { * Add value to the output data at the given path. * Path is an indexed array, each element specifing the branch at which to add the new value * Setting $path to array('a','b','c') is equivalent to data['a']['b']['c'] = $value - * If $name is empty, the $value is added as a next list element data[] = $value + * If $name is empty, the $value is added as a next list element data[] = $value */ public function addValue($path, $name, $value) { @@ -191,7 +191,7 @@ class ApiResult extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiResult.php 26855 2007-10-20 18:27:39Z catrope $'; + return __CLASS__ . ': $Id: ApiResult.php 35098 2008-05-20 17:13:28Z ialex $'; } } @@ -199,17 +199,17 @@ class ApiResult extends ApiBase { if (!function_exists('array_intersect_key')) { function array_intersect_key($isec, $keys) { $argc = func_num_args(); - + if ($argc > 2) { for ($i = 1; !empty($isec) && $i < $argc; $i++) { $arr = func_get_arg($i); - + foreach (array_keys($isec) as $key) { - if (!isset($arr[$key])) + if (!isset($arr[$key])) unset($isec[$key]); } } - + return $isec; } else { $res = array(); @@ -217,7 +217,7 @@ if (!function_exists('array_intersect_key')) { if (isset($keys[$key])) $res[$key] = $isec[$key]; } - + return $res; } } diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php index d714f99c..3739f694 100644 --- a/includes/api/ApiRollback.php +++ b/includes/api/ApiRollback.php @@ -28,7 +28,7 @@ if (!defined('MEDIAWIKI')) { } /** - * @addtogroup API + * @ingroup API */ class ApiRollback extends ApiBase { @@ -40,7 +40,7 @@ class ApiRollback extends ApiBase { global $wgUser; $this->getMain()->requestWriteMode(); $params = $this->extractRequestParams(); - + $titleObj = NULL; if(!isset($params['title'])) $this->dieUsageMsg(array('missingparam', 'title')); @@ -62,15 +62,12 @@ class ApiRollback extends ApiBase { $articleObj = new Article($titleObj); $summary = (isset($params['summary']) ? $params['summary'] : ""); $details = null; - $dbw = wfGetDb(DB_MASTER); - $dbw->begin(); $retval = $articleObj->doRollback($username, $summary, $params['token'], $params['markbot'], $details); if(!empty($retval)) // We don't care about multiple errors, just report one of them $this->dieUsageMsg(current($retval)); - $dbw->commit(); $current = $target = $summary = NULL; extract($details); @@ -85,9 +82,9 @@ class ApiRollback extends ApiBase { $this->getResult()->addValue(null, $this->getModuleName(), $info); } - + public function mustBePosted() { return true; } - + public function getAllowedParams() { return array ( 'title' => null, @@ -123,6 +120,6 @@ class ApiRollback extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiRollback.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiRollback.php 35098 2008-05-20 17:13:28Z ialex $'; } } diff --git a/includes/api/ApiUnblock.php b/includes/api/ApiUnblock.php index afbd3f0e..d6a02a2a 100644 --- a/includes/api/ApiUnblock.php +++ b/includes/api/ApiUnblock.php @@ -31,7 +31,7 @@ if (!defined('MEDIAWIKI')) { * API module that facilitates the unblocking of users. Requires API write mode * to be enabled. * - * @addtogroup API + * @ingroup API */ class ApiUnblock extends ApiBase { @@ -41,7 +41,7 @@ class ApiUnblock extends ApiBase { /** * Unblocks the specified user or provides the reason the unblock failed. - */ + */ public function execute() { global $wgUser; $this->getMain()->requestWriteMode(); @@ -70,19 +70,16 @@ class ApiUnblock extends ApiBase { $id = $params['id']; $user = $params['user']; $reason = (is_null($params['reason']) ? '' : $params['reason']); - $dbw = wfGetDb(DB_MASTER); - $dbw->begin(); $retval = IPUnblockForm::doUnblock($id, $user, $reason, $range); if(!empty($retval)) $this->dieUsageMsg($retval); - $dbw->commit(); $res['id'] = $id; $res['user'] = $user; $res['reason'] = $reason; $this->getResult()->addValue(null, $this->getModuleName(), $res); } - + public function mustBePosted() { return true; } public function getAllowedParams() { @@ -119,6 +116,6 @@ class ApiUnblock extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiUnblock.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiUnblock.php 35098 2008-05-20 17:13:28Z ialex $'; } } diff --git a/includes/api/ApiUndelete.php b/includes/api/ApiUndelete.php index b27841a8..e054a70e 100644 --- a/includes/api/ApiUndelete.php +++ b/includes/api/ApiUndelete.php @@ -28,7 +28,7 @@ if (!defined('MEDIAWIKI')) { } /** - * @addtogroup API + * @ingroup API */ class ApiUndelete extends ApiBase { @@ -40,7 +40,7 @@ class ApiUndelete extends ApiBase { global $wgUser; $this->getMain()->requestWriteMode(); $params = $this->extractRequestParams(); - + $titleObj = NULL; if(!isset($params['title'])) $this->dieUsageMsg(array('missingparam', 'title')); @@ -61,6 +61,8 @@ class ApiUndelete extends ApiBase { $this->dieUsageMsg(array('invalidtitle', $params['title'])); // Convert timestamps + if(!isset($params['timestamps'])) + $params['timestamps'] = array(); if(!is_array($params['timestamps'])) $params['timestamps'] = array($params['timestamps']); foreach($params['timestamps'] as $i => $ts) @@ -73,16 +75,19 @@ class ApiUndelete extends ApiBase { if(!is_array($retval)) $this->dieUsageMsg(array('cannotundelete')); - $dbw->commit(); + if($retval[1]) + wfRunHooks( 'FileUndeleteComplete', + array($titleObj, array(), $wgUser, $params['reason']) ); + $info['title'] = $titleObj->getPrefixedText(); $info['revisions'] = $retval[0]; $info['fileversions'] = $retval[1]; $info['reason'] = $retval[2]; $this->getResult()->addValue(null, $this->getModuleName(), $info); } - + public function mustBePosted() { return true; } - + public function getAllowedParams() { return array ( 'title' => null, @@ -118,6 +123,6 @@ class ApiUndelete extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiUndelete.php 30222 2008-01-28 19:05:26Z catrope $'; + return __CLASS__ . ': $Id: ApiUndelete.php 35348 2008-05-26 10:51:31Z catrope $'; } } -- cgit v1.2.2