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 ===
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 ===
* 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 ===
* …
'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',
&$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
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.
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;
return $this->getService( 'StatsdDataFactory' );
}
+ /**
+ * @since 1.34
+ * @return TempFSFileFactory
+ */
+ public function getTempFSFileFactory() : TempFSFileFactory {
+ return $this->getService( 'TempFSFileFactory' );
+ }
+
/**
* @since 1.28
* @return TitleFormatter
use Action;
use Exception;
use Hooks;
+use MediaWiki\Config\ServiceOptions;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\Revision\RevisionLookup;
use MediaWiki\Revision\RevisionRecord;
/** @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;
];
/**
+ * @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;
}
}
/**
- * 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
* @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;
// 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
# 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() ) {
$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;
}
// 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'
) {
if (
$user->isLoggedIn() &&
- $this->blockDisablesLogin &&
+ $this->options->get( 'BlockDisablesLogin' ) &&
$user->getBlock()
) {
$anon = new User;
* @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] );
}
/**
$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 );
*/
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;
}
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;
*/
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;
$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 ) {
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;
},
'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()
);
},
);
},
+ 'TempFSFileFactory' => function ( MediaWikiServices $services ) : TempFSFileFactory {
+ return new TempFSFileFactory( $services->getMainConfig()->get( 'TmpDirectory' ) );
+ },
+
'TitleFormatter' => function ( MediaWikiServices $services ) : TitleFormatter {
return $services->getService( '_MediaWikiTitleCodec' );
},
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,
* 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 = [
'reason' => '',
'timestamp' => '',
'byText' => '',
+ 'hideName' => false,
];
$options += $defaults;
$this->setReason( $options['reason'] );
$this->setTimestamp( wfTimestamp( TS_MW, $options['timestamp'] ) );
+ $this->setHideName( (bool)$options['hideName'] );
}
/**
use DateTime;
use DateTimeZone;
use DeferredUpdates;
+use Hooks;
use IP;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Permissions\PermissionManager;
// 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 ];
'originalBlocks' => $blocks,
] );
}
- return $block;
}
- return null;
+ Hooks::run( 'GetUserBlock', [ clone $user, $ip, &$block ] );
+
+ return $block;
}
/**
}
}
- return array_merge( $systemBlocks, $databaseBlocks );
+ return array_values( array_merge( $systemBlocks, $databaseBlocks ) );
}
/**
* 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
'anonOnly' => false,
'createAccount' => false,
'enableAutoblock' => false,
- 'hideName' => false,
'blockEmail' => false,
'allowUsertalk' => false,
'sitewide' => true,
# 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'] );
* @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
*
* @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).
* 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
* 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,
/**
* @return string Originating method name
*/
- function getOrigin();
+ public function getOrigin();
}
$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
'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' ),
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 );
}
*/
protected function makeTransformTmpFile( $thumbPath ) {
$thumbExt = FileBackend::extensionFromPath( $thumbPath );
- return TempFSFile::factory( 'transform_', $thumbExt, wfTempDir() );
+ return MediaWikiServices::getInstance()->getTempFSFileFactory()
+ ->newTempFSFile( 'transform_', $thumbExt );
}
/**
}
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'] );
} 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 {
* @file
* @ingroup FileBackend
*/
+use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Wikimedia\ScopedCallback;
/** @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;
* - 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.
}
$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 ) {
} 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'] ) ) {
// 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 ) {
<?php
+
+use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
+
/**
* Location holder of files stored temporarily
*
/** @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
}
}
* 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' ] );
* 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 ) {
--- /dev/null
+<?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
+ }
+}
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()
}
// 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;
}
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 ];
}
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 */
* @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
*/
// 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 &&
<?php
+
+use MediaWiki\MediaWikiServices;
+
/**
* Backend for uploading files from chunks.
*
// 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
<?php
+
+use MediaWiki\MediaWikiServices;
+
/**
* Backend for uploading files from a HTTP resource.
*
* @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();
$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 );
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
);
};
.mw-widget-calendarWidget-upButton {
position: absolute;
+ top: 0;
right: 3em;
+ pointer-events: none;
}
.mw-widget-calendarWidget-prevButton {
use Action;
use ContentHandler;
use FauxRequest;
+use LoggedServiceOptions;
use MediaWiki\Block\DatabaseBlock;
use MediaWiki\Block\Restriction\NamespaceRestriction;
use MediaWiki\Block\Restriction\PageRestriction;
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;
* @covers \MediaWiki\Permissions\PermissionManager
*/
class PermissionManagerTest extends MediaWikiLangTestCase {
+ use TestAllServiceOptionsUsed;
/**
* @var string
}
} );
$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 );
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;
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;
'/rest',
new \EmptyBagOStuff(),
new ResponseFactory(),
- new MWBasicAuthorizer( $user, $pm ) );
+ new MWBasicAuthorizer( $user, MediaWikiServices::getInstance()->getPermissionManager() ) );
}
public function testReadDenied() {
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" );
+ }
}
<?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() {
$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}};';
--- /dev/null
+<?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' );
+ }
+}