{
"preset": "wikimedia",
+ "es3": true,
- "disallowQuotedKeysInObjects": null,
- "requireSpacesInsideParentheses": null,
- "requireSpacesInsideArrayBrackets": null,
+ "requireVarDeclFirst": null,
+
+ "disallowQuotedKeysInObjects": "allButReserved",
+ "requireDotNotation": { "allExcept": [ "keywords" ] },
+ "jsDoc": {
+ "requireNewlineAfterDescription": true,
+ "requireParamTypes": true,
+ "requireReturnTypes": true
+ },
"excludeFiles": [
"docs/**",
#
language: php
-php:
- - hhvm-nightly
- - 5.3
-
-env:
- - dbtype=mysql
- - dbtype=postgres
-
-# TODO: Travis CI's hhvm does not support PostgreSQL at the moment.
matrix:
- exclude:
- - php: hhvm-nightly
- env: dbtype=postgres
+ fast_finish: true
+ include:
+ - env: dbtype=mysql
+ php: 5.3
+ - env: dbtype=postgres
+ php: 5.3
+ - env: dbtype=mysql
+ php: hhvm
+ - env: dbtype=mysql
+ php: 7
services:
- mysql
wgScriptPath = process.env.MW_SCRIPT_PATH,
karmaProxy = {};
- karmaProxy[wgScriptPath] = wgServer + wgScriptPath;
+ karmaProxy[ wgScriptPath ] = wgServer + wgScriptPath;
grunt.initConfig( {
jshint: {
}
if ( !process.env.MW_SCRIPT_PATH ) {
grunt.log.error( 'Environment variable MW_SCRIPT_PATH must be set.\n' +
- 'Set this like $wgScriptPath, e.g. "/w"');
+ 'Set this like $wgScriptPath, e.g. "/w"' );
}
return !!( process.env.MW_SERVER && process.env.MW_SCRIPT_PATH );
} );
- grunt.registerTask( 'lint', ['jshint', 'jscs', 'jsonlint', 'banana'] );
+ grunt.registerTask( 'lint', [ 'jshint', 'jscs', 'jsonlint', 'banana' ] );
grunt.registerTask( 'qunit', [ 'assert-mw-env', 'karma:main' ] );
- grunt.registerTask( 'test', ['lint'] );
+ grunt.registerTask( 'test', [ 'lint' ] );
grunt.registerTask( 'default', 'test' );
};
MediaWiki 1.26, in where ResourceLoader became fully asynchronous.
* $wgMasterWaitTimeout was removed (deprecated in 1.24).
* Fields in ParserOptions are now private. Use the accessors instead.
+* Custom LESS functions (defined via $wgResourceLoaderLESSFunctions)
+ have been removed, after being deprecated in 1.24.
+* $wgAlwaysUseTidy has been removed.
=== New features in 1.26 ===
* (T51506) Now action=info gives estimates of actual watchers for a page.
for potentially slow POST requests that need to be as atomic as possible.
* ResourceLoader now loads all scripts asynchronously. The top-queue and
startup modules are no longer synchronously loaded.
+* 'mediawiki.ui.button' styles are no longer unconditionally loaded on every
+ page. During the deprecation period, the styles will only be loaded on pages
+ which contain 'mw-ui-button' in their HTML. Starting in 1.28, the styles will
+ only be loaded if explicitly required.
==== External libraries ====
* Update es5-shim from v4.0.0 to v4.1.5.
* (T53283) load.php sometimes sends 304 response without full headers
* (T65198) Talk page tabs now have a "rel=discussion" attribute
* (T98841) {{msgnw:}} now preserves comments even when subst: is not used.
+* (T104142) $wgEmergencyContact and $wgPasswordSender now use their default
+ value if set to an empty string.
=== Action API changes in 1.26 ===
* New-style continuation is now the default for action=continue. Clients may
sometimes being numerically-indexed objects with formatversion=2.
* When errors about users being blocked are returned, they now include
information about the relevant block.
+* (T99926) list=random has higher limits, in line with other API modules.
+* list=random's rnredirect parameter is deprecated in favor of a new
+ rnfilterredir parameter that also allows for listing both redirects and
+ non-redirects.
+* list=random now supports continuation.
+* API responses to GET requests may now include ETag and Last-Modified headers,
+ and will honor corresponding If-None-Match and If-Modified-Since on such
+ requests.
=== Action API internal changes in 1.26 ===
* New metadata item ApiResult::META_KVP_MERGE to allow for merging the KVP key
into the value when the value is an assoc.
+* API action modules may now provide values for the RFC 7232 ETag and
+ Last-Modified headers. The API will check these against If-None-Match and
+ If-Modified-Since request headers on GET requests and avoid executing the
+ module when appropriate.
=== Languages updated in 1.26 ===
a lengthy deprecation period.
* The ScopedPHPTimeout class was removed.
* Removed maintenance script fixSlaveDesync.php.
+* Watchlist tokens, SpecialResetTokens, and User::getTokenFromOption()
+ are deprecated. Applications using those can work via the OAuth
+ extension instead. New tokens types should not be added.
== Compatibility ==
'MWOldPassword' => __DIR__ . '/includes/password/MWOldPassword.php',
'MWSaltedPassword' => __DIR__ . '/includes/password/MWSaltedPassword.php',
'MWTidy' => __DIR__ . '/includes/parser/MWTidy.php',
- 'MWTidyWrapper' => __DIR__ . '/includes/parser/MWTidy.php',
'MWTimestamp' => __DIR__ . '/includes/MWTimestamp.php',
'MachineReadableRCFeedFormatter' => __DIR__ . '/includes/rcfeed/MachineReadableRCFeedFormatter.php',
'MagicWord' => __DIR__ . '/includes/MagicWord.php',
'MediaWiki\\Logger\\Monolog\\WikiProcessor' => __DIR__ . '/includes/debug/logger/monolog/WikiProcessor.php',
'MediaWiki\\Logger\\NullSpi' => __DIR__ . '/includes/debug/logger/NullSpi.php',
'MediaWiki\\Logger\\Spi' => __DIR__ . '/includes/debug/logger/Spi.php',
+ 'MediaWiki\\Tidy\\Html5Depurate' => __DIR__ . '/includes/tidy/Html5Depurate.php',
+ 'MediaWiki\\Tidy\\RaggettBase' => __DIR__ . '/includes/tidy/RaggettBase.php',
+ 'MediaWiki\\Tidy\\RaggettExternal' => __DIR__ . '/includes/tidy/RaggettExternal.php',
+ 'MediaWiki\\Tidy\\RaggettInternalHHVM' => __DIR__ . '/includes/tidy/RaggettInternalHHVM.php',
+ 'MediaWiki\\Tidy\\RaggettInternalPHP' => __DIR__ . '/includes/tidy/RaggettInternalPHP.php',
+ 'MediaWiki\\Tidy\\RaggettWrapper' => __DIR__ . '/includes/tidy/RaggettWrapper.php',
+ 'MediaWiki\\Tidy\\TidyDriverBase' => __DIR__ . '/includes/tidy/TidyDriverBase.php',
'MediaWiki\\Widget\\ComplexNamespaceInputWidget' => __DIR__ . '/includes/widget/ComplexNamespaceInputWidget.php',
'MediaWiki\\Widget\\NamespaceInputWidget' => __DIR__ . '/includes/widget/NamespaceInputWidget.php',
'MediaWiki\\Widget\\TitleInputWidget' => __DIR__ . '/includes/widget/TitleInputWidget.php',
"leafo/lessphp": "0.5.0",
"liuggio/statsd-php-client": "1.0.16",
"mediawiki/at-ease": "1.0.0",
- "oojs/oojs-ui": "0.12.6",
+ "oojs/oojs-ui": "0.12.8",
"php": ">=5.3.3",
"psr/log": "1.0.0",
"wikimedia/assert": "0.2.2",
- "wikimedia/cdb": "1.0.1",
+ "wikimedia/cdb": "1.3.0",
"wikimedia/composer-merge-plugin": "1.2.1",
"wikimedia/ip-set": "1.0.1",
- "wikimedia/utfnormal": "1.0.2",
+ "wikimedia/utfnormal": "1.0.3",
"wikimedia/wrappedstring": "2.0.0",
"zordius/lightncandy": "0.21"
},
"type": "object",
"description": "ResourceLoader LESS variables"
},
- "ResourceLoaderLESSFunctions": {
- "type": "object",
- "description": "ResourceLoader LESS functions"
- },
"ResourceLoaderLESSImportPaths": {
"type": "object",
"description": "ResourceLoader import paths"
"config": {
"type": "object",
"description": "Configuration options for this extension",
+ "properties": {
+ "_prefix": {
+ "type": "string",
+ "default": "wg",
+ "description": "Prefix to put in front of configuration settings when exporting them to $GLOBALS"
+ }
+ },
"patternProperties": {
"^[a-zA-Z_\u007f-\u00ff][a-zA-Z0-9_\u007f-\u00ff]*$": {
"type": ["object", "array", "string", "integer", "null", "boolean"],
/**
* The URL path of the skins directory.
- * Defaults to "{$wgScriptPath}/skins".
+ * Defaults to "{$wgResourceBasePath}/skins".
* @since 1.3
*/
$wgStylePath = false;
/**
* The URL path of the extensions directory.
- * Defaults to "{$wgScriptPath}/extensions".
+ * Defaults to "{$wgResourceBasePath}/extensions".
* @since 1.16
*/
$wgExtensionAssetsPath = false;
$wgResourceLoaderSources = array();
/**
- * Default 'remoteBasePath' value for instances of ResourceLoaderFileModule.
- * If not set, then $wgScriptPath will be used as a fallback.
+ * The default 'remoteBasePath' value for instances of ResourceLoaderFileModule.
+ * Defaults to $wgScriptPath.
*/
$wgResourceBasePath = null;
*/
$wgResourceLoaderLESSVars = array();
-/**
- * Custom LESS functions. An associative array mapping function name to PHP
- * callable.
- *
- * Changes to LESS functions do not trigger cache invalidation.
- *
- * @since 1.22
- * @deprecated since 1.24 Questionable usefulness and problematic to support,
- * will be removed in the future.
- */
-$wgResourceLoaderLESSFunctions = array();
-
/**
* Default import paths for LESS modules. LESS files referenced in @import
* statements will be looked up here first, and relative to the importing file
$wgAllowImageTag = false;
/**
- * $wgUseTidy: use tidy to make sure HTML output is sane.
- * Tidy is a free tool that fixes broken HTML.
- * See http://www.w3.org/People/Raggett/tidy/
+ * Configuration for HTML postprocessing tool. Set this to a configuration
+ * array to enable an external tool. Dave Raggett's "HTML Tidy" is typically
+ * used. See http://www.w3.org/People/Raggett/tidy/
*
- * - $wgTidyBin should be set to the path of the binary and
- * - $wgTidyConf to the path of the configuration file.
- * - $wgTidyOpts can include any number of parameters.
- * - $wgTidyInternal controls the use of the PECL extension or the
- * libtidy (PHP >= 5) extension to use an in-process tidy library instead
- * of spawning a separate program.
- * Normally you shouldn't need to override the setting except for
- * debugging. To install, use 'pear install tidy' and add a line
- * 'extension=tidy.so' to php.ini.
+ * If this is null and $wgUseTidy is true, the deprecated configuration
+ * parameters will be used instead.
+ *
+ * If this is null and $wgUseTidy is false, a pure PHP fallback will be used.
+ *
+ * Keys are:
+ * - driver: May be:
+ * - RaggettInternalHHVM: Use the limited-functionality HHVM extension
+ * - RaggettInternalPHP: Use the PECL extension
+ * - RaggettExternal: Shell out to an external binary (tidyBin)
+ *
+ * - tidyConfigFile: Path to configuration file for any of the Raggett drivers
+ * - debugComment: True to add a comment to the output with warning messages
+ * - tidyBin: For RaggettExternal, the path to the tidy binary.
+ * - tidyCommandLine: For RaggettExternal, additional command line options.
*/
-$wgUseTidy = false;
+$wgTidyConfig = null;
/**
- * @see $wgUseTidy
+ * Set this to true to use the deprecated tidy configuration parameters.
+ * @deprecated use $wgTidyConfig
*/
-$wgAlwaysUseTidy = false;
+$wgUseTidy = false;
/**
- * @see $wgUseTidy
+ * The path to the tidy binary.
+ * @deprecated Use $wgTidyConfig['tidyBin']
*/
$wgTidyBin = 'tidy';
/**
- * @see $wgUseTidy
+ * The path to the tidy config file
+ * @deprecated Use $wgTidyConfig['tidyConfigFile']
*/
-$wgTidyConf = $IP . '/includes/tidy.conf';
+$wgTidyConf = $IP . '/includes/tidy/tidy.conf';
/**
- * @see $wgUseTidy
+ * The command line options to the tidy binary
+ * @deprecated Use $wgTidyConfig['tidyCommandLine']
*/
$wgTidyOpts = '';
/**
- * @see $wgUseTidy
+ * Set this to true to use the tidy extension
+ * @deprecated Use $wgTidyConfig['driver']
*/
$wgTidyInternal = extension_loaded( 'tidy' );
*/
$wgAPIListModules = array();
-/**
- * This variable is ignored. To add your module to the API, please add it to $wgAPI*Modules
- * @deprecated since 1.21
- */
-$wgAPIGeneratorModules = array();
-
/**
* Maximum amount of rows to scan in a DB query in the API
* The default value is generally fine
$this->getContextTitle()->getPrefixedText()
) );
$wgOut->addBacklinkSubtitle( $this->getContextTitle() );
+ $wgOut->addHTML( $this->editFormPageTop );
+ $wgOut->addHTML( $this->editFormTextTop );
+
$wgOut->addWikiText( $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' ) );
$wgOut->addHTML( "<hr />\n" );
$wgOut->addWikiMsg( 'viewsourcetext' );
}
+ $wgOut->addHTML( $this->editFormTextBeforeContent );
$this->showTextbox( $text, 'wpTextbox1', array( 'readonly' ) );
+ $wgOut->addHTML( $this->editFormTextAfterContent );
$wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ),
Linker::formatTemplates( $this->getTemplates() ) ) );
$wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
+ $wgOut->addHTML( $this->editFormTextBottom );
if ( $this->mTitle->exists() ) {
$wgOut->returnToMain( null, $this->mTitle );
}
$wgDisableOutputCompression = true;
}
while ( $status = ob_get_status() ) {
- if ( $status['type'] == 0 /* PHP_OUTPUT_HANDLER_INTERNAL */ ) {
- // Probably from zlib.output_compression or other
- // PHP-internal setting which can't be removed.
- //
+ if ( isset( $status['flags'] ) ) {
+ $flags = PHP_OUTPUT_HANDLER_CLEANABLE | PHP_OUTPUT_HANDLER_REMOVABLE;
+ $deleteable = ( $status['flags'] & $flags ) === $flags;
+ } elseif ( isset( $status['del'] ) ) {
+ $deleteable = $status['del'];
+ } else {
+ // Guess that any PHP-internal setting can't be removed.
+ $deleteable = $status['type'] !== 0; /* PHP_OUTPUT_HANDLER_INTERNAL */
+ }
+ if ( !$deleteable ) {
// Give up, and hope the result doesn't break
// output behavior.
break;
}
+ if ( $status['name'] === 'MediaWikiTestCase::wfResetOutputBuffersBarrier' ) {
+ // Unit testing barrier to prevent this function from breaking PHPUnit.
+ break;
+ }
if ( !ob_end_clean() ) {
// Could not remove output buffer handler; abort now
// to avoid getting in some kind of infinite loop.
return true;
}
+
+/**
+ * Merges two (possibly) 2 dimensional arrays into the target array ($baseArray).
+ *
+ * Values that exist in both values will be combined with += (all values of the array
+ * of $newValues will be added to the values of the array of $baseArray, while values,
+ * that exists in both, the value of $baseArray will be used).
+ *
+ * @param array $baseArray The array where you want to add the values of $newValues to
+ * @param array $newValues An array with new values
+ * @return array The combined array
+ * @since 1.26
+ */
+function wfArrayPlus2d( array $baseArray, array $newValues ) {
+ // First merge items that are in both arrays
+ foreach ( $baseArray as $name => &$groupVal ) {
+ if ( isset( $newValues[$name] ) ) {
+ $groupVal += $newValues[$name];
+ }
+ }
+ // Now add items that didn't exist yet
+ $baseArray += $newValues;
+
+ return $baseArray;
+}
}
/**
- * Handle PHP errors issued inside a hook. Catch errors that have to do with
- * a function expecting a reference, and let all others pass through.
- *
- * This REALLY should be protected... but it's public for compatibility
+ * Handle PHP errors issued inside a hook. Catch errors that have to do
+ * with a function expecting a reference, and pass all others through to
+ * MWExceptionHandler::handleError() for default processing.
*
* @since 1.18
*
* @param int $errno Error number (unused)
* @param string $errstr Error message
* @throws MWHookException If the error has to do with the function signature
- * @return bool Always returns false
+ * @return bool
*/
public static function hookErrorHandler( $errno, $errstr ) {
if ( strpos( $errstr, 'expected to be a reference, value given' ) !== false ) {
throw new MWHookException( $errstr, $errno );
}
- return false;
+
+ // Delegate unhandled errors to the default MW handler
+ return call_user_func_array(
+ 'MWExceptionHandler::handleError', func_get_args()
+ );
}
}
*/
public function getDoc() {
if ( !$this->doc ) {
- $html = mb_convert_encoding( $this->html, 'HTML-ENTITIES', 'UTF-8' );
+ // DOMDocument::loadHTML apparently isn't very good with encodings, so
+ // convert input to ASCII by encoding everything above 128 as entities.
+ if ( function_exists( 'mb_convert_encoding' ) ) {
+ $html = mb_convert_encoding( $this->html, 'HTML-ENTITIES', 'UTF-8' );
+ } else {
+ $html = preg_replace_callback( '/[\x{80}-\x{10ffff}]/u', function ( $m ) {
+ return '&#' . UtfNormal\Utils::utf8ToCodepoint( $m[0] ) . ';';
+ }, $this->html );
+ }
// Workaround for bug that caused spaces before references
// to disappear during processing:
) );
}
$html = $replacements->replace( $html );
- $html = mb_convert_encoding( $html, 'UTF-8', 'HTML-ENTITIES' );
+
+ if ( function_exists( 'mb_convert_encoding' ) ) {
+ // Just in case the conversion in getDoc() above used named
+ // entities that aren't known to html_entity_decode().
+ $html = mb_convert_encoding( $html, 'UTF-8', 'HTML-ENTITIES' );
+ } else {
+ $html = html_entity_decode( $html, ENT_COMPAT, 'utf-8' );
+ }
return $html;
}
class PhpHttpRequest extends MWHttpRequest {
+ private $fopenErrors = array();
+
/**
* @param string $url
* @return string
return 'tcp://' . $parsedUrl['host'] . ':' . $parsedUrl['port'];
}
+ /**
+ * Returns an array with a 'capath' or 'cafile' key that is suitable to be merged into the 'ssl' sub-array of a
+ * stream context options array. Uses the 'caInfo' option of the class if it is provided, otherwise uses the system
+ * default CA bundle if PHP supports that, or searches a few standard locations.
+ * @return array
+ * @throws DomainException
+ */
+ protected function getCertOptions() {
+ $certOptions = array();
+ $certLocations = array();
+ if ( $this->caInfo ) {
+ $certLocations = array( 'manual' => $this->caInfo );
+ } elseif ( version_compare( PHP_VERSION, '5.6.0', '<' ) ) {
+ // Default locations, based on
+ // https://www.happyassassin.net/2015/01/12/a-note-about-ssltls-trusted-certificate-stores-and-platforms/
+ // PHP 5.5 and older doesn't have any defaults, so we try to guess ourselves. PHP 5.6+ gets the CA location
+ // from OpenSSL as long as it is not set manually, so we should leave capath/cafile empty there.
+ $certLocations = array_filter( array(
+ getenv( 'SSL_CERT_DIR' ),
+ getenv( 'SSL_CERT_PATH' ),
+ '/etc/pki/tls/certs/ca-bundle.crt', # Fedora et al
+ '/etc/ssl/certs', # Debian et al
+ '/etc/pki/tls/certs/ca-bundle.trust.crt',
+ '/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem',
+ '/System/Library/OpenSSL', # OSX
+ ) );
+ }
+
+ foreach( $certLocations as $key => $cert ) {
+ if ( is_dir( $cert ) ) {
+ $certOptions['capath'] = $cert;
+ break;
+ } elseif ( is_file( $cert ) ) {
+ $certOptions['cafile'] = $cert;
+ break;
+ } elseif ( $key === 'manual' ) {
+ // fail more loudly if a cert path was manually configured and it is not valid
+ throw new DomainException( "Invalid CA info passed: $cert" );
+ }
+ }
+
+ return $certOptions;
+ }
+
+ /**
+ * Custom error handler for dealing with fopen() errors. fopen() tends to fire multiple errors in succession, and the last one
+ * is completely useless (something like "fopen: failed to open stream") so normal methods of handling errors programmatically
+ * like get_last_error() don't work.
+ */
+ public function errorHandler( $errno, $errstr ) {
+ $n = count( $this->fopenErrors ) + 1;
+ $this->fopenErrors += array( "errno$n" => $errno, "errstr$n" => $errstr );
+ }
+
public function execute() {
parent::execute();
}
}
- if ( is_dir( $this->caInfo ) ) {
- $options['ssl']['capath'] = $this->caInfo;
- } elseif ( is_file( $this->caInfo ) ) {
- $options['ssl']['cafile'] = $this->caInfo;
- } elseif ( $this->caInfo ) {
- throw new MWException( "Invalid CA info passed: {$this->caInfo}" );
- }
+ $options['ssl'] += $this->getCertOptions();
$context = stream_context_create( $options );
}
do {
$reqCount++;
- MediaWiki\suppressWarnings();
+ $this->fopenErrors = array();
+ set_error_handler( array( $this, 'errorHandler' ) );
$fh = fopen( $url, "r", false, $context );
- MediaWiki\restoreWarnings();
+ restore_error_handler();
if ( !$fh ) {
// HACK for instant commons.
$this->setStatus();
if ( $fh === false ) {
+ if ( $this->fopenErrors ) {
+ LoggerFactory::getInstance( 'http' )->warning( __CLASS__
+ . ': error opening connection: {errstr1}', $this->fopenErrors );
+ }
$this->status->fatal( 'http-request-error' );
return $this->status;
}
private $regex;
- /** @todo Unused? */
- private $matches;
-
/**
* @param array $names
*/
/** @var string Inline CSS styles. Use addInlineStyle() sparingly */
protected $mInlineStyles = '';
- /** @todo Unused? */
- private $mLinkColours;
-
/**
* @var string Used by skin template.
* Example: $tpl->set( 'displaytitle', $out->mPageLinkTitle );
* Add an HTTP header that will influence on the cache
*
* @param string $header Header name
- * @param array|null $option
- * @todo FIXME: Document the $option parameter; it appears to be for
- * X-Vary-Options but what format is acceptable?
+ * @param string[]|null $option Options for X-Vary-Options. Possible options are:
+ * - "string-contains=$XXX" varies on whether the header value as a string
+ * contains $XXX as a substring.
+ * - "list-contains=$XXX" varies on whether the header value as a
+ * comma-separated list contains $XXX as one of the list items.
*/
- public function addVaryHeader( $header, $option = null ) {
+ public function addVaryHeader( $header, array $option = null ) {
if ( !array_key_exists( $header, $this->mVaryHeader ) ) {
- $this->mVaryHeader[$header] = (array)$option;
- } elseif ( is_array( $option ) ) {
- if ( is_array( $this->mVaryHeader[$header] ) ) {
- $this->mVaryHeader[$header] = array_merge( $this->mVaryHeader[$header], $option );
- } else {
- $this->mVaryHeader[$header] = $option;
- }
+ $this->mVaryHeader[$header] = array();
+ }
+ if ( !is_array( $option ) ) {
+ $option = array();
}
- $this->mVaryHeader[$header] = array_unique( (array)$this->mVaryHeader[$header] );
+ $this->mVaryHeader[$header] = array_unique( array_merge( $this->mVaryHeader[$header], $option ) );
}
/**
// Automatically select style/script elements
if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
- $media = $group === 'print' ? 'print' : 'all';
- $link = Html::linkedStyle( $url, $media );
+ $link = Html::linkedStyle( $url );
} else {
if ( $context->getRaw() || $isRaw ) {
// Startup module can't load itself, needs to use <script> instead of mw.loader.load
'type' => 'select',
'section' => 'rendering/advancedrendering',
'options' => $stubThresholdOptions,
- 'label-raw' => $context->msg( 'stub-threshold' )->text(), // Raw HTML message. Yay?
+ // This is not a raw HTML message; label-raw is needed for the manual <a></a>
+ 'label-raw' => $context->msg( 'stub-threshold' )->rawParams(
+ '<a href="#" class="stub">' .
+ $context->msg( 'stub-threshold-sample-link' )->parse() .
+ '</a>' )->parse(),
);
$defaultPreferences['showhiddencats'] = array(
$ns = NS_MAIN; // if searching on many always default to main
}
- $t = Title::newFromText( $search, $ns );
+ $t = null;
+ if ( is_string( $search ) ) {
+ $t = Title::newFromText( $search, $ns );
+ }
+
$prefix = $t ? $t->getDBkey() : '';
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select( 'page',
($space*=$space*
(?:
# The attribute value: quoted or alone
- \"([^<\"]*)\"
- | '([^<']*)'
+ \"([^<\"]*)(?:\"|\$)
+ | '([^<']*)(?:'|\$)
| ([a-zA-Z0-9!#$%&()*,\\-.\\/:;<>?@[\\]^_`{|}~]+)
)
)?(?=$space|\$)/sx";
public static function removeHTMLtags( $text, $processCallback = null,
$args = array(), $extratags = array(), $removetags = array()
) {
- global $wgUseTidy;
-
extract( self::getRecognizedTagData( $extratags, $removetags ) );
# Remove HTML comments
$text = Sanitizer::removeHTMLcomments( $text );
$bits = explode( '<', $text );
$text = str_replace( '>', '>', array_shift( $bits ) );
- if ( !$wgUseTidy ) {
+ if ( !MWTidy::isEnabled() ) {
$tagstack = $tablestack = array();
foreach ( $bits as $x ) {
$regs = array();
$wgActionPaths['view'] = $wgArticlePath;
}
+if ( $wgResourceBasePath === null ) {
+ $wgResourceBasePath = $wgScriptPath;
+}
if ( $wgStylePath === false ) {
- $wgStylePath = "$wgScriptPath/skins";
+ $wgStylePath = "$wgResourceBasePath/skins";
}
if ( $wgLocalStylePath === false ) {
+ // Avoid wgResourceBasePath here since that may point to a different domain (e.g. CDN)
$wgLocalStylePath = "$wgScriptPath/skins";
}
if ( $wgExtensionAssetsPath === false ) {
- $wgExtensionAssetsPath = "$wgScriptPath/extensions";
-}
-if ( $wgResourceBasePath === null ) {
- $wgResourceBasePath = $wgScriptPath;
+ $wgExtensionAssetsPath = "$wgResourceBasePath/extensions";
}
if ( $wgLogo === false ) {
// Set defaults for configuration variables
// that are derived from the server name by default
-if ( $wgEmergencyContact === false ) {
+// Note: $wgEmergencyContact and $wgPasswordSender may be false or empty string (T104142)
+if ( !$wgEmergencyContact ) {
$wgEmergencyContact = 'wikiadmin@' . $wgServerName;
}
-
-if ( $wgPasswordSender === false ) {
+if ( !$wgPasswordSender ) {
$wgPasswordSender = 'apache@' . $wgServerName;
}
public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
global $wgLang;
$this->_unstub( 'findVariantLink', 3 );
- return $wgLang->findVariantLink( $link, $nt, $ignoreOtherCond );
+ $wgLang->findVariantLink( $link, $nt, $ignoreOtherCond );
}
/**
* - quick : does cheap permission checks from slaves (usable for GUI creation)
* - full : does cheap and expensive checks possibly from a slave
* - secure : does cheap and expensive checks, using the master as needed
- * @param bool $short Set this to true to stop after the first permission error.
* @param array $ignoreErrors Array of Strings Set this to a list of message keys
* whose corresponding errors may be ignored.
* @return array Array of arguments to wfMessage to explain permissions problems.
*/
public function setInternalPassword( $str ) {
$this->setToken();
+ $this->setOption( 'watchlisttoken', false );
$passwordFactory = self::getPasswordFactory();
$this->mPassword = $passwordFactory->newFromPlaintext( $str );
* @return string|bool User's current value for the option, or false if this option is disabled.
* @see resetTokenFromOption()
* @see getOption()
+ * @deprecated 1.26 Applications should use the OAuth extension
*/
public function getTokenFromOption( $oname ) {
global $wgHiddenPrefs;
- if ( in_array( $oname, $wgHiddenPrefs ) ) {
+
+ $id = $this->getId();
+ if ( !$id || in_array( $oname, $wgHiddenPrefs ) ) {
return false;
}
$token = $this->getOption( $oname );
if ( !$token ) {
- $token = $this->resetTokenFromOption( $oname );
- if ( !wfReadOnly() ) {
- $this->saveSettings();
- }
+ // Default to a value based on the user token to avoid space
+ // wasted on storing tokens for all users. When this option
+ // is set manually by the user, only then is it stored.
+ $token = hash_hmac( 'sha1', "$oname:$id", $this->getToken() );
}
+
return $token;
}
/**
* Check if user is allowed to access a feature / make an action
*
- * @param string $permissions,... Permissions to test
+ * @param string ... Permissions to test
* @return bool True if user is allowed to perform *any* of the given actions
*/
- public function isAllowedAny( /*...*/ ) {
+ public function isAllowedAny() {
$permissions = func_get_args();
foreach ( $permissions as $permission ) {
if ( $this->isAllowed( $permission ) ) {
/**
*
- * @param string $permissions,... Permissions to test
+ * @param string ... Permissions to test
* @return bool True if the user is allowed to perform *all* of the given actions
*/
- public function isAllowedAll( /*...*/ ) {
+ public function isAllowedAll() {
$permissions = func_get_args();
foreach ( $permissions as $permission ) {
if ( !$this->isAllowed( $permission ) ) {
'9只' => '9隻',
'9余' => '9餘',
'·范' => '·范',
-'’s ' => '’s',
+'’s' => '’s',
'、面点' => '、麵點',
'。个中' => '。箇中',
'〇周后' => '〇周後',
'不好干预' => '不好干預',
'不嫌母丑' => '不嫌母醜',
'不寒而栗' => '不寒而慄',
-'不干事' => '不干事',
-'不干他' => '不干他',
-'不干休' => '不干休',
-'不干你' => '不干你',
-'不干她' => '不干她',
-'不干它' => '不干它',
-'不干我' => '不干我',
-'不干扰' => '不干擾',
-'不干擾' => '不干擾',
-'不干涉' => '不干涉',
-'不干牠' => '不干牠',
-'不干犯' => '不干犯',
-'不干預' => '不干預',
-'不干预' => '不干預',
-'不干' => '不幹',
'不吊' => '不弔',
'不卷' => '不捲',
'不采' => '不採',
'佛罗棱萨' => '佛羅稜薩',
'佛钟' => '佛鐘',
'作品里' => '作品裡',
-'作奸犯科' => '作姦犯科',
'作准' => '作準',
'你夸' => '你誇',
'佣金' => '佣金',
'千钧一发' => '千鈞一髮',
'千只' => '千隻',
'千余' => '千餘',
+'升高后' => '升高後',
'半制品' => '半制品',
'半只可' => '半只可',
'半只够' => '半只夠',
'呼吁' => '呼籲',
'命中注定' => '命中注定',
'和奸' => '和姦',
+'和制汉' => '和製漢',
'咎征' => '咎徵',
'咕咕钟' => '咕咕鐘',
'咪表' => '咪錶',
'墓志' => '墓誌',
'增辟' => '增闢',
'墨子里' => '墨子里',
+'墨斗' => '墨斗',
'墨沈沈' => '墨沈沈',
'墨沈' => '墨瀋',
'垦辟' => '墾闢',
+'压制出' => '壓製出',
+'压制机' => '壓製機',
'壮游' => '壯遊',
'壮面' => '壯麵',
'壹郁' => '壹鬱',
'采薇' => '採薇',
'采薪' => '採薪',
'采药' => '採藥',
+'采血' => '採血',
'采行' => '採行',
'采补' => '採補',
'采访' => '採訪',
'提子干' => '提子乾',
'提心吊胆' => '提心弔膽',
'提摩太后书' => '提摩太後書',
+'提高后' => '提高後',
'插于' => '插於',
'换签' => '換籤',
'换只' => '換隻',
'历始' => '曆始',
'历室' => '曆室',
'历尾' => '曆尾',
-'历数' => 'æ\9b\86æ\95¸',
+'历数书' => 'æ\9b\86æ\95¸æ\9b¸',
'历日' => '曆日',
'历书' => '曆書',
'历本' => '曆本',
'望后石' => '望后石',
'朝乾夕惕' => '朝乾夕惕',
'朝钟' => '朝鐘',
+'朝鲜于' => '朝鮮於',
'朦胧' => '朦朧',
'蒙胧' => '朦朧',
'木偶戏扎' => '木偶戲紮',
'洗发' => '洗髮',
'洛钟东应' => '洛鐘東應',
'洞里' => '洞裡',
+'洞里萨' => '洞里薩',
+'洞里薩' => '洞里薩',
'泄欲' => '洩慾',
'洪范' => '洪範',
'洪谷子' => '洪谷子',
'渠冲' => '渠衝',
'测不准' => '測不準',
'港制' => '港製',
-'游牧民族' => '游牧民族',
'游离' => '游離',
'浑朴' => '渾樸',
'浑个' => '渾箇',
'癸丑' => '癸丑',
'发干' => '發乾',
'发呆' => '發獃',
-'发蒙' => '發矇',
'发签' => '發籤',
'发松' => '發鬆',
'发面' => '發麵',
'谷草' => '穀草',
'谷贵饿农' => '穀貴餓農',
'谷贱伤农' => '穀賤傷農',
-'谷道' => '穀道',
'谷雨' => '穀雨',
'谷类' => '穀類',
'谷食' => '穀食',
'胜肽' => '胜肽',
'胜键' => '胜鍵',
'胡云' => '胡云',
+'胡子婴' => '胡子嬰',
'胡子昂' => '胡子昂',
'胡杰' => '胡杰',
'胡朴安' => '胡樸安',
'体范' => '體範',
'体系' => '體系',
'高几' => '高几',
+'高后' => '高后',
'高干扰' => '高干擾',
'高干预' => '高干預',
'高干' => '高幹',
'魔表' => '魔錶',
'鱼干' => '魚乾',
'鱼松' => '魚鬆',
-'鮮于樞' => '鮮于樞',
-'鲜于枢' => '鮮于樞',
+'鮮于' => '鮮于',
+'鲜于' => '鮮于',
'鲸须' => '鯨鬚',
'鳥栖' => '鳥栖',
'鸟栖市' => '鳥栖市',
'黃杰' => '黃杰',
'黄杰' => '黃杰',
'黄历史' => '黃歷史',
+'黄白术' => '黃白術',
'黃詩杰' => '黃詩杰',
'黄诗杰' => '黃詩杰',
'黄金表' => '黃金表',
'掌上壓' => '伏地挺身',
'伯明翰' => '伯明罕',
'服务器' => '伺服器',
-'字節' => '位元組',
-'字节' => '位元組',
'佛罗伦萨' => '佛羅倫斯',
'操作系统' => '作業系統',
'系数' => '係數',
'戒烟' => '戒菸',
'戒煙' => '戒菸',
'戴克里先' => '戴克里先',
+'打印度' => '打印度',
'抽烟' => '抽菸',
'抽煙' => '抽菸',
'拉普兰' => '拉布蘭',
'搜索引擎' => '搜尋引擎',
'摩根士丹利' => '摩根史坦利',
'台球' => '撞球',
-'攻打印' => '攻打印',
+'攻打' => '攻打',
'数字技术' => '數位技術',
'數碼技術' => '數位技術',
'数字照相机' => '數位照相機',
'撒切尔' => '柴契爾',
'格林納達' => '格瑞那達',
'格林纳达' => '格瑞那達',
+'台式电脑' => '桌上型電腦',
'乒乓' => '桌球',
'乒乓球' => '桌球',
'杆弟' => '桿弟',
'弗吉尼亚' => '維吉尼亞',
'佛得角' => '維德角',
'维特根斯坦' => '維根斯坦',
+'網絡遊戲' => '網路遊戲',
+'网络游戏' => '網路遊戲',
'互联网' => '網際網路',
'互联网络' => '網際網路',
'互聯網' => '網際網路',
'链接' => '連結',
'連結他' => '連結他',
'进制' => '進位',
-'算子' => '運算元',
'达·芬奇' => '達·文西',
'达芬奇' => '達文西',
'溫納圖萬' => '那杜',
'扛著錄' => '扛著錄',
'找不著' => '找不着',
'找得著' => '找得着',
+'承宣布政' => '承宣布政',
'抓著' => '抓着',
'抓著作' => '抓著作',
'抓著名' => '抓著名',
'葛萊美獎' => '格林美獎',
'格鲁吉亚' => '格魯吉亞',
'框里' => '框裏',
+'台式电脑' => '桌上型電腦',
'台球' => '桌球',
'撞球' => '桌球',
'梅鐸' => '梅鐸',
'遇著述' => '遇著述',
'遇著錄' => '遇著錄',
'遍布' => '遍佈',
+'遍佈著' => '遍佈着',
+'遍布著' => '遍佈着',
'過著' => '過着',
'达·芬奇' => '達·文西',
'达芬奇' => '達文西',
'叫著稱' => '叫著称',
'叫著者' => '叫著者',
'叫著述' => '叫著述',
+'桌上型電腦' => '台式电脑',
'撞球' => '台球',
'台帳' => '台账',
'叱吒' => '叱咤',
'遇著稱' => '遇著称',
'遇著者' => '遇著者',
'遇著述' => '遇著述',
+'遍佈著' => '遍布着',
+'遍布著' => '遍布着',
'部份' => '部分',
'配合著' => '配合着',
'配合著名' => '配合著名',
'鋪著稱' => '铺著称',
'鋪著者' => '铺著者',
'鋪著述' => '铺著述',
+'鏈結' => '链接',
'銷帳' => '销账',
'鉲' => '锎',
'鎝' => '锝',
return null;
}
+ /**
+ * Returns data for HTTP conditional request mechanisms.
+ *
+ * @since 1.26
+ * @param string $condition Condition being queried:
+ * - last-modified: Return a timestamp representing the maximum of the
+ * last-modified dates for all resources involved in the request. See
+ * RFC 7232 § 2.2 for semantics.
+ * - etag: Return an entity-tag representing the state of all resources involved
+ * in the request. Quotes must be included. See RFC 7232 § 2.3 for semantics.
+ * @return string|boolean|null As described above, or null if no value is available.
+ */
+ public function getConditionalRequestData( $condition ) {
+ return null;
+ }
+
/**@}*/
/************************************************************************//**
class ApiFormatRaw extends ApiFormatBase {
private $errorFallback;
+ private $mFailWithHTTPError = false;
+
/**
* @param ApiMain $main
- * @param ApiFormatBase $errorFallback Object to fall back on for errors
+ * @param ApiFormatBase |null $errorFallback Object to fall back on for errors
*/
- public function __construct( ApiMain $main, ApiFormatBase $errorFallback ) {
+ public function __construct( ApiMain $main, ApiFormatBase $errorFallback = null ) {
parent::__construct( $main, 'raw' );
- $this->errorFallback = $errorFallback;
+ if ( $errorFallback === null ) {
+ $this->errorFallback = $main->createPrinterByName( $main->getParameter( 'format' ) );
+ } else {
+ $this->errorFallback = $errorFallback;
+ }
}
public function getMimeType() {
$data = $this->getResult()->getResultData();
if ( isset( $data['error'] ) ) {
$this->errorFallback->initPrinter( $unused );
+ if ( $this->mFailWithHTTPError ) {
+ $this->getMain()->getRequest()->response()->statusHeader( 400 );
+ }
} else {
parent::initPrinter( $unused );
}
}
$this->printText( $data['text'] );
}
+
+ /**
+ * Output HTTP error code 400 when if an error is encountered
+ *
+ * The purpose is for output formats where the user-agent will
+ * not be able to interpret the validity of the content in any
+ * other way. For example subtitle files read by browser video players.
+ *
+ * @param bool $fail
+ */
+ public function setFailWithHTTPError( $fail ) {
+ $this->mFailWithHTTPError = $fail;
+ }
}
case LoginForm::CREATE_BLOCKED:
$result['result'] = 'CreateBlocked';
$result['details'] = 'Your IP address is blocked from account creation';
- $result = array_merge(
- $result,
- ApiQueryUserInfo::getBlockInfo( $context->getUser()->getBlock() )
- );
+ $block = $context->getUser()->getBlock();
+ if ( $block ) {
+ $result = array_merge( $result, ApiQueryUserInfo::getBlockInfo( $block ) );
+ }
break;
case LoginForm::THROTTLED:
case LoginForm::USER_BLOCKED:
$result['result'] = 'Blocked';
- $result = array_merge(
- $result,
- ApiQueryUserInfo::getBlockInfo( User::newFromName( $params['name'] )->getBlock() )
- );
+ $block = User::newFromName( $params['name'] )->getBlock();
+ if ( $block ) {
+ $result = array_merge( $result, ApiQueryUserInfo::getBlockInfo( $block ) );
+ }
break;
case LoginForm::ABORTED:
// In case an error occurs during data output,
// clear the output buffer and print just the error information
+ $obLevel = ob_get_level();
ob_start();
$t = microtime( true );
try {
$this->executeAction();
+ $isError = false;
} catch ( Exception $e ) {
$this->handleException( $e );
+ $isError = true;
}
// Log the request whether or not there was an error
// Send cache headers after any code which might generate an error, to
// avoid sending public cache headers for errors.
- $this->sendCacheHeaders();
+ $this->sendCacheHeaders( $isError );
- ob_end_flush();
+ // Executing the action might have already messed with the output
+ // buffers.
+ while ( ob_get_level() > $obLevel ) {
+ ob_end_flush();
+ }
}
/**
// Log the request and reset cache headers
$main->logRequest( 0 );
- $main->sendCacheHeaders();
+ $main->sendCacheHeaders( true );
ob_end_flush();
}
return "/^https?:\/\/$wildcard$/";
}
- protected function sendCacheHeaders() {
+ /**
+ * Send caching headers
+ * @param boolean $isError Whether an error response is being output
+ * @since 1.26 added $isError parameter
+ */
+ protected function sendCacheHeaders( $isError ) {
$response = $this->getRequest()->response();
$out = $this->getOutput();
$out->addVaryHeader( 'X-Forwarded-Proto' );
}
+ if ( !$isError && $this->mModule &&
+ ( $this->getRequest()->getMethod() === 'GET' || $this->getRequest()->getMethod() === 'HEAD' )
+ ) {
+ $etag = $this->mModule->getConditionalRequestData( 'etag' );
+ if ( $etag !== null ) {
+ $response->header( "ETag: $etag" );
+ }
+ $lastMod = $this->mModule->getConditionalRequestData( 'last-modified' );
+ if ( $lastMod !== null ) {
+ $response->header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $lastMod ) );
+ }
+ }
+
// The logic should be:
// $this->mCacheControl['max-age'] is set?
// Use it, the module knows better than our guess.
return true;
}
+ /**
+ * Check selected RFC 7232 precondition headers
+ *
+ * RFC 7232 envisions a particular model where you send your request to "a
+ * resource", and for write requests that you can read "the resource" by
+ * changing the method to GET. When the API receives a GET request, it
+ * works out even though "the resource" from RFC 7232's perspective might
+ * be many resources from MediaWiki's perspective. But it totally fails for
+ * a POST, since what HTTP sees as "the resource" is probably just
+ * "/api.php" with all the interesting bits in the body.
+ *
+ * Therefore, we only support RFC 7232 precondition headers for GET (and
+ * HEAD). That means we don't need to bother with If-Match and
+ * If-Unmodified-Since since they only apply to modification requests.
+ *
+ * And since we don't support Range, If-Range is ignored too.
+ *
+ * @since 1.26
+ * @param ApiBase $module Api module being used
+ * @return bool True on success, false should exit immediately
+ */
+ protected function checkConditionalRequestHeaders( $module ) {
+ if ( $this->mInternalMode ) {
+ // No headers to check in internal mode
+ return true;
+ }
+
+ if ( $this->getRequest()->getMethod() !== 'GET' && $this->getRequest()->getMethod() !== 'HEAD' ) {
+ // Don't check POSTs
+ return true;
+ }
+
+ $return304 = false;
+
+ $ifNoneMatch = array_diff(
+ $this->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST ) ?: array(),
+ array( '' )
+ );
+ if ( $ifNoneMatch ) {
+ if ( $ifNoneMatch === array( '*' ) ) {
+ // API responses always "exist"
+ $etag = '*';
+ } else {
+ $etag = $module->getConditionalRequestData( 'etag' );
+ }
+ }
+ if ( $ifNoneMatch && $etag !== null ) {
+ $test = substr( $etag, 0, 2 ) === 'W/' ? substr( $etag, 2 ) : $etag;
+ $match = array_map( function ( $s ) {
+ return substr( $s, 0, 2 ) === 'W/' ? substr( $s, 2 ) : $s;
+ }, $ifNoneMatch );
+ $return304 = in_array( $test, $match, true );
+ } else {
+ $value = trim( $this->getRequest()->getHeader( 'If-Modified-Since' ) );
+
+ // Some old browsers sends sizes after the date, like this:
+ // Wed, 20 Aug 2003 06:51:19 GMT; length=5202
+ // Ignore that.
+ $i = strpos( $value, ';' );
+ if ( $i !== false ) {
+ $value = trim( substr( $value, 0, $i ) );
+ }
+
+ if ( $value !== '' ) {
+ try {
+ $ts = new MWTimestamp( $value );
+ if (
+ // RFC 7231 IMF-fixdate
+ $ts->getTimestamp( TS_RFC2822 ) === $value ||
+ // RFC 850
+ $ts->format( 'l, d-M-y H:i:s' ) . ' GMT' === $value ||
+ // asctime (with and without space-padded day)
+ $ts->format( 'D M j H:i:s Y' ) === $value ||
+ $ts->format( 'D M j H:i:s Y' ) === $value
+ ) {
+ $lastMod = $module->getConditionalRequestData( 'last-modified' );
+ if ( $lastMod !== null ) {
+ // Mix in some MediaWiki modification times
+ $modifiedTimes = array(
+ 'page' => $lastMod,
+ 'user' => $this->getUser()->getTouched(),
+ 'epoch' => $this->getConfig()->get( 'CacheEpoch' ),
+ );
+ if ( $this->getConfig()->get( 'UseSquid' ) ) {
+ // T46570: the core page itself may not change, but resources might
+ $modifiedTimes['sepoch'] = wfTimestamp(
+ TS_MW, time() - $this->getConfig()->get( 'SquidMaxage' )
+ );
+ }
+ Hooks::run( 'OutputPageCheckLastModified', array( &$modifiedTimes ) );
+ $lastMod = max( $modifiedTimes );
+ $return304 = wfTimestamp( TS_MW, $lastMod ) <= $ts->getTimestamp( TS_MW );
+ }
+ }
+ } catch ( TimestampException $e ) {
+ // Invalid timestamp, ignore it
+ }
+ }
+ }
+
+ if ( $return304 ) {
+ $this->getRequest()->response()->statusHeader( 304 );
+
+ // Avoid outputting the compressed representation of a zero-length body
+ MediaWiki\suppressWarnings();
+ ini_set( 'zlib.output_compression', 0 );
+ MediaWiki\restoreWarnings();
+ wfClearOutputBuffers();
+
+ return false;
+ }
+
+ return true;
+ }
+
/**
* Check for sufficient permissions to execute
* @param ApiBase $module An Api module
return;
}
+ if ( !$this->checkConditionalRequestHeaders( $module ) ) {
+ return;
+ }
+
if ( !$this->mInternalMode ) {
$this->setupExternalResponse( $module, $params );
}
$result = $this->getResult();
$pageSet = $this->getPageSet();
- $titles = $pageSet->getTitles();
// This module operates in two modes:
// 'user': List deleted revs by a certain user
}
}
+ // If we're generating titles only, we can use DISTINCT for a better
+ // query. But we can't do that in 'user' mode (wrong index), and we can
+ // only do it when sorting ASC (because MySQL apparently can't use an
+ // index backwards for grouping even though it can for ORDER BY, WTF?)
+ $dir = $params['dir'];
+ $optimizeGenerateTitles = false;
+ if ( $mode === 'all' && $params['generatetitles'] && $resultPageSet !== null ) {
+ if ( $dir === 'newer' ) {
+ $optimizeGenerateTitles = true;
+ } else {
+ $p = $this->getModulePrefix();
+ $this->setWarning( "For better performance when generating titles, set {$p}dir=newer" );
+ }
+ }
+
$this->addTables( 'archive' );
if ( $resultPageSet === null ) {
$this->parseParameters( $params );
$this->addFields( array( 'ar_title', 'ar_namespace' ) );
} else {
$this->limit = $this->getParameter( 'limit' ) ?: 10;
- $this->addFields( array( 'ar_title', 'ar_namespace', 'ar_timestamp', 'ar_rev_id', 'ar_id' ) );
+ $this->addFields( array( 'ar_title', 'ar_namespace' ) );
+ if ( $optimizeGenerateTitles ) {
+ $this->addOption( 'DISTINCT' );
+ } else {
+ $this->addFields( array( 'ar_timestamp', 'ar_rev_id', 'ar_id' ) );
+ }
}
if ( $this->fld_tags ) {
}
}
- $dir = $params['dir'];
$miser_ns = null;
if ( $mode == 'all' ) {
if ( !is_null( $params['continue'] ) ) {
$cont = explode( '|', $params['continue'] );
$op = ( $dir == 'newer' ? '>' : '<' );
- if ( $mode == 'all' ) {
+ if ( $optimizeGenerateTitles ) {
+ $this->dieContinueUsageIf( count( $cont ) != 2 );
+ $ns = intval( $cont[0] );
+ $this->dieContinueUsageIf( strval( $ns ) !== $cont[0] );
+ $title = $db->addQuotes( $cont[1] );
+ $this->addWhere( "ar_namespace $op $ns OR " .
+ "(ar_namespace = $ns AND ar_title $op= $title)" );
+ } elseif ( $mode == 'all' ) {
$this->dieContinueUsageIf( count( $cont ) != 4 );
$ns = intval( $cont[0] );
$this->dieContinueUsageIf( strval( $ns ) !== $cont[0] );
$sort = ( $dir == 'newer' ? '' : ' DESC' );
$orderby = array();
- if ( $mode == 'all' ) {
+ if ( $optimizeGenerateTitles ) {
+ // Targeting index name_title_timestamp
+ if ( $params['namespace'] === null || count( array_unique( $params['namespace'] ) ) > 1 ) {
+ $orderby[] = "ar_namespace $sort";
+ }
+ $orderby[] = "ar_title $sort";
+ } elseif ( $mode == 'all' ) {
// Targeting index name_title_timestamp
if ( $params['namespace'] === null || count( array_unique( $params['namespace'] ) ) > 1 ) {
$orderby[] = "ar_namespace $sort";
foreach ( $res as $row ) {
if ( ++$count > $this->limit ) {
// We've had enough
- if ( $mode == 'all' ) {
+ if ( $optimizeGenerateTitles ) {
+ $this->setContinueEnumParameter( 'continue', "$row->ar_namespace|$row->ar_title" );
+ } elseif ( $mode == 'all' ) {
$this->setContinueEnumParameter( 'continue',
"$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id"
);
* @ingroup API
*/
class ApiQueryRandom extends ApiQueryGeneratorBase {
- private $pageIDs;
-
public function __construct( ApiQuery $query, $moduleName ) {
parent::__construct( $query, $moduleName, 'rn' );
}
}
/**
- * @param string $randstr
- * @param int $limit
- * @param int $namespace
- * @param ApiPageSet $resultPageSet
- * @param bool $redirect
- * @return void
+ * Actually perform the query and add pages to the result.
+ * @param ApiPageSet|null $resultPageSet
+ * @param int $limit Number of pages to fetch
+ * @param string|null $start Starting page_random
+ * @param int|null $startId Starting page_id
+ * @param string|null $end Ending page_random
+ * @return array (int, string|null) Number of pages left to query and continuation string
*/
- protected function prepareQuery( $randstr, $limit, $namespace, &$resultPageSet, $redirect ) {
+ protected function runQuery( $resultPageSet, $limit, $start, $startId, $end ) {
+ $params = $this->extractRequestParams();
+
$this->resetQueryParams();
$this->addTables( 'page' );
- $this->addOption( 'LIMIT', $limit );
- $this->addWhereFld( 'page_namespace', $namespace );
- $this->addWhereRange( 'page_random', 'newer', $randstr, null );
- $this->addWhereFld( 'page_is_redirect', $redirect );
+ $this->addFields( array( 'page_id', 'page_random' ) );
if ( is_null( $resultPageSet ) ) {
- $this->addFields( array( 'page_id', 'page_title', 'page_namespace' ) );
+ $this->addFields( array( 'page_title', 'page_namespace' ) );
} else {
$this->addFields( $resultPageSet->getPageTableFields() );
}
- }
+ $this->addWhereFld( 'page_namespace', $params['namespace'] );
+ if ( $params['redirect'] || $params['filterredir'] === 'redirects' ) {
+ $this->addWhereFld( 'page_is_redirect', 1 );
+ } elseif ( $params['filterredir'] === 'nonredirects' ) {
+ $this->addWhereFld( 'page_is_redirect', 0 );
+ } elseif ( is_null( $resultPageSet ) ) {
+ $this->addFields( array( 'page_is_redirect' ) );
+ }
+ $this->addOption( 'LIMIT', $limit + 1 );
+
+ if ( $start !== null ) {
+ $start = $this->getDB()->addQuotes( $start );
+ if ( $startId !== null ) {
+ $startId = (int)$startId;
+ $this->addWhere( "page_random = $start AND page_id >= $startId OR page_random > $start" );
+ } else {
+ $this->addWhere( "page_random >= $start" );
+ }
+ }
+ if ( $end !== null ) {
+ $this->addWhere( 'page_random < ' . $this->getDB()->addQuotes( $end ) );
+ }
+ $this->addOption( 'ORDER BY', array( 'page_random', 'page_id' ) );
+
+ $result = $this->getResult();
+ $path = array( 'query', $this->getModuleName() );
- /**
- * @param ApiPageSet $resultPageSet
- * @return int
- */
- protected function runQuery( $resultPageSet = null ) {
$res = $this->select( __METHOD__ );
$count = 0;
foreach ( $res as $row ) {
- $count++;
+ if ( $count++ >= $limit ) {
+ return array( 0, "{$row->page_random}|{$row->page_id}" );
+ }
if ( is_null( $resultPageSet ) ) {
- // Prevent duplicates
- if ( !in_array( $row->page_id, $this->pageIDs ) ) {
- $fit = $this->getResult()->addValue(
- array( 'query', $this->getModuleName() ),
- null, $this->extractRowInfo( $row ) );
- if ( !$fit ) {
- // We can't really query-continue a random list.
- // Return an insanely high value so
- // $count < $limit is false
- return 1E9;
- }
- $this->pageIDs[] = $row->page_id;
+ $title = Title::makeTitle( $row->page_namespace, $row->page_title );
+ $page = array(
+ 'id' => (int)$row->page_id,
+ );
+ ApiQueryBase::addTitleInfo( $page, $title );
+ if ( isset( $row->page_is_redirect ) ) {
+ $page['redirect'] = (bool)$row->page_is_redirect;
+ }
+ $fit = $result->addValue( $path, null, $page );
+ if ( !$fit ) {
+ return array( 0, "{$row->page_random}|{$row->page_id}" );
}
} else {
$resultPageSet->processDbRow( $row );
}
}
- return $count;
+ return array( $limit - $count, null );
}
/**
- * @param ApiPageSet $resultPageSet
- * @return void
+ * @param ApiPageSet|null $resultPageSet
*/
public function run( $resultPageSet = null ) {
$params = $this->extractRequestParams();
- $result = $this->getResult();
- $this->pageIDs = array();
-
- $this->prepareQuery(
- wfRandom(),
- $params['limit'],
- $params['namespace'],
- $resultPageSet,
- $params['redirect']
- );
- $count = $this->runQuery( $resultPageSet );
- if ( $count < $params['limit'] ) {
- /* We got too few pages, we probably picked a high value
- * for page_random. We'll just take the lowest ones, see
- * also the comment in Title::getRandomTitle()
- */
- $this->prepareQuery(
- 0,
- $params['limit'] - $count,
- $params['namespace'],
- $resultPageSet,
- $params['redirect']
- );
- $this->runQuery( $resultPageSet );
+
+ // Since 'filterredir" will always be set in $params, we have to dig
+ // into the WebRequest to see if it was actually passed.
+ $request = $this->getMain()->getRequest();
+ if ( $request->getCheck( $this->encodeParamName( 'filterredir' ) ) ) {
+ $this->requireMaxOneParameter( $params, 'filterredir', 'redirect' );
}
- if ( is_null( $resultPageSet ) ) {
- $result->addIndexedTagName( array( 'query', $this->getModuleName() ), 'page' );
+ if ( $params['redirect'] ) {
+ $this->logFeatureUsage( "list=random&rnredirect=" );
+ }
+
+ if ( isset( $params['continue'] ) ) {
+ $cont = explode( '|', $params['continue'] );
+ $this->dieContinueUsageIf( count( $cont ) != 4 );
+ $rand = $cont[0];
+ $start = $cont[1];
+ $startId = (int)$cont[2];
+ $end = $cont[3] ? $rand : null;
+ $this->dieContinueUsageIf( !preg_match( '/^0\.\d+$/', $rand ) );
+ $this->dieContinueUsageIf( !preg_match( '/^0\.\d+$/', $start ) );
+ $this->dieContinueUsageIf( $cont[2] !== (string)$startId );
+ $this->dieContinueUsageIf( $cont[3] !== '0' && $cont[3] !== '1' );
+ } else {
+ $rand = wfRandom();
+ $start = $rand;
+ $startId = null;
+ $end = null;
}
- }
- private function extractRowInfo( $row ) {
- $title = Title::makeTitle( $row->page_namespace, $row->page_title );
- $vals = array();
- $vals['id'] = intval( $row->page_id );
- ApiQueryBase::addTitleInfo( $vals, $title );
+ list( $left, $continue ) = $this->runQuery( $resultPageSet, $params['limit'], $start, $startId, $end );
+ if ( $end === null && $continue === null ) {
+ // Wrap around. We do this even if $left === 0 for continuation
+ // (saving a DB query in this rare case probably isn't worth the
+ // added code complexity it would require).
+ $end = $rand;
+ list( $left, $continue ) = $this->runQuery( $resultPageSet, $left, null, null, $end );
+ }
+
+ if ( $continue !== null ) {
+ $endFlag = $end === null ? 0 : 1;
+ $this->setContinueEnumParameter( 'continue', "$rand|$continue|$endFlag" );
+ }
- return $vals;
+ if ( is_null( $resultPageSet ) ) {
+ $this->getResult()->addIndexedTagName( array( 'query', $this->getModuleName() ), 'page' );
+ }
}
public function getCacheMode( $params ) {
ApiBase::PARAM_TYPE => 'namespace',
ApiBase::PARAM_ISMULTI => true
),
+ 'filterredir' => array(
+ ApiBase::PARAM_TYPE => array( 'all', 'redirects', 'nonredirects' ),
+ ApiBase::PARAM_DFLT => 'nonredirects', // for BC
+ ),
+ 'redirect' => array(
+ ApiBase::PARAM_DEPRECATED => true,
+ ApiBase::PARAM_DFLT => false,
+ ),
'limit' => array(
ApiBase::PARAM_TYPE => 'limit',
ApiBase::PARAM_DFLT => 1,
ApiBase::PARAM_MIN => 1,
- ApiBase::PARAM_MAX => 10,
- ApiBase::PARAM_MAX2 => 20
+ ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
+ ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
+ ),
+ 'continue' => array(
+ ApiBase::PARAM_HELP_MSG => 'api-help-param-continue'
),
- 'redirect' => false,
);
}
protected function getCurrentUserInfo() {
$user = $this->getUser();
- $result = $this->getResult();
$vals = array();
$vals['id'] = intval( $user->getId() );
$vals['name'] = $user->getName();
const META_TYPE = '_type';
/**
- * Key for the metatata item whose value specifies the name used for the
+ * Key for the metadata item whose value specifies the name used for the
* kvp key in the alternative output format with META_TYPE 'kvp' or
* 'BCkvp', i.e. the "name" in <container><item name="key">value</item></container>.
* Value is string.
* @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
*/
public static function setValue( array &$arr, $name, $value, $flags = 0 ) {
- if ( !( $flags & ApiResult::NO_VALIDATE ) ) {
+ if ( ( $flags & ApiResult::NO_VALIDATE ) !== ApiResult::NO_VALIDATE ) {
$value = self::validateValue( $value );
}
$arr = &$this->path( $path, ( $flags & ApiResult::ADD_ON_TOP ) ? 'prepend' : 'append' );
if ( $this->checkingSize && !( $flags & ApiResult::NO_SIZE_CHECK ) ) {
+ // self::valueSize needs the validated value. Then flag
+ // to not re-validate later.
+ $value = self::validateValue( $value );
+ $flags |= ApiResult::NO_VALIDATE;
+
$newsize = $this->size + self::valueSize( $value );
if ( $this->maxSize !== false && $newsize > $this->maxSize ) {
/// @todo Add i18n message when replacing calls to ->setWarning()
* or the sum of the strlen()s of the elements if the item is an array.
* @note Once the deprecated public self::size is removed, we can rename
* this back to a less awkward name.
- * @param mixed $value
+ * @param mixed $value Validated value (see self::validateValue())
* @return int
*/
private static function valueSize( $value ) {
$s = 0;
- if ( is_array( $value ) ||
- is_object( $value ) && !is_callable( array( $value, '__toString' ) )
- ) {
+ if ( is_array( $value ) ) {
foreach ( $value as $k => $v ) {
if ( !self::isMetadataKey( $s ) ) {
$s += self::valueSize( $v );
*/
public static function setElement( &$arr, $name, $value, $flags = 0 ) {
wfDeprecated( __METHOD__, '1.25' );
- return self::setValue( $arr, $name, $value, $flags );
+ self::setValue( $arr, $name, $value, $flags );
}
/**
*/
public static function size( $value ) {
wfDeprecated( __METHOD__, '1.25' );
- return self::valueSize( $value );
+ return self::valueSize( self::validateValue( $value ) );
}
/**
"Meno25",
"أحمد المحمودي",
"Khaled",
- "Fatz"
+ "Fatz",
+ "Hiba Alshawi"
]
},
"apihelp-main-param-format": "صيغة الخرج.",
"apihelp-edit-param-watch": "أضف الصفحة إلى لائحة مراقبة المستعمل الحالي",
"apihelp-emailuser-description": "مراسلة المستخدم",
"apihelp-patrol-example-rcid": "ابحث عن تغيير جديد",
+ "apihelp-query+imageinfo-paramvalue-prop-userid": "إضافة هوية المستخدم الذي قام بتحميل كل إصدار ملف.",
"apihelp-query+prefixsearch-param-offset": "عدد النتائج المراد تخطيها."
}
"Purodha",
"Andreasburmeister",
"Anomie",
- "Duder"
+ "Duder",
+ "Ljonka"
]
},
"apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page/de|Dokumentation]]\n* [[mw:API:FAQ/de|Häufig gestellte Fragen]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Mailingliste]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-Ankündigungen]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Fehlerberichte und Anfragen]\n</div>\n<strong>Status:</strong> Alle auf dieser Seite gezeigten Funktionen sollten funktionieren, allerdings ist die API in aktiver Entwicklung und kann sich zu jeder Zeit ändern. Abonniere die [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ MediaWiki-API-Ankündigungs-Mailingliste], um über Aktualisierungen informiert zu werden.\n\n<strong>Fehlerhafte Anfragen:</strong> Wenn fehlerhafte Anfragen an die API gesendet werden, wird ein HTTP-Header mit dem Schlüssel „MediaWiki-API-Error“ gesendet. Der Wert des Headers und der Fehlercode werden auf den gleichen Wert gesetzt. Für weitere Informationen siehe [[mw:API:Errors_and_warnings|API: Fehler und Warnungen]].",
"apihelp-query+alldeletedrevisions-param-namespace": "Nur Seiten in diesem Namensraum auflisten.",
"apihelp-query+allfileusages-param-from": "Titel der Datei, bei der die Aufzählung beginnen soll.",
"apihelp-query+allfileusages-param-to": "Titel der Datei, bei der die Aufzählung enden soll.",
+ "apihelp-query+allfileusages-param-prop": "Informationsteile zum Einbinden:",
"apihelp-query+allfileusages-param-limit": "Wie viele Gesamtobjekte zurückgegeben werden sollen.",
"apihelp-query+allfileusages-param-dir": "Aufzählungsrichtung.",
"apihelp-query+allfileusages-example-unique": "Einheitliche Dateititel auflisten",
"apihelp-query+filearchive-param-from": "Der Bildertitel, bei dem die Auflistung beginnen soll.",
"apihelp-query+filearchive-param-to": "Der Bildertitel, bei dem die Auflistung enden soll.",
"apihelp-query+filearchive-param-limit": "Wie viele Bilder insgesamt zurückgegeben werden sollen.",
+ "apihelp-query+filearchive-paramvalue-prop-sha1": "Ergänzt die SHA-1-Prüfsumme für das Bild.",
"apihelp-query+filearchive-example-simple": "Eine Liste aller gelöschten Dateien auflisten",
"apihelp-query+imageinfo-param-limit": "Wie viele Dateiversionen pro Datei zurückgegeben werden sollen.",
"apihelp-query+imageinfo-param-start": "Zeitstempel, von dem die Liste beginnen soll.",
"apihelp-query+links-example-simple": "Links von der <kbd>Hauptseite</kbd> abrufen",
"apihelp-query+linkshere-description": "Alle Seiten finden, die auf die angegebenen Seiten verlinken.",
"apihelp-query+logevents-description": "Ereignisse von den Logbüchern abrufen.",
+ "apihelp-query+pageswithprop-paramvalue-prop-ids": "Fügt die Seitenkennung hinzu.",
"apihelp-query+prefixsearch-param-search": "Such-Zeichenfolge.",
+ "apihelp-query+search-param-prop": "Eigenschaften zur Rückgabe:",
"apihelp-query+search-example-simple": "Nach <kbd>meaning</kbd> suchen.",
"apihelp-query+search-example-text": "Texte nach <kbd>meaning</kbd> durchsuchen.",
"apihelp-query+siteinfo-example-simple": "Websiteinformationen abrufen",
"apihelp-query+tags-description": "Änderungs-Tags auflisten.",
"apihelp-query+tags-example-simple": "Verfügbare Tags auflisten",
"apihelp-query+usercontribs-description": "Alle Bearbeitungen von einem Benutzer abrufen.",
+ "apihelp-query+usercontribs-paramvalue-prop-ids": "Ergänzt die Seiten- und Versionskennung.",
+ "apihelp-query+userinfo-paramvalue-prop-realname": "Fügt den bürgerlichen Namen des Benutzers hinzu.",
"apihelp-query+userinfo-example-simple": "Informationen über den aktuellen Benutzer abrufen",
"apihelp-query+users-description": "Informationen über eine Liste von Benutzern abrufen.",
"apihelp-rsd-description": "Ein RSD-Schema (Really Simple Discovery) exportieren.",
"apihelp-query+imageinfo-param-prop": "Which file information to get:",
"apihelp-query+imageinfo-paramvalue-prop-timestamp": "Adds timestamp for the uploaded version.",
"apihelp-query+imageinfo-paramvalue-prop-user": "Adds the user who uploaded each file version.",
- "apihelp-query+imageinfo-paramvalue-prop-userid": "Add the user ID that uploaded each file version.",
+ "apihelp-query+imageinfo-paramvalue-prop-userid": "Add the ID of the user that uploaded each file version.",
"apihelp-query+imageinfo-paramvalue-prop-comment": "Comment on the version.",
"apihelp-query+imageinfo-paramvalue-prop-parsedcomment": "Parse the comment on the version.",
"apihelp-query+imageinfo-paramvalue-prop-canonicaltitle": "Adds the canonical title of the file.",
"apihelp-query+logevents-paramvalue-prop-details": "Lists additional details about the log event.",
"apihelp-query+logevents-paramvalue-prop-tags": "Lists tags for the log event.",
"apihelp-query+logevents-param-type": "Filter log entries to only this type.",
- "apihelp-query+logevents-param-action": "Filter log actions to only this action. Overrides <var>$1type</var>. Wildcard actions like <kbd>action/*</kbd> allows to specify any string for the asterisk.",
+ "apihelp-query+logevents-param-action": "Filter log actions to only this action. Overrides <var>$1type</var>. In the list of possible values, values with the asterisk wildcard such as <kbd>action/*</kbd> can have different strings after the slash (/).",
"apihelp-query+logevents-param-start": "The timestamp to start enumerating from.",
"apihelp-query+logevents-param-end": "The timestamp to end enumerating.",
"apihelp-query+logevents-param-user": "Filter entries to those made by the given user.",
"apihelp-query+querypage-param-limit": "Number of results to return.",
"apihelp-query+querypage-example-ancientpages": "Return results from [[Special:Ancientpages]].",
- "apihelp-query+random-description": "Get a set of random pages.\n\nPages are listed in a fixed sequence, only the starting point is random. This means that if, for example, <samp>Main Page</samp> is the first random page in the list, <samp>List of fictional monkeys</samp> will <em>always</em> be second, <samp>List of people on stamps of Vanuatu</samp> third, etc.\n\nIf the number of pages in the namespace is lower than <var>$1limit</var>, fewer pages will be returned. The same page will not be returned twice.",
+ "apihelp-query+random-description": "Get a set of random pages.\n\nPages are listed in a fixed sequence, only the starting point is random. This means that if, for example, <samp>Main Page</samp> is the first random page in the list, <samp>List of fictional monkeys</samp> will <em>always</em> be second, <samp>List of people on stamps of Vanuatu</samp> third, etc.",
"apihelp-query+random-param-namespace": "Return pages in these namespaces only.",
"apihelp-query+random-param-limit": "Limit how many random pages will be returned.",
- "apihelp-query+random-param-redirect": "Load a random redirect instead of a random page.",
+ "apihelp-query+random-param-redirect": "Use <kbd>$1filterredir=redirects</kbd> instead.",
+ "apihelp-query+random-param-filterredir": "How to filter for redirects.",
"apihelp-query+random-example-simple": "Return two random pages from the main namespace.",
"apihelp-query+random-example-generator": "Return page info about two random pages from the main namespace.",
"api-help-param-type-password": "",
"api-help-param-type-timestamp": "Type: {{PLURAL:$1|1=timestamp|2=list of timestamps}} ([[Special:ApiHelp/main#main/datatypes|allowed formats]])",
"api-help-param-type-user": "Type: {{PLURAL:$1|1=user name|2=list of user names}}",
- "api-help-param-list": "{{PLURAL:$1|1=One value|2=Values (separate with <kbd>{{!}}</kbd>)}}: $2",
+ "api-help-param-list": "{{PLURAL:$1|1=One of the following values|2=Values (separate with <kbd>{{!}}</kbd>)}}: $2",
"api-help-param-list-can-be-empty": "{{PLURAL:$1|0=Must be empty|Can be empty, or $2}}",
"api-help-param-limit": "No more than $1 allowed.",
"api-help-param-limit2": "No more than $1 ($2 for bots) allowed.",
"apihelp-query+categoryinfo-description": "Devuelve información acerca de las categorías dadas.",
"apihelp-query+categoryinfo-example-simple": "Obtener información acerca de <kbd>Category:Foo&l