summaryrefslogtreecommitdiff
path: root/includes
diff options
context:
space:
mode:
Diffstat (limited to 'includes')
-rw-r--r--includes/DefaultSettings.php20
-rw-r--r--includes/GlobalFunctions.php5
-rw-r--r--includes/HtmlFormatter.php19
-rw-r--r--includes/HttpFunctions.php105
-rw-r--r--includes/MWNamespace.php2
-rw-r--r--includes/Setup.php15
-rw-r--r--includes/User.php27
-rw-r--r--includes/api/ApiQuerySiteinfo.php1
-rw-r--r--includes/api/ApiStashEdit.php63
-rw-r--r--includes/api/ApiUpload.php88
-rw-r--r--includes/filerepo/FileRepo.php6
-rw-r--r--includes/filerepo/ForeignAPIRepo.php5
-rw-r--r--includes/media/Bitmap.php2
-rw-r--r--includes/registration/ExtensionProcessor.php41
-rw-r--r--includes/registration/ExtensionRegistry.php84
-rw-r--r--includes/revisiondelete/RevDelList.php19
-rw-r--r--includes/specials/SpecialConfirmemail.php4
-rw-r--r--includes/specials/SpecialRevisiondelete.php3
-rw-r--r--includes/specials/SpecialUpload.php8
-rw-r--r--includes/upload/UploadBase.php10
20 files changed, 430 insertions, 97 deletions
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index 75ed529e..c13aa5f4 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -75,7 +75,7 @@ $wgConfigRegistry = array(
* MediaWiki version number
* @since 1.2
*/
-$wgVersion = '1.25.2';
+$wgVersion = '1.25.3';
/**
* Name of the site. It must be changed in LocalSettings.php
@@ -703,6 +703,14 @@ $wgCopyUploadAsyncTimeout = false;
$wgMaxUploadSize = 1024 * 1024 * 100; # 100MB
/**
+ * Minimum upload chunk size, in bytes. When using chunked upload, non-final
+ * chunks smaller than this will be rejected. May be reduced based on the
+ * 'upload_max_filesize' or 'post_max_size' PHP settings.
+ * @since 1.26
+ */
+$wgMinUploadChunkSize = 1024; # 1KB
+
+/**
* Point the upload navigation link to an external URL
* Useful if you want to use a shared repository by default
* without disabling local uploads (use $wgEnableUploads = false for that).
@@ -3636,8 +3644,8 @@ $wgMetaNamespaceTalk = false;
* Additional namespaces. If the namespaces defined in Language.php and
* Namespace.php are insufficient, you can create new ones here, for example,
* to import Help files in other languages. You can also override the namespace
- * names of existing namespaces. Extensions developers should use
- * $wgCanonicalNamespaceNames.
+ * names of existing namespaces. Extensions should use the CanonicalNamespaces
+ * hook or extension.json.
*
* @warning Once you delete a namespace, the pages in that namespace will
* no longer be accessible. If you rename it, then you can access them through
@@ -5016,6 +5024,12 @@ $wgRateLimits = array(
'ip' => null, // for each anon and recent account
'subnet' => null, // ... within a /24 subnet in IPv4 or /64 in IPv6
),
+ 'upload' => array(
+ 'user' => null,
+ 'newbie' => null,
+ 'ip' => null,
+ 'subnet' => null,
+ ),
'move' => array(
'user' => null,
'newbie' => null,
diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php
index 45cd7ea5..ab3f019f 100644
--- a/includes/GlobalFunctions.php
+++ b/includes/GlobalFunctions.php
@@ -3860,12 +3860,13 @@ function wfMemoryLimit() {
* Converts shorthand byte notation to integer form
*
* @param string $string
+ * @param int $default Returned if $string is empty
* @return int
*/
-function wfShorthandToInteger( $string = '' ) {
+function wfShorthandToInteger( $string = '', $default = -1 ) {
$string = trim( $string );
if ( $string === '' ) {
- return -1;
+ return $default;
}
$last = $string[strlen( $string ) - 1];
$val = intval( $string );
diff --git a/includes/HtmlFormatter.php b/includes/HtmlFormatter.php
index b2926d17..221cefbb 100644
--- a/includes/HtmlFormatter.php
+++ b/includes/HtmlFormatter.php
@@ -63,7 +63,15 @@ class HtmlFormatter {
*/
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:
@@ -244,7 +252,14 @@ class HtmlFormatter {
) );
}
$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;
}
diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php
index 8e05f597..fa54487a 100644
--- a/includes/HttpFunctions.php
+++ b/includes/HttpFunctions.php
@@ -25,6 +25,8 @@
* @defgroup HTTP HTTP
*/
+use MediaWiki\Logger\LoggerFactory;
+
/**
* Various HTTP related functions
* @ingroup HTTP
@@ -73,11 +75,14 @@ class Http {
$req = MWHttpRequest::factory( $url, $options, $caller );
$status = $req->execute();
- $content = false;
if ( $status->isOK() ) {
- $content = $req->getContent();
+ return $req->getContent();
+ } else {
+ $errors = $status->getErrorsByType( 'error' );
+ $logger = LoggerFactory::getInstance( 'http' );
+ $logger->warning( $status->getWikiText(), array( 'caller' => $caller ) );
+ return false;
}
- return $content;
}
/**
@@ -850,6 +855,8 @@ class CurlHttpRequest extends MWHttpRequest {
class PhpHttpRequest extends MWHttpRequest {
+ private $fopenErrors = array();
+
/**
* @param string $url
* @return string
@@ -860,6 +867,60 @@ class PhpHttpRequest extends MWHttpRequest {
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();
@@ -912,16 +973,16 @@ class PhpHttpRequest extends MWHttpRequest {
}
if ( $this->sslVerifyHost ) {
- $options['ssl']['CN_match'] = $this->parsedUrl['host'];
+ // PHP 5.6.0 deprecates CN_match, in favour of peer_name which
+ // actually checks SubjectAltName properly.
+ if ( version_compare( PHP_VERSION, '5.6.0', '>=' ) ) {
+ $options['ssl']['peer_name'] = $this->parsedUrl['host'];
+ } else {
+ $options['ssl']['CN_match'] = $this->parsedUrl['host'];
+ }
}
- 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 );
@@ -938,11 +999,25 @@ class PhpHttpRequest extends MWHttpRequest {
}
do {
$reqCount++;
- wfSuppressWarnings();
+ $this->fopenErrors = array();
+ set_error_handler( array( $this, 'errorHandler' ) );
$fh = fopen( $url, "r", false, $context );
- wfRestoreWarnings();
+ restore_error_handler();
if ( !$fh ) {
+ // HACK for instant commons.
+ // If we are contacting (commons|upload).wikimedia.org
+ // try again with CN_match for en.wikipedia.org
+ // as php does not handle SubjectAltName properly
+ // prior to "peer_name" option in php 5.6
+ if ( isset( $options['ssl']['CN_match'] )
+ && ( $options['ssl']['CN_match'] === 'commons.wikimedia.org'
+ || $options['ssl']['CN_match'] === 'upload.wikimedia.org' )
+ ) {
+ $options['ssl']['CN_match'] = 'en.wikipedia.org';
+ $context = stream_context_create( $options );
+ continue;
+ }
break;
}
@@ -973,6 +1048,10 @@ class PhpHttpRequest extends MWHttpRequest {
$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;
}
diff --git a/includes/MWNamespace.php b/includes/MWNamespace.php
index bd685514..e370bf10 100644
--- a/includes/MWNamespace.php
+++ b/includes/MWNamespace.php
@@ -210,6 +210,8 @@ class MWNamespace {
if ( $namespaces === null || $rebuild ) {
global $wgExtraNamespaces, $wgCanonicalNamespaceNames;
$namespaces = array( NS_MAIN => '' ) + $wgCanonicalNamespaceNames;
+ // Add extension namespaces
+ $namespaces += ExtensionRegistry::getInstance()->getAttribute( 'ExtensionNamespaces' );
if ( is_array( $wgExtraNamespaces ) ) {
$namespaces += $wgExtraNamespaces;
}
diff --git a/includes/Setup.php b/includes/Setup.php
index b3bf0fca..1b6d66c0 100644
--- a/includes/Setup.php
+++ b/includes/Setup.php
@@ -368,6 +368,15 @@ if ( $wgResourceLoaderMaxQueryLength === false ) {
unset($suhosinMaxValueLength);
}
+// Ensure the minimum chunk size is less than PHP upload limits or the maximum
+// upload size.
+$wgMinUploadChunkSize = min(
+ $wgMinUploadChunkSize,
+ $wgMaxUploadSize,
+ wfShorthandToInteger( ini_get( 'upload_max_filesize' ), 1e100 ),
+ wfShorthandToInteger( ini_get( 'post_max_size' ), 1e100) - 1024 # Leave room for other parameters
+);
+
/**
* Definitions of the NS_ constants are in Defines.php
* @private
@@ -502,11 +511,11 @@ unset( $serverParts );
// 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;
}
diff --git a/includes/User.php b/includes/User.php
index 8ea491ce..663a80b7 100644
--- a/includes/User.php
+++ b/includes/User.php
@@ -526,19 +526,24 @@ class User implements IDBAccessObject {
* If the code is invalid or has expired, returns NULL.
*
* @param string $code Confirmation code
+ * @param int $flags User::READ_* bitfield
* @return User|null
*/
- public static function newFromConfirmationCode( $code ) {
- $dbr = wfGetDB( DB_SLAVE );
- $id = $dbr->selectField( 'user', 'user_id', array(
- 'user_email_token' => md5( $code ),
- 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
- ) );
- if ( $id !== false ) {
- return User::newFromId( $id );
- } else {
- return null;
- }
+ public static function newFromConfirmationCode( $code, $flags = 0 ) {
+ $db = ( $flags & self::READ_LATEST ) == self::READ_LATEST
+ ? wfGetDB( DB_MASTER )
+ : wfGetDB( DB_SLAVE );
+
+ $id = $db->selectField(
+ 'user',
+ 'user_id',
+ array(
+ 'user_email_token' => md5( $code ),
+ 'user_email_token_expires > ' . $db->addQuotes( $db->timestamp() ),
+ )
+ );
+
+ return $id ? User::newFromId( $id ) : null;
}
/**
diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php
index b81e993b..b7b10846 100644
--- a/includes/api/ApiQuerySiteinfo.php
+++ b/includes/api/ApiQuerySiteinfo.php
@@ -245,6 +245,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$data['misermode'] = (bool)$config->get( 'MiserMode' );
$data['maxuploadsize'] = UploadBase::getMaxUploadSize();
+ $data['minuploadchunksize'] = (int)$this->getConfig()->get( 'MinUploadChunkSize' );
$data['thumblimits'] = $config->get( 'ThumbLimits' );
ApiResult::setArrayType( $data['thumblimits'], 'BCassoc' );
diff --git a/includes/api/ApiStashEdit.php b/includes/api/ApiStashEdit.php
index c4b717c7..d068e945 100644
--- a/includes/api/ApiStashEdit.php
+++ b/includes/api/ApiStashEdit.php
@@ -276,36 +276,55 @@ class ApiStashEdit extends ApiBase {
}
$dbr = wfGetDB( DB_SLAVE );
- // Check that no templates used in the output changed...
- $cWhr = array(); // conditions to find changes/creations
- $dWhr = array(); // conditions to find deletions
+
+ $templates = array(); // conditions to find changes/creations
+ $templateUses = 0; // expected existing templates
foreach ( $editInfo->output->getTemplateIds() as $ns => $stuff ) {
foreach ( $stuff as $dbkey => $revId ) {
- $cWhr[] = array( 'page_namespace' => $ns, 'page_title' => $dbkey,
- 'page_latest != ' . intval( $revId ) );
- $dWhr[] = array( 'page_namespace' => $ns, 'page_title' => $dbkey );
+ $templates[(string)$ns][$dbkey] = (int)$revId;
+ ++$templateUses;
}
}
- $change = $dbr->selectField( 'page', '1', $dbr->makeList( $cWhr, LIST_OR ), __METHOD__ );
- $n = $dbr->selectField( 'page', 'COUNT(*)', $dbr->makeList( $dWhr, LIST_OR ), __METHOD__ );
- if ( $change || $n != count( $dWhr ) ) {
- wfDebugLog( 'StashEdit', "Stale cache for key '$key'; template changed." );
- return false;
+ // Check that no templates used in the output changed...
+ if ( count( $templates ) ) {
+ $res = $dbr->select(
+ 'page',
+ array( 'ns' => 'page_namespace', 'dbk' => 'page_title', 'page_latest' ),
+ $dbr->makeWhereFrom2d( $templates, 'page_namespace', 'page_title' ),
+ __METHOD__
+ );
+ $changed = false;
+ foreach ( $res as $row ) {
+ $changed = $changed || ( $row->page_latest != $templates[$row->ns][$row->dbk] );
+ }
+
+ if ( $changed || $res->numRows() != $templateUses ) {
+ wfDebugLog( 'StashEdit', "Stale cache for key '$key'; template changed." );
+ return false;
+ }
}
- // Check that no files used in the output changed...
- $cWhr = array(); // conditions to find changes/creations
- $dWhr = array(); // conditions to find deletions
+ $files = array(); // conditions to find changes/creations
foreach ( $editInfo->output->getFileSearchOptions() as $name => $options ) {
- $cWhr[] = array( 'img_name' => $dbkey,
- 'img_sha1 != ' . $dbr->addQuotes( strval( $options['sha1'] ) ) );
- $dWhr[] = array( 'img_name' => $dbkey );
+ $files[$name] = (string)$options['sha1'];
}
- $change = $dbr->selectField( 'image', '1', $dbr->makeList( $cWhr, LIST_OR ), __METHOD__ );
- $n = $dbr->selectField( 'image', 'COUNT(*)', $dbr->makeList( $dWhr, LIST_OR ), __METHOD__ );
- if ( $change || $n != count( $dWhr ) ) {
- wfDebugLog( 'StashEdit', "Stale cache for key '$key'; file changed." );
- return false;
+ // Check that no files used in the output changed...
+ if ( count( $files ) ) {
+ $res = $dbr->select(
+ 'image',
+ array( 'name' => 'img_name', 'img_sha1' ),
+ array( 'img_name' => array_keys( $files ) ),
+ __METHOD__
+ );
+ $changed = false;
+ foreach ( $res as $row ) {
+ $changed = $changed || ( $row->img_sha1 != $files[$row->name] );
+ }
+
+ if ( $changed || $res->numRows() != count( $files ) ) {
+ wfDebugLog( 'StashEdit', "Stale cache for key '$key'; file changed." );
+ return false;
+ }
}
wfDebugLog( 'StashEdit', "Cache hit for key '$key'." );
diff --git a/includes/api/ApiUpload.php b/includes/api/ApiUpload.php
index 74ae05a8..7661625c 100644
--- a/includes/api/ApiUpload.php
+++ b/includes/api/ApiUpload.php
@@ -81,7 +81,7 @@ class ApiUpload extends ApiBase {
// Check if the uploaded file is sane
if ( $this->mParams['chunk'] ) {
- $maxSize = $this->mUpload->getMaxUploadSize();
+ $maxSize = UploadBase::getMaxUploadSize();
if ( $this->mParams['filesize'] > $maxSize ) {
$this->dieUsage( 'The file you submitted was too large', 'file-too-large' );
}
@@ -138,6 +138,12 @@ class ApiUpload extends ApiBase {
return $this->getStashResult( $warnings );
}
+ // Check throttle after we've handled warnings
+ if ( UploadBase::isThrottled( $this->getUser() )
+ ) {
+ $this->dieUsageMsg( 'actionthrottledtext' );
+ }
+
// This is the most common case -- a normal upload with no warnings
// performUpload will return a formatted properly for the API with status
return $this->performUpload( $warnings );
@@ -197,13 +203,30 @@ class ApiUpload extends ApiBase {
private function getChunkResult( $warnings ) {
$result = array();
- $result['result'] = 'Continue';
if ( $warnings && count( $warnings ) > 0 ) {
$result['warnings'] = $warnings;
}
+
$request = $this->getMain()->getRequest();
$chunkPath = $request->getFileTempname( 'chunk' );
$chunkSize = $request->getUpload( 'chunk' )->getSize();
+ $totalSoFar = $this->mParams['offset'] + $chunkSize;
+ $minChunkSize = $this->getConfig()->get( 'MinUploadChunkSize' );
+
+ // Sanity check sizing
+ if ( $totalSoFar > $this->mParams['filesize'] ) {
+ $this->dieUsage(
+ 'Offset plus current chunk is greater than claimed file size', 'invalid-chunk'
+ );
+ }
+
+ // Enforce minimum chunk size
+ if ( $totalSoFar != $this->mParams['filesize'] && $chunkSize < $minChunkSize ) {
+ $this->dieUsage(
+ "Minimum chunk size is $minChunkSize bytes for non-final chunks", 'chunk-too-small'
+ );
+ }
+
if ( $this->mParams['offset'] == 0 ) {
try {
$filekey = $this->performStash();
@@ -215,6 +238,18 @@ class ApiUpload extends ApiBase {
}
} else {
$filekey = $this->mParams['filekey'];
+
+ // Don't allow further uploads to an already-completed session
+ $progress = UploadBase::getSessionStatus( $this->getUser(), $filekey );
+ if ( !$progress ) {
+ // Probably can't get here, but check anyway just in case
+ $this->dieUsage( 'No chunked upload session with this key', 'stashfailed' );
+ } elseif ( $progress['result'] !== 'Continue' || $progress['stage'] !== 'uploading' ) {
+ $this->dieUsage(
+ 'Chunked upload is already completed, check status for details', 'stashfailed'
+ );
+ }
+
$status = $this->mUpload->addChunk(
$chunkPath, $chunkSize, $this->mParams['offset'] );
if ( !$status->isGood() ) {
@@ -223,18 +258,12 @@ class ApiUpload extends ApiBase {
);
$this->dieUsage( $status->getWikiText(), 'stashfailed', 0, $extradata );
-
- return array();
}
}
// Check we added the last chunk:
- if ( $this->mParams['offset'] + $chunkSize == $this->mParams['filesize'] ) {
+ if ( $totalSoFar == $this->mParams['filesize'] ) {
if ( $this->mParams['async'] ) {
- $progress = UploadBase::getSessionStatus( $this->getUser(), $filekey );
- if ( $progress && $progress['result'] === 'Poll' ) {
- $this->dieUsage( "Chunk assembly already in progress.", 'stashfailed' );
- }
UploadBase::setSessionStatus(
$this->getUser(),
$filekey,
@@ -254,21 +283,37 @@ class ApiUpload extends ApiBase {
} else {
$status = $this->mUpload->concatenateChunks();
if ( !$status->isGood() ) {
+ UploadBase::setSessionStatus(
+ $this->getUser(),
+ $filekey,
+ array( 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status )
+ );
$this->dieUsage( $status->getWikiText(), 'stashfailed' );
-
- return array();
}
// The fully concatenated file has a new filekey. So remove
// the old filekey and fetch the new one.
+ UploadBase::setSessionStatus( $this->getUser(), $filekey, false );
$this->mUpload->stash->removeFile( $filekey );
$filekey = $this->mUpload->getLocalFile()->getFileKey();
$result['result'] = 'Success';
}
+ } else {
+ UploadBase::setSessionStatus(
+ $this->getUser(),
+ $filekey,
+ array(
+ 'result' => 'Continue',
+ 'stage' => 'uploading',
+ 'offset' => $totalSoFar,
+ 'status' => Status::newGood(),
+ )
+ );
+ $result['result'] = 'Continue';
+ $result['offset'] = $totalSoFar;
}
$result['filekey'] = $filekey;
- $result['offset'] = $this->mParams['offset'] + $chunkSize;
return $result;
}
@@ -378,6 +423,10 @@ class ApiUpload extends ApiBase {
// Chunk upload
$this->mUpload = new UploadFromChunks();
if ( isset( $this->mParams['filekey'] ) ) {
+ if ( $this->mParams['offset'] === 0 ) {
+ $this->dieUsage( 'Cannot supply a filekey when offset is 0', 'badparams' );
+ }
+
// handle new chunk
$this->mUpload->continueChunks(
$this->mParams['filename'],
@@ -385,6 +434,10 @@ class ApiUpload extends ApiBase {
$request->getUpload( 'chunk' )
);
} else {
+ if ( $this->mParams['offset'] !== 0 ) {
+ $this->dieUsage( 'Must supply a filekey when offset is non-zero', 'badparams' );
+ }
+
// handle first chunk
$this->mUpload->initialize(
$this->mParams['filename'],
@@ -760,8 +813,15 @@ class ApiUpload extends ApiBase {
),
'stash' => false,
- 'filesize' => null,
- 'offset' => null,
+ 'filesize' => array(
+ ApiBase::PARAM_TYPE => 'integer',
+ ApiBase::PARAM_MIN => 0,
+ ApiBase::PARAM_MAX => UploadBase::getMaxUploadSize(),
+ ),
+ 'offset' => array(
+ ApiBase::PARAM_TYPE => 'integer',
+ ApiBase::PARAM_MIN => 0,
+ ),
'chunk' => array(
ApiBase::PARAM_TYPE => 'upload',
),
diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php
index 5b42c2c6..cef1176d 100644
--- a/includes/filerepo/FileRepo.php
+++ b/includes/filerepo/FileRepo.php
@@ -431,7 +431,9 @@ class FileRepo {
# Now try an old version of the file
if ( $time !== false ) {
$img = $this->newFile( $title, $time );
- $img->load( $flags );
+ if ( $img ) {
+ $img->load( $flags );
+ }
if ( $img && $img->exists() ) {
if ( !$img->isDeleted( File::DELETED_FILE ) ) {
return $img; // always OK
@@ -452,10 +454,10 @@ class FileRepo {
$redir = $this->checkRedirect( $title );
if ( $redir && $title->getNamespace() == NS_FILE ) {
$img = $this->newFile( $redir );
- $img->load( $flags );
if ( !$img ) {
return false;
}
+ $img->load( $flags );
if ( $img->exists() ) {
$img->redirectedFrom( $title->getDBkey() );
diff --git a/includes/filerepo/ForeignAPIRepo.php b/includes/filerepo/ForeignAPIRepo.php
index 3c031921..71d2b919 100644
--- a/includes/filerepo/ForeignAPIRepo.php
+++ b/includes/filerepo/ForeignAPIRepo.php
@@ -21,6 +21,8 @@
* @ingroup FileRepo
*/
+use MediaWiki\Logger\LoggerFactory;
+
/**
* A foreign repository with a remote MediaWiki with an API thingy
*
@@ -521,7 +523,8 @@ class ForeignAPIRepo extends FileRepo {
if ( $status->isOK() ) {
return $req->getContent();
} else {
- wfDebug( "ForeignAPIRepo: ERROR on GET: " . $status->getWikiText() );
+ $logger = LoggerFactory::getInstance( 'http' );
+ $logger->warning( $status->getWikiText(), array( 'caller' => 'ForeignAPIRepo::httpGet' ) );
return false;
}
}
diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php
index eadcf94b..3b1d978e 100644
--- a/includes/media/Bitmap.php
+++ b/includes/media/Bitmap.php
@@ -162,6 +162,8 @@ class BitmapHandler extends TransformationalImageHandler {
( $params['comment'] !== ''
? array( '-set', 'comment', $this->escapeMagickProperty( $params['comment'] ) )
: array() ),
+ // T108616: Avoid exposure of local file path
+ array( '+set', 'Thumb::URI'),
array( '-depth', 8 ),
$sharpen,
array( '-rotate', "-$rotation" ),
diff --git a/includes/registration/ExtensionProcessor.php b/includes/registration/ExtensionProcessor.php
index bb8fb329..0b594b42 100644
--- a/includes/registration/ExtensionProcessor.php
+++ b/includes/registration/ExtensionProcessor.php
@@ -47,6 +47,24 @@ class ExtensionProcessor implements Processor {
);
/**
+ * Mapping of global settings to their specific merge strategies.
+ *
+ * @see ExtensionRegistry::exportExtractedData
+ * @see getExtractedInfo
+ * @var array
+ */
+ protected static $mergeStrategies = array(
+ 'wgGroupPermissions' => 'array_plus_2d',
+ 'wgRevokePermissions' => 'array_plus_2d',
+ 'wgHooks' => 'array_merge_recursive',
+ // credits are handled in the ExtensionRegistry
+ //'wgExtensionCredits' => 'array_merge_recursive',
+ 'wgExtraGenderNamespaces' => 'array_plus',
+ 'wgNamespacesWithSubpages' => 'array_plus',
+ 'wgNamespaceContentModels' => 'array_plus',
+ );
+
+ /**
* Keys that are part of the extension credits
*
* @var array
@@ -155,6 +173,13 @@ class ExtensionProcessor implements Processor {
}
public function getExtractedInfo() {
+ // Make sure the merge strategies are set
+ foreach ( $this->globals as $key => $val ) {
+ if ( isset( self::$mergeStrategies[$key] ) ) {
+ $this->globals[$key][ExtensionRegistry::MERGE_STRATEGY] = self::$mergeStrategies[$key];
+ }
+ }
+
return array(
'globals' => $this->globals,
'defines' => $this->defines,
@@ -166,8 +191,10 @@ class ExtensionProcessor implements Processor {
protected function extractHooks( array $info ) {
if ( isset( $info['Hooks'] ) ) {
- foreach ( $info['Hooks'] as $name => $callable ) {
- $this->globals['wgHooks'][$name][] = $callable;
+ foreach ( $info['Hooks'] as $name => $value ) {
+ foreach ( (array)$value as $callback ) {
+ $this->globals['wgHooks'][$name][] = $callback;
+ }
}
}
}
@@ -182,7 +209,7 @@ class ExtensionProcessor implements Processor {
foreach ( $info['namespaces'] as $ns ) {
$id = $ns['id'];
$this->defines[$ns['constant']] = $id;
- $this->globals['wgExtraNamespaces'][$id] = $ns['name'];
+ $this->attributes['ExtensionNamespaces'][$id] = $ns['name'];
if ( isset( $ns['gender'] ) ) {
$this->globals['wgExtraGenderNamespaces'][$id] = $ns['gender'];
}
@@ -269,9 +296,15 @@ class ExtensionProcessor implements Processor {
*/
protected function extractConfig( array $info ) {
if ( isset( $info['config'] ) ) {
+ if ( isset( $info['config']['_prefix'] ) ) {
+ $prefix = $info['config']['_prefix'];
+ unset( $info['config']['_prefix'] );
+ } else {
+ $prefix = 'wg';
+ }
foreach ( $info['config'] as $key => $val ) {
if ( $key[0] !== '@' ) {
- $this->globals["wg$key"] = $val;
+ $this->globals["$prefix$key"] = $val;
}
}
}
diff --git a/includes/registration/ExtensionRegistry.php b/includes/registration/ExtensionRegistry.php
index 4e690aa8..16d83356 100644
--- a/includes/registration/ExtensionRegistry.php
+++ b/includes/registration/ExtensionRegistry.php
@@ -12,6 +12,28 @@
class ExtensionRegistry {
/**
+ * Version of the highest supported manifest version
+ */
+ const MANIFEST_VERSION = 1;
+
+ /**
+ * Version of the oldest supported manifest version
+ */
+ const OLDEST_MANIFEST_VERSION = 1;
+
+ /**
+ * Bump whenever the registration cache needs resetting
+ */
+ const CACHE_VERSION = 1;
+
+ /**
+ * Special key that defines the merge strategy
+ *
+ * @since 1.26
+ */
+ const MERGE_STRATEGY = '_merge_strategy';
+
+ /**
* @var BagOStuff
*/
protected $cache;
@@ -92,7 +114,7 @@ class ExtensionRegistry {
}
// See if this queue is in APC
- $key = wfMemcKey( 'registration', md5( json_encode( $this->queued ) ) );
+ $key = wfMemcKey( 'registration', md5( json_encode( $this->queued ) ), self::CACHE_VERSION );
$data = $this->cache->get( $key );
if ( $data ) {
$this->exportExtractedData( $data );
@@ -155,31 +177,61 @@ class ExtensionRegistry {
foreach ( $data['credits'] as $credit ) {
$data['globals']['wgExtensionCredits'][$credit['type']][] = $credit;
}
+ $data['globals']['wgExtensionCredits'][self::MERGE_STRATEGY] = 'array_merge_recursive';
$data['autoload'] = $autoloadClasses;
return $data;
}
protected function exportExtractedData( array $info ) {
foreach ( $info['globals'] as $key => $val ) {
+ // If a merge strategy is set, read it and remove it from the value
+ // so it doesn't accidentally end up getting set.
+ // Need to check $val is an array for PHP 5.3 which will return
+ // true on isset( 'string'['foo'] ).
+ if ( isset( $val[self::MERGE_STRATEGY] ) && is_array( $val ) ) {
+ $mergeStrategy = $val[self::MERGE_STRATEGY];
+ unset( $val[self::MERGE_STRATEGY] );
+ } else {
+ $mergeStrategy = 'array_merge';
+ }
+
+ // Optimistic: If the global is not set, or is an empty array, replace it entirely.
+ // Will be O(1) performance.
if ( !isset( $GLOBALS[$key] ) || ( is_array( $GLOBALS[$key] ) && !$GLOBALS[$key] ) ) {
$GLOBALS[$key] = $val;
- } elseif ( $key === 'wgHooks' || $key === 'wgExtensionCredits' ) {
- // Special case $wgHooks and $wgExtensionCredits, which require a recursive merge.
- // Ideally it would have been taken care of in the first if block though.
- $GLOBALS[$key] = array_merge_recursive( $GLOBALS[$key], $val );
- } elseif ( $key === 'wgGroupPermissions' ) {
- // First merge individual groups
- foreach ( $GLOBALS[$key] as $name => &$groupVal ) {
- if ( isset( $val[$name] ) ) {
- $groupVal += $val[$name];
+ continue;
+ }
+
+ if ( !is_array( $GLOBALS[$key] ) || !is_array( $val ) ) {
+ // config setting that has already been overridden, don't set it
+ continue;
+ }
+
+ switch ( $mergeStrategy ) {
+ case 'array_merge_recursive':
+ $GLOBALS[$key] = array_merge_recursive( $GLOBALS[$key], $val );
+ break;
+ case 'array_plus_2d':
+ // First merge items that are in both arrays
+ foreach ( $GLOBALS[$key] as $name => &$groupVal ) {
+ if ( isset( $val[$name] ) ) {
+ $groupVal += $val[$name];
+ }
}
- }
- // Now merge groups that didn't exist yet
- $GLOBALS[$key] += $val;
- } elseif ( is_array( $GLOBALS[$key] ) && is_array( $val ) ) {
- $GLOBALS[$key] = array_merge( $val, $GLOBALS[$key] );
- } // else case is a config setting where it has already been overriden, so don't set it
+ // Now add items that didn't exist yet
+ $GLOBALS[$key] += $val;
+ break;
+ case 'array_plus':
+ $GLOBALS[$key] += $val;
+ break;
+ case 'array_merge':
+ $GLOBALS[$key] = array_merge( $val, $GLOBALS[$key] );
+ break;
+ default:
+ throw new UnexpectedValueException( "Unknown merge strategy '$mergeStrategy'" );
+ }
}
+
foreach ( $info['defines'] as $name => $val ) {
define( $name, $val );
}
diff --git a/includes/revisiondelete/RevDelList.php b/includes/revisiondelete/RevDelList.php
index 840fd772..c31c42b3 100644
--- a/includes/revisiondelete/RevDelList.php
+++ b/includes/revisiondelete/RevDelList.php
@@ -74,6 +74,25 @@ abstract class RevDelList extends RevisionListBase {
}
/**
+ * Indicate whether any item in this list is suppressed
+ * @since 1.25
+ * @return bool
+ */
+ public function areAnySuppressed() {
+ $bit = $this->getSuppressBit();
+
+ // @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
+ for ( $this->reset(); $this->current(); $this->next() ) {
+ // @codingStandardsIgnoreEnd
+ $item = $this->current();
+ if ( $item->getBits() & $bit ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
* Set the visibility for the revisions in this list. Logging and
* transactions are done here.
*
diff --git a/includes/specials/SpecialConfirmemail.php b/includes/specials/SpecialConfirmemail.php
index b6ab112b..63561552 100644
--- a/includes/specials/SpecialConfirmemail.php
+++ b/includes/specials/SpecialConfirmemail.php
@@ -120,7 +120,7 @@ class EmailConfirmation extends UnlistedSpecialPage {
* @param string $code Confirmation code
*/
function attemptConfirm( $code ) {
- $user = User::newFromConfirmationCode( $code );
+ $user = User::newFromConfirmationCode( $code, User::READ_LATEST );
if ( !is_object( $user ) ) {
$this->getOutput()->addWikiMsg( 'confirmemail_invalid' );
@@ -164,7 +164,7 @@ class EmailInvalidation extends UnlistedSpecialPage {
* @param string $code Confirmation code
*/
function attemptInvalidate( $code ) {
- $user = User::newFromConfirmationCode( $code );
+ $user = User::newFromConfirmationCode( $code, User::READ_LATEST );
if ( !is_object( $user ) ) {
$this->getOutput()->addWikiMsg( 'confirmemail_invalid' );
diff --git a/includes/specials/SpecialRevisiondelete.php b/includes/specials/SpecialRevisiondelete.php
index 9e2ca277..3b5ef9d4 100644
--- a/includes/specials/SpecialRevisiondelete.php
+++ b/includes/specials/SpecialRevisiondelete.php
@@ -161,11 +161,10 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
$this->typeLabels = self::$UILabels[$this->typeName];
$list = $this->getList();
$list->reset();
- $bitfield = $list->current()->getBits();
$this->mIsAllowed = $user->isAllowed( RevisionDeleter::getRestriction( $this->typeName ) );
$canViewSuppressedOnly = $this->getUser()->isAllowed( 'viewsuppressed' ) &&
!$this->getUser()->isAllowed( 'suppressrevision' );
- $pageIsSuppressed = $bitfield & Revision::DELETED_RESTRICTED;
+ $pageIsSuppressed = $list->areAnySuppressed();
$this->mIsAllowed = $this->mIsAllowed && !( $canViewSuppressedOnly && $pageIsSuppressed );
$this->otherReason = $request->getVal( 'wpReason' );
diff --git a/includes/specials/SpecialUpload.php b/includes/specials/SpecialUpload.php
index 640562e4..2e0699af 100644
--- a/includes/specials/SpecialUpload.php
+++ b/includes/specials/SpecialUpload.php
@@ -460,6 +460,14 @@ class SpecialUpload extends SpecialPage {
}
}
+ // This is as late as we can throttle, after expected issues have been handled
+ if ( UploadBase::isThrottled( $this->getUser() ) ) {
+ $this->showRecoverableUploadError(
+ $this->msg( 'actionthrottledtext' )->escaped()
+ );
+ return;
+ }
+
// Get the page text if this is not a reupload
if ( !$this->mForReUpload ) {
$pageText = self::getInitialPageText( $this->mComment, $this->mLicense,
diff --git a/includes/upload/UploadBase.php b/includes/upload/UploadBase.php
index 6da8250b..9e113749 100644
--- a/includes/upload/UploadBase.php
+++ b/includes/upload/UploadBase.php
@@ -128,6 +128,16 @@ abstract class UploadBase {
return true;
}
+ /**
+ * Returns true if the user has surpassed the upload rate limit, false otherwise.
+ *
+ * @param User $user
+ * @return bool
+ */
+ public static function isThrottled( $user ) {
+ return $user->pingLimiter( 'upload' );
+ }
+
// Upload handlers. Should probably just be a global.
private static $uploadHandlers = array( 'Stash', 'File', 'Url' );