* Variables defined in extensions will override conflicting variables defined
* in the core.
*
+ * Since MediaWiki 1.23, use of this variable to define messages is discouraged; instead, store
+ * messages in JSON format and use $wgMessagesDirs. For setting other variables than
+ * $messages, $wgExtensionMessagesFiles should still be used.
+ *
+ * If there is an entry in $wgMessagesDirs with the same key as one in
+ * $wgExtensionMessagesFiles, then any $messages variables set in the $wgExtensionMessagesFiles file
+ * will be ignored. This means an extension that only provides messages can be backwards compatible
+ * by using both $wgExtensionMessagesFiles and $wgMessagesDirs, and only one of the two
+ * will be used depending on what the version of MediaWiki supports.
+ *
* @par Example:
* @code
* $wgExtensionMessagesFiles['ConfirmEdit'] = __DIR__.'/ConfirmEdit.i18n.php';
*/
$wgExtensionMessagesFiles = array();
+/**
+ * Extension messages directories.
+ *
+ * Associative array mapping extension name to the path of the directory where message files can
+ * be found. The message files are expected to be JSON files named for their language code, e.g.
+ * en.json, de.json, etc. Extensions with messages in multiple places may specify an array of
+ * message directories.
+ *
+ * @par Simple example:
+ * @code
+ * $wgMessagesDirs['ConfirmEdit'] = __DIR__ . '/i18n';
+ * @endcode
+ *
+ * @par Complex example:
+ * @code
+ * $wgMessagesDirs['VisualEditor'] = array(
+ * __DIR__ . '/i18n',
+ * __DIR__ . '/modules/ve-core/i18n',
+ * __DIR__ . '/modules/qunit/localisation',
+ * __DIR__ . '/modules/oojs-ui/messages',
+ * )
+ * @endcode
+ * @since 1.23
+ */
+$wgMessagesDirs = array();
+
/**
* Array of files with list(s) of extension entry points to be used in
* maintenance/mergeMessageFileList.php
wfRunHooks( 'CanIPUseHTTPS', array( $ip, &$canDo ) );
return !!$canDo;
}
+
+/**
+ * Work out the IP address based on various globals
+ * For trusted proxies, use the XFF client IP (first of the chain)
+ *
+ * @deprecated in 1.19; call $wgRequest->getIP() directly.
+ * @return string
+ */
+function wfGetIP() {
+ wfDeprecated( __METHOD__, '1.19' );
+ global $wgRequest;
+ return $wgRequest->getIP();
+}
+
+/**
+ * Checks if an IP is a trusted proxy provider.
+ * Useful to tell if X-Forwarded-For data is possibly bogus.
+ * Squid cache servers for the site are whitelisted.
+ *
+ * @param $ip String
+ * @return bool
+ */
+function wfIsTrustedProxy( $ip ) {
+ $trusted = wfIsConfiguredProxy( $ip );
+ wfRunHooks( 'IsTrustedProxy', array( &$ip, &$trusted ) );
+ return $trusted;
+}
+
+/**
+ * Checks if an IP matches a proxy we've configured.
+ * @param $ip String
+ * @return bool
+ * @since 1.23 Supports CIDR ranges in $wgSquidServersNoPurge
+ */
+function wfIsConfiguredProxy( $ip ) {
+ global $wgSquidServers, $wgSquidServersNoPurge;
+
+ // quick check of known proxy servers
+ $trusted = in_array( $ip, $wgSquidServers )
+ || in_array( $ip, $wgSquidServersNoPurge );
+
+ if ( !$trusted ) {
+ // slightly slower check to see if the ip is listed directly or in a CIDR
+ // block in $wgSquidServersNoPurge
+ foreach ( $wgSquidServersNoPurge as $block ) {
+ if ( strpos( $block, '/' ) !== false && IP::isInRange( $ip, $block ) ) {
+ $trusted = true;
+ break;
+ }
+ }
+ }
+ return $trusted;
+}
+++ /dev/null
-<?php
-/**
- * Functions for dealing with proxies.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * Extracts the XFF string from the request header
- * Note: headers are spoofable
- *
- * @deprecated in 1.19; use $wgRequest->getHeader( 'X-Forwarded-For' ) instead.
- * @return string
- */
-function wfGetForwardedFor() {
- wfDeprecated( __METHOD__, '1.19' );
- global $wgRequest;
- return $wgRequest->getHeader( 'X-Forwarded-For' );
-}
-
-/**
- * Returns the browser/OS data from the request header
- * Note: headers are spoofable
- *
- * @deprecated in 1.18; use $wgRequest->getHeader( 'User-Agent' ) instead.
- * @return string
- */
-function wfGetAgent() {
- wfDeprecated( __METHOD__, '1.18' );
- global $wgRequest;
- return $wgRequest->getHeader( 'User-Agent' );
-}
-
-/**
- * Work out the IP address based on various globals
- * For trusted proxies, use the XFF client IP (first of the chain)
- *
- * @deprecated in 1.19; call $wgRequest->getIP() directly.
- * @return string
- */
-function wfGetIP() {
- wfDeprecated( __METHOD__, '1.19' );
- global $wgRequest;
- return $wgRequest->getIP();
-}
-
-/**
- * Checks if an IP is a trusted proxy provider.
- * Useful to tell if X-Forwarded-For data is possibly bogus.
- * Squid cache servers for the site are whitelisted.
- *
- * @param $ip String
- * @return bool
- */
-function wfIsTrustedProxy( $ip ) {
- $trusted = wfIsConfiguredProxy( $ip );
- wfRunHooks( 'IsTrustedProxy', array( &$ip, &$trusted ) );
- return $trusted;
-}
-
-/**
- * Checks if an IP matches a proxy we've configured.
- * @param $ip String
- * @return bool
- * @since 1.23 Supports CIDR ranges in $wgSquidServersNoPurge
- */
-function wfIsConfiguredProxy( $ip ) {
- global $wgSquidServers, $wgSquidServersNoPurge;
-
- // quick check of known proxy servers
- $trusted = in_array( $ip, $wgSquidServers )
- || in_array( $ip, $wgSquidServersNoPurge );
-
- if ( !$trusted ) {
- // slightly slower check to see if the ip is listed directly or in a CIDR
- // block in $wgSquidServersNoPurge
- foreach ( $wgSquidServersNoPurge as $block ) {
- if ( strpos( $block, '/' ) !== false && IP::isInRange( $ip, $block ) ) {
- $trusted = true;
- break;
- }
- }
- }
- return $trusted;
-}
wfProfileIn( $fname . '-includes' );
require_once "$IP/includes/normal/UtfNormalUtil.php";
require_once "$IP/includes/GlobalFunctions.php";
-require_once "$IP/includes/ProxyTools.php";
require_once "$IP/includes/normal/UtfNormalDefines.php";
wfProfileOut( $fname . '-includes' );
$this->addWhereFld( 'page_namespace', $namespace );
$this->addWhereRange( 'page_random', 'newer', $randstr, null );
$this->addWhereFld( 'page_is_redirect', $redirect );
- $this->addOption( 'USE INDEX', 'page_random' );
if ( is_null( $resultPageSet ) ) {
$this->addFields( array( 'page_id', 'page_title', 'page_namespace' ) );
} else {
return $data;
}
+ /**
+ * Read a JSON file containing localisation messages.
+ * @param string $fileName Name of file to read
+ * @throws MWException if there is a syntax error in the JSON file
+ * @return array with a 'messages' key, or empty array if the file doesn't exist
+ */
+ protected function readJSONFile( $fileName ) {
+ wfProfileIn( __METHOD__ );
+ if ( !is_readable( $fileName ) ) {
+ return array();
+ }
+
+ $json = file_get_contents( $fileName );
+ if ( $json === false ) {
+ return array();
+ }
+ $data = FormatJson::decode( $json, true );
+ if ( $data === null ) {
+ throw new MWException( __METHOD__ . ": Invalid JSON file: $fileName" );
+ }
+ // Remove keys starting with '@', they're reserved for metadata and non-message data
+ foreach ( $data as $key => $unused ) {
+ if ( $key === '' || $key[0] === '@' ) {
+ unset( $data[$key] );
+ }
+ }
+ // The JSON format only supports messages, none of the other variables, so wrap the data
+ return array( 'messages' => $data );
+ }
+
/**
* Get the compiled plural rules for a given language from the XML files.
* @since 1.20
* @throws MWException
*/
public function recache( $code ) {
- global $wgExtensionMessagesFiles;
+ global $wgExtensionMessagesFiles, $wgMessagesDirs;
wfProfileIn( __METHOD__ );
if ( !$code ) {
# like site-specific message overrides.
wfProfileIn( __METHOD__ . '-extensions' );
$allData = $initialData;
- foreach ( $wgExtensionMessagesFiles as $fileName ) {
+ foreach ( $wgMessagesDirs as $dirs ) {
+ foreach ( (array)$dirs as $dir ) {
+ foreach ( $codeSequence as $csCode ) {
+ $fileName = "$dir/$csCode.json";
+ $data = $this->readJSONFile( $fileName );
+
+ foreach ( $data as $key => $item ) {
+ $this->mergeItem( $key, $allData[$key], $item );
+ }
+
+ $deps[] = new FileDependency( $fileName );
+ }
+ }
+ }
+
+ foreach ( $wgExtensionMessagesFiles as $extension => $fileName ) {
$data = $this->readPHPFile( $fileName, 'extension' );
$used = false;
foreach ( $data as $key => $item ) {
+ if ( $key === 'messages' && isset( $wgMessagesDirs[$extension] ) ) {
+ # For backwards compatibility, ignore messages from extensions in
+ # $wgExtensionMessagesFiles that are also present in $wgMessagesDirs.
+ # This allows extensions to use both and be backwards compatible.
+ # Variables other than $messages still need to be supported though.
+ continue;
+ }
if ( $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item ) ) {
$used = true;
}
# Add cache dependencies for any referenced globals
$deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' );
+ $deps['wgMessagesDirs'] = new GlobalDependency( 'wgMessagesDirs' );
$deps['version'] = new ConstantDependency( 'MW_LC_VERSION' );
# Add dependencies to the cache entry
if ( $text === null || $text === false ) {
wfWarn( "TextContent constructed with \$text = " . var_export( $text, true ) . "! "
- . "This may indicate an error in the caller's scope." );
+ . "This may indicate an error in the caller's scope.", 2 );
$text = '';
}
*/
public function pop( $qtype = self::TYPE_DEFAULT, $flags = 0 ) {
if ( is_string( $qtype ) ) { // specific job type
- if ( ( $flags & self::USE_PRIORITY ) && $this->isQueueDeprioritized( $qtype ) ) {
- return false; // back off
- }
$job = $this->get( $qtype )->pop();
if ( !$job ) {
JobQueueAggregator::singleton()->notifyQueueEmpty( $this->wiki, $qtype );
shuffle( $types ); // avoid starvation
foreach ( $types as $type ) { // for each queue...
- if ( ( $flags & self::USE_PRIORITY ) && $this->isQueueDeprioritized( $type ) ) {
- continue; // back off
- }
$job = $this->get( $type )->pop();
if ( $job ) { // found
return $job;
return $this->coalescedQueues;
}
- /**
- * Check if jobs should not be popped of a queue right now.
- * This is only used for performance, such as to avoid spamming
- * the queue with many sub-jobs before they actually get run.
- *
- * @param string $type
- * @return bool
- */
- public function isQueueDeprioritized( $type ) {
- if ( $this->cache->has( 'isDeprioritized', $type, 5 ) ) {
- return $this->cache->get( 'isDeprioritized', $type );
- }
- if ( $type === 'refreshLinks2' ) {
- // Don't keep converting refreshLinksPartition => refreshLinks jobs if the
- // later jobs have not been done yet. This helps throttle queue spam.
- // @TODO: this is mostly a WMF-specific hack and should be removed when
- // refreshLinks2 jobs are drained.
- $deprioritized = $this->get( 'refreshLinks' )->getSize() > 10000;
- $this->cache->set( 'isDeprioritized', $type, $deprioritized );
-
- return $deprioritized;
- }
-
- return false;
- }
-
/**
* Execute any due periodic queue maintenance tasks for all queues.
*
), $this->extra ),
'options' => array(
'ORDER BY' => 'page_random',
- 'USE INDEX' => 'page_random',
'LIMIT' => 1,
),
'join_conds' => array()
) );
}
+ /**
+ * Handle Special:Redirect/page/xxx (by redirecting to index.php?curid=xxx)
+ *
+ * @return string|null url to redirect to, or null if $mValue is invalid.
+ */
+ function dispatchPage() {
+ $curid = $this->mValue;
+ if ( !ctype_digit( $curid ) ) {
+ return null;
+ }
+ $curid = (int)$curid;
+ if ( $curid === 0 ) {
+ return null;
+ }
+ return wfAppendQuery( wfScript( 'index' ), array(
+ 'curid' => $curid
+ ) );
+ }
+
/**
* Use appropriate dispatch* method to obtain a redirection URL,
* and either: redirect, set a 404 error code and error message,
case 'revision':
$url = $this->dispatchRevision();
break;
+ case 'page':
+ $url = $this->dispatchPage();
+ break;
default:
$this->getOutput()->setStatusCode( 404 );
$url = null;
$mp = $this->getMessagePrefix();
$ns = array(
// subpage => message
- // Messages: redirect-user, redirect-revision, redirect-file
+ // Messages: redirect-user, redirect-page, redirect-revision,
+ // redirect-file
'user' => $mp . '-user',
+ 'page' => $mp . '-page',
'revision' => $mp . '-revision',
'file' => $mp . '-file',
);
/**
* Determine if an IP address really is an IP address, and if it is public,
* i.e. not RFC 1918 or similar
- * Comes from ProxyTools.php
*
* @param $ip String
* @return Boolean
/**
* Given an IP address in dotted-quad/octet notation, returns an unsigned integer.
* Like ip2long() except that it actually works and has a consistent error return value.
- * Comes from ProxyTools.php
*
* @param string $ip quad dotted IP address.
* @return Mixed: string/int/false
'version-entrypoints-load-php' => '[https://www.mediawiki.org/wiki/Manual:load.php load.php]', # do not translate or duplicate this message to other languages
# Special:Redirect
-'redirect' => 'Redirect by file, user, or revision ID',
+'redirect' => 'Redirect by file, user, page or revision ID',
'redirect-legend' => 'Redirect to a file or page',
'redirect-text' => '', # do not translate or duplicate this message to other languages
-'redirect-summary' => 'This special page redirects to a file (given the file name), a page (given a revision ID), or a user page (given a numeric user ID). Usage: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/revision/328429]], or [[{{#Special:Redirect}}/user/101]].',
+'redirect-summary' => 'This special page redirects to a file (given the file name), a page (given a revision ID or page ID), or a user page (given a numeric user ID). Usage: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], or [[{{#Special:Redirect}}/user/101]].',
'redirect-submit' => 'Go',
'redirect-lookup' => 'Lookup:',
'redirect-value' => 'Value:',
'redirect-user' => 'User ID',
'redirect-revision' => 'Page revision',
+'redirect-page' => 'Page ID',
'redirect-file' => 'File name',
'redirect-not-exists' => 'Value not found',
# Special:Redirect
'redirect' => "{{doc-special|Redirect}}
-This means \"Redirect by file'''name''', user '''ID''', or revision ID\".",
+This means \"Redirect by file'''name''', user '''ID''', page ID, or revision ID\".",
'redirect-legend' => 'Legend of fieldset around input box in [[Special:Redirect]]',
'redirect-summary' => 'Shown at top of [[Special:Redirect]]',
'redirect-submit' => 'Button label in [[Special:Redirect]].
Followed by the select box which has the following options:
* {{msg-mw|Redirect-user}}
+* {{msg-mw|Redirect-page}}
* {{msg-mw|Redirect-revision}}
* {{msg-mw|Redirect-file}}',
'redirect-value' => 'Second field label in [[Special:Redirect]]
{{Identical|Value}}',
'redirect-user' => 'Description of lookup type for [[Special:Redirect]].
{{Identical|User ID}}',
+'redirect-page' => 'Description of lookup type for [[Special:Redirect]].',
'redirect-revision' => "Description of lookup type for [[Special:Redirect]].
This means \"Page revision '''ID'''\".",
--- /dev/null
+<?php
+
+/**
+ * Convert a PHP messages file to a set of JSON messages files.
+ *
+ * Usage:
+ * php generateJsonI18n.php ExtensionName.i18n.php i18n/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Maintenance
+ */
+
+require_once __DIR__ . '/Maintenance.php';
+
+/**
+ * Maintenance script to generate JSON i18n files from a PHP i18n file.
+ *
+ * @ingroup Maintenance
+ */
+class GenerateJsonI18n extends Maintenance {
+ public function __construct() {
+ parent::__construct();
+ $this->mDescription = "Build JSON messages files from a PHP messages file";
+ $this->addArg( 'phpfile', 'PHP file defining a $messages array', true );
+ $this->addArg( 'jsondir', 'Directory to write JSON files to', true );
+ $this->addOption( 'langcode', 'Language code; only needed for converting core i18n files',
+ false, true );
+ }
+
+ public function execute() {
+ $phpfile = $this->getArg( 0 );
+ $jsondir = $this->getArg( 1 );
+
+ if ( !is_readable( $phpfile ) ) {
+ $this->error( "Error reading $phpfile\n", 1 );
+ }
+ include $phpfile;
+ $phpfileContents = file_get_contents( $phpfile );
+
+ if ( !isset( $messages ) ) {
+ $this->error( "PHP file $phpfile does not define \$messages array\n", 1 );
+ }
+
+ $extensionStyle = true;
+ if ( !isset( $messages['en'] ) || !is_array( $messages['en'] ) ) {
+ if ( !$this->hasOption( 'langcode' ) ) {
+ $this->error( "PHP file $phpfile does not set language codes, --langcode " .
+ "is required.\n", 1 );
+ }
+ $extensionStyle = false;
+ $langcode = $this->getOption( 'langcode' );
+ $messages = array( $langcode => $messages );
+ } else if ( $this->hasOption( 'langcode' ) ) {
+ $this->output( "Warning: --langcode option set but will not be used.\n" );
+ }
+
+ foreach ( $messages as $langcode => $langmsgs ) {
+ $authors = $this->getAuthorsFromComment( $this->findCommentBefore(
+ $extensionStyle ? "\$messages['$langcode'] =" : '$messages =',
+ $phpfileContents
+ ) );
+ // Make sure the @metadata key is the first key in the output
+ $langmsgs = array_merge(
+ array( '@metadata' => array( 'authors' => $authors ) ),
+ $langmsgs
+ );
+
+ $jsonfile = "$jsondir/$langcode.json";
+ $success = file_put_contents(
+ $jsonfile,
+ FormatJson::encode( $langmsgs, true, FormatJson::ALL_OK )
+ );
+ if ( $success === false ) {
+ $this->error( "FAILED to write $jsonfile", 1 );
+ }
+ $this->output( "$jsonfile\n" );
+ }
+ $this->output( "All done.\n" );
+ }
+
+ /**
+ * Find the documentation comment immediately before a given search string
+ * @param string $needle String to search for
+ * @param string $haystack String to search in
+ * @return string Substring of $haystack starting at '/**' ending right before $needle, or empty
+ */
+ protected function findCommentBefore( $needle, $haystack ) {
+ $needlePos = strpos( $haystack, $needle );
+ if ( $needlePos === false ) {
+ return '';
+ }
+ // Need to pass a negative offset to strrpos() so it'll search backwards from the
+ // offset
+ $startPos = strrpos( $haystack, '/**', $needlePos - strlen( $haystack ) );
+ if ( $startPos === false ) {
+ return '';
+ }
+
+ return substr( $haystack, $startPos, $needlePos - $startPos );
+ }
+
+ /**
+ * Get an array of author names from a documentation comment containing @author declarations.
+ * @param string $comment Documentation comment
+ * @return Array of author names (strings)
+ */
+ protected function getAuthorsFromComment( $comment ) {
+ $matches = null;
+ preg_match_all( '/@author (.*?)$/m', $comment, $matches );
+ return $matches && $matches[1] ? $matches[1] : array();
+ }
+}
+
+$maintClass = "GenerateJsonI18n";
+require_once RUN_MAINTENANCE_IF_MAIN;
'redirect-lookup',
'redirect-value',
'redirect-user',
+ 'redirect-page',
'redirect-revision',
'redirect-file',
'redirect-not-exists',
$this->addOption( 'list-file', 'A file containing a list of extension setup files, one per line.', false, true );
$this->addOption( 'extensions-dir', 'Path where extensions can be found.', false, true );
$this->addOption( 'output', 'Send output to this file (omit for stdout)', false, true );
- $this->mDescription = 'Merge $wgExtensionMessagesFiles from various extensions to produce a ' .
- 'single array containing all message files.';
+ $this->mDescription = 'Merge $wgExtensionMessagesFiles and $wgMessagesDirs from ' .
+ ' various extensions to produce a single file listing all message files and dirs.';
}
public function execute() {
"<" . "?php\n" .
"## This file is generated by mergeMessageFileList.php. Do not edit it directly.\n\n" .
"if ( defined( 'MW_NO_EXTENSION_MESSAGES' ) ) return;\n\n" .
- '$wgExtensionMessagesFiles = ' . var_export( $wgExtensionMessagesFiles, true ) . ";\n\n";
+ '$wgExtensionMessagesFiles = ' . var_export( $wgExtensionMessagesFiles, true ) . ";\n\n" .
+ '$wgMessagesDirs = ' . var_export( $wgMessagesDirs, true ) . ";\n\n";
$dirs = array(
$IP,
}
list( $type, $db ) = $candidates[mt_rand( 0, count( $candidates ) - 1 )];
- if ( JobQueueGroup::singleton( $db )->isQueueDeprioritized( $type ) ) {
- $pendingDBs[$type] = array_diff( $pendingDBs[$type], array( $db ) );
- $again = true;
- }
} while ( $again );
if ( $this->hasOption( 'types' ) ) {
} elseif ( $this->hasOption( 'group' ) ) {
foreach ( $group->getQueueTypes() as $type ) {
$queue = $group->get( $type );
+ $delayed = $queue->getDelayedCount();
$pending = $queue->getSize();
$claimed = $queue->getAcquiredCount();
$abandoned = $queue->getAbandonedCount();
$active = max( 0, $claimed - $abandoned );
- if ( ( $pending + $claimed ) > 0 ) {
+ if ( ( $pending + $claimed + $delayed ) > 0 ) {
$this->output(
"{$type}: $pending queued; " .
- "$claimed claimed ($active active, $abandoned abandoned)\n"
+ "$claimed claimed ($active active, $abandoned abandoned); " .
+ "$delayed delayed\n"
);
}
}
border: 1px solid #aaaaaa;
background-color: #f9f9f9;
padding: 5px;
+ display: -moz-inline-block;
+ display: inline-block;
+ display: table;
+ /* IE7 and earlier */
+ zoom: 1;
+ *display: inline;
}
/* images */