Merge "Add support for native image lazy loading"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 22 Aug 2019 01:48:49 +0000 (01:48 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 22 Aug 2019 01:48:49 +0000 (01:48 +0000)
41 files changed:
RELEASE-NOTES-1.34
autoload.php
docs/hooks.txt
includes/MediaWikiServices.php
includes/Permissions/PermissionManager.php
includes/Rest/EntryPoint.php
includes/ServiceWiring.php
includes/api/ApiImageRotate.php
includes/block/AbstractBlock.php
includes/block/BlockManager.php
includes/block/DatabaseBlock.php
includes/content/AbstractContent.php
includes/content/Content.php
includes/content/WikitextContent.php
includes/deferred/DeferrableCallback.php
includes/deferred/DeferredUpdates.php
includes/filebackend/FileBackendGroup.php
includes/filerepo/file/File.php
includes/libs/filebackend/FSFileBackend.php
includes/libs/filebackend/FileBackend.php
includes/libs/filebackend/MemoryFileBackend.php
includes/libs/filebackend/SwiftFileBackend.php
includes/libs/filebackend/fsfile/TempFSFile.php
includes/libs/filebackend/fsfile/TempFSFileFactory.php [new file with mode: 0644]
includes/libs/objectcache/MemcachedClient.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseMysqlBase.php
includes/libs/rdbms/database/domain/DatabaseDomain.php
includes/parser/Parser.php
includes/specialpage/RedirectSpecialArticle.php
includes/upload/UploadFromChunks.php
includes/upload/UploadFromUrl.php
maintenance/includes/BackupDumper.php
resources/src/mediawiki.widgets/mw.widgets.CalendarWidget.js
resources/src/mediawiki.widgets/mw.widgets.CalendarWidget.less
tests/phpunit/includes/Permissions/PermissionManagerTest.php
tests/phpunit/includes/Rest/BasicAccess/MWBasicRequestAuthorizerTest.php
tests/phpunit/includes/deferred/DeferredUpdatesTest.php
tests/phpunit/includes/libs/filebackend/fsfile/TempFSFileIntegrationTest.php
tests/phpunit/includes/parser/ParserMethodsTest.php
tests/phpunit/unit/includes/libs/filebackend/fsfile/TempFSFileTest.php [new file with mode: 0644]

index 6e78948..00e4aad 100644 (file)
@@ -98,6 +98,8 @@ For notes on 1.33.x and older releases, see HISTORY.
   to add fields to Special:Mute.
 * (T100896) Skin authors can define custom OOUI themes using OOUIThemePaths.
   See <https://www.mediawiki.org/wiki/OOUI/Themes> for details.
+* (T229035) The GetUserBlock hook was added. Use this instead of
+  GetBlockedStatus.
 
 === External library changes in 1.34 ===
 
@@ -347,6 +349,8 @@ because of Phabricator reports.
   MediaWikiTestCase::resetServices().
 * SearchResult protected field $searchEngine is removed and no longer
   initialized after calling SearchResult::initFromTitle().
+* The UserIsBlockedFrom hook is only called if a block is found first, and
+  should only be used to unblock a blocked user.
 * …
 
 === Deprecations in 1.34 ===
@@ -453,6 +457,7 @@ because of Phabricator reports.
 * MessageCache::singleton() is deprecated. Use
   MediaWikiServices::getMessageCache().
 * Constructing MovePage directly is deprecated. Use MovePageFactory.
+* TempFSFile::factory() has been deprecated. Use TempFSFileFactory instead.
 
 === Other changes in 1.34 ===
 * …
index acdf8dd..20c19e5 100644 (file)
@@ -880,6 +880,7 @@ $wgAutoloadLocalClasses = [
        'MediaWiki\\DB\\PatchFileLocation' => __DIR__ . '/includes/db/PatchFileLocation.php',
        'MediaWiki\\Diff\\ComplexityException' => __DIR__ . '/includes/diff/ComplexityException.php',
        'MediaWiki\\Diff\\WordAccumulator' => __DIR__ . '/includes/diff/WordAccumulator.php',
+       'MediaWiki\\FileBackend\\FSFile\\TempFSFileFactory' => __DIR__ . '/includes/libs/filebackend/fsfile/TempFSFileFactory.php',
        'MediaWiki\\HeaderCallback' => __DIR__ . '/includes/HeaderCallback.php',
        'MediaWiki\\Http\\HttpRequestFactory' => __DIR__ . '/includes/http/HttpRequestFactory.php',
        'MediaWiki\\Installer\\InstallException' => __DIR__ . '/includes/installer/InstallException.php',
index 09b2c97..719c60f 100644 (file)
@@ -1726,6 +1726,11 @@ $contentHandler: ContentHandler for which the slot diff renderer is fetched.
 &$slotDiffRenderer: SlotDiffRenderer to change or replace.
 $context: IContextSource
 
+'GetUserBlock': Modify the block found by the block manager. This may be a
+single block or a composite block made from multiple blocks; the original
+blocks can be seen using CompositeBlock::getOriginalBlocks()
+&$block: AbstractBlock object
+
 'getUserPermissionsErrors': Add a permissions error when permissions errors are
 checked for. Use instead of userCan for most cases. Return false if the user
 can't do it, and populate $result with the reason in the form of
@@ -3700,7 +3705,7 @@ $newUGMs: An associative array (group name => UserGroupMembership object) of
 the user's current group memberships.
 
 'UserIsBlockedFrom': Check if a user is blocked from a specific page (for
-specific block exemptions).
+specific block exemptions if a user is already blocked).
 $user: User in question
 $title: Title of the page in question
 &$blocked: Out-param, whether or not the user is blocked from that page.
index bb9c05f..ec82f8e 100644 (file)
@@ -16,6 +16,7 @@ use IBufferingStatsdDataFactory;
 use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
 use MediaWiki\Block\BlockManager;
 use MediaWiki\Block\BlockRestrictionStore;
+use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
 use MediaWiki\Http\HttpRequestFactory;
 use MediaWiki\Page\MovePageFactory;
 use MediaWiki\Permissions\PermissionManager;
@@ -964,6 +965,14 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'StatsdDataFactory' );
        }
 
+       /**
+        * @since 1.34
+        * @return TempFSFileFactory
+        */
+       public function getTempFSFileFactory() : TempFSFileFactory {
+               return $this->getService( 'TempFSFileFactory' );
+       }
+
        /**
         * @since 1.28
         * @return TitleFormatter
index 248ba14..2d4885e 100644 (file)
@@ -22,6 +22,7 @@ namespace MediaWiki\Permissions;
 use Action;
 use Exception;
 use Hooks;
+use MediaWiki\Config\ServiceOptions;
 use MediaWiki\Linker\LinkTarget;
 use MediaWiki\Revision\RevisionLookup;
 use MediaWiki\Revision\RevisionRecord;
@@ -54,36 +55,34 @@ class PermissionManager {
        /** @var string Does cheap and expensive checks, using the master as needed */
        const RIGOR_SECURE = 'secure';
 
+       /**
+        * TODO Make this const when HHVM support is dropped (T192166)
+        *
+        * @since 1.34
+        * @var array
+        */
+       public static $constructorOptions = [
+               'WhitelistRead',
+               'WhitelistReadRegexp',
+               'EmailConfirmToEdit',
+               'BlockDisablesLogin',
+               'GroupPermissions',
+               'RevokePermissions',
+               'AvailableRights'
+       ];
+
+       /** @var ServiceOptions */
+       private $options;
+
        /** @var SpecialPageFactory */
        private $specialPageFactory;
 
        /** @var RevisionLookup */
        private $revisionLookup;
 
-       /** @var string[] List of pages names anonymous user may see */
-       private $whitelistRead;
-
-       /** @var string[] Whitelists publicly readable titles with regular expressions */
-       private $whitelistReadRegexp;
-
-       /** @var bool Require users to confirm email address before they can edit */
-       private $emailConfirmToEdit;
-
-       /** @var bool If set to true, blocked users will no longer be allowed to log in */
-       private $blockDisablesLogin;
-
        /** @var NamespaceInfo */
        private $nsInfo;
 
-       /** @var string[][] Access rights for groups and users in these groups */
-       private $groupPermissions;
-
-       /** @var string[][] Permission keys revoked from users in each group */
-       private $revokePermissions;
-
-       /** @var string[] A list of available rights, in addition to the ones defined by the core */
-       private $availableRights;
-
        /** @var string[] Cached results of getAllRights() */
        private $allRights = false;
 
@@ -189,38 +188,21 @@ class PermissionManager {
        ];
 
        /**
+        * @param ServiceOptions $options
         * @param SpecialPageFactory $specialPageFactory
         * @param RevisionLookup $revisionLookup
-        * @param string[] $whitelistRead
-        * @param string[] $whitelistReadRegexp
-        * @param bool $emailConfirmToEdit
-        * @param bool $blockDisablesLogin
-        * @param string[][] $groupPermissions
-        * @param string[][] $revokePermissions
-        * @param string[] $availableRights
         * @param NamespaceInfo $nsInfo
         */
        public function __construct(
+               ServiceOptions $options,
                SpecialPageFactory $specialPageFactory,
                RevisionLookup $revisionLookup,
-               $whitelistRead,
-               $whitelistReadRegexp,
-               $emailConfirmToEdit,
-               $blockDisablesLogin,
-               $groupPermissions,
-               $revokePermissions,
-               $availableRights,
                NamespaceInfo $nsInfo
        ) {
+               $options->assertRequiredOptions( self::$constructorOptions );
+               $this->options = $options;
                $this->specialPageFactory = $specialPageFactory;
                $this->revisionLookup = $revisionLookup;
-               $this->whitelistRead = $whitelistRead;
-               $this->whitelistReadRegexp = $whitelistReadRegexp;
-               $this->emailConfirmToEdit = $emailConfirmToEdit;
-               $this->blockDisablesLogin = $blockDisablesLogin;
-               $this->groupPermissions = $groupPermissions;
-               $this->revokePermissions = $revokePermissions;
-               $this->availableRights = $availableRights;
                $this->nsInfo = $nsInfo;
        }
 
@@ -289,7 +271,8 @@ class PermissionManager {
        }
 
        /**
-        * Check if user is blocked from editing a particular article
+        * Check if user is blocked from editing a particular article. If the user does not
+        * have a block, this will return false.
         *
         * @param User $user
         * @param LinkTarget $page Title to check
@@ -298,27 +281,29 @@ class PermissionManager {
         * @return bool
         */
        public function isBlockedFrom( User $user, LinkTarget $page, $fromReplica = false ) {
-               $blocked = $user->isHidden();
+               $block = $user->getBlock( $fromReplica );
+               if ( !$block ) {
+                       return false;
+               }
 
                // TODO: remove upon further migration to LinkTarget
                $title = Title::newFromLinkTarget( $page );
 
+               $blocked = $user->isHidden();
                if ( !$blocked ) {
-                       $block = $user->getBlock( $fromReplica );
-                       if ( $block ) {
-                               // Special handling for a user's own talk page. The block is not aware
-                               // of the user, so this must be done here.
-                               if ( $title->equals( $user->getTalkPage() ) ) {
-                                       $blocked = $block->appliesToUsertalk( $title );
-                               } else {
-                                       $blocked = $block->appliesToTitle( $title );
-                               }
+                       // Special handling for a user's own talk page. The block is not aware
+                       // of the user, so this must be done here.
+                       if ( $title->equals( $user->getTalkPage() ) ) {
+                               $blocked = $block->appliesToUsertalk( $title );
+                       } else {
+                               $blocked = $block->appliesToTitle( $title );
                        }
                }
 
                // only for the purpose of the hook. We really don't need this here.
                $allowUsertalk = $user->isAllowUsertalk();
 
+               // Allow extensions to let a blocked user access a particular page
                Hooks::run( 'UserIsBlockedFrom', [ $user, $title, &$blocked, &$allowUsertalk ] );
 
                return $blocked;
@@ -500,6 +485,7 @@ class PermissionManager {
                // TODO: remove when LinkTarget usage will expand further
                $title = Title::newFromLinkTarget( $page );
 
+               $whiteListRead = $this->options->get( 'WhitelistRead' );
                $whitelisted = false;
                if ( $this->isEveryoneAllowed( 'read' ) ) {
                        # Shortcut for public wikis, allows skipping quite a bit of code
@@ -514,20 +500,20 @@ class PermissionManager {
                        # Always grant access to the login page.
                        # Even anons need to be able to log in.
                        $whitelisted = true;
-               } elseif ( is_array( $this->whitelistRead ) && count( $this->whitelistRead ) ) {
+               } elseif ( is_array( $whiteListRead ) && count( $whiteListRead ) ) {
                        # Time to check the whitelist
                        # Only do these checks is there's something to check against
                        $name = $title->getPrefixedText();
                        $dbName = $title->getPrefixedDBkey();
 
                        // Check for explicit whitelisting with and without underscores
-                       if ( in_array( $name, $this->whitelistRead, true )
-                                || in_array( $dbName, $this->whitelistRead, true ) ) {
+                       if ( in_array( $name, $whiteListRead, true )
+                                || in_array( $dbName, $whiteListRead, true ) ) {
                                $whitelisted = true;
                        } elseif ( $title->getNamespace() == NS_MAIN ) {
                                # Old settings might have the title prefixed with
                                # a colon for main-namespace pages
-                               if ( in_array( ':' . $name, $this->whitelistRead ) ) {
+                               if ( in_array( ':' . $name, $whiteListRead ) ) {
                                        $whitelisted = true;
                                }
                        } elseif ( $title->isSpecialPage() ) {
@@ -537,18 +523,19 @@ class PermissionManager {
                                        $this->specialPageFactory->resolveAlias( $name );
                                if ( $name ) {
                                        $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
-                                       if ( in_array( $pure, $this->whitelistRead, true ) ) {
+                                       if ( in_array( $pure, $whiteListRead, true ) ) {
                                                $whitelisted = true;
                                        }
                                }
                        }
                }
 
-               if ( !$whitelisted && is_array( $this->whitelistReadRegexp )
-                        && !empty( $this->whitelistReadRegexp ) ) {
+               $whitelistReadRegexp = $this->options->get( 'WhitelistReadRegexp' );
+               if ( !$whitelisted && is_array( $whitelistReadRegexp )
+                        && !empty( $whitelistReadRegexp ) ) {
                        $name = $title->getPrefixedText();
                        // Check for regex whitelisting
-                       foreach ( $this->whitelistReadRegexp as $listItem ) {
+                       foreach ( $whitelistReadRegexp as $listItem ) {
                                if ( preg_match( $listItem, $name ) ) {
                                        $whitelisted = true;
                                        break;
@@ -636,11 +623,11 @@ class PermissionManager {
                }
 
                // Optimize for a very common case
-               if ( $action === 'read' && !$this->blockDisablesLogin ) {
+               if ( $action === 'read' && !$this->options->get( 'BlockDisablesLogin' ) ) {
                        return $errors;
                }
 
-               if ( $this->emailConfirmToEdit
+               if ( $this->options->get( 'EmailConfirmToEdit' )
                         && !$user->isEmailConfirmed()
                         && $action === 'edit'
                ) {
@@ -1246,7 +1233,7 @@ class PermissionManager {
 
                        if (
                                $user->isLoggedIn() &&
-                               $this->blockDisablesLogin &&
+                               $this->options->get( 'BlockDisablesLogin' ) &&
                                $user->getBlock()
                        ) {
                                $anon = new User;
@@ -1296,10 +1283,10 @@ class PermissionManager {
         * @return bool
         */
        public function groupHasPermission( $group, $role ) {
-               return isset( $this->groupPermissions[$group][$role] ) &&
-                          $this->groupPermissions[$group][$role] &&
-                          !( isset( $this->revokePermissions[$group][$role] ) &&
-                                 $this->revokePermissions[$group][$role] );
+               $groupPermissions = $this->options->get( 'GroupPermissions' );
+               $revokePermissions = $this->options->get( 'RevokePermissions' );
+               return isset( $groupPermissions[$group][$role] ) && $groupPermissions[$group][$role] &&
+                          !( isset( $revokePermissions[$group][$role] ) && $revokePermissions[$group][$role] );
        }
 
        /**
@@ -1314,17 +1301,17 @@ class PermissionManager {
                $rights = [];
                // grant every granted permission first
                foreach ( $groups as $group ) {
-                       if ( isset( $this->groupPermissions[$group] ) ) {
+                       if ( isset( $this->options->get( 'GroupPermissions' )[$group] ) ) {
                                $rights = array_merge( $rights,
                                        // array_filter removes empty items
-                                       array_keys( array_filter( $this->groupPermissions[$group] ) ) );
+                                       array_keys( array_filter( $this->options->get( 'GroupPermissions' )[$group] ) ) );
                        }
                }
                // now revoke the revoked permissions
                foreach ( $groups as $group ) {
-                       if ( isset( $this->revokePermissions[$group] ) ) {
+                       if ( isset( $this->options->get( 'RevokePermissions' )[$group] ) ) {
                                $rights = array_diff( $rights,
-                                       array_keys( array_filter( $this->revokePermissions[$group] ) ) );
+                                       array_keys( array_filter( $this->options->get( 'RevokePermissions' )[$group] ) ) );
                        }
                }
                return array_unique( $rights );
@@ -1340,7 +1327,7 @@ class PermissionManager {
         */
        public function getGroupsWithPermission( $role ) {
                $allowedGroups = [];
-               foreach ( array_keys( $this->groupPermissions ) as $group ) {
+               foreach ( array_keys( $this->options->get( 'GroupPermissions' ) ) as $group ) {
                        if ( $this->groupHasPermission( $group, $role ) ) {
                                $allowedGroups[] = $group;
                        }
@@ -1370,14 +1357,14 @@ class PermissionManager {
                        return $this->cachedRights[$right];
                }
 
-               if ( !isset( $this->groupPermissions['*'][$right] )
-                        || !$this->groupPermissions['*'][$right] ) {
+               if ( !isset( $this->options->get( 'GroupPermissions' )['*'][$right] )
+                        || !$this->options->get( 'GroupPermissions' )['*'][$right] ) {
                        $this->cachedRights[$right] = false;
                        return false;
                }
 
                // If it's revoked anywhere, then everyone doesn't have it
-               foreach ( $this->revokePermissions as $rights ) {
+               foreach ( $this->options->get( 'RevokePermissions' ) as $rights ) {
                        if ( isset( $rights[$right] ) && $rights[$right] ) {
                                $this->cachedRights[$right] = false;
                                return false;
@@ -1415,10 +1402,10 @@ class PermissionManager {
         */
        public function getAllPermissions() {
                if ( $this->allRights === false ) {
-                       if ( count( $this->availableRights ) ) {
+                       if ( count( $this->options->get( 'AvailableRights' ) ) ) {
                                $this->allRights = array_unique( array_merge(
                                        $this->coreRights,
-                                       $this->availableRights
+                                       $this->options->get( 'AvailableRights' )
                                ) );
                        } else {
                                $this->allRights = $this->coreRights;
index a14c1a1..ae01ae4 100644 (file)
@@ -90,6 +90,9 @@ class EntryPoint {
                                $cookie['options'] );
                }
 
+               // Clear all errors that might have been displayed if display_errors=On
+               ob_clean();
+
                $stream = $response->getBody();
                $stream->rewind();
                if ( $stream instanceof CopyableStreamInterface ) {
index 663f371..0ae35c3 100644 (file)
@@ -43,6 +43,7 @@ use MediaWiki\Block\BlockManager;
 use MediaWiki\Block\BlockRestrictionStore;
 use MediaWiki\Config\ConfigRepository;
 use MediaWiki\Config\ServiceOptions;
+use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
 use MediaWiki\Http\HttpRequestFactory;
 use MediaWiki\Interwiki\ClassicInterwikiLookup;
 use MediaWiki\Interwiki\InterwikiLookup;
@@ -497,17 +498,12 @@ return [
        },
 
        'PermissionManager' => function ( MediaWikiServices $services ) : PermissionManager {
-               $config = $services->getMainConfig();
                return new PermissionManager(
+                       new ServiceOptions(
+                               PermissionManager::$constructorOptions, $services->getMainConfig()
+                       ),
                        $services->getSpecialPageFactory(),
                        $services->getRevisionLookup(),
-                       $config->get( 'WhitelistRead' ),
-                       $config->get( 'WhitelistReadRegexp' ),
-                       $config->get( 'EmailConfirmToEdit' ),
-                       $config->get( 'BlockDisablesLogin' ),
-                       $config->get( 'GroupPermissions' ),
-                       $config->get( 'RevokePermissions' ),
-                       $config->get( 'AvailableRights' ),
                        $services->getNamespaceInfo()
                );
        },
@@ -716,6 +712,10 @@ return [
                );
        },
 
+       'TempFSFileFactory' => function ( MediaWikiServices $services ) : TempFSFileFactory {
+               return new TempFSFileFactory( $services->getMainConfig()->get( 'TmpDirectory' ) );
+       },
+
        'TitleFormatter' => function ( MediaWikiServices $services ) : TitleFormatter {
                return $services->getService( '_MediaWikiTitleCodec' );
        },
index 668bd0e..ccb26a8 100644 (file)
@@ -101,7 +101,8 @@ class ApiImageRotate extends ApiBase {
                                continue;
                        }
                        $ext = strtolower( pathinfo( "$srcPath", PATHINFO_EXTENSION ) );
-                       $tmpFile = TempFSFile::factory( 'rotate_', $ext, wfTempDir() );
+                       $tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory()
+                               ->newTempFSFile( 'rotate_', $ext );
                        $dstPath = $tmpFile->getPath();
                        $err = $handler->rotate( $file, [
                                'srcPath' => $srcPath,
index 9ad7534..fcc624e 100644 (file)
@@ -96,6 +96,7 @@ abstract class AbstractBlock {
         *     reason string        Reason of the block
         *     timestamp string     The time at which the block comes into effect
         *     byText string        Username of the blocker (for foreign users)
+        *     hideName bool        Hide the target user name
         */
        public function __construct( array $options = [] ) {
                $defaults = [
@@ -104,6 +105,7 @@ abstract class AbstractBlock {
                        'reason'          => '',
                        'timestamp'       => '',
                        'byText'          => '',
+                       'hideName'        => false,
                ];
 
                $options += $defaults;
@@ -120,6 +122,7 @@ abstract class AbstractBlock {
 
                $this->setReason( $options['reason'] );
                $this->setTimestamp( wfTimestamp( TS_MW, $options['timestamp'] ) );
+               $this->setHideName( (bool)$options['hideName'] );
        }
 
        /**
index dc0dc79..03043e1 100644 (file)
@@ -23,6 +23,7 @@ namespace MediaWiki\Block;
 use DateTime;
 use DateTimeZone;
 use DeferredUpdates;
+use Hooks;
 use IP;
 use MediaWiki\Config\ServiceOptions;
 use MediaWiki\Permissions\PermissionManager;
@@ -184,6 +185,7 @@ class BlockManager {
                // Filter out any duplicated blocks, e.g. from the cookie
                $blocks = $this->getUniqueBlocks( $blocks );
 
+               $block = null;
                if ( count( $blocks ) > 0 ) {
                        if ( count( $blocks ) === 1 ) {
                                $block = $blocks[ 0 ];
@@ -195,10 +197,11 @@ class BlockManager {
                                        'originalBlocks' => $blocks,
                                ] );
                        }
-                       return $block;
                }
 
-               return null;
+               Hooks::run( 'GetUserBlock', [ clone $user, $ip, &$block ] );
+
+               return $block;
        }
 
        /**
@@ -227,7 +230,7 @@ class BlockManager {
                        }
                }
 
-               return array_merge( $systemBlocks, $databaseBlocks );
+               return array_values( array_merge( $systemBlocks, $databaseBlocks ) );
        }
 
        /**
index 2fd62ee..79286c5 100644 (file)
@@ -93,7 +93,6 @@ class DatabaseBlock extends AbstractBlock {
         *     anonOnly bool        Only disallow anonymous actions
         *     createAccount bool   Disallow creation of new accounts
         *     enableAutoblock bool Enable automatic blocking
-        *     hideName bool        Hide the target user name
         *     blockEmail bool      Disallow sending emails
         *     allowUsertalk bool   Allow the target to edit its own talk page
         *     sitewide bool        Disallow editing all pages and all contribution
@@ -112,7 +111,6 @@ class DatabaseBlock extends AbstractBlock {
                        'anonOnly'        => false,
                        'createAccount'   => false,
                        'enableAutoblock' => false,
-                       'hideName'        => false,
                        'blockEmail'      => false,
                        'allowUsertalk'   => false,
                        'sitewide'        => true,
@@ -129,7 +127,6 @@ class DatabaseBlock extends AbstractBlock {
 
                # Boolean settings
                $this->mAuto = (bool)$options['auto'];
-               $this->setHideName( (bool)$options['hideName'] );
                $this->isHardblock( !$options['anonOnly'] );
                $this->isAutoblocking( (bool)$options['enableAutoblock'] );
                $this->isSitewide( (bool)$options['sitewide'] );
index c82b473..eb9ef85 100644 (file)
@@ -529,7 +529,7 @@ abstract class AbstractContent implements Content {
         * @since 1.24
         *
         * @param Title $title Context title for parsing
-        * @param int|null $revId Revision ID (for {{REVISIONID}})
+        * @param int|null $revId Revision ID being rendered
         * @param ParserOptions|null $options
         * @param bool $generateHtml Whether or not to generate HTML
         *
@@ -575,7 +575,8 @@ abstract class AbstractContent implements Content {
         * @since 1.24
         *
         * @param Title $title Context title for parsing
-        * @param int|null $revId Revision ID (for {{REVISIONID}})
+        * @param int|null $revId ID of the revision being rendered.
+        *  See Parser::parse() for the ramifications.
         * @param ParserOptions $options
         * @param bool $generateHtml Whether or not to generate HTML
         * @param ParserOutput &$output The output object to fill (reference).
index 2637aa6..8596619 100644 (file)
@@ -269,7 +269,8 @@ interface Content {
         *       may call ParserOutput::recordOption() on the output object.
         *
         * @param Title $title The page title to use as a context for rendering.
-        * @param int|null $revId Optional revision ID being rendered.
+        * @param int|null $revId ID of the revision being rendered.
+        *  See Parser::parse() for the ramifications. (default: null)
         * @param ParserOptions|null $options Any parser options.
         * @param bool $generateHtml Whether to generate HTML (default: true). If false,
         *        the result of calling getText() on the ParserOutput object returned by
index 8e5e0a8..70b638b 100644 (file)
@@ -329,7 +329,8 @@ class WikitextContent extends TextContent {
         * using the global Parser service.
         *
         * @param Title $title
-        * @param int|null $revId Revision to pass to the parser (default: null)
+        * @param int|null $revId ID of the revision being rendered.
+        *  See Parser::parse() for the ramifications. (default: null)
         * @param ParserOptions $options (default: null)
         * @param bool $generateHtml (default: true)
         * @param ParserOutput &$output ParserOutput representing the HTML form of the text,
index 2eb0d5d..33961ed 100644 (file)
@@ -9,5 +9,5 @@ interface DeferrableCallback {
        /**
         * @return string Originating method name
         */
-       function getOrigin();
+       public function getOrigin();
 }
index d43ffbc..3380364 100644 (file)
@@ -362,11 +362,16 @@ class DeferredUpdates {
                        $update->setTransactionTicket( $ticket );
                }
 
-               $fnameTrxOwner = get_class( $update ) . '::doUpdate';
+               // Designate $update::doUpdate() as the write round owner
+               $fnameTrxOwner = ( $update instanceof DeferrableCallback )
+                       ? $update->getOrigin()
+                       : get_class( $update ) . '::doUpdate';
+               // Determine whether the write round will be explicit or implicit
                $useExplicitTrxRound = !(
                        $update instanceof TransactionRoundAwareUpdate &&
                        $update->getTransactionRoundRequirement() == $update::TRX_ROUND_ABSENT
                );
+
                // Flush any pending changes left over from an implicit transaction round
                if ( $useExplicitTrxRound ) {
                        $lbFactory->beginMasterChanges( $fnameTrxOwner ); // new explicit round
index 7ec2357..db7141f 100644 (file)
@@ -186,7 +186,7 @@ class FileBackendGroup {
                                'mimeCallback' => [ $this, 'guessMimeInternal' ],
                                'obResetFunc' => 'wfResetOutputBuffers',
                                'streamMimeFunc' => [ StreamFile::class, 'contentTypeFromPath' ],
-                               'tmpDirectory' => wfTempDir(),
+                               'tmpFileFactory' => MediaWikiServices::getInstance()->getTempFSFileFactory(),
                                'statusWrapper' => [ Status::class, 'wrap' ],
                                'wanCache' => $services->getMainWANObjectCache(),
                                'srvCache' => ObjectCache::getLocalServerInstance( 'hash' ),
@@ -241,7 +241,8 @@ class FileBackendGroup {
                if ( !$type && $fsPath ) {
                        $type = $magic->guessMimeType( $fsPath, false );
                } elseif ( !$type && strlen( $content ) ) {
-                       $tmpFile = TempFSFile::factory( 'mime_', '', wfTempDir() );
+                       $tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory()
+                               ->newTempFSFile( 'mime_', '' );
                        file_put_contents( $tmpFile->getPath(), $content );
                        $type = $magic->guessMimeType( $tmpFile->getPath(), false );
                }
index ee7ee6f..5f6a0cb 100644 (file)
@@ -1352,7 +1352,8 @@ abstract class File implements IDBAccessObject {
         */
        protected function makeTransformTmpFile( $thumbPath ) {
                $thumbExt = FileBackend::extensionFromPath( $thumbPath );
-               return TempFSFile::factory( 'transform_', $thumbExt, wfTempDir() );
+               return MediaWikiServices::getInstance()->getTempFSFileFactory()
+                       ->newTempFSFile( 'transform_', $thumbExt );
        }
 
        /**
index 593e617..e6a49c7 100644 (file)
@@ -228,7 +228,7 @@ class FSFileBackend extends FileBackendStore {
                }
 
                if ( !empty( $params['async'] ) ) { // deferred
-                       $tempFile = TempFSFile::factory( 'create_', 'tmp', $this->tmpDirectory );
+                       $tempFile = $this->tmpFileFactory->newTempFSFile( 'create_', 'tmp' );
                        if ( !$tempFile ) {
                                $status->fatal( 'backend-fail-create', $params['dst'] );
 
@@ -688,7 +688,7 @@ class FSFileBackend extends FileBackendStore {
                        } else {
                                // Create a new temporary file with the same extension...
                                $ext = FileBackend::extensionFromPath( $src );
-                               $tmpFile = TempFSFile::factory( 'localcopy_', $ext, $this->tmpDirectory );
+                               $tmpFile = $this->tmpFileFactory->newTempFSFile( 'localcopy_', $ext );
                                if ( !$tmpFile ) {
                                        $tmpFiles[$src] = null;
                                } else {
index f65619f..b187fe5 100644 (file)
@@ -27,6 +27,7 @@
  * @file
  * @ingroup FileBackend
  */
+use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerInterface;
 use Wikimedia\ScopedCallback;
@@ -106,8 +107,8 @@ abstract class FileBackend implements LoggerAwareInterface {
        /** @var int How many operations can be done in parallel */
        protected $concurrency;
 
-       /** @var string Temporary file directory */
-       protected $tmpDirectory;
+       /** @var TempFSFileFactory */
+       protected $tmpFileFactory;
 
        /** @var LockManager */
        protected $lockManager;
@@ -152,8 +153,10 @@ abstract class FileBackend implements LoggerAwareInterface {
         *   - parallelize : When to do file operations in parallel (when possible).
         *      Allowed values are "implicit", "explicit" and "off".
         *   - concurrency : How many file operations can be done in parallel.
-        *   - tmpDirectory : Directory to use for temporary files. If this is not set or null,
-        *      then the backend will try to discover a usable temporary directory.
+        *   - tmpDirectory : Directory to use for temporary files.
+        *   - tmpFileFactory : Optional TempFSFileFactory object. Only has an effect if tmpDirectory is
+        *      not set. If both are unset or null, then the backend will try to discover a usable
+        *      temporary directory.
         *   - obResetFunc : alternative callback to clear the output buffer
         *   - streamMimeFunc : alternative method to determine the content type from the path
         *   - logger : Optional PSR logger object.
@@ -193,7 +196,12 @@ abstract class FileBackend implements LoggerAwareInterface {
                }
                $this->logger = $config['logger'] ?? new NullLogger();
                $this->statusWrapper = $config['statusWrapper'] ?? null;
-               $this->tmpDirectory = $config['tmpDirectory'] ?? null;
+               // tmpDirectory gets precedence for backward compatibility
+               if ( isset( $config['tmpDirectory'] ) ) {
+                       $this->tmpFileFactory = new TempFSFileFactory( $config['tmpDirectory'] );
+               } else {
+                       $this->tmpFileFactory = $config['tmpFileFactory'] ?? new TempFSFileFactory();
+               }
        }
 
        public function setLogger( LoggerInterface $logger ) {
index 548c85c..3932f34 100644 (file)
@@ -168,7 +168,7 @@ class MemoryFileBackend extends FileBackendStore {
                        } else {
                                // Create a new temporary file with the same extension...
                                $ext = FileBackend::extensionFromPath( $src );
-                               $fsFile = TempFSFile::factory( 'localcopy_', $ext, $this->tmpDirectory );
+                               $fsFile = $this->tmpFileFactory->newTempFSFile( 'localcopy_', $ext );
                                if ( $fsFile ) {
                                        $bytes = file_put_contents( $fsFile->getPath(), $this->files[$src]['data'] );
                                        if ( $bytes !== strlen( $this->files[$src]['data'] ) ) {
index a1b2460..d6a0c6c 100644 (file)
@@ -1150,7 +1150,7 @@ class SwiftFileBackend extends FileBackendStore {
                        // Get source file extension
                        $ext = FileBackend::extensionFromPath( $path );
                        // Create a new temporary file...
-                       $tmpFile = TempFSFile::factory( 'localcopy_', $ext, $this->tmpDirectory );
+                       $tmpFile = $this->tmpFileFactory->newTempFSFile( 'localcopy_', $ext );
                        if ( $tmpFile ) {
                                $handle = fopen( $tmpFile->getPath(), 'wb' );
                                if ( $handle ) {
index b993626..378dbe7 100644 (file)
@@ -1,4 +1,7 @@
 <?php
+
+use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
+
 /**
  * Location holder of files stored temporarily
  *
@@ -34,12 +37,17 @@ class TempFSFile extends FSFile {
        /** @var array Map of (path => 1) for paths to delete on shutdown */
        protected static $pathsCollect = null;
 
+       /**
+        * Do not call directly. Use TempFSFileFactory.
+        */
        public function __construct( $path ) {
                parent::__construct( $path );
 
                if ( self::$pathsCollect === null ) {
+                       // @codeCoverageIgnoreStart
                        self::$pathsCollect = [];
                        register_shutdown_function( [ __CLASS__, 'purgeAllOnShutdown' ] );
+                       // @codeCoverageIgnoreEnd
                }
        }
 
@@ -47,40 +55,23 @@ class TempFSFile extends FSFile {
         * Make a new temporary file on the file system.
         * Temporary files may be purged when the file object falls out of scope.
         *
+        * @deprecated since 1.34, use TempFSFileFactory directly
+        *
         * @param string $prefix
         * @param string $extension Optional file extension
         * @param string|null $tmpDirectory Optional parent directory
         * @return TempFSFile|null
         */
        public static function factory( $prefix, $extension = '', $tmpDirectory = null ) {
-               $ext = ( $extension != '' ) ? ".{$extension}" : '';
-
-               $attempts = 5;
-               while ( $attempts-- ) {
-                       $hex = sprintf( '%06x%06x', mt_rand( 0, 0xffffff ), mt_rand( 0, 0xffffff ) );
-                       if ( !is_string( $tmpDirectory ) ) {
-                               $tmpDirectory = self::getUsableTempDirectory();
-                       }
-                       $path = $tmpDirectory . '/' . $prefix . $hex . $ext;
-                       Wikimedia\suppressWarnings();
-                       $newFileHandle = fopen( $path, 'x' );
-                       Wikimedia\restoreWarnings();
-                       if ( $newFileHandle ) {
-                               fclose( $newFileHandle );
-                               $tmpFile = new self( $path );
-                               $tmpFile->autocollect();
-                               // Safely instantiated, end loop.
-                               return $tmpFile;
-                       }
-               }
-
-               // Give up
-               return null;
+               return ( new TempFSFileFactory( $tmpDirectory ) )->newTempFSFile( $prefix, $extension );
        }
 
        /**
+        * @todo Is there any useful way to test this? Would it be useful to make this non-static on
+        * TempFSFileFactory?
+        *
         * @return string Filesystem path to a temporary directory
-        * @throws RuntimeException
+        * @throws RuntimeException if no writable temporary directory can be found
         */
        public static function getUsableTempDirectory() {
                $tmpDir = array_map( 'getenv', [ 'TMPDIR', 'TMP', 'TEMP' ] );
@@ -176,6 +167,8 @@ class TempFSFile extends FSFile {
         * Try to make sure that all files are purged on error
         *
         * This method should only be called internally
+        *
+        * @codeCoverageIgnore
         */
        public static function purgeAllOnShutdown() {
                foreach ( self::$pathsCollect as $path => $unused ) {
diff --git a/includes/libs/filebackend/fsfile/TempFSFileFactory.php b/includes/libs/filebackend/fsfile/TempFSFileFactory.php
new file mode 100644 (file)
index 0000000..1120973
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+
+namespace MediaWiki\FileBackend\FSFile;
+
+use TempFSFile;
+
+/**
+ * @ingroup FileBackend
+ */
+class TempFSFileFactory {
+       /** @var string|null */
+       private $tmpDirectory;
+
+       /**
+        * @param string|null $tmpDirectory A directory to put the temporary files in, e.g.,
+        *   $wgTmpDirectory. If null, we'll try to find one ourselves.
+        */
+       public function __construct( $tmpDirectory = null ) {
+               $this->tmpDirectory = $tmpDirectory;
+       }
+
+       /**
+        * Make a new temporary file on the file system.
+        * Temporary files may be purged when the file object falls out of scope.
+        *
+        * @param string $prefix
+        * @param string $extension Optional file extension
+        * @return TempFSFile|null
+        */
+       public function newTempFSFile( $prefix, $extension = '' ) {
+               $ext = ( $extension != '' ) ? ".{$extension}" : '';
+               $tmpDirectory = $this->tmpDirectory;
+               if ( !is_string( $tmpDirectory ) ) {
+                       $tmpDirectory = TempFSFile::getUsableTempDirectory();
+               }
+
+               $attempts = 5;
+               while ( $attempts-- ) {
+                       $hex = sprintf( '%06x%06x', mt_rand( 0, 0xffffff ), mt_rand( 0, 0xffffff ) );
+                       $path = "$tmpDirectory/$prefix$hex$ext";
+                       \Wikimedia\suppressWarnings();
+                       $newFileHandle = fopen( $path, 'x' );
+                       \Wikimedia\restoreWarnings();
+                       if ( $newFileHandle ) {
+                               fclose( $newFileHandle );
+                               $tmpFile = new TempFSFile( $path );
+                               $tmpFile->autocollect();
+                               // Safely instantiated, end loop.
+                               return $tmpFile;
+                       }
+               }
+
+               // Give up
+               return null; // @codeCoverageIgnore
+       }
+}
index eecf7ec..2c40854 100644 (file)
@@ -417,25 +417,6 @@ class MemcachedClient {
                return false;
        }
 
-       /**
-        * @param string $key
-        * @param int $timeout
-        * @return bool
-        */
-       public function lock( $key, $timeout = 0 ) {
-               /* stub */
-               return true;
-       }
-
-       /**
-        * @param string $key
-        * @return bool
-        */
-       public function unlock( $key ) {
-               /* stub */
-               return true;
-       }
-
        // }}}
        // {{{ disconnect_all()
 
index 6029f77..0e08044 100644 (file)
@@ -4279,10 +4279,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                }
 
                // This will reconnect if possible or return false if not
-               $this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
-               $ok = ( $this->query( self::$PING_QUERY, __METHOD__, true ) !== false );
-               $this->restoreFlags( self::RESTORE_PRIOR );
-
+               $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_SILENCE_ERRORS;
+               $ok = ( $this->query( self::$PING_QUERY, __METHOD__, $flags ) !== false );
                if ( $ok ) {
                        $rtt = $this->lastRoundTripEstimate;
                }
index a9223ac..ac8c7c3 100644 (file)
@@ -814,22 +814,16 @@ abstract class DatabaseMysqlBase extends Database {
        protected function getHeartbeatData( array $conds ) {
                // Query time and trip time are not counted
                $nowUnix = microtime( true );
-               // Do not bother starting implicit transactions here
-               $this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
-               try {
-                       $whereSQL = $this->makeList( $conds, self::LIST_AND );
-                       // Use ORDER BY for channel based queries since that field might not be UNIQUE.
-                       // Note: this would use "TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6))" but the
-                       // percision field is not supported in MySQL <= 5.5.
-                       $res = $this->query(
-                               "SELECT ts FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1",
-                               __METHOD__,
-                               self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX
-                       );
-                       $row = $res ? $res->fetchObject() : false;
-               } finally {
-                       $this->restoreFlags();
-               }
+               $whereSQL = $this->makeList( $conds, self::LIST_AND );
+               // Use ORDER BY for channel based queries since that field might not be UNIQUE.
+               // Note: this would use "TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6))" but the
+               // percision field is not supported in MySQL <= 5.5.
+               $res = $this->query(
+                       "SELECT ts FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1",
+                       __METHOD__,
+                       self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX
+               );
+               $row = $res ? $res->fetchObject() : false;
 
                return [ $row ? $row->ts : null, $nowUnix ];
        }
index 5698cf8..3ceb339 100644 (file)
@@ -23,7 +23,19 @@ namespace Wikimedia\Rdbms;
 use InvalidArgumentException;
 
 /**
- * Class to handle database/prefix specification for IDatabase domains
+ * Class to handle database/schema/prefix specifications for IDatabase
+ *
+ * The components of a database domain are defined as follows:
+ *   - database: name of a server-side collection of schemas that is client-selectable
+ *   - schema: name of a server-side collection of tables within the given database
+ *   - prefix: table name prefix of an application-defined table collection
+ *
+ * If an RDBMS does not support server-side collections of table collections (schemas) then
+ * the schema component should be null and the "database" component treated as a collection
+ * of exactly one table collection (the implied schema for that "database").
+ *
+ * The above criteria should determine how components should map to RDBMS specific keywords
+ * rather than "database"/"schema" always mapping to "DATABASE"/"SCHEMA" as used by the RDBMS.
  */
 class DatabaseDomain {
        /** @var string|null */
index b643c3f..f982de4 100644 (file)
@@ -530,7 +530,10 @@ class Parser {
         * @param ParserOptions $options
         * @param bool $linestart
         * @param bool $clearState
-        * @param int|null $revid Number to pass in {{REVISIONID}}
+        * @param int|null $revid ID of the revision being rendered. This is used to render
+        *  REVISION* magic words. 0 means that any current revision will be used. Null means
+        *  that {{REVISIONID}}/{{REVISIONUSER}} will be empty and {{REVISIONTIMESTAMP}} will
+        *  use the current timestamp.
         * @return ParserOutput A ParserOutput
         * @return-taint escaped
         */
index 27cd2ab..0a1ce61 100644 (file)
@@ -116,7 +116,7 @@ abstract class RedirectSpecialArticle extends RedirectSpecialPage {
                // Avoid double redirect for action=edit&redlink=1 for existing pages
                // (compare to the check in EditPage::edit)
                if (
-                       $query &&
+                       $query && isset( $query['action'] ) && isset( $query['redlink'] ) &&
                        ( $query['action'] === 'edit' || $query['action'] === 'submit' ) &&
                        (bool)$query['redlink'] &&
                        $title instanceof Title &&
index cc527e7..8c6b2f9 100644 (file)
@@ -1,4 +1,7 @@
 <?php
+
+use MediaWiki\MediaWikiServices;
+
 /**
  * Backend for uploading files from chunks.
  *
@@ -157,7 +160,8 @@ class UploadFromChunks extends UploadFromFile {
                // Get the file extension from the last chunk
                $ext = FileBackend::extensionFromPath( $this->mVirtualTempPath );
                // Get a 0-byte temp file to perform the concatenation at
-               $tmpFile = TempFSFile::factory( 'chunkedupload_', $ext, wfTempDir() );
+               $tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory()
+                       ->getTempFSFile( 'chunkedupload_', $ext );
                $tmpPath = false; // fail in concatenate()
                if ( $tmpFile ) {
                        // keep alive with $this
index b071774..b92fcc5 100644 (file)
@@ -1,4 +1,7 @@
 <?php
+
+use MediaWiki\MediaWikiServices;
+
 /**
  * Backend for uploading files from a HTTP resource.
  *
@@ -201,7 +204,8 @@ class UploadFromUrl extends UploadBase {
         * @return string Path to the file
         */
        protected function makeTemporaryFile() {
-               $tmpFile = TempFSFile::factory( 'URL', 'urlupload_', wfTempDir() );
+               $tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory()
+                       ->newTempFSFile( 'URL', 'urlupload_' );
                $tmpFile->bind( $this );
 
                return $tmpFile->getPath();
index 08eade9..358dc21 100644 (file)
@@ -316,7 +316,7 @@ abstract class BackupDumper extends Maintenance {
 
                $dbr = $this->forcedDb;
                if ( $this->forcedDb === null ) {
-                       $dbr = wfGetDB( DB_REPLICA );
+                       $dbr = $this->getDB( DB_REPLICA );
                }
                $this->maxCount = $dbr->selectField( $table, "MAX($field)", '', __METHOD__ );
                $this->startTime = microtime( true );
index 94aa3b9..997110c 100644 (file)
                switch ( this.displayLayer ) {
                        case 'month':
                                this.labelButton.setLabel( this.moment.format( 'MMMM YYYY' ) );
+                               this.labelButton.toggle( true );
                                this.upButton.toggle( true );
 
                                // First week displayed is the first week spanned by the month, unless it begins on Monday, in
 
                        case 'year':
                                this.labelButton.setLabel( this.moment.format( 'YYYY' ) );
+                               this.labelButton.toggle( true );
                                this.upButton.toggle( true );
 
                                currentMonth = moment( this.moment ).startOf( 'year' );
 
                        case 'duodecade':
                                this.labelButton.setLabel( null );
+                               this.labelButton.toggle( false );
                                this.upButton.toggle( false );
 
                                currentYear = moment( { year: Math.floor( this.moment.year() / 20 ) * 20 } );
                        framed: false,
                        classes: [ 'mw-widget-calendarWidget-labelButton' ]
                } );
+               // FIXME This button is actually not clickable because labelButton covers it,
+               // should it just be a plain icon?
                this.upButton = new OO.ui.ButtonWidget( {
                        tabIndex: -1,
                        framed: false,
                this.$header.append(
                        this.prevButton.$element,
                        this.nextButton.$element,
-                       this.upButton.$element,
-                       this.labelButton.$element
+                       this.labelButton.$element,
+                       this.upButton.$element
                );
        };
 
index 7932f73..ba5ae33 100644 (file)
@@ -39,7 +39,9 @@
 
 .mw-widget-calendarWidget-upButton {
        position: absolute;
+       top: 0;
        right: 3em;
+       pointer-events: none;
 }
 
 .mw-widget-calendarWidget-prevButton {
index 86942fd..3c5f43b 100644 (file)
@@ -5,6 +5,7 @@ namespace MediaWiki\Tests\Permissions;
 use Action;
 use ContentHandler;
 use FauxRequest;
+use LoggedServiceOptions;
 use MediaWiki\Block\DatabaseBlock;
 use MediaWiki\Block\Restriction\NamespaceRestriction;
 use MediaWiki\Block\Restriction\PageRestriction;
@@ -14,6 +15,7 @@ use MediaWiki\MediaWikiServices;
 use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Revision\MutableRevisionRecord;
 use MediaWiki\Revision\RevisionLookup;
+use TestAllServiceOptionsUsed;
 use Wikimedia\ScopedCallback;
 use MediaWiki\Session\SessionId;
 use MediaWiki\Session\TestUtils;
@@ -30,6 +32,7 @@ use Wikimedia\TestingAccessWrapper;
  * @covers \MediaWiki\Permissions\PermissionManager
  */
 class PermissionManagerTest extends MediaWikiLangTestCase {
+       use TestAllServiceOptionsUsed;
 
        /**
         * @var string
@@ -725,15 +728,21 @@ class PermissionManagerTest extends MediaWikiLangTestCase {
                                }
                        } );
                $permissionManager = new PermissionManager(
+                       new LoggedServiceOptions(
+                               self::$serviceOptionsAccessLog,
+                               PermissionManager::$constructorOptions,
+                               [
+                                       'WhitelistRead' => [],
+                                       'WhitelistReadRegexp' => [],
+                                       'EmailConfirmToEdit' => false,
+                                       'BlockDisablesLogin' => false,
+                                       'GroupPermissions' => [],
+                                       'RevokePermissions' => [],
+                                       'AvailableRights' => []
+                               ]
+                       ),
                        $services->getSpecialPageFactory(),
                        $revisionLookup,
-                       [],
-                       [],
-                       false,
-                       false,
-                       [],
-                       [],
-                       [],
                        MediaWikiServices::getInstance()->getNamespaceInfo()
                );
                $this->setService( 'PermissionManager', $permissionManager );
index 076ff36..49dcf07 100644 (file)
@@ -3,13 +3,12 @@
 namespace MediaWiki\Tests\Rest\BasicAccess;
 
 use GuzzleHttp\Psr7\Uri;
-use MediaWiki\Permissions\PermissionManager;
+use MediaWiki\MediaWikiServices;
 use MediaWiki\Rest\BasicAccess\MWBasicAuthorizer;
 use MediaWiki\Rest\Handler;
 use MediaWiki\Rest\RequestData;
 use MediaWiki\Rest\ResponseFactory;
 use MediaWiki\Rest\Router;
-use MediaWiki\User\UserIdentity;
 use MediaWikiTestCase;
 use User;
 
@@ -24,23 +23,15 @@ use User;
 class MWBasicRequestAuthorizerTest extends MediaWikiTestCase {
        private function createRouter( $userRights ) {
                $user = User::newFromName( 'Test user' );
-
-               $pm = new class( $user, $userRights ) extends PermissionManager {
-                       private $testUser;
-                       private $testUserRights;
-
-                       public function __construct( $user, $userRights ) {
-                               $this->testUser = $user;
-                               $this->testUserRights = $userRights;
-                       }
-
-                       public function userHasRight( UserIdentity $user, $action = '' ) {
-                               if ( $user === $this->testUser ) {
-                                       return $this->testUserRights[$action] ?? false;
-                               }
-                               return parent::userHasRight( $user, $action );
-                       }
-               };
+               // Don't allow the rights to everybody so that user rights kick in.
+               $this->mergeMwGlobalArrayValue( 'wgGroupPermissions', [ '*' => $userRights ] );
+               $this->resetServices();
+               $this->overrideUserPermissions(
+                       $user,
+                       array_keys( array_filter( $userRights ), function ( $value ) {
+                               return $value === true;
+                       } )
+               );
 
                global $IP;
 
@@ -50,7 +41,7 @@ class MWBasicRequestAuthorizerTest extends MediaWikiTestCase {
                        '/rest',
                        new \EmptyBagOStuff(),
                        new ResponseFactory(),
-                       new MWBasicAuthorizer( $user, $pm ) );
+                       new MWBasicAuthorizer( $user, MediaWikiServices::getInstance()->getPermissionManager() ) );
        }
 
        public function testReadDenied() {
index b377c63..7e5ff84 100644 (file)
@@ -373,4 +373,27 @@ class DeferredUpdatesTest extends MediaWikiTestCase {
                DeferredUpdates::tryOpportunisticExecute( 'run' );
                $this->assertEquals( [ 'oti', 1, 2 ], $calls );
        }
+
+       /**
+        * @covers DeferredUpdates::attemptUpdate
+        */
+       public function testCallbackUpdateRounds() {
+               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+
+               $fname = __METHOD__;
+               $called = false;
+               DeferredUpdates::attemptUpdate(
+                       new MWCallableUpdate(
+                               function () use ( $lbFactory, $fname, &$called ) {
+                                       $lbFactory->flushReplicaSnapshots( $fname );
+                                       $lbFactory->commitMasterChanges( $fname );
+                                       $called = true;
+                               },
+                               $fname
+                       ),
+                       $lbFactory
+               );
+
+               $this->assertTrue( $called, "Callback ran" );
+       }
 }
index 42805b2..325babc 100644 (file)
@@ -1,6 +1,22 @@
 <?php
 
+use MediaWiki\MediaWikiServices;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * Just to test one deprecated method and one line of ServiceWiring code.
+ */
 class TempFSFileIntegrationTest extends MediaWikiIntegrationTestCase {
+       /**
+        * @coversNothing
+        */
+       public function testServiceWiring() {
+               $this->setMwGlobals( 'wgTmpDirectory', '/hopefully invalid' );
+               $factory = MediaWikiServices::getInstance()->getTempFSFileFactory();
+               $this->assertSame( '/hopefully invalid',
+                       ( TestingAccessWrapper::newFromObject( $factory ) )->tmpDirectory );
+       }
+
        use TempFSFileTestTrait;
 
        private function newFile() {
index 651c871..4175ead 100644 (file)
@@ -234,6 +234,7 @@ class ParserMethodsTest extends MediaWikiLangTestCase {
                $po = new ParserOptions( $frank );
 
                yield 'current' => [ $text, $po, 0, 'user:CurrentAuthor;id:200;time:20160606000000;' ];
+               yield 'current' => [ $text, $po, null, 'user:;id:;time:' ];
                yield 'current with ID' => [ $text, $po, 200, 'user:CurrentAuthor;id:200;time:20160606000000;' ];
 
                $text = '* user:{{REVISIONUSER}};id:{{REVISIONID}};time:{{REVISIONTIMESTAMP}};';
diff --git a/tests/phpunit/unit/includes/libs/filebackend/fsfile/TempFSFileTest.php b/tests/phpunit/unit/includes/libs/filebackend/fsfile/TempFSFileTest.php
new file mode 100644 (file)
index 0000000..743ac63
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
+
+/**
+ * @coversDefaultClass \MediaWiki\FileBackend\FSFile\TempFSFileFactory
+ * @covers ::__construct
+ * @covers ::newTempFSFile
+ */
+class TempFSFileTest extends MediaWikiUnitTestCase {
+       use TempFSFileTestTrait;
+
+       private function newFile() {
+               return ( new TempFSFileFactory() )->newTempFSFile( 'tmp' );
+       }
+}