{
"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/**",
"mediaWiki": true,
"JSON": true,
"OO": true,
- "performance": true,
+ "mwPerformance": true,
"jQuery": false,
"QUnit": false,
"sinon": false
#
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: {
]
},
banana: {
+ options: {
+ disallowBlankTranslations: false,
+ disallowDuplicateTranslations: false,
+ disallowUnusedTranslations: false
+ },
core: 'languages/i18n/',
api: 'includes/api/i18n/',
installer: 'includes/installer/i18n/'
}
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 ===
* ChangeTags::tagDescription() will return false if the interface message
for the tag is disabled.
* Added PageHistoryPager::doBatchLookups hook.
+* Added $wikiId parameter to FormatAutocomments hook.
* Added ParserCacheSaveComplete to ParserCache
* supportsDirectEditing and supportsDirectApiEditing methods added to
ContentHandler, to provide a way for ApiEditPage and EditPage to check
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\\ComplexTitleInputWidget' => __DIR__ . '/includes/widget/ComplexTitleInputWidget.php',
'MediaWiki\\Widget\\NamespaceInputWidget' => __DIR__ . '/includes/widget/NamespaceInputWidget.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"],
$post: Boolean, true if there is text after this autocomment
$title: An optional title object used to links to sections. Can be null.
$local: Boolean indicating whether section links should refer to local page.
+$wikiId: String containing the ID (as used by WikiMap) of the wiki from which the
+ autocomment originated; null for the local wiki. Added in 1.26, should default
+ to null in handler functions, for backwards compatibility.
'GalleryGetModes': Get list of classes that can render different modes of a
gallery.
<html lang="en" dir="ltr">
<head>
<link rel="stylesheet" href="../../resources/src/mediawiki.legacy/shared.css">
+ <link rel="stylesheet" href="../../resources/src/mediawiki/mediawiki.feedlink.css">
</head>
<body style="font-size: small;">
/**
* 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;
}
* @param string $comment
* @param Title|null $title Title object (to generate link to the section in autocomment) or null
* @param bool $local Whether section links should refer to local page
+ * @param string|null $wikiId Id (as used by WikiMap) of the wiki to generate links to. For use with external changes.
+ *
* @return mixed|string
*/
- public static function formatComment( $comment, $title = null, $local = false ) {
+ public static function formatComment( $comment, $title = null, $local = false, $wikiId = null ) {
# Sanitize text a bit:
$comment = str_replace( "\n", " ", $comment );
$comment = Sanitizer::escapeHtmlAllowEntities( $comment );
# Render autocomments and make links:
- $comment = self::formatAutocomments( $comment, $title, $local );
- $comment = self::formatLinksInComment( $comment, $title, $local );
+ $comment = self::formatAutocomments( $comment, $title, $local, $wikiId );
+ $comment = self::formatLinksInComment( $comment, $title, $local, $wikiId );
return $comment;
}
* @param string $comment Comment text
* @param Title|null $title An optional title object used to links to sections
* @param bool $local Whether section links should refer to local page
- * @return string Formatted comment
+ * @param string|null $wikiId Id of the wiki to link to (if not the local wiki), as used by WikiMap.
+ *
+ * @return string Formatted comment (wikitext)
*/
- private static function formatAutocomments( $comment, $title = null, $local = false ) {
+ private static function formatAutocomments( $comment, $title = null, $local = false, $wikiId = null ) {
// @todo $append here is something of a hack to preserve the status
// quo. Someone who knows more about bidi and such should decide
// (1) what sane rendering even *is* for an LTR edit summary on an RTL
// zero-width assertions optional, so wrap them in a non-capturing
// group.
'!(?:(?<=(.)))?/\*\s*(.*?)\s*\*/(?:(?=(.)))?!',
- function ( $match ) use ( $title, $local, &$append ) {
+ function ( $match ) use ( $title, $local, $wikiId, &$append ) {
global $wgLang;
// Ensure all match positions are defined
$auto = $match[2];
$post = $match[3] !== '';
$comment = null;
- Hooks::run( 'FormatAutocomments', array( &$comment, $pre, $auto, $post, $title, $local ) );
+ Hooks::run( 'FormatAutocomments', array( &$comment, $pre, $auto, $post, $title, $local, $wikiId ) );
if ( $comment === null ) {
$link = '';
if ( $title ) {
$title->getDBkey(), $section );
}
if ( $sectionTitle ) {
- $link = Linker::link( $sectionTitle,
- $wgLang->getArrow(), array(), array(),
- 'noclasses' );
+ $link = Linker::makeCommentLink( $sectionTitle, $wgLang->getArrow(), $wikiId, 'noclasses' );
} else {
$link = '';
}
* @param string $comment Text to format links in
* @param Title|null $title An optional title object used to links to sections
* @param bool $local Whether section links should refer to local page
- * @param string|null $wikiId Id of the wiki to link to (if not the local wiki), as used by WikiMap
+ * @param string|null $wikiId Id of the wiki to link to (if not the local wiki), as used by WikiMap.
*
* @return string
*/
$newTarget = clone ( $title );
$newTarget->setFragment( '#' . $target->getFragment() );
$target = $newTarget;
-
- }
-
- if ( $wikiId !== null ) {
- $thelink = Linker::makeExternalLink(
- WikiMap::getForeignURL( $wikiId, $target->getFullText() ),
- $linkText . $inside,
- /* escape = */ false // Already escaped
- ) . $trail;
- } else {
- $thelink = Linker::link(
- $target,
- $linkText . $inside
- ) . $trail;
}
+ $thelink = Linker::makeCommentLink( $target, $linkText . $inside, $wikiId ) . $trail;
}
}
if ( $thelink ) {
);
}
+ /**
+ * Generates a link to the given Title
+ *
+ * @note This is only public for technical reasons. It's not intended for use outside Linker.
+ *
+ * @param Title $title
+ * @param string $text
+ * @param string|null $wikiId Id of the wiki to link to (if not the local wiki), as used by WikiMap.
+ * @param string|string[] $options See the $options parameter in Linker::link.
+ *
+ * @return string HTML link
+ */
+ public static function makeCommentLink( Title $title, $text, $wikiId = null, $options = array() ) {
+ if ( $wikiId !== null && !$title->isExternal() ) {
+ $link = Linker::makeExternalLink(
+ WikiMap::getForeignURL( $wikiId, $title->getPrefixedText(), $title->getFragment() ),
+ $text,
+ /* escape = */ false // Already escaped
+ );
+ } else {
+ $link = Linker::link( $title, $text, array(), array(), $options );
+ }
+
+ return $link;
+ }
+
/**
* @param Title $contextTitle
* @param string $target
* @param string $comment
* @param Title|null $title Title object (to generate link to section in autocomment) or null
* @param bool $local Whether section links should refer to local page
+ * @param string|null $wikiId Id (as used by WikiMap) of the wiki to generate links to. For use with external changes.
*
* @return string
*/
- public static function commentBlock( $comment, $title = null, $local = false ) {
+ public static function commentBlock( $comment, $title = null, $local = false, $wikiId = null ) {
// '*' used to be the comment inserted by the software way back
// in antiquity in case none was provided, here for backwards
// compatibility, acc. to brion -ævar
if ( $comment == '' || $comment == '*' ) {
return '';
} else {
- $formatted = self::formatComment( $comment, $title, $local );
+ $formatted = self::formatComment( $comment, $title, $local, $wikiId );
$formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped();
return " <span class=\"comment\">$formatted</span>";
}
'title' => $tooltip
) );
}
+
}
/**
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 ) ) {
Hooks::run( 'UserSaveSettings', array( $this ) );
$this->clearSharedCache();
$this->getUserPage()->invalidateCache();
-
- // T95839: clear the cache again post-commit to reduce race conditions
- // where stale values are written back to the cache by other threads.
- // Note: this *still* doesn't deal with REPEATABLE-READ snapshot lag...
- $that = $this;
- $dbw->onTransactionIdle( function() use ( $that ) {
- $that->clearSharedCache();
- } );
}
/**
$this->protocol = $protocol;
}
- /**
- * @param string $method
- * @throws MWException
- */
- private function notImplemented( $method ) {
- throw new MWException( "{$method}() not implemented" );
- }
-
/**
* @param string $name
* @param string $default
return $this->protocol;
}
- private function initHeaders() {
- return;
- }
-
/**
* @param string $name
* @param string $val
*
* @param string $wikiID Wiki'd id (generally database name)
* @param string $page Page name (must be normalised before calling this function!)
+ * @param string|null $fragmentId
+ *
* @return string|bool URL or false if the wiki was not found
*/
- public static function getForeignURL( $wikiID, $page ) {
+ public static function getForeignURL( $wikiID, $page, $fragmentId = null ) {
$wiki = WikiMap::getWiki( $wikiID );
if ( $wiki ) {
- return $wiki->getFullUrl( $page );
+ return $wiki->getFullUrl( $page, $fragmentId );
}
return false;
$this->mServer = $server === null ? $canonicalServer : $server;
}
- /**
- * @return string
- * @throws MWException
- */
- public function getHostname() {
- $prefixes = array( 'http://', 'https://' );
- foreach ( $prefixes as $prefix ) {
- if ( substr( $this->mCanonicalServer, 0, strlen( $prefix ) ) ) {
- return substr( $this->mCanonicalServer, strlen( $prefix ) );
- }
- }
- throw new MWException( "Invalid hostname for wiki {$this->mMinor}.{$this->mMajor}" );
- }
-
/**
* Get the URL in a way to be displayed to the user
* More or less Wikimedia specific
* @return string
*/
public function getDisplayName() {
- $url = $this->getUrl( '' );
- $parsed = wfParseUrl( $url );
+ $parsed = wfParseUrl( $this->mCanonicalServer );
if ( $parsed ) {
return $parsed['host'];
} else {
- // Invalid URL. There's no sane thing to do here, so just return it
- return $url;
+ // Invalid server spec. There's no sane thing to do here, so just return the canonical server name in full
+ return $this->mCanonicalServer;
}
}
* Helper function for getUrl()
*
* @todo FIXME: This may be generalized...
- * @param string $page Page name (must be normalised before calling this function!)
- * @return string Url fragment
+ *
+ * @param string $page Page name (must be normalised before calling this function! May contain a section part.)
+ * @param string|null $fragmentId
+ *
+ * @return string relative URL, without the server part.
*/
- private function getLocalUrl( $page ) {
- return str_replace( '$1', wfUrlEncode( str_replace( ' ', '_', $page ) ), $this->mPath );
+ private function getLocalUrl( $page, $fragmentId = null ) {
+ $page = wfUrlEncode( str_replace( ' ', '_', $page ) );
+
+ if ( is_string( $fragmentId ) && $fragmentId !== '' ) {
+ $page .= '#' . wfUrlEncode( $fragmentId );
+ }
+
+ return str_replace( '$1', $page, $this->mPath );
}
/**
* Get a canonical (i.e. based on $wgCanonicalServer) URL to a page on this foreign wiki
*
* @param string $page Page name (must be normalised before calling this function!)
+ * @param string|null $fragmentId
+ *
* @return string Url
*/
- public function getCanonicalUrl( $page ) {
- return $this->mCanonicalServer . $this->getLocalUrl( $page );
+ public function getCanonicalUrl( $page, $fragmentId = null ) {
+ return $this->mCanonicalServer . $this->getLocalUrl( $page, $fragmentId );
}
/**
/**
* Alias for getCanonicalUrl(), for backwards compatibility.
* @param string $page
+ * @param string|null $fragmentId
+ *
* @return string
*/
- public function getUrl( $page ) {
- return $this->getCanonicalUrl( $page );
+ public function getUrl( $page, $fragmentId = null ) {
+ return $this->getCanonicalUrl( $page, $fragmentId );
}
/**
* when called locally on the wiki.
*
* @param string $page Page name (must be normalized before calling this function!)
+ * @param string|null $fragmentId
+ *
* @return string URL
*/
- public function getFullUrl( $page ) {
+ public function getFullUrl( $page, $fragmentId = null ) {
return $this->mServer .
- $this->getLocalUrl( $page );
+ $this->getLocalUrl( $page, $fragmentId );
}
}
}
/**
- * @param string $default
+ * @param string|array $default
*/
public function setDefault( $default ) {
$this->default = $default;
* label => ( label => value, label => value )
*
* @param array $options
- * @param string $default
+ * @param string|array $default
* @return string
*/
static function formatOptions( $options, $default = false ) {
$contents = self::formatOptions( $value, $default );
$data .= Html::rawElement( 'optgroup', array( 'label' => $label ), $contents ) . "\n";
} else {
- $data .= Xml::option( $label, $value, $value === $default ) . "\n";
+ // If $default is an array, then the <select> probably has the multiple attribute,
+ // so we should check if each $value is in $default, rather than checking if
+ // $value is equal to $default.
+ $selected = is_array( $default ) ? in_array( $value, $default ) : $value === $default;
+ $data .= Xml::option( $label, $value, $selected ) . "\n";
}
}
'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' );
&