Merge "tests: Add helper function for ini_set with automatic cleanup"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 11 Oct 2018 19:05:51 +0000 (19:05 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 11 Oct 2018 19:05:51 +0000 (19:05 +0000)
19 files changed:
RELEASE-NOTES-1.32
docs/hooks.txt
includes/EditPage.php
includes/api/ApiBase.php
includes/cache/MessageCache.php
includes/deferred/DeferredUpdates.php
includes/filerepo/file/File.php
includes/installer/CliInstaller.php
includes/installer/Installer.php
includes/installer/i18n/en.json
includes/installer/i18n/qqq.json
includes/jobqueue/jobs/ThumbnailRenderJob.php
includes/media/MediaHandler.php
includes/parser/ParserOutput.php
maintenance/install.php
maintenance/update.php
tests/phpunit/includes/parser/ParserOutputTest.php
tests/phpunit/includes/specialpage/AbstractChangesListSpecialPageTestCase.php
tests/phpunit/includes/specials/SpecialWatchlistTest.php

index efc3a2e..991708e 100644 (file)
@@ -340,6 +340,10 @@ because of Phabricator reports.
   ChangesListSpecialPageQuery.
 * The global function wfUsePHP, deprecated since 1.30, has now been removed. To
   assert a newer version of PHP than MediaWiki does, use extension registration.
+* The hook 'ChangesListSpecialPageFilters', deprecated in 1.29, has now been
+  removed. Use the 'ChangesListSpecialPageStructuredFilters' hook instead.
+* DeferredUpdates::setImmediateMode(), deprecated since 1.29, has been removed.
+* File / MediaHandler::getStreamHeaders(), deprecated since 1.30, was removed.
 
 === Deprecations in 1.32 ===
 * HTMLForm::setSubmitProgressive() is deprecated. No need to call it. Submit
index 78ed1b4..a47f68f 100644 (file)
@@ -1015,16 +1015,6 @@ $rows: The data that will be rendered. May be a ResultWrapper instance or
 $unpatrolled: Whether or not we are showing unpatrolled changes.
 $watched: Whether or not the change is watched by the user.
 
-'ChangesListSpecialPageFilters': DEPRECATED since 1.29! Use
-'ChangesListSpecialPageStructuredFilters' instead.
-Called after building form options on pages
-inheriting from ChangesListSpecialPage (in core: RecentChanges,
-RecentChangesLinked and Watchlist).
-$special: ChangesListSpecialPage instance
-&$filters: associative array of filter definitions. The keys are the HTML
-  name/URL parameters. Each key maps to an associative array with a 'msg'
-  (message key) and a 'default' value.
-
 'ChangesListSpecialPageQuery': Called when building SQL query on pages
 inheriting from ChangesListSpecialPage (in core: RecentChanges,
 RecentChangesLinked and Watchlist).
index 6b79538..7143c3f 100644 (file)
@@ -2991,7 +2991,7 @@ ERROR;
                                        $this->contentFormat,
                                        $ex->getMessage()
                                );
-                               $out->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
+                               $out->addWikiText( '<div class="error">' . $msg->plain() . '</div>' );
                        }
                }
 
@@ -3466,7 +3466,7 @@ ERROR;
                                        $this->contentFormat,
                                        $ex->getMessage()
                                );
-                               $out->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
+                               $out->addWikiText( '<div class="error">' . $msg->plain() . '</div>' );
                        }
                }
        }
index ac07eeb..c2e37e0 100644 (file)
@@ -2247,7 +2247,7 @@ abstract class ApiBase extends ContextSource {
 
                // Avoid PHP 7.1 warning of passing $this by reference
                $apiModule = $this;
-               Hooks::run( 'APIGetDescription', [ &$apiModule, &$desc ] );
+               Hooks::run( 'APIGetDescription', [ &$apiModule, &$desc ], '1.25' );
                $desc = self::escapeWikiText( $desc );
                if ( is_array( $desc ) ) {
                        $desc = implode( "\n", $desc );
@@ -2337,7 +2337,7 @@ abstract class ApiBase extends ContextSource {
 
                // Avoid PHP 7.1 warning of passing $this by reference
                $apiModule = $this;
-               Hooks::run( 'APIGetParamDescription', [ &$apiModule, &$desc ] );
+               Hooks::run( 'APIGetParamDescription', [ &$apiModule, &$desc ], '1.25' );
 
                if ( !$desc ) {
                        $desc = [];
index 76d31ff..733b1b7 100644 (file)
@@ -464,13 +464,7 @@ class MessageCache {
 
                $cache = [];
 
-               # Common conditions
-               $conds = [
-                       'page_is_redirect' => 0,
-                       'page_namespace' => NS_MEDIAWIKI,
-               ];
-
-               $mostused = [];
+               $mostused = []; // list of "<cased message key>/<code>"
                if ( $wgAdaptiveMessageCache && $code !== $wgLanguageCode ) {
                        if ( !$this->cache->has( $wgLanguageCode ) ) {
                                $this->load( $wgLanguageCode );
@@ -481,6 +475,14 @@ class MessageCache {
                        }
                }
 
+               // Get the list of software-defined messages in core/extensions
+               $overridable = array_flip( Language::getMessageKeysFor( $wgLanguageCode ) );
+
+               // Common conditions
+               $conds = [
+                       'page_is_redirect' => 0,
+                       'page_namespace' => NS_MEDIAWIKI,
+               ];
                if ( count( $mostused ) ) {
                        $conds['page_title'] = $mostused;
                } elseif ( $code !== $wgLanguageCode ) {
@@ -492,31 +494,28 @@ class MessageCache {
                                $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() );
                }
 
-               # Conditions to fetch oversized pages to ignore them
-               $bigConds = $conds;
-               $bigConds[] = 'page_len > ' . intval( $wgMaxMsgCacheEntrySize );
-
-               # Load titles for all oversized pages in the MediaWiki namespace
+               // Set the stubs for oversized software-defined messages in the main cache map
                $res = $dbr->select(
                        'page',
                        [ 'page_title', 'page_latest' ],
-                       $bigConds,
+                       array_merge( $conds, [ 'page_len > ' . intval( $wgMaxMsgCacheEntrySize ) ] ),
                        __METHOD__ . "($code)-big"
                );
                foreach ( $res as $row ) {
-                       $cache[$row->page_title] = '!TOO BIG';
+                       $name = $this->contLang->lcfirst( $row->page_title );
+                       // Include entries/stubs for all keys in $mostused in adaptive mode
+                       if ( $wgAdaptiveMessageCache || isset( $overridable[$name] ) ) {
+                               $cache[$row->page_title] = '!TOO BIG';
+                       }
                        // At least include revision ID so page changes are reflected in the hash
                        $cache['EXCESSIVE'][$row->page_title] = $row->page_latest;
                }
 
-               # Conditions to load the remaining pages with their contents
-               $smallConds = $conds;
-               $smallConds[] = 'page_len <= ' . intval( $wgMaxMsgCacheEntrySize );
-
+               // Set the text for small software-defined messages in the main cache map
                $res = $dbr->select(
                        [ 'page', 'revision', 'text' ],
-                       [ 'page_title', 'old_id', 'old_text', 'old_flags' ],
-                       $smallConds,
+                       [ 'page_title', 'page_latest', 'old_id', 'old_text', 'old_flags' ],
+                       array_merge( $conds, [ 'page_len <= ' . intval( $wgMaxMsgCacheEntrySize ) ] ),
                        __METHOD__ . "($code)-small",
                        [],
                        [
@@ -524,23 +523,30 @@ class MessageCache {
                                'text' => [ 'JOIN', 'rev_text_id=old_id' ],
                        ]
                );
-
                foreach ( $res as $row ) {
-                       $text = Revision::getRevisionText( $row );
-                       if ( $text === false ) {
-                               // Failed to fetch data; possible ES errors?
-                               // Store a marker to fetch on-demand as a workaround...
-                               // TODO Use a differnt marker
-                               $entry = '!TOO BIG';
-                               wfDebugLog(
-                                       'MessageCache',
-                                       __METHOD__
-                                       . ": failed to load message page text for {$row->page_title} ($code)"
-                               );
+                       $name = $this->contLang->lcfirst( $row->page_title );
+                       // Include entries/stubs for all keys in $mostused in adaptive mode
+                       if ( $wgAdaptiveMessageCache || isset( $overridable[$name] ) ) {
+                               $text = Revision::getRevisionText( $row );
+                               if ( $text === false ) {
+                                       // Failed to fetch data; possible ES errors?
+                                       // Store a marker to fetch on-demand as a workaround...
+                                       // TODO Use a differnt marker
+                                       $entry = '!TOO BIG';
+                                       wfDebugLog(
+                                               'MessageCache',
+                                               __METHOD__
+                                               . ": failed to load message page text for {$row->page_title} ($code)"
+                                       );
+                               } else {
+                                       $entry = ' ' . $text;
+                               }
+                               $cache[$row->page_title] = $entry;
                        } else {
-                               $entry = ' ' . $text;
+                               // T193271: cache object gets too big and slow to generate.
+                               // At least include revision ID so page changes are reflected in the hash.
+                               $cache['EXCESSIVE'][$row->page_title] = $row->page_latest;
                        }
-                       $cache[$row->page_title] = $entry;
                }
 
                $cache['VERSION'] = MSG_CACHE_VERSION;
@@ -818,9 +824,8 @@ class MessageCache {
                Hooks::run( 'MessageCache::get', [ &$lckey ] );
 
                // Loop through each language in the fallback list until we find something useful
-               $lang = wfGetLangObj( $langcode );
                $message = $this->getMessageFromFallbackChain(
-                       $lang,
+                       wfGetLangObj( $langcode ),
                        $lckey,
                        !$this->mDisable && $useDB
                );
@@ -912,7 +917,6 @@ class MessageCache {
                                        $this->getMessagePageName( $langcode, $uckey ),
                                        $langcode
                                );
-
                                if ( $message !== false ) {
                                        return $message;
                                }
@@ -987,44 +991,54 @@ class MessageCache {
                $this->load( $code );
 
                $entry = $this->cache->getField( $code, $title );
+
                if ( $entry !== null ) {
+                       // Message page exists as an override of a software messages
                        if ( substr( $entry, 0, 1 ) === ' ' ) {
                                // The message exists and is not '!TOO BIG'
                                return (string)substr( $entry, 1 );
                        } elseif ( $entry === '!NONEXISTENT' ) {
+                               // The text might be '-' or missing due to some data loss
                                return false;
                        }
-                       // Fall through and try invididual message cache below
-               } else {
-                       // Message does not have a MediaWiki page definition
-                       $message = false;
-                       Hooks::run( 'MessagesPreLoad', [ $title, &$message, $code ] );
-                       if ( $message !== false ) {
-                               $this->cache->setField( $code, $title, ' ' . $message );
-                       } else {
-                               $this->cache->setField( $code, $title, '!NONEXISTENT' );
-                       }
-
-                       return $message;
-               }
-
-               if ( $this->cacheVolatile[$code] ) {
-                       $entry = false;
-                       // Make sure that individual keys respect the WAN cache holdoff period too
-                       LoggerFactory::getInstance( 'MessageCache' )->debug(
-                               __METHOD__ . ': loading volatile key \'{titleKey}\'',
-                               [ 'titleKey' => $title, 'code' => $code ] );
+                       // Load the message page, utilizing the individual message cache.
+                       // If the page does not exist, there will be no hook handler fallbacks.
+                       $entry = $this->loadCachedMessagePageEntry(
+                               $title,
+                               $code,
+                               $this->cache->getField( $code, 'HASH' )
+                       );
                } else {
-                       // Try the individual message cache
+                       // Message page does not exist or does not override a software message.
+                       // Load the message page, utilizing the individual message cache.
                        $entry = $this->loadCachedMessagePageEntry(
                                $title,
                                $code,
                                $this->cache->getField( $code, 'HASH' )
                        );
+                       if ( substr( $entry, 0, 1 ) !== ' ' ) {
+                               // Message does not have a MediaWiki page definition; try hook handlers
+                               $message = false;
+                               Hooks::run( 'MessagesPreLoad', [ $title, &$message, $code ] );
+                               if ( $message !== false ) {
+                                       $this->cache->setField( $code, $title, ' ' . $message );
+                               } else {
+                                       $this->cache->setField( $code, $title, '!NONEXISTENT' );
+                               }
+
+                               return $message;
+                       }
                }
 
                if ( $entry !== false && substr( $entry, 0, 1 ) === ' ' ) {
-                       $this->cache->setField( $code, $title, $entry );
+                       if ( $this->cacheVolatile[$code] ) {
+                               // Make sure that individual keys respect the WAN cache holdoff period too
+                               LoggerFactory::getInstance( 'MessageCache' )->debug(
+                                       __METHOD__ . ': loading volatile key \'{titleKey}\'',
+                                       [ 'titleKey' => $title, 'code' => $code ] );
+                       } else {
+                               $this->cache->setField( $code, $title, $entry );
+                       }
                        // The message exists, so make sure a string is returned
                        return (string)substr( $entry, 1 );
                }
index 8543c4b..e6a0e81 100644 (file)
@@ -135,15 +135,6 @@ class DeferredUpdates {
                }
        }
 
-       /**
-        * @param bool $value Whether to just immediately run updates in addUpdate()
-        * @since 1.28
-        * @deprecated 1.29 Causes issues in Web-executed jobs - see T165714 and T100085.
-        */
-       public static function setImmediateMode( $value ) {
-               wfDeprecated( __METHOD__, '1.29' );
-       }
-
        /**
         * @param DeferrableUpdate[] $queue
         * @param DeferrableUpdate $update
index 6a788b6..6d01c29 100644 (file)
@@ -2168,14 +2168,6 @@ abstract class File implements IDBAccessObject {
                return true;
        }
 
-       /**
-        * @deprecated since 1.30, use File::getContentHeaders instead
-        */
-       function getStreamHeaders() {
-               wfDeprecated( __METHOD__, '1.30' );
-               return $this->getContentHeaders();
-       }
-
        /**
         * @return string[] HTTP header name/value map to use for HEAD/GET request responses
         * @since 1.30
index aee51e7..f59b5da 100644 (file)
@@ -50,30 +50,30 @@ class CliInstaller extends Installer {
        /**
         * @param string $siteName
         * @param string|null $admin
-        * @param array $option
+        * @param array $options
         */
-       function __construct( $siteName, $admin = null, array $option = [] ) {
+       function __construct( $siteName, $admin = null, array $options = [] ) {
                global $wgContLang;
 
                parent::__construct();
 
-               if ( isset( $option['scriptpath'] ) ) {
+               if ( isset( $options['scriptpath'] ) ) {
                        $this->specifiedScriptPath = true;
                }
 
                foreach ( $this->optionMap as $opt => $global ) {
-                       if ( isset( $option[$opt] ) ) {
-                               $GLOBALS[$global] = $option[$opt];
-                               $this->setVar( $global, $option[$opt] );
+                       if ( isset( $options[$opt] ) ) {
+                               $GLOBALS[$global] = $options[$opt];
+                               $this->setVar( $global, $options[$opt] );
                        }
                }
 
-               if ( isset( $option['lang'] ) ) {
+               if ( isset( $options['lang'] ) ) {
                        global $wgLang, $wgLanguageCode;
-                       $this->setVar( '_UserLang', $option['lang'] );
-                       $wgLanguageCode = $option['lang'];
+                       $this->setVar( '_UserLang', $options['lang'] );
+                       $wgLanguageCode = $options['lang'];
                        $wgContLang = MediaWikiServices::getInstance()->getContentLanguage();
-                       $wgLang = Language::factory( $option['lang'] );
+                       $wgLang = Language::factory( $options['lang'] );
                        RequestContext::getMain()->setLanguage( $wgLang );
                }
 
@@ -89,32 +89,47 @@ class CliInstaller extends Installer {
                        $this->setVar( '_AdminName', $admin );
                }
 
-               if ( !isset( $option['installdbuser'] ) ) {
+               if ( !isset( $options['installdbuser'] ) ) {
                        $this->setVar( '_InstallUser',
                                $this->getVar( 'wgDBuser' ) );
                        $this->setVar( '_InstallPassword',
                                $this->getVar( 'wgDBpassword' ) );
                } else {
                        $this->setVar( '_InstallUser',
-                               $option['installdbuser'] );
+                               $options['installdbuser'] );
                        $this->setVar( '_InstallPassword',
-                               $option['installdbpass'] ?? "" );
+                               $options['installdbpass'] ?? "" );
 
                        // Assume that if we're given the installer user, we'll create the account.
                        $this->setVar( '_CreateDBAccount', true );
                }
 
-               if ( isset( $option['pass'] ) ) {
-                       $this->setVar( '_AdminPassword', $option['pass'] );
+               if ( isset( $options['pass'] ) ) {
+                       $this->setVar( '_AdminPassword', $options['pass'] );
                }
 
                // Detect and inject any extension found
-               if ( isset( $option['with-extensions'] ) ) {
+               if ( isset( $options['extensions'] ) ) {
+                       $status = $this->validateExtensions(
+                               'extension', 'extensions', $options['extensions'] );
+                       if ( !$status->isOK() ) {
+                               $this->showStatusMessage( $status );
+                       }
+                       $this->setVar( '_Extensions', $status->value );
+               } elseif ( isset( $options['with-extensions'] ) ) {
                        $this->setVar( '_Extensions', array_keys( $this->findExtensions() ) );
                }
 
                // Set up the default skins
-               $skins = array_keys( $this->findExtensions( 'skins' ) );
+               if ( isset( $options['skins'] ) ) {
+                       $status = $this->validateExtensions( 'skin', 'skins', $options['skins'] );
+                       if ( !$status->isOK() ) {
+                               $this->showStatusMessage( $status );
+                       }
+                       $skins = $status->value;
+               } else {
+                       $skins = array_keys( $this->findExtensions( 'skins' ) );
+               }
                $this->setVar( '_Skins', $skins );
 
                if ( $skins ) {
@@ -123,6 +138,28 @@ class CliInstaller extends Installer {
                }
        }
 
+       private function validateExtensions( $type, $directory, $nameLists ) {
+               $extensions = [];
+               $status = new Status;
+               foreach ( (array)$nameLists as $nameList ) {
+                       foreach ( explode( ',', $nameList ) as $name ) {
+                               $name = trim( $name );
+                               if ( $name === '' ) {
+                                       continue;
+                               }
+                               $extStatus = $this->getExtensionInfo( $type, $directory, $name );
+                               if ( $extStatus->isOK() ) {
+                                       $extensions[] = $name;
+                               } else {
+                                       $status->merge( $extStatus );
+                               }
+                       }
+               }
+               $extensions = array_unique( $extensions );
+               $status->value = $extensions;
+               return $status;
+       }
+
        /**
         * Main entry point.
         */
index 0f8a5b0..d51ea2e 100644 (file)
@@ -1264,15 +1264,33 @@ abstract class Installer {
        }
 
        /**
-        * Finds extensions that follow the format /$directory/Name/Name.php,
-        * and returns an array containing the value for 'Name' for each found extension.
+        * Find extensions or skins in a subdirectory of $IP.
+        * Returns an array containing the value for 'Name' for each found extension.
         *
-        * Reasonable values for $directory include 'extensions' (the default) and 'skins'.
-        *
-        * @param string $directory Directory to search in
+        * @param string $directory Directory to search in, relative to $IP, must be either "extensions"
+        *     or "skins"
         * @return array [ $extName => [ 'screenshots' => [ '...' ] ]
         */
        public function findExtensions( $directory = 'extensions' ) {
+               switch ( $directory ) {
+                       case 'extensions':
+                               return $this->findExtensionsByType( 'extension', 'extensions' );
+                       case 'skins':
+                               return $this->findExtensionsByType( 'skin', 'skins' );
+                       default:
+                               throw new InvalidArgumentException( "Invalid extension type" );
+               }
+       }
+
+       /**
+        * Find extensions or skins, and return an array containing the value for 'Name' for each found
+        * extension.
+        *
+        * @param string $type Either "extension" or "skin"
+        * @param string $directory Directory to search in, relative to $IP
+        * @return array [ $extName => [ 'screenshots' => [ '...' ] ]
+        */
+       protected function findExtensionsByType( $type = 'extension', $directory = 'extensions' ) {
                if ( $this->getVar( 'IP' ) === null ) {
                        return [];
                }
@@ -1282,40 +1300,15 @@ abstract class Installer {
                        return [];
                }
 
-               // extensions -> extension.json, skins -> skin.json
-               $jsonFile = substr( $directory, 0, strlen( $directory ) - 1 ) . '.json';
-
                $dh = opendir( $extDir );
                $exts = [];
                while ( ( $file = readdir( $dh ) ) !== false ) {
                        if ( !is_dir( "$extDir/$file" ) ) {
                                continue;
                        }
-                       $fullJsonFile = "$extDir/$file/$jsonFile";
-                       $isJson = file_exists( $fullJsonFile );
-                       $isPhp = false;
-                       if ( !$isJson ) {
-                               // Only fallback to PHP file if JSON doesn't exist
-                               $fullPhpFile = "$extDir/$file/$file.php";
-                               $isPhp = file_exists( $fullPhpFile );
-                       }
-                       if ( $isJson || $isPhp ) {
-                               // Extension exists. Now see if there are screenshots
-                               $exts[$file] = [];
-                               if ( is_dir( "$extDir/$file/screenshots" ) ) {
-                                       $paths = glob( "$extDir/$file/screenshots/*.png" );
-                                       foreach ( $paths as $path ) {
-                                               $exts[$file]['screenshots'][] = str_replace( $extDir, "../$directory", $path );
-                                       }
-
-                               }
-                       }
-                       if ( $isJson ) {
-                               $info = $this->readExtension( $fullJsonFile );
-                               if ( $info === false ) {
-                                       continue;
-                               }
-                               $exts[$file] += $info;
+                       $status = $this->getExtensionInfo( $type, $directory, $file );
+                       if ( $status->isOK() ) {
+                               $exts[$file] = $status->value;
                        }
                }
                closedir( $dh );
@@ -1324,12 +1317,65 @@ abstract class Installer {
                return $exts;
        }
 
+       /**
+        * @param string $type Either "extension" or "skin"
+        * @param string $parentRelPath The parent directory relative to $IP
+        * @param string $name The extension or skin name
+        * @return Status An object containing an error list. If there were no errors, an associative
+        *     array of information about the extension can be found in $status->value.
+        */
+       protected function getExtensionInfo( $type, $parentRelPath, $name ) {
+               if ( $this->getVar( 'IP' ) === null ) {
+                       throw new Exception( 'Cannot find extensions since the IP variable is not yet set' );
+               }
+               if ( $type !== 'extension' && $type !== 'skin' ) {
+                       throw new InvalidArgumentException( "Invalid extension type" );
+               }
+               $absDir = $this->getVar( 'IP' ) . "/$parentRelPath/$name";
+               $relDir = "../$parentRelPath/$name";
+               if ( !is_dir( $absDir ) ) {
+                       return Status::newFatal( 'config-extension-not-found', $name );
+               }
+               $jsonFile = $type . '.json';
+               $fullJsonFile = "$absDir/$jsonFile";
+               $isJson = file_exists( $fullJsonFile );
+               $isPhp = false;
+               if ( !$isJson ) {
+                       // Only fallback to PHP file if JSON doesn't exist
+                       $fullPhpFile = "$absDir/$name.php";
+                       $isPhp = file_exists( $fullPhpFile );
+               }
+               if ( !$isJson && !$isPhp ) {
+                       return Status::newFatal( 'config-extension-not-found', $name );
+               }
+
+               // Extension exists. Now see if there are screenshots
+               $info = [];
+               if ( is_dir( "$absDir/screenshots" ) ) {
+                       $paths = glob( "$absDir/screenshots/*.png" );
+                       foreach ( $paths as $path ) {
+                               $info['screenshots'][] = str_replace( $absDir, $relDir, $path );
+                       }
+               }
+
+               if ( $isJson ) {
+                       $jsonStatus = $this->readExtension( $fullJsonFile );
+                       if ( !$jsonStatus->isOK() ) {
+                               return $jsonStatus;
+                       }
+                       $info += $jsonStatus->value;
+               }
+
+               return Status::newGood( $info );
+       }
+
        /**
         * @param string $fullJsonFile
         * @param array $extDeps
         * @param array $skinDeps
         *
-        * @return array|bool False if this extension can't be loaded
+        * @return Status On success, an array of extension information is in $status->value. On
+        *    failure, the Status object will have an error list.
         */
        private function readExtension( $fullJsonFile, $extDeps = [], $skinDeps = [] ) {
                $load = [
@@ -1340,7 +1386,7 @@ abstract class Installer {
                        foreach ( $extDeps as $dep ) {
                                $fname = "$extDir/$dep/extension.json";
                                if ( !file_exists( $fname ) ) {
-                                       return false;
+                                       return Status::newFatal( 'config-extension-not-found', $dep );
                                }
                                $load[$fname] = 1;
                        }
@@ -1350,7 +1396,7 @@ abstract class Installer {
                        foreach ( $skinDeps as $dep ) {
                                $fname = "$skinDir/$dep/skin.json";
                                if ( !file_exists( $fname ) ) {
-                                       return false;
+                                       return Status::newFatal( 'config-extension-not-found', $dep );
                                }
                                $load[$fname] = 1;
                        }
@@ -1364,7 +1410,8 @@ abstract class Installer {
                        ) {
                                // If something is incompatible with a dependency, we have no real
                                // option besides skipping it
-                               return false;
+                               return Status::newFatal( 'config-extension-dependency',
+                                       basename( dirname( $fullJsonFile ) ), $e->getMessage() );
                        } elseif ( $e->missingExtensions || $e->missingSkins ) {
                                // There's an extension missing in the dependency tree,
                                // so add those to the dependency list and try again
@@ -1375,7 +1422,8 @@ abstract class Installer {
                                );
                        }
                        // Some other kind of dependency error?
-                       return false;
+                       return Status::newFatal( 'config-extension-dependency',
+                               basename( dirname( $fullJsonFile ) ), $e->getMessage() );
                }
                $ret = [];
                // The order of credits will be the order of $load,
@@ -1397,7 +1445,7 @@ abstract class Installer {
                }
                $ret['type'] = $credits['type'];
 
-               return $ret;
+               return Status::newGood( $ret );
        }
 
        /**
index c89be17..893df5a 100644 (file)
        "config-skins-screenshot": "$1 ($2)",
        "config-extensions-requires": "$1 (requires $2)",
        "config-screenshot": "screenshot",
+       "config-extension-not-found": "Could not find the registration file for the extension \"$1\"",
+       "config-extension-dependency": "A dependency error was encountered while installing the extension \"$1\": $2",
        "mainpagetext": "<strong>MediaWiki has been installed.</strong>",
        "mainpagedocfooter": "Consult the [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents User's Guide] for information on using the wiki software.\n\n== Getting started ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Configuration settings list]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Localise MediaWiki for your language]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Learn how to combat spam on your wiki]"
 }
index e423bcd..39dbbca 100644 (file)
        "config-skins-screenshot": "Radio button text, $1 is the skin name, and $2 is a link to a screenshot of that skin, where the link text is {{msg-mw|config-screenshot}}.",
        "config-extensions-requires": "Radio button text, $1 is the extension name, and $2 are links to other extensions that this one requires.\n{{Identical|Require}}",
        "config-screenshot": "Link text for the link in {{msg-mw|config-skins-screenshot}}\n{{Identical|Screenshot}}",
+       "config-extension-not-found": "An error shown when an extension or skin named by the user could not be found.\n* $1 is the extension name",
+       "config-extension-dependency": "An error shown if an extension could not be loaded due to it depending on the wrong version of MediaWiki or an uninstallable extension.\n* $1 is the extension name\n* $2 is a more detailed explanation, in English",
        "mainpagetext": "Along with {{msg-mw|mainpagedocfooter}}, the text you will see on the Main Page when your wiki is installed.",
        "mainpagedocfooter": "Along with {{msg-mw|mainpagetext}}, the text you will see on the Main Page when your wiki is installed.\nThis might be a good place to put information about <nowiki>{{GRAMMAR:}}</nowiki>. See [[{{NAMESPACE}}:{{BASEPAGENAME}}/fi]] for an example. For languages having grammatical distinctions and not having an appropriate <nowiki>{{GRAMMAR:}}</nowiki> software available, a suggestion to check and possibly amend the messages having <nowiki>{{SITENAME}}</nowiki> may be valuable. See [[{{NAMESPACE}}:{{BASEPAGENAME}}/ksh]] for an example."
 }
index f87a336..63575eb 100644 (file)
@@ -134,4 +134,16 @@ class ThumbnailRenderJob extends Job {
                }
                return false;
        }
+
+       /**
+        * Whether to retry the job.
+        * @return bool
+        */
+       public function allowRetries() {
+               // ThumbnailRenderJob is a warmup for the thumbnails cache,
+               // so loosing it is not a problem. Most times the job fails
+               // for non-renderable or missing images which will not be fixed
+               // by a retry, but will create additional load on the renderer.
+               return false;
+       }
 }
index 0f820ae..71db975 100644 (file)
@@ -303,16 +303,6 @@ abstract class MediaHandler {
                return [ $ext, $mime ];
        }
 
-       /**
-        * @deprecated since 1.30, use MediaHandler::getContentHeaders instead
-        * @param array $metadata
-        * @return array
-        */
-       public function getStreamHeaders( $metadata ) {
-               wfDeprecated( __METHOD__, '1.30' );
-               return $this->getContentHeaders( $metadata );
-       }
-
        /**
         * True if the handled types can be transformed
         *
index 445981b..48ba111 100644 (file)
@@ -1290,42 +1290,6 @@ class ParserOutput extends CacheTime {
                );
        }
 
-       // TODO remove this method once old parser cache objects have expired, probably mid-October 2018
-       public function __wakeup() {
-               // T203716 remove wrapper that was added by logic in an older version of this class,
-               // where the wrapper was included in mText. This might sometimes remove a wrapper that's
-               // genuine content (manually added to a system message), but that will work out OK, see below.
-               $text = $this->getRawText();
-               $start = Html::openElement( 'div', [
-                       'class' => 'mw-parser-output'
-               ] );
-               $startLen = strlen( $start );
-               $end = Html::closeElement( 'div' );
-               $endPos = strrpos( $text, $end );
-               $endLen = strlen( $end );
-               if ( substr( $text, 0, $startLen ) === $start && $endPos !== false
-                        // if the closing div is followed by real content, bail out of unwrapping
-                        && preg_match( '/^(?>\s*<!--.*?-->)*\s*$/s', substr( $text, $endPos + $endLen ) )
-               ) {
-                       $text = substr( $text, $startLen );
-                       $text = substr( $text, 0, $endPos - $startLen ) .
-                                       substr( $text, $endPos - $startLen + $endLen );
-                       $this->setText( $text );
-                       // We found a wrapper to remove, so the ParserOutput was probably created by the
-                       // code path that now contains an addWrapperDivClass( 'mw-parser-output' ) call,
-                       // but it did not contain it when this object was cached, so we need to fix the
-                       // wrapper class variable.
-                       // If this was a message with a manually added wrapper, we are technically wrong about
-                       // this but we were wrong about the unwrapping as well so it will work out just right,
-                       // except when this is a normal page view of such a message page, in which case
-                       // it will be single-wrapped instead of double-wrapped (harmless) or something wants
-                       // render the message with unwrap=true (in which case the message won't be wrapped even
-                       // though it should, but the few code paths using unwrap=true only do it for real pages).
-                       $this->clearWrapperDivClass();
-                       $this->addWrapperDivClass( 'mw-parser-output' );
-               }
-       }
-
        /**
         * Merges internal metadata such as flags, accessed options, and profiling info
         * from $source into this ParserOutput. This should be used whenever the state of $source
index 438e9dc..3395458 100644 (file)
@@ -90,6 +90,10 @@ class CommandLineInstaller extends Maintenance {
                $this->addOption( 'env-checks', "Run environment checks only, don't change anything" );
 
                $this->addOption( 'with-extensions', "Detect and include extensions" );
+               $this->addOption( 'extensions', 'Comma-separated list of extensions to install',
+                       false, true, false, true );
+               $this->addOption( 'skins', 'Comma-separated list of skins to install (default: all)',
+                       false, true, false, true );
        }
 
        public function getDbType() {
index c780b6a..2a1feb4 100755 (executable)
@@ -85,7 +85,7 @@ class UpdateMediaWiki extends Maintenance {
        }
 
        function execute() {
-               global $wgVersion, $wgLang, $wgAllowSchemaUpdates;
+               global $wgVersion, $wgLang, $wgAllowSchemaUpdates, $wgMessagesDirs;
 
                if ( !$wgAllowSchemaUpdates
                        && !( $this->hasOption( 'force' )
@@ -111,6 +111,9 @@ class UpdateMediaWiki extends Maintenance {
                        }
                }
 
+               // T206765: We need to load the installer i18n files as some of errors come installer/updater code
+               $wgMessagesDirs['MediawikiInstaller'] = dirname( __DIR__ ) . '/includes/installer/i18n';
+
                $lang = Language::factory( 'en' );
                // Set global language to ensure localised errors are in English (T22633)
                RequestContext::getMain()->setLanguage( $lang );
index 94cbf5c..390ea41 100644 (file)
@@ -149,37 +149,6 @@ class ParserOutputTest extends MediaWikiLangTestCase {
                $this->assertNotContains( 'class="foo bar"', $text );
        }
 
-       public function testT203716() {
-               // simulate extra wrapping from old parser cache
-               $out = new ParserOutput( '<div class="mw-parser-output">Foo</div>' );
-               $out = unserialize( serialize( $out ) );
-
-               $plainText = $out->getText( [ 'unwrap' => true ] );
-               $wrappedText = $out->getText( [ 'unwrap' => false ] );
-               $wrappedText2 = $out->getText( [ 'wrapperDivClass' => 'mw-parser-output' ] );
-
-               $this->assertNotContains( '<div', $plainText );
-               $this->assertContains( '<div', $wrappedText );
-               $this->assertStringNotMatchesFormat( '<div%s<div%s', $wrappedText );
-               $this->assertContains( '<div', $wrappedText2 );
-               $this->assertStringNotMatchesFormat( '<div%s<div%s', $wrappedText2 );
-
-               // simulate ParserOuput creation by new parser code
-               $out = new ParserOutput( 'Foo' );
-               $out->addWrapperDivClass( 'mw-parser-outout' );
-               $out = unserialize( serialize( $out ) );
-
-               $plainText = $out->getText( [ 'unwrap' => true ] );
-               $wrappedText = $out->getText( [ 'unwrap' => false ] );
-               $wrappedText2 = $out->getText( [ 'wrapperDivClass' => 'mw-parser-output' ] );
-
-               $this->assertNotContains( '<div', $plainText );
-               $this->assertContains( '<div', $wrappedText );
-               $this->assertStringNotMatchesFormat( '<div%s<div%s', $wrappedText );
-               $this->assertContains( '<div', $wrappedText2 );
-               $this->assertStringNotMatchesFormat( '<div%s<div%s', $wrappedText2 );
-       }
-
        /**
         * @covers ParserOutput::getText
         * @dataProvider provideGetText
index d57d489..92d986d 100644 (file)
@@ -32,12 +32,6 @@ abstract class AbstractChangesListSpecialPageTestCase extends MediaWikiTestCase
                        'patrol' => true,
                ];
 
-               // Deprecated
-               $this->setTemporaryHook(
-                       'ChangesListSpecialPageFilters',
-                       null
-               );
-
                # setup the ChangesListSpecialPage (or subclass) object
                $this->changesListSpecialPage = $this->getPage();
                $context = $this->changesListSpecialPage->getContext();
index 2ebefac..28e26a0 100644 (file)
@@ -13,11 +13,6 @@ class SpecialWatchlistTest extends SpecialPageTestBase {
        public function setUp() {
                parent::setUp();
 
-               $this->setTemporaryHook(
-                       'ChangesListSpecialPageFilters',
-                       null
-               );
-
                $this->setTemporaryHook(
                        'ChangesListSpecialPageQuery',
                        null
@@ -102,11 +97,6 @@ class SpecialWatchlistTest extends SpecialPageTestBase {
                        $this->newSpecialPage()
                );
 
-               $this->setTemporaryHook(
-                       'ChangesListSpecialPageFilters',
-                       null
-               );
-
                $page->registerFilters();
 
                // Does not consider $preferences, just wiki's defaults