cache: true
},
all: [
- '**/*.js{,on}',
+ '**/*.{js,json}',
'!docs/**',
'!node_modules/**',
'!resources/lib/**',
password setups and deprecated since 1.24, is now removed.
* $wgDBOracleDRCP - If you must use persistent connections, set DBO_PERSISTENT
in the 'flags' field for servers in $wgDBServers (or $wgLBFactoryConf).
+* $wgMemCachedDebug - Set the cache "debug" field in $wgObjectCaches instead.
=== New user-facing features in 1.34 ===
* Special:Mute has been added as a quick way for users to block unwanted emails
SearchResult::newFromTitle(). This class is being refactored into an abstract
class. If you extend this class please be sure to override all its methods
or extend RevisionSearchResult.
+* Skin::getSkinNameMessages() is deprecated and no longer used.
=== Other changes in 1.34 ===
* …
'UncategorizedTemplatesPage' => __DIR__ . '/includes/specials/SpecialUncategorizedtemplates.php',
'Undelete' => __DIR__ . '/maintenance/undelete.php',
'UnifiedDiffFormatter' => __DIR__ . '/includes/diff/UnifiedDiffFormatter.php',
+ 'UnknownContent' => __DIR__ . '/includes/content/UnknownContent.php',
+ 'UnknownContentHandler' => __DIR__ . '/includes/content/UnknownContentHandler.php',
'UnlistedSpecialPage' => __DIR__ . '/includes/specialpage/UnlistedSpecialPage.php',
'UnprotectAction' => __DIR__ . '/includes/actions/UnprotectAction.php',
'UnregisteredLocalFile' => __DIR__ . '/includes/filerepo/file/UnregisteredLocalFile.php',
+ 'UnsupportedSlotDiffRenderer' => __DIR__ . '/includes/diff/UnsupportedSlotDiffRenderer.php',
'UnusedCategoriesPage' => __DIR__ . '/includes/specials/SpecialUnusedcategories.php',
'UnusedimagesPage' => __DIR__ . '/includes/specials/SpecialUnusedimages.php',
'UnusedtemplatesPage' => __DIR__ . '/includes/specials/SpecialUnusedtemplates.php',
*/
$wgSessionPbkdf2Iterations = 10001;
-/**
- * If enabled, will send MemCached debugging information to $wgDebugLogFile
- */
-$wgMemCachedDebug = false;
-
/**
* The list of MemCached servers and port numbers
*/
# checking, etc.
if ( $this->formtype == 'initial' || $this->firsttime ) {
if ( $this->initialiseForm() === false ) {
- $out = $this->context->getOutput();
- if ( $out->getRedirect() === '' ) { // mcrundo hack redirects, don't override it
- $this->noSuchSectionPage();
- }
return;
}
$content = $this->getContentObject( false ); # TODO: track content object?!
if ( $content === false ) {
+ $out = $this->context->getOutput();
+ if ( $out->getRedirect() === '' ) { // mcrundo hack redirects, don't override it
+ $this->noSuchSectionPage();
+ }
+ return false;
+ }
+
+ if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
+ $modelMsg = $this->getContext()->msg( 'content-model-' . $content->getModel() );
+ $modelName = $modelMsg->exists() ? $modelMsg->text() : $content->getModel();
+
+ $out = $this->context->getOutput();
+ $out->showErrorPage(
+ 'modeleditnotsupported-title',
+ 'modeleditnotsupported-text',
+ $modelName
+ );
return false;
}
+
$this->textbox1 = $this->toEditText( $content );
$user = $this->context->getUser();
$vars['wgUserVariant'] = $contLang->getPreferredVariant();
}
// Same test as SkinTemplate
- $vars['wgIsProbablyEditable'] = $title->quickUserCan( 'edit', $user )
- && ( $title->exists() || $title->quickUserCan( 'create', $user ) );
+ $vars['wgIsProbablyEditable'] = $this->userCanEditOrCreate( $user, $title );
- $vars['wgRelevantPageIsProbablyEditable'] = $relevantTitle
- && $relevantTitle->quickUserCan( 'edit', $user )
- && ( $relevantTitle->exists() || $relevantTitle->quickUserCan( 'create', $user ) );
+ $vars['wgRelevantPageIsProbablyEditable'] = $relevantTitle &&
+ $this->userCanEditOrCreate( $user, $relevantTitle );
foreach ( $title->getRestrictionTypes() as $type ) {
// Following keys are set in $vars:
return true;
}
+ /**
+ * @param User $user
+ * @param LinkTarget $title
+ * @return bool
+ */
+ private function userCanEditOrCreate(
+ User $user,
+ LinkTarget $title
+ ) {
+ $pm = MediaWikiServices::getInstance()->getPermissionManager();
+ return $pm->quickUserCan( 'edit', $user, $title )
+ && ( $this->getTitle()->exists() ||
+ $pm->quickUserCan( 'create', $user, $title ) );
+ }
+
/**
* @return array Array in format "link name or number => 'link html'".
*/
# Universal edit button
if ( $config->get( 'UniversalEditButton' ) && $this->isArticleRelated() ) {
- $user = $this->getUser();
- if ( $this->getTitle()->quickUserCan( 'edit', $user )
- && ( $this->getTitle()->exists() ||
- $this->getTitle()->quickUserCan( 'create', $user ) )
- ) {
+ if ( $this->userCanEditOrCreate( $this->getUser(), $this->getTitle() ) ) {
// Original UniversalEditButton
$msg = $this->msg( 'edit' )->text();
$tags['universal-edit-button'] = Html::element( 'link', [
return !count( $this->getPermissionErrorsInternal( $action, $user, $page, $rigor, true ) );
}
+ /**
+ * A convenience method for calling PermissionManager::userCan
+ * with PermissionManager::RIGOR_QUICK
+ *
+ * Suitable for use for nonessential UI controls in common cases, but
+ * _not_ for functional access control.
+ * May provide false positives, but should never provide a false negative.
+ *
+ * @see PermissionManager::userCan()
+ *
+ * @param string $action
+ * @param User $user
+ * @param LinkTarget $page
+ * @return bool
+ */
+ public function quickUserCan( $action, User $user, LinkTarget $page ) {
+ return $this->userCan( $action, $user, $page, self::RIGOR_QUICK );
+ }
+
/**
* Can $user perform $action on a page?
*
*/
public function getUserPermissions( UserIdentity $user ) {
$user = User::newFromIdentity( $user );
- if ( !isset( $this->usersRights[ $user->getId() ] ) ) {
- $this->usersRights[ $user->getId() ] = $this->getGroupPermissions(
+ $rightsCacheKey = $this->getRightsCacheKey( $user );
+ if ( !isset( $this->usersRights[ $rightsCacheKey ] ) ) {
+ $this->usersRights[ $rightsCacheKey ] = $this->getGroupPermissions(
$user->getEffectiveGroups()
);
- Hooks::run( 'UserGetRights', [ $user, &$this->usersRights[ $user->getId() ] ] );
+ Hooks::run( 'UserGetRights', [ $user, &$this->usersRights[ $rightsCacheKey ] ] );
// Deny any rights denied by the user's session, unless this
// endpoint has no sessions.
// FIXME: $user->getRequest().. need to be replaced with something else
$allowedRights = $user->getRequest()->getSession()->getAllowedUserRights();
if ( $allowedRights !== null ) {
- $this->usersRights[ $user->getId() ] = array_intersect(
- $this->usersRights[ $user->getId() ],
+ $this->usersRights[ $rightsCacheKey ] = array_intersect(
+ $this->usersRights[ $rightsCacheKey ],
$allowedRights
);
}
}
- Hooks::run( 'UserGetRightsRemove', [ $user, &$this->usersRights[ $user->getId() ] ] );
+ Hooks::run( 'UserGetRightsRemove', [ $user, &$this->usersRights[ $rightsCacheKey ] ] );
// Force reindexation of rights when a hook has unset one of them
- $this->usersRights[ $user->getId() ] = array_values(
- array_unique( $this->usersRights[ $user->getId() ] )
+ $this->usersRights[ $rightsCacheKey ] = array_values(
+ array_unique( $this->usersRights[ $rightsCacheKey ] )
);
if (
$user->getBlock()
) {
$anon = new User;
- $this->usersRights[ $user->getId() ] = array_intersect(
- $this->usersRights[ $user->getId() ],
+ $this->usersRights[ $rightsCacheKey ] = array_intersect(
+ $this->usersRights[ $rightsCacheKey ],
$this->getUserPermissions( $anon )
);
}
}
- $rights = $this->usersRights[ $user->getId() ];
+ $rights = $this->usersRights[ $rightsCacheKey ];
foreach ( $this->temporaryUserRights[ $user->getId() ] ?? [] as $overrides ) {
$rights = array_values( array_unique( array_merge( $rights, $overrides ) ) );
}
*/
public function invalidateUsersRightsCache( $user = null ) {
if ( $user !== null ) {
- if ( isset( $this->usersRights[ $user->getId() ] ) ) {
- unset( $this->usersRights[$user->getId()] );
+ $rightsCacheKey = $this->getRightsCacheKey( $user );
+ if ( isset( $this->usersRights[ $rightsCacheKey ] ) ) {
+ unset( $this->usersRights[ $rightsCacheKey ] );
}
} else {
$this->usersRights = null;
}
}
+ /**
+ * Gets a unique key for user rights cache.
+ * @param UserIdentity $user
+ * @return string
+ */
+ private function getRightsCacheKey( UserIdentity $user ) {
+ return $user->isRegistered() ? "u:{$user->getId()}" : "anon:{$user->getName()}";
+ }
+
/**
* Check, if the given group has the given permission
*
if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
throw new Exception( __METHOD__ . ' can not be called outside of tests' );
}
- $this->usersRights[ $user->getId() ] = is_array( $rights ) ? $rights : [ $rights ];
+ $this->usersRights[ $this->getRightsCacheKey( $user ) ] =
+ is_array( $rights ) ? $rights : [ $rights ];
}
}
namespace MediaWiki\Storage;
+use StatusValue;
+
/**
* Service for loading and storing data blobs.
*
*/
public function getBlob( $blobAddress, $queryFlags = 0 );
+ /**
+ * A batched version of BlobStore::getBlob.
+ *
+ * @param string[] $blobAddresses An array of blob addresses.
+ * @param int $queryFlags See IDBAccessObject.
+ * @throws BlobAccessException
+ * @return StatusValue A status with a map of blobAddress => binary blob data or null
+ * if fetching the blob has failed. Fetch failures errors are the
+ * warnings in the status object.
+ * @since 1.34
+ */
+ public function getBlobBatch( $blobAddresses, $queryFlags = 0 );
+
/**
* Stores an arbitrary blob of data and returns an address that can be used with
* getBlob() to retrieve the same blob of data,
namespace MediaWiki\Storage;
+use AppendIterator;
use DBAccessObjectUtils;
use IDBAccessObject;
use IExpiringStore;
use InvalidArgumentException;
use Language;
use MWException;
+use StatusValue;
use WANObjectCache;
use ExternalStoreAccess;
use Wikimedia\Assert\Assert;
public function getBlob( $blobAddress, $queryFlags = 0 ) {
Assert::parameterType( 'string', $blobAddress, '$blobAddress' );
- // No negative caching; negative hits on text rows may be due to corrupted replica DBs
+ $error = null;
$blob = $this->cache->getWithSetCallback(
$this->getCacheKey( $blobAddress ),
$this->getCacheTTL(),
- function ( $unused, &$ttl, &$setOpts ) use ( $blobAddress, $queryFlags ) {
+ function ( $unused, &$ttl, &$setOpts ) use ( $blobAddress, $queryFlags, &$error ) {
// Ignore $setOpts; blobs are immutable and negatives are not cached
- return $this->fetchBlob( $blobAddress, $queryFlags );
+ list( $result, $errors ) = $this->fetchBlobs( [ $blobAddress ], $queryFlags );
+ // No negative caching; negative hits on text rows may be due to corrupted replica DBs
+ $error = $errors[$blobAddress] ?? null;
+ return $result[$blobAddress];
},
[ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => IExpiringStore::TTL_PROC_LONG ]
);
- if ( $blob === false ) {
- throw new BlobAccessException( 'Failed to load blob from address ' . $blobAddress );
+ if ( $error ) {
+ throw new BlobAccessException( $error );
}
+ Assert::postcondition( is_string( $blob ), 'Blob must not be null' );
return $blob;
}
+ /**
+ * A batched version of BlobStore::getBlob.
+ *
+ * @param string[] $blobAddresses An array of blob addresses.
+ * @param int $queryFlags See IDBAccessObject.
+ * @throws BlobAccessException
+ * @return StatusValue A status with a map of blobAddress => binary blob data or null
+ * if fetching the blob has failed. Fetch failures errors are the
+ * warnings in the status object.
+ * @since 1.34
+ */
+ public function getBlobBatch( $blobAddresses, $queryFlags = 0 ) {
+ $errors = null;
+ $addressByCacheKey = $this->cache->makeMultiKeys(
+ $blobAddresses,
+ function ( $blobAddress ) {
+ return $this->getCacheKey( $blobAddress );
+ }
+ );
+ $blobsByCacheKey = $this->cache->getMultiWithUnionSetCallback(
+ $addressByCacheKey,
+ $this->getCacheTTL(),
+ function ( array $blobAddresses, array &$ttls, array &$setOpts ) use ( $queryFlags, &$errors ) {
+ // Ignore $setOpts; blobs are immutable and negatives are not cached
+ list( $result, $errors ) = $this->fetchBlobs( $blobAddresses, $queryFlags );
+ return $result;
+ },
+ [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => IExpiringStore::TTL_PROC_LONG ]
+ );
+
+ // Remap back to incoming blob addresses. The return value of the
+ // WANObjectCache::getMultiWithUnionSetCallback is keyed on the internal
+ // keys from WANObjectCache::makeMultiKeys, so we need to remap them
+ // before returning to the client.
+ $blobsByAddress = [];
+ foreach ( $blobsByCacheKey as $cacheKey => $blob ) {
+ $blobsByAddress[ $addressByCacheKey[ $cacheKey ] ] = $blob !== false ? $blob : null;
+ }
+
+ $result = StatusValue::newGood( $blobsByAddress );
+ if ( $errors ) {
+ foreach ( $errors as $error ) {
+ $result->warning( 'internalerror', $error );
+ }
+ }
+ return $result;
+ }
+
/**
* MCR migration note: this corresponds to Revision::fetchText
*
- * @param string $blobAddress
+ * @param string[] $blobAddresses
* @param int $queryFlags
*
* @throws BlobAccessException
- * @return string|false
- */
- private function fetchBlob( $blobAddress, $queryFlags ) {
- list( $schema, $id, ) = self::splitBlobAddress( $blobAddress );
+ * @return array [ $result, $errors ] A map of blob addresses to successfully fetched blobs
+ * or false if fetch failed, plus and array of errors
+ */
+ private function fetchBlobs( $blobAddresses, $queryFlags ) {
+ $textIdToBlobAddress = [];
+ $result = [];
+ $errors = [];
+ foreach ( $blobAddresses as $blobAddress ) {
+ list( $schema, $id ) = self::splitBlobAddress( $blobAddress );
+ //TODO: MCR: also support 'ex' schema with ExternalStore URLs, plus flags encoded in the URL!
+ if ( $schema === 'tt' ) {
+ $textId = intval( $id );
+ $textIdToBlobAddress[$textId] = $blobAddress;
+ } else {
+ $errors[$blobAddress] = "Unknown blob address schema: $schema";
+ $result[$blobAddress] = false;
+ continue;
+ }
- //TODO: MCR: also support 'ex' schema with ExternalStore URLs, plus flags encoded in the URL!
- if ( $schema === 'tt' ) {
- $textId = intval( $id );
- } else {
- // XXX: change to better exceptions! That makes migration more difficult, though.
- throw new BlobAccessException( "Unknown blob address schema: $schema" );
+ if ( !$textId || $id !== (string)$textId ) {
+ $errors[$blobAddress] = "Bad blob address: $blobAddress";
+ $result[$blobAddress] = false;
+ }
}
- if ( !$textId || $id !== (string)$textId ) {
- // XXX: change to better exceptions! That makes migration more difficult, though.
- throw new BlobAccessException( "Bad blob address: $blobAddress" );
+ $textIds = array_keys( $textIdToBlobAddress );
+ if ( !$textIds ) {
+ return [ $result, $errors ];
}
-
// Callers doing updates will pass in READ_LATEST as usual. Since the text/blob tables
// do not normally get rows changed around, set READ_LATEST_IMMUTABLE in those cases.
$queryFlags |= DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST )
? self::READ_LATEST_IMMUTABLE
: 0;
-
list( $index, $options, $fallbackIndex, $fallbackOptions ) =
DBAccessObjectUtils::getDBOptions( $queryFlags );
-
// Text data is immutable; check replica DBs first.
- $row = $this->getDBConnection( $index )->selectRow(
+ $dbConnection = $this->getDBConnection( $index );
+ $rows = $dbConnection->select(
'text',
- [ 'old_text', 'old_flags' ],
- [ 'old_id' => $textId ],
+ [ 'old_id', 'old_text', 'old_flags' ],
+ [ 'old_id' => $textIds ],
__METHOD__,
$options
);
- // Fallback to DB_MASTER in some cases if the row was not found, using the appropriate
+ // Fallback to DB_MASTER in some cases if not all the rows were found, using the appropriate
// options, such as FOR UPDATE to avoid missing rows due to REPEATABLE-READ.
- if ( !$row && $fallbackIndex !== null ) {
- $row = $this->getDBConnection( $fallbackIndex )->selectRow(
+ if ( $dbConnection->numRows( $rows ) !== count( $textIds ) && $fallbackIndex !== null ) {
+ $fetchedTextIds = [];
+ foreach ( $rows as $row ) {
+ $fetchedTextIds[] = $row->old_id;
+ }
+ $missingTextIds = array_diff( $textIds, $fetchedTextIds );
+ $dbConnection = $this->getDBConnection( $fallbackIndex );
+ $rowsFromFallback = $dbConnection->select(
'text',
- [ 'old_text', 'old_flags' ],
- [ 'old_id' => $textId ],
+ [ 'old_id', 'old_text', 'old_flags' ],
+ [ 'old_id' => $missingTextIds ],
__METHOD__,
$fallbackOptions
);
+ $appendIterator = new AppendIterator();
+ $appendIterator->append( $rows );
+ $appendIterator->append( $rowsFromFallback );
+ $rows = $appendIterator;
}
- if ( !$row ) {
- wfWarn( __METHOD__ . ": No text row with ID $textId." );
- return false;
+ foreach ( $rows as $row ) {
+ $blobAddress = $textIdToBlobAddress[$row->old_id];
+ $blob = $this->expandBlob( $row->old_text, $row->old_flags, $blobAddress );
+ if ( $blob === false ) {
+ $errors[$blobAddress] = "Bad data in text row {$row->old_id}.";
+ }
+ $result[$blobAddress] = $blob;
}
- $blob = $this->expandBlob( $row->old_text, $row->old_flags, $blobAddress );
-
- if ( $blob === false ) {
- wfLogWarning( __METHOD__ . ": Bad data in text row $textId." );
- return false;
+ // If we're still missing some of the rows, set errors for missing blobs.
+ if ( count( $result ) !== count( $blobAddresses ) ) {
+ foreach ( $blobAddresses as $blobAddress ) {
+ if ( !isset( $result[$blobAddress ] ) ) {
+ $errors[$blobAddress] = "Unable to fetch blob at $blobAddress";
+ $result[$blobAddress] = false;
+ }
+ }
}
-
- return $blob;
+ return [ $result, $errors ];
}
/**
use MediaWiki\Linker\LinkRenderer;
use MediaWiki\Linker\LinkTarget;
+use MediaWiki\MediaWikiServices;
/**
* Handles formatting for the "templates used on this page"
* Return a link to the edit page, with the text
* saying "view source" if the user can't edit the page
*
- * @param Title $titleObj
+ * @param LinkTarget $titleObj
* @return string
*/
- private function buildEditLink( Title $titleObj ) {
- if ( $titleObj->quickUserCan( 'edit', $this->context->getUser() ) ) {
+ private function buildEditLink( LinkTarget $titleObj ) {
+ if ( MediaWikiServices::getInstance()->getPermissionManager()
+ ->quickUserCan( 'edit', $this->context->getUser(), $titleObj )
+ ) {
$linkMsg = 'editlink';
} else {
$linkMsg = 'viewsourcelink';
$tools = [];
# Rollback and undo links
- if ( $prevRev && $this->getTitle()->quickUserCan( 'edit', $user ) ) {
- if ( $latest && $this->getTitle()->quickUserCan( 'rollback', $user ) ) {
+
+ if ( $prevRev && $permissionManager->quickUserCan( 'edit', $user, $this->getTitle() ) ) {
+ if ( $latest && $permissionManager->quickUserCan( 'rollback',
+ $user, $this->getTitle() )
+ ) {
// Get a rollback link without the brackets
$rollbackLink = Linker::generateRollback(
$rev,
' [' . $config->get( 'LanguageCode' ) . ']';
$feedUrl = SpecialPage::getTitleFor( 'Contributions', $params['user'] )->getFullURL();
- $target = 'newbies';
- if ( $params['user'] != 'newbies' ) {
- try {
- $target = $this->titleParser
- ->parseTitle( $params['user'], NS_USER )
- ->getText();
- } catch ( MalformedTitleException $e ) {
- $this->dieWithError(
- [ 'apierror-baduser', 'user', wfEscapeWikiText( $params['user'] ) ],
- 'baduser_' . $this->encodeParamName( 'user' )
- );
- }
+ try {
+ $target = $this->titleParser
+ ->parseTitle( $params['user'], NS_USER )
+ ->getText();
+ } catch ( MalformedTitleException $e ) {
+ $this->dieWithError(
+ [ 'apierror-baduser', 'user', wfEscapeWikiText( $params['user'] ) ],
+ 'baduser_' . $this->encodeParamName( 'user' )
+ );
}
$feed = new $feedClasses[$params['feedformat']] (
ApiBase::PARAM_ISMULTI => true,
],
'rights' => [
- ApiBase::PARAM_TYPE => User::getAllRights(),
+ ApiBase::PARAM_TYPE => $this->getPermissionManager()->getAllPermissions(),
ApiBase::PARAM_ISMULTI => true,
],
'prop' => [
public function getAllowedParams() {
$userGroups = User::getAllGroups();
- $userRights = User::getAllRights();
+ $userRights = $this->getPermissionManager()->getAllPermissions();
return [
'group' => [
/**
* Create a new block with specified parameters on a user, IP or IP range.
*
- * @param array $options Parameters of the block:
- * address string|User Target user name, User object, IP address or IP range
- * by int User ID of the blocker
- * 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
+ * @param array $options Parameters of the block, with supported options:
+ * - address: (string|User) Target user name, User object, IP address or IP range
+ * - by: (int) User ID of the blocker
+ * - 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 = [
/**
* Create a new block with specified parameters on a user, IP or IP range.
*
- * @param array $options Parameters of the block:
- * originalBlocks Block[] Blocks that this block is composed from
+ * @param array $options Parameters of the block, with options supported by
+ * `AbstractBlock::__construct`, and also:
+ * - originalBlocks: (Block[]) Blocks that this block is composed from
*/
public function __construct( array $options = [] ) {
parent::__construct( $options );
/**
* Create a new block with specified option parameters on a user, IP or IP range.
*
- * @param array $options Parameters of the block:
- * user int Override target user ID (for foreign users)
- * auto bool Is this an automatic block?
- * expiry string Timestamp of expiration of the block or 'infinity'
- * anonOnly bool Only disallow anonymous actions
- * createAccount bool Disallow creation of new accounts
- * enableAutoblock bool Enable automatic blocking
- * 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
- * actions, except those specifically allowed by
- * other block flags
+ * @param array $options Parameters of the block, with options supported by
+ * `AbstractBlock::__construct`, and also:
+ * - user: (int) Override target user ID (for foreign users)
+ * - auto: (bool) Is this an automatic block?
+ * - expiry: (string) Timestamp of expiration of the block or 'infinity'
+ * - anonOnly: (bool) Only disallow anonymous actions
+ * - createAccount: (bool) Disallow creation of new accounts
+ * - enableAutoblock: (bool) Enable automatic blocking
+ * - 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 actions,
+ * except those specifically allowed by other block flags
*
* @since 1.26 $options array
*/
/**
* Create a new block with specified parameters on a user, IP or IP range.
*
- * @param array $options Parameters of the block:
- * systemBlock string Indicate that this block is automatically
- * created by MediaWiki rather than being stored
- * in the database. Value is a string to return
- * from self::getSystemBlockType().
+ * @param array $options Parameters of the block, with options supported by
+ * `AbstractBlock::__construct`, and also:
+ * - systemBlock: (string) Indicate that this block is automatically created by
+ * MediaWiki rather than being stored in the database. Value is a string to
+ * return from self::getSystemBlockType().
*/
public function __construct( array $options = [] ) {
parent::__construct( $options );
global $wgUser;
if ( !$this->mParserOptions ) {
- if ( !$wgUser->isSafeToLoad() ) {
- // $wgUser isn't unstubbable yet, so don't try to get a
+ if ( !$wgUser || !$wgUser->isSafeToLoad() ) {
+ // $wgUser isn't available yet, so don't try to get a
// ParserOptions for it. And don't cache this ParserOptions
// either.
$po = ParserOptions::newFromAnon();
/** Check for rollback permissions, disallow special pages, and only
* show a link on the top-most revision
*/
- if ( $title->quickUserCan( 'rollback', $this->getUser() ) ) {
+ if ( MediaWikiServices::getInstance()->getPermissionManager()
+ ->quickUserCan( 'rollback', $this->getUser(), $title )
+ ) {
$rev = new Revision( [
'title' => $title,
'id' => $rc->mAttribs['rc_this_oldid'],
--- /dev/null
+<?php
+/**
+ * Content object implementation for representing unknown content.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @since 1.34
+ *
+ * @file
+ * @ingroup Content
+ *
+ * @author Daniel Kinzler
+ */
+
+/**
+ * Content object implementation representing unknown content.
+ *
+ * This can be used to handle content for which no ContentHandler exists on the system,
+ * perhaps because the extension that provided it has been removed.
+ *
+ * UnknownContent instances are immutable.
+ *
+ * @ingroup Content
+ */
+class UnknownContent extends AbstractContent {
+
+ /** @var string */
+ private $data;
+
+ /**
+ * @param string $data
+ * @param string $model_id The model ID to handle
+ */
+ public function __construct( $data, $model_id ) {
+ parent::__construct( $model_id );
+
+ $this->data = $data;
+ }
+
+ /**
+ * @return Content $this
+ */
+ public function copy() {
+ // UnknownContent is immutable, so no need to copy.
+ return $this;
+ }
+
+ /**
+ * Returns an empty string.
+ *
+ * @param int $maxlength
+ *
+ * @return string
+ */
+ public function getTextForSummary( $maxlength = 250 ) {
+ return '';
+ }
+
+ /**
+ * Returns the data size in bytes.
+ *
+ * @return int
+ */
+ public function getSize() {
+ return strlen( $this->data );
+ }
+
+ /**
+ * Returns false.
+ *
+ * @param bool|null $hasLinks If it is known whether this content contains links,
+ * provide this information here, to avoid redundant parsing to find out.
+ *
+ * @return bool
+ */
+ public function isCountable( $hasLinks = null ) {
+ return false;
+ }
+
+ /**
+ * @return string data of unknown format and meaning
+ */
+ public function getNativeData() {
+ return $this->getData();
+ }
+
+ /**
+ * @return string data of unknown format and meaning
+ */
+ public function getData() {
+ return $this->data;
+ }
+
+ /**
+ * Returns an empty string.
+ *
+ * @return string The raw text.
+ */
+ public function getTextForSearchIndex() {
+ return '';
+ }
+
+ /**
+ * Returns false.
+ */
+ public function getWikitextForTransclusion() {
+ return false;
+ }
+
+ /**
+ * Fills the ParserOutput with an error message.
+ */
+ protected function fillParserOutput( Title $title, $revId,
+ ParserOptions $options, $generateHtml, ParserOutput &$output
+ ) {
+ $msg = wfMessage( 'unsupported-content-model', [ $this->getModel() ] );
+ $html = Html::rawElement( 'div', [ 'class' => 'error' ], $msg->inContentLanguage()->parse() );
+ $output->setText( $html );
+ }
+
+ /**
+ * Returns false.
+ */
+ public function convert( $toModel, $lossy = '' ) {
+ return false;
+ }
+
+ protected function equalsInternal( Content $that ) {
+ if ( !$that instanceof UnknownContent ) {
+ return false;
+ }
+
+ return $this->getData() == $that->getData();
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * Base content handler class for flat text contents.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @since 1.34
+ *
+ * @file
+ * @ingroup Content
+ */
+
+/**
+ * Content handler implementation for unknown content.
+ *
+ * This can be used to handle content for which no ContentHandler exists on the system,
+ * perhaps because the extension that provided it has been removed.
+ *
+ * @ingroup Content
+ */
+class UnknownContentHandler extends ContentHandler {
+
+ /**
+ * Constructs an UnknownContentHandler. Since UnknownContentHandler can be registered
+ * for multiple model IDs on a system, multiple instances of UnknownContentHandler may
+ * coexist.
+ *
+ * To preserve the serialization format of the original content model, it must be supplied
+ * to the constructor via the $formats parameter. If not given, the default format is
+ * reported as 'application/octet-stream'.
+ *
+ * @param string $modelId
+ * @param string[]|null $formats
+ */
+ public function __construct( $modelId, $formats = null ) {
+ parent::__construct(
+ $modelId,
+ $formats ?? [
+ 'application/octet-stream',
+ 'application/unknown',
+ 'application/x-binary',
+ 'text/unknown',
+ 'unknown/unknown',
+ ]
+ );
+ }
+
+ /**
+ * Returns the content's data as-is.
+ *
+ * @param Content $content
+ * @param string|null $format The serialization format to check
+ *
+ * @return mixed
+ */
+ public function serializeContent( Content $content, $format = null ) {
+ /** @var UnknownContent $content */
+ return $content->getData();
+ }
+
+ /**
+ * Constructs an UnknownContent instance wrapping the given data.
+ *
+ * @since 1.21
+ *
+ * @param string $blob serialized content in an unknown format
+ * @param string|null $format ignored
+ *
+ * @return Content The UnknownContent object wrapping $data
+ */
+ public function unserializeContent( $blob, $format = null ) {
+ return new UnknownContent( $blob, $this->getModelID() );
+ }
+
+ /**
+ * Creates an empty UnknownContent object.
+ *
+ * @since 1.21
+ *
+ * @return Content A new UnknownContent object with empty text.
+ */
+ public function makeEmptyContent() {
+ return $this->unserializeContent( '' );
+ }
+
+ /**
+ * @return false
+ */
+ public function supportsDirectEditing() {
+ return false;
+ }
+
+ /**
+ * @param IContextSource $context
+ *
+ * @return SlotDiffRenderer
+ */
+ protected function getSlotDiffRendererInternal( IContextSource $context ) {
+ return new UnsupportedSlotDiffRenderer( $context );
+ }
+}
* @param array $lbConf Config for LBFactory::__construct()
* @param ServiceOptions $options
* @param ConfiguredReadOnlyMode $readOnlyMode
- * @param BagOStuff $srvCace
+ * @param BagOStuff $srvCache
* @param BagOStuff $mainStash
* @param WANObjectCache $wanCache
* @return array
array $lbConf,
ServiceOptions $options,
ConfiguredReadOnlyMode $readOnlyMode,
- BagOStuff $srvCace,
+ BagOStuff $srvCache,
BagOStuff $mainStash,
WANObjectCache $wanCache
) {
$options->get( 'DBprefix' )
);
- $lbConf = self::injectObjectCaches( $lbConf, $srvCace, $mainStash, $wanCache );
+ $lbConf = self::injectObjectCaches( $lbConf, $srvCache, $mainStash, $wanCache );
return $lbConf;
}
private static function injectObjectCaches(
array $lbConf, BagOStuff $sCache, BagOStuff $mStash, WANObjectCache $wCache
) {
+ // Fallback if APC style caching is not an option
+ if ( $sCache instanceof EmptyBagOStuff ) {
+ $sCache = new HashBagOStuff( [ 'maxKeys' => 100 ] );
+ }
+
// Use APC/memcached style caching, but avoids loops with CACHE_DB (T141804)
if ( $sCache->getQoS( $sCache::ATTR_EMULATION ) > $sCache::QOS_EMULATION_SQL ) {
$lbConf['srvCache'] = $sCache;
*/
use MediaWiki\MediaWikiServices;
-use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\Storage\NameTableAccessException;
$permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
- if ( $samePage && $this->mNewPage && $permissionManager->userCan(
- 'edit', $user, $this->mNewPage, PermissionManager::RIGOR_QUICK
+ if ( $samePage && $this->mNewPage && $permissionManager->quickUserCan(
+ 'edit', $user, $this->mNewPage
) ) {
if ( $this->mNewRev->isCurrent() && $permissionManager->userCan(
'rollback', $user, $this->mNewPage
}
}
- if ( !$this->mOldRev->isDeleted( RevisionRecord::DELETED_TEXT ) &&
- !$this->mNewRev->isDeleted( RevisionRecord::DELETED_TEXT )
+ if ( $this->userCanEdit( $this->mOldRev ) &&
+ $this->userCanEdit( $this->mNewRev )
) {
$undoLink = Html::element( 'a', [
'href' => $this->mNewPage->getLocalURL( [
protected function getMarkPatrolledLinkInfo() {
$user = $this->getUser();
$config = $this->getConfig();
+ $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
// Prepare a change patrol link, if applicable
if (
// Is patrolling enabled and the user allowed to?
$config->get( 'UseRCPatrol' ) &&
- $this->mNewPage && $this->mNewPage->quickUserCan( 'patrol', $user ) &&
+ $this->mNewPage &&
+ $permissionManager->quickUserCan( 'patrol', $user, $this->mNewPage ) &&
// Only do this if the revision isn't more than 6 hours older
// than the Max RC age (6h because the RC might not be cleaned out regularly)
RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 )
// Build the link
if ( $rcid ) {
$this->getOutput()->preventClickjacking();
- if ( MediaWikiServices::getInstance()->getPermissionManager()
- ->userHasRight( $user, 'writeapi' ) ) {
+ if ( $permissionManager->userHasRight( $user, 'writeapi' ) ) {
$this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' );
}
) {
$out->addParserOutput( $parserOutput, [
'enableSectionEditLinks' => $this->mNewRev->isCurrent()
- && $this->mNewRev->getTitle()->quickUserCan( 'edit', $this->getUser() ),
+ && MediaWikiServices::getInstance()->getPermissionManager()->quickUserCan(
+ 'edit',
+ $this->getUser(),
+ $this->mNewRev->getTitle()
+ )
] );
}
}
return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
}
+ /**
+ * @param Revision $rev
+ * @return bool whether the user can see and edit the revision.
+ */
+ private function userCanEdit( Revision $rev ) {
+ $user = $this->getUser();
+
+ if ( !$rev->getContentHandler()->supportsDirectEditing() ) {
+ return false;
+ }
+
+ if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
/**
* Get a header for a specified revision.
*
$header = Linker::linkKnown( $title, $header, [],
[ 'oldid' => $rev->getId() ] );
- if ( $rev->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
+ if ( $this->userCanEdit( $rev ) ) {
$editQuery = [ 'action' => 'edit' ];
if ( !$rev->isCurrent() ) {
$editQuery['oldid'] = $rev->getId();
}
- $key = $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold';
+ $key = MediaWikiServices::getInstance()->getPermissionManager()
+ ->quickUserCan( 'edit', $user, $title ) ? 'editold' : 'viewsourceold';
$msg = $this->msg( $key )->escaped();
$editLink = $this->msg( 'parentheses' )->rawParams(
Linker::linkKnown( $title, $msg, [], $editQuery ) )->escaped();
* must have the same content model that was used to obtain this diff renderer.
* @param Content|null $oldContent
* @param Content|null $newContent
- * @return string
+ * @return string HTML, one or more <tr> tags.
*/
abstract public function getDiff( Content $oldContent = null, Content $newContent = null );
* Diff the text representations of two content objects (or just two pieces of text in general).
* @param string $oldText
* @param string $newText
- * @return string
+ * @return string HTML, one or more <tr> tags.
*/
public function getTextDiff( $oldText, $newText ) {
Assert::parameterType( 'string', $oldText, '$oldText' );
--- /dev/null
+<?php
+/**
+ * Renders a slot diff by doing a text diff on the native representation.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup DifferenceEngine
+ */
+
+/**
+ * Produces a warning message about not being able to render a slot diff.
+ *
+ * @since 1.34
+ *
+ * @ingroup DifferenceEngine
+ */
+class UnsupportedSlotDiffRenderer extends SlotDiffRenderer {
+
+ /**
+ * @var MessageLocalizer
+ */
+ private $localizer;
+
+ /**
+ * UnsupportedSlotDiffRenderer constructor.
+ *
+ * @param MessageLocalizer $localizer
+ */
+ public function __construct( MessageLocalizer $localizer ) {
+ $this->localizer = $localizer;
+ }
+
+ /** @inheritDoc */
+ public function getDiff( Content $oldContent = null, Content $newContent = null ) {
+ $this->normalizeContents( $oldContent, $newContent );
+
+ $oldModel = $oldContent->getModel();
+ $newModel = $newContent->getModel();
+
+ if ( $oldModel !== $newModel ) {
+ $msg = $this->localizer->msg( 'unsupported-content-diff2', $oldModel, $newModel );
+ } else {
+ $msg = $this->localizer->msg( 'unsupported-content-diff', $oldModel );
+ }
+
+ return Html::rawElement(
+ 'tr',
+ [],
+ Html::rawElement(
+ 'td',
+ [ 'colspan' => 4, 'class' => 'error' ],
+ $msg->parse()
+ )
+ );
+ }
+
+}
$class = $config['class'];
if ( $class === FileBackendMultiWrite::class ) {
+ // @todo How can we test this? What's the intended use-case?
foreach ( $config['backends'] as $index => $beConfig ) {
if ( isset( $beConfig['template'] ) ) {
// Config is just a modified version of a registered backend's.
}
// Resolve source to a storage path if virtual
- $srcPath = $this->resolveToStoragePath( $srcPath );
+ $srcPath = $this->resolveToStoragePathIfVirtual( $srcPath );
- // Get the appropriate file operation
- if ( FileBackend::isStoragePath( $srcPath ) ) {
- $opName = 'copy';
- } else {
- $opName = 'store';
- }
+ // Copy the source file to the destination
$operations[] = [
- 'op' => $opName,
- 'src' => $srcPath,
+ 'op' => FileBackend::isStoragePath( $srcPath ) ? 'copy' : 'store',
+ 'src' => $srcPath, // storage path (copy) or local file path (store)
'dst' => $dstPath,
- 'overwrite' => $flags & self::OVERWRITE,
- 'overwriteSame' => $flags & self::OVERWRITE_SAME,
+ 'overwrite' => ( $flags & self::OVERWRITE ) ? true : false,
+ 'overwriteSame' => ( $flags & self::OVERWRITE_SAME ) ? true : false,
];
}
$path = $this->getZonePath( $zone ) . "/$rel";
} else {
// Resolve source to a storage path if virtual
- $path = $this->resolveToStoragePath( $path );
+ $path = $this->resolveToStoragePathIfVirtual( $path );
}
$operations[] = [ 'op' => 'delete', 'src' => $path ];
}
public function quickCleanDir( $dir ) {
$status = $this->newGood();
$status->merge( $this->backend->clean(
- [ 'dir' => $this->resolveToStoragePath( $dir ) ] ) );
+ [ 'dir' => $this->resolveToStoragePathIfVirtual( $dir ) ] ) );
return $status;
}
if ( $src instanceof FSFile ) {
$op = 'store';
} else {
- $src = $this->resolveToStoragePath( $src );
+ $src = $this->resolveToStoragePathIfVirtual( $src );
$op = FileBackend::isStoragePath( $src ) ? 'copy' : 'store';
}
- $dst = $this->resolveToStoragePath( $dst );
+ $dst = $this->resolveToStoragePathIfVirtual( $dst );
if ( !isset( $triple[2] ) ) {
$headers = [];
foreach ( $paths as $path ) {
$operations[] = [
'op' => 'delete',
- 'src' => $this->resolveToStoragePath( $path ),
+ 'src' => $this->resolveToStoragePathIfVirtual( $path ),
'ignoreMissingSource' => true
];
}
$sources = [];
foreach ( $srcPaths as $srcPath ) {
// Resolve source to a storage path if virtual
- $source = $this->resolveToStoragePath( $srcPath );
+ $source = $this->resolveToStoragePathIfVirtual( $srcPath );
$sources[] = $source; // chunk to merge
}
$options = $ntuple[3] ?? [];
// Resolve source to a storage path if virtual
- $srcPath = $this->resolveToStoragePath( $srcPath );
+ $srcPath = $this->resolveToStoragePathIfVirtual( $srcPath );
if ( !$this->validateFilename( $dstRel ) ) {
throw new MWException( 'Validation error in $dstRel' );
}
// Copy (or move) the source file to the destination
if ( FileBackend::isStoragePath( $srcPath ) ) {
- if ( $flags & self::DELETE_SOURCE ) {
- $operations[] = [
- 'op' => 'move',
- 'src' => $srcPath,
- 'dst' => $dstPath,
- 'overwrite' => true, // replace current
- 'headers' => $headers
- ];
- } else {
- $operations[] = [
- 'op' => 'copy',
- 'src' => $srcPath,
- 'dst' => $dstPath,
- 'overwrite' => true, // replace current
- 'headers' => $headers
- ];
- }
- } else { // FS source path
+ $operations[] = [
+ 'op' => ( $flags & self::DELETE_SOURCE ) ? 'move' : 'copy',
+ 'src' => $srcPath,
+ 'dst' => $dstPath,
+ 'overwrite' => true, // replace current
+ 'headers' => $headers
+ ];
+ } else {
$operations[] = [
'op' => 'store',
- 'src' => $src, // prefer FSFile objects
+ 'src' => $src, // FSFile (preferred) or local file path
'dst' => $dstPath,
'overwrite' => true, // replace current
'headers' => $headers
* @return Status
*/
protected function initDirectory( $dir ) {
- $path = $this->resolveToStoragePath( $dir );
+ $path = $this->resolveToStoragePathIfVirtual( $dir );
list( , $container, ) = FileBackend::splitStoragePath( $path );
$params = [ 'dir' => $path ];
$status = $this->newGood();
$status->merge( $this->backend->clean(
- [ 'dir' => $this->resolveToStoragePath( $dir ) ] ) );
+ [ 'dir' => $this->resolveToStoragePathIfVirtual( $dir ) ] ) );
return $status;
}
* @return array Map of files and existence flags, or false
*/
public function fileExistsBatch( array $files ) {
- $paths = array_map( [ $this, 'resolveToStoragePath' ], $files );
+ $paths = array_map( [ $this, 'resolveToStoragePathIfVirtual' ], $files );
$this->backend->preloadFileStat( [ 'srcs' => $paths ] );
$result = [];
foreach ( $files as $key => $file ) {
- $path = $this->resolveToStoragePath( $file );
+ $path = $this->resolveToStoragePathIfVirtual( $file );
$result[$key] = $this->backend->fileExists( [ 'src' => $path ] );
}
* @return string
* @throws MWException
*/
- protected function resolveToStoragePath( $path ) {
+ protected function resolveToStoragePathIfVirtual( $path ) {
if ( self::isVirtualUrl( $path ) ) {
return $this->resolveVirtualUrl( $path );
}
* @return TempFSFile|null Returns null on failure
*/
public function getLocalCopy( $virtualUrl ) {
- $path = $this->resolveToStoragePath( $virtualUrl );
+ $path = $this->resolveToStoragePathIfVirtual( $virtualUrl );
return $this->backend->getLocalCopy( [ 'src' => $path ] );
}
* @return FSFile|null Returns null on failure.
*/
public function getLocalReference( $virtualUrl ) {
- $path = $this->resolveToStoragePath( $virtualUrl );
+ $path = $this->resolveToStoragePathIfVirtual( $virtualUrl );
return $this->backend->getLocalReference( [ 'src' => $path ] );
}
* @return string|bool False on failure
*/
public function getFileTimestamp( $virtualUrl ) {
- $path = $this->resolveToStoragePath( $virtualUrl );
+ $path = $this->resolveToStoragePathIfVirtual( $virtualUrl );
return $this->backend->getFileTimestamp( [ 'src' => $path ] );
}
* @return int|bool False on failure
*/
public function getFileSize( $virtualUrl ) {
- $path = $this->resolveToStoragePath( $virtualUrl );
+ $path = $this->resolveToStoragePathIfVirtual( $virtualUrl );
return $this->backend->getFileSize( [ 'src' => $path ] );
}
* @return string|bool
*/
public function getFileSha1( $virtualUrl ) {
- $path = $this->resolveToStoragePath( $virtualUrl );
+ $path = $this->resolveToStoragePathIfVirtual( $virtualUrl );
return $this->backend->getFileSha1Base36( [ 'src' => $path ] );
}
* @since 1.27
*/
public function streamFileWithStatus( $virtualUrl, $headers = [], $optHeaders = [] ) {
- $path = $this->resolveToStoragePath( $virtualUrl );
+ $path = $this->resolveToStoragePathIfVirtual( $virtualUrl );
$params = [ 'src' => $path, 'headers' => $headers, 'options' => $optHeaders ];
// T172851: HHVM does not flush the output properly, causing OOM
wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
- // Purge the source and target files...
+ // Purge the source and target files outside the transaction...
$oldTitleFile = $localRepo->newFile( $this->title );
$newTitleFile = $localRepo->newFile( $target );
- // To avoid slow purges in the transaction, move them outside...
DeferredUpdates::addUpdate(
new AutoCommitUpdate(
$this->getRepo()->getMasterDB(),
function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
$oldTitleFile->purgeEverything();
foreach ( $archiveNames as $archiveName ) {
+ /** @var OldLocalFile $oldTitleFile */
$oldTitleFile->purgeOldThumbnails( $archiveName );
}
$newTitleFile->purgeEverything();
/** @var int[] Map of (location => UNIX timestamp) */
protected $ejectExpiryByLocation;
- /** @var array[] Non-empty list of (float, node name, location name) */
+ /** @var array[] Non-empty position-ordered list of (position, location name) */
protected $baseRing;
- /** @var array[] Non-empty list of (float, node name, location name) */
+ /** @var array[] Non-empty position-ordered list of (position, location name) */
protected $liveRing;
/** @var float Number of positions on the ring */
$this->algo = $algo;
$this->weightByLocation = $weightByLocation;
$this->ejectExpiryByLocation = $ejections;
- $this->baseRing = $this->buildLocationRing( $this->weightByLocation, $this->algo );
+ $this->baseRing = $this->buildLocationRing( $this->weightByLocation );
}
/**
}
/**
- * Get the location of an item on the ring, as well as the next locations
+ * Get the location of an item on the ring followed by the next ring locations
*
* @param string $item
* @param int $limit Maximum number of locations to return
* @param int $from One of the RING_* class constants
* @return string[] List of locations
+ * @throws InvalidArgumentException
* @throws UnexpectedValueException
*/
public function getLocations( $item, $limit, $from = self::RING_ALL ) {
throw new InvalidArgumentException( "Invalid ring source specified." );
}
- // Locate this item's position on the hash ring
- $position = $this->getItemPosition( $item );
- $itemNodeIndex = $this->findNodeIndexForPosition( $position, $ring );
+ // Locate the node index for this item's position on the hash ring
+ $itemIndex = $this->findNodeIndexForPosition( $this->getItemPosition( $item ), $ring );
$locations = [];
- $currentIndex = $itemNodeIndex;
+ $currentIndex = null;
while ( count( $locations ) < $limit ) {
+ if ( $currentIndex === null ) {
+ $currentIndex = $itemIndex;
+ } else {
+ $currentIndex = $this->getNextClockwiseNodeIndex( $currentIndex, $ring );
+ if ( $currentIndex === $itemIndex ) {
+ break; // all nodes visited
+ }
+ }
$nodeLocation = $ring[$currentIndex][self::KEY_LOCATION];
if ( !in_array( $nodeLocation, $locations, true ) ) {
// Ignore other nodes for the same locations already added
$locations[] = $nodeLocation;
}
- $currentIndex = $this->getNextClockwiseNodeIndex( $currentIndex, $ring );
- if ( $currentIndex === $itemNodeIndex ) {
- break; // all nodes visited
- }
}
return $locations;
if ( $count === 0 ) {
return null;
}
+
+ $index = null;
$lowPos = 0;
$highPos = $count;
while ( true ) {
- $midPos = intval( ( $lowPos + $highPos ) / 2 );
+ $midPos = (int)( ( $lowPos + $highPos ) / 2 );
if ( $midPos === $count ) {
- return 0;
+ $index = 0;
+ break;
}
- $midVal = $ring[$midPos][self::KEY_POS];
- $midMinusOneVal = $midPos === 0 ? 0 : $ring[$midPos - 1][self::KEY_POS];
+ $midVal = $ring[$midPos][self::KEY_POS];
+ $midMinusOneVal = ( $midPos === 0 ) ? 0 : $ring[$midPos - 1][self::KEY_POS];
if ( $position <= $midVal && $position > $midMinusOneVal ) {
- return $midPos;
+ $index = $midPos;
+ break;
}
if ( $midVal < $position ) {
}
if ( $lowPos > $highPos ) {
- return 0;
+ $index = 0;
+ break;
}
}
+
+ return $index;
}
/**
/**
* @param int[] $weightByLocation
- * @param string $algo Hashing algorithm
* @return array[]
*/
- private function buildLocationRing( array $weightByLocation, $algo ) {
+ private function buildLocationRing( array $weightByLocation ) {
$locationCount = count( $weightByLocation );
$totalWeight = array_sum( $weightByLocation );
throw new UnexpectedValueException( __METHOD__ . ": {$this->algo} is < 32 bits." );
}
- return (float)sprintf( '%u', unpack( 'V', $octets )[1] );
+ $pos = unpack( 'V', $octets )[1];
+ if ( $pos < 0 ) {
+ // Most-significant-bit is set, causing unpack() to return a negative integer due
+ // to the fact that it returns a signed int. Cast it to an unsigned integer string.
+ $pos = sprintf( '%u', $pos );
+ }
+
+ return (float)$pos;
}
/**
} elseif ( !$hadError ) {
return false; // file does not exist
} else {
- return null; // failure
+ return self::UNKNOWN; // failure
}
}
$exists = is_dir( $dir );
$hadError = $this->untrapWarnings();
- return $hadError ? null : $exists;
+ return $hadError ? self::UNKNOWN : $exists;
}
/**
} elseif ( !is_readable( $dir ) ) {
$this->logger->warning( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
- return null; // bad permissions?
+ return self::UNKNOWN; // bad permissions?
}
return new FSFileBackendDirList( $dir, $params );
} elseif ( !is_readable( $dir ) ) {
$this->logger->warning( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
- return null; // bad permissions?
+ return self::UNKNOWN; // bad permissions?
}
return new FSFileBackendFileList( $dir, $params );
const ATTR_METADATA = 2; // files can be stored with metadata key/values
const ATTR_UNICODE_PATHS = 4; // files can have Unicode paths (not just ASCII)
+ /** @var null Idiom for "could not determine due to I/O errors" */
+ const UNKNOWN = null;
+
/**
* Create a new backend instance from configuration.
* This should only be called from within FileBackendGroup.
}
/**
- * Get the unique backend name.
+ * Get the unique backend name
+ *
* We may have multiple different backends of the same type.
* For example, we can have two Swift backends using different proxies.
*
/**
* Alias to getDomainId()
+ *
* @return string
* @since 1.20
* @deprecated Since 1.34 Use getDomainId()
abstract public function getFileHttpUrl( array $params );
/**
- * Check if a directory exists at a given storage path.
- * Backends using key/value stores will check if the path is a
- * virtual directory, meaning there are files under the given directory.
+ * Check if a directory exists at a given storage path
+ *
+ * For backends using key/value stores, a directory is said to exist whenever
+ * there exist any files with paths using the given directory path as a prefix
+ * followed by a forward slash. For example, if there is a file called
+ * "mwstore://backend/container/dir/path.svg" then directories are said to exist
+ * at "mwstore://backend/container" and "mwstore://backend/container/dir". These
+ * can be thought of as "virtual" directories.
+ *
+ * Backends that directly use a filesystem layer might enumerate empty directories.
+ * The clean() method should always be used when files are deleted or moved if this
+ * is a concern. This is a trade-off to avoid write amplication/contention on file
+ * changes or read amplification when calling this method.
*
* Storage backends with eventual consistency might return stale data.
*
+ * @see FileBackend::clean()
+ *
* @param array $params Parameters include:
* - dir : storage directory
- * @return bool|null Returns null on failure
+ * @return bool|null Whether a directory exists or null on failure
* @since 1.20
*/
abstract public function directoryExists( array $params );
/**
- * Get an iterator to list *all* directories under a storage directory.
+ * Get an iterator to list *all* directories under a storage directory
+ *
* If the directory is of the form "mwstore://backend/container",
* then all directories in the container will be listed.
* If the directory is of form "mwstore://backend/container/dir",
*
* Failures during iteration can result in FileBackendError exceptions (since 1.22).
*
+ * @see FileBackend::directoryExists()
+ *
* @param array $params Parameters include:
* - dir : storage directory
* - topOnly : only return direct child dirs of the directory
- * @return Traversable|array|null Returns null on failure
+ * @return Traversable|array|null Directory list enumerator null on failure
* @since 1.20
*/
abstract public function getDirectoryList( array $params );
*
* Failures during iteration can result in FileBackendError exceptions (since 1.22).
*
+ * @see FileBackend::directoryExists()
+ *
* @param array $params Parameters include:
* - dir : storage directory
- * @return Traversable|array|null Returns null on failure
+ * @return Traversable|array|null Directory list enumerator or null on failure
* @since 1.20
*/
final public function getTopDirectoryList( array $params ) {
}
/**
- * Get an iterator to list *all* stored files under a storage directory.
- * If the directory is of the form "mwstore://backend/container",
- * then all files in the container will be listed.
- * If the directory is of form "mwstore://backend/container/dir",
- * then all files under that directory will be listed.
- * Results will be storage paths relative to the given directory.
+ * Get an iterator to list *all* stored files under a storage directory
+ *
+ * If the directory is of the form "mwstore://backend/container", then all
+ * files in the container will be listed. If the directory is of form
+ * "mwstore://backend/container/dir", then all files under that directory will
+ * be listed. Results will be storage paths relative to the given directory.
*
* Storage backends with eventual consistency might return stale data.
*
* - dir : storage directory
* - topOnly : only return direct child files of the directory (since 1.20)
* - adviseStat : set to true if stat requests will be made on the files (since 1.22)
- * @return Traversable|array|null Returns null on failure
+ * @return Traversable|array|null File list enumerator or null on failure
*/
abstract public function getFileList( array $params );
* @param array $params Parameters include:
* - dir : storage directory
* - adviseStat : set to true if stat requests will be made on the files (since 1.22)
- * @return Traversable|array|null Returns null on failure
+ * @return Traversable|array|null File list enumerator or null on failure
* @since 1.20
*/
final public function getTopFileList( array $params ) {
* @param array $params Parameters include:
* - srcs : list of source storage paths
* - latest : use the latest available data
- * @return bool All requests proceeded without I/O errors (since 1.24)
+ * @return bool Whether all requests proceeded without I/O errors (since 1.24)
* @since 1.23
*/
abstract public function preloadFileStat( array $params );
*
* @param string $type One of (attachment, inline)
* @param string $filename Suggested file name (should not contain slashes)
- * @throws FileBackendError
+ * @throws InvalidArgumentException
* @return string
* @since 1.20
*/
$ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
$stat = $this->getFileStat( $params );
- return ( $stat === null ) ? null : (bool)$stat; // null => failure
+ return ( $stat === self::UNKNOWN ) ? self::UNKNOWN : (bool)$stat;
}
final public function getFileTimestamp( array $params ) {
// cache entries from mass object listings that do not include the SHA-1. In that
// case, loading the persistent stat cache will likely yield the SHA-1.
if (
- $stat === null ||
+ $stat === self::UNKNOWN ||
( $requireSHA1 && is_array( $stat ) && !isset( $stat['sha1'] ) )
) {
$this->primeFileCache( [ $path ] ); // check persistent cache
$res = true;
break; // found one!
} elseif ( $exists === null ) { // error?
- $res = null; // if we don't find anything, it is indeterminate
+ $res = self::UNKNOWN; // if we don't find anything, it is indeterminate
}
}
final public function getDirectoryList( array $params ) {
list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
if ( $dir === null ) { // invalid storage path
- return null;
+ return self::UNKNOWN;
}
if ( $shard !== null ) {
// File listing is confined to a single container/shard
final public function getFileList( array $params ) {
list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
if ( $dir === null ) { // invalid storage path
- return null;
+ return self::UNKNOWN;
}
if ( $shard !== null ) {
// File listing is confined to a single container/shard
protected function doGetFileStat( array $params ) {
$src = $this->resolveHashKey( $params['src'] );
if ( $src === null ) {
- return null;
+ return false; // invalid path
}
if ( isset( $this->files[$src] ) ) {
$stat = $this->getContainerStat( $fullCont );
if ( is_array( $stat ) ) {
return $status; // already there
- } elseif ( $stat === null ) {
+ } elseif ( $stat === self::UNKNOWN ) {
$status->fatal( 'backend-fail-internal', $this->name );
$this->logger->error( __METHOD__ . ': cannot get container stat' );
return ( count( $status->value ) ) > 0;
}
- return null; // error
+ return self::UNKNOWN; // error
}
/**
if ( !$this->containerStatCache->hasField( $container, 'stat' ) ) {
$auth = $this->getAuthentication();
if ( !$auth ) {
- return null;
+ return self::UNKNOWN;
}
list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
$this->onError( null, __METHOD__,
[ 'cont' => $container ], $rerr, $rcode, $rdesc );
- return null;
+ return self::UNKNOWN;
}
}
$stats[$path] = false;
continue; // invalid storage path
} elseif ( !$auth ) {
- $stats[$path] = null;
+ $stats[$path] = self::UNKNOWN;
continue;
}
$stats[$path] = false;
continue; // ok, nothing to do
} elseif ( !is_array( $cstat ) ) {
- $stats[$path] = null;
+ $stats[$path] = self::UNKNOWN;
continue;
}
} elseif ( $rcode === 404 ) {
$stat = false;
} else {
- $stat = null;
+ $stat = self::UNKNOWN;
$this->onError( null, __METHOD__, $params, $rerr, $rcode, $rdesc );
}
$stats[$path] = $stat;
* @ingroup FileJournal
*/
+use Wikimedia\ObjectFactory;
use Wikimedia\Timestamp\ConvertibleTimestamp;
/**
protected $ttlDays;
/**
- * Construct a new instance from configuration.
+ * Construct a new instance from configuration. Do not call this directly, use factory().
*
* @param array $config Includes:
* 'ttlDays' : days to keep log entries around (false means "forever")
*/
- protected function __construct( array $config ) {
+ public function __construct( array $config ) {
$this->ttlDays = $config['ttlDays'] ?? false;
}
* @return FileJournal
*/
final public static function factory( array $config, $backend ) {
- $class = $config['class'];
- $jrn = new $class( $config );
- if ( !$jrn instanceof self ) {
- throw new InvalidArgumentException( "$class is not an instance of " . __CLASS__ );
- }
+ $jrn = ObjectFactory::getObjectFromSpec(
+ $config,
+ [ 'specIsArg' => true, 'assertClass' => __CLASS__ ]
+ );
$jrn->backend = $backend;
return $jrn;
return $this->unpack( $block, 0, $struct );
}
- private function unpackSector( $sectorNumber, $struct ) {
- $offset = $this->sectorOffset( $sectorNumber );
- return $this->unpackOffset( $offset, array_sum( $struct ) );
- }
-
private function unpack( $block, $offset, $struct ) {
$data = [];
foreach ( $struct as $key => $length ) {
/**
* @var array
+ * @deprecated since 1.34, no longer used.
*/
protected $mDefaultQuery;
];
}
- private function getDefaultQuery() {
- if ( !isset( $this->mDefaultQuery ) ) {
- $this->mDefaultQuery = $this->getRequest()->getQueryValues();
- unset( $this->mDefaultQuery['title'] );
- unset( $this->mDefaultQuery['dir'] );
- unset( $this->mDefaultQuery['offset'] );
- unset( $this->mDefaultQuery['limit'] );
- unset( $this->mDefaultQuery['order'] );
- unset( $this->mDefaultQuery['month'] );
- unset( $this->mDefaultQuery['year'] );
- }
-
- return $this->mDefaultQuery;
- }
-
/**
* @param array $queryTypes
* @return array Form descriptor
if ( !isset( $params['servers'] ) ) {
$params['servers'] = $GLOBALS['wgMemCachedServers'];
}
- if ( !isset( $params['debug'] ) ) {
- $params['debug'] = $GLOBALS['wgMemCachedDebug'];
- }
if ( !isset( $params['persistent'] ) ) {
$params['persistent'] = $GLOBALS['wgMemCachedPersistent'];
}
*/
public static function detectLocalServerCache() {
if ( function_exists( 'apcu_fetch' ) ) {
- return 'apcu';
+ // Make sure the APCu methods actually store anything
+ if ( PHP_SAPI !== 'cli' || ini_get( 'apc.enable_cli' ) ) {
+ return 'apcu';
+ }
} elseif ( function_exists( 'apc_fetch' ) ) {
- return 'apc';
+ // Make sure the APC methods actually store anything
+ if ( PHP_SAPI !== 'cli' || ini_get( 'apc.enable_cli' ) ) {
+ return 'apc';
+ }
} elseif ( function_exists( 'wincache_ucache_get' ) ) {
return 'wincache';
}
+
return CACHE_NONE;
}
}
if ( $outputPage->isPrintable() ) {
$parserOptions->setIsPrintable( true );
$poOptions['enableSectionEditLinks'] = false;
- } elseif ( $this->viewIsRenderAction
- || !$this->isCurrent() || !$this->getTitle()->quickUserCan( 'edit', $user )
+ } elseif ( $this->viewIsRenderAction || !$this->isCurrent() ||
+ !MediaWikiServices::getInstance()->getPermissionManager()
+ ->quickUserCan( 'edit', $user, $this->getTitle() )
) {
$poOptions['enableSectionEditLinks'] = false;
}
$title = $this->getTitle();
$rc = false;
- if ( !$title->quickUserCan( 'patrol', $user )
+ if ( !MediaWikiServices::getInstance()->getPermissionManager()
+ ->quickUserCan( 'patrol', $user, $title )
|| !( $wgUseRCPatrol || $wgUseNPPatrol
|| ( $wgUseFilePatrol && $title->inNamespace( NS_FILE ) ) )
) {
# Show error message
$oldid = $this->getOldID();
+ $pm = MediaWikiServices::getInstance()->getPermissionManager();
if ( !$oldid && $title->getNamespace() === NS_MEDIAWIKI && $title->hasSourceText() ) {
// use fake Content object for system message
$parserOptions = ParserOptions::newCanonical( 'canonical' );
} else {
if ( $oldid ) {
$text = wfMessage( 'missing-revision', $oldid )->plain();
- } elseif ( $title->quickUserCan( 'create', $this->getContext()->getUser() )
- && $title->quickUserCan( 'edit', $this->getContext()->getUser() )
+ } elseif ( $pm->quickUserCan( 'create', $this->getContext()->getUser(), $title ) &&
+ $pm->quickUserCan( 'edit', $this->getContext()->getUser(), $title )
) {
$message = $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon';
$text = wfMessage( $message )->plain();
public function imageHistoryLine( $iscur, $file ) {
$user = $this->getUser();
$lang = $this->getLanguage();
+ $pm = MediaWikiServices::getInstance()->getPermissionManager();
$timestamp = wfTimestamp( TS_MW, $file->getTimestamp() );
$img = $iscur ? $file->getName() : $file->getArchiveName();
$userId = $file->getUser( 'id' );
$row = $selected = '';
// Deletion link
- if ( $local && ( MediaWikiServices::getInstance()
- ->getPermissionManager()
- ->userHasAnyRight( $user, 'delete', 'deletedhistory' ) )
+ if ( $local && ( $pm->userHasAnyRight( $user, 'delete', 'deletedhistory' ) )
) {
$row .= '<td>';
# Link to remove from history
$row .= '<td>';
if ( $iscur ) {
$row .= $this->msg( 'filehist-current' )->escaped();
- } elseif ( $local && $this->title->quickUserCan( 'edit', $user )
- && $this->title->quickUserCan( 'upload', $user )
+ } elseif ( $local && $pm->quickUserCan( 'edit', $user, $this->title )
+ && $pm->quickUserCan( 'upload', $user, $this->title )
) {
if ( $file->isDeleted( File::DELETED_FILE ) ) {
$row .= $this->msg( 'filehist-revert' )->escaped();
return;
}
- $canUpload = $this->getTitle()->quickUserCan( 'upload', $this->getContext()->getUser() );
+ $canUpload = MediaWikiServices::getInstance()->getPermissionManager()
+ ->quickUserCan( 'upload', $this->getContext()->getUser(), $this->getTitle() );
if ( $canUpload && UploadBase::userCanReUpload(
$this->getContext()->getUser(),
$this->mPage->getFile() )
* @since 1.26
*/
public function logDataPageOutputOnly() {
+ if ( !$this->allowOutput ) {
+ return;
+ }
+
$outputs = [];
foreach ( $this->getOutputs() as $output ) {
if ( $output->logsToOutput() ) {
* @param bool $t
*/
public function setTemplated( $t ) {
- // wfDeprecated( __METHOD__, '1.34' );
+ wfDeprecated( __METHOD__, '1.34' );
$this->allowOutput = ( $t === true );
}
* @return bool
*/
public function getTemplated() {
- // wfDeprecated( __METHOD__, '1.34' );
+ wfDeprecated( __METHOD__, '1.34' );
return $this->getAllowOutput();
}
}
/**
- * Does log() just send the data to the request/script output?
+ * May the log() try to write to standard output?
* @return bool
* @since 1.33
*/
}
/**
- * Log MediaWiki-style profiling data
+ * Log MediaWiki-style profiling data.
+ *
+ * For classes that enable logsToOutput(), this must not
+ * be called unless Profiler::setAllowOutput is enabled.
*
* @param array $stats Result of Profiler::getFunctionStats()
*/
*/
class ProfilerOutputText extends ProfilerOutput {
/** @var float Min real time display threshold */
- protected $thresholdMs;
+ private $thresholdMs;
+
+ /** @var bool Whether to use visible text or a comment (for HTML responses) */
+ private $visible;
function __construct( Profiler $collector, array $params ) {
parent::__construct( $collector, $params );
$this->thresholdMs = $params['thresholdMs'] ?? 1.0;
+ $this->visible = $params['visible'] ?? false;
}
public function logsToOutput() {
}
public function log( array $stats ) {
- if ( $this->collector->getTemplated() ) {
- $out = '';
+ $out = '';
- // Filter out really tiny entries
- $min = $this->thresholdMs;
- $stats = array_filter( $stats, function ( $a ) use ( $min ) {
- return $a['real'] > $min;
- } );
- // Sort descending by time elapsed
- usort( $stats, function ( $a, $b ) {
- return $b['real'] <=> $a['real'];
- } );
+ // Filter out really tiny entries
+ $min = $this->thresholdMs;
+ $stats = array_filter( $stats, function ( $a ) use ( $min ) {
+ return $a['real'] > $min;
+ } );
+ // Sort descending by time elapsed
+ usort( $stats, function ( $a, $b ) {
+ return $b['real'] <=> $a['real'];
+ } );
- array_walk( $stats,
- function ( $item ) use ( &$out ) {
- $out .= sprintf( "%6.2f%% %3.3f %6d - %s\n",
- $item['%real'], $item['real'], $item['calls'], $item['name'] );
- }
- );
+ array_walk( $stats,
+ function ( $item ) use ( &$out ) {
+ $out .= sprintf( "%6.2f%% %3.3f %6d - %s\n",
+ $item['%real'], $item['real'], $item['calls'], $item['name'] );
+ }
+ );
- $contentType = $this->collector->getContentType();
- if ( wfIsCLI() ) {
+ $contentType = $this->collector->getContentType();
+ if ( wfIsCLI() ) {
+ print "<!--\n{$out}\n-->\n";
+ } elseif ( $contentType === 'text/html' ) {
+ if ( $this->visible ) {
+ print "<pre>{$out}</pre>";
+ } else {
print "<!--\n{$out}\n-->\n";
- } elseif ( $contentType === 'text/html' ) {
- $visible = $this->params['visible'] ?? false;
- if ( $visible ) {
- print "<pre>{$out}</pre>";
- } else {
- print "<!--\n{$out}\n-->\n";
- }
- } elseif ( $contentType === 'text/javascript' || $contentType === 'text/css' ) {
- print "\n/*\n{$out}*/\n";
}
+ } elseif ( $contentType === 'text/javascript' || $contentType === 'text/css' ) {
+ print "\n/*\n{$out}*/\n";
}
}
}
/**
* Fetch the skinname messages for available skins.
+ * @deprecated since 1.34, no longer used.
* @return string[]
*/
static function getSkinNameMessages() {
+ wfDeprecated( __METHOD__, '1.34' );
$messages = [];
foreach ( self::getSkinNames() as $skinKey => $skinName ) {
$messages[] = "skinname-$skinKey";
$type = 'ns-subject';
}
// T208315: add HTML class when the user can edit the page
- if ( $title->quickUserCan( 'edit', $user ) ) {
+ if ( MediaWikiServices::getInstance()->getPermissionManager()
+ ->quickUserCan( 'edit', $user, $title )
+ ) {
$type .= ' mw-editable';
}
}
$action = $this->getRequest()->getVal( 'action', 'view' );
$title = $this->getTitle();
$linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
+ $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
if ( ( !$title->exists() || $action == 'history' ) &&
- $title->quickUserCan( 'deletedhistory', $this->getUser() )
+ $permissionManager->quickUserCan( 'deletedhistory', $this->getUser(), $title )
) {
$n = $title->isDeleted();
if ( $n ) {
- if ( $this->getTitle()->quickUserCan( 'undelete', $this->getUser() ) ) {
+ if ( $permissionManager->quickUserCan( 'undelete',
+ $this->getUser(), $this->getTitle() )
+ ) {
$msg = 'thisisdeleted';
} else {
$msg = 'viewdeleted';
* @return array
*/
public function buildSidebar() {
+ $services = MediaWikiServices::getInstance();
$callback = function ( $old = null, &$ttl = null ) {
$bar = [];
$this->addToSidebar( $bar, 'sidebar' );
Hooks::run( 'SkinBuildSidebar', [ $this, &$bar ] );
- if ( MessageCache::singleton()->isDisabled() ) {
+ $msgCache = MediaWikiServices::getInstance()->getMessageCache();
+ if ( $msgCache->isDisabled() ) {
$ttl = WANObjectCache::TTL_UNCACHEABLE; // bug T133069
}
return $bar;
};
- $msgCache = MessageCache::singleton();
- $wanCache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+ $msgCache = $services->getMessageCache();
+ $wanCache = $services->getMainWANObjectCache();
$config = $this->getConfig();
$sidebar = $config->get( 'EnableSidebarCache' )
* Initialize various variables and generate the template
*/
function outputPage() {
- Profiler::instance()->setTemplated( true );
+ Profiler::instance()->setAllowOutput();
$out = $this->getOutput();
$this->initPage( $out );
$out = $this->getOutput();
$request = $this->getRequest();
$user = $this->getUser();
+ $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
$content_navigation = [
'namespaces' => [],
// parameters
$action = $request->getVal( 'action', 'view' );
- $userCanRead = $title->quickUserCan( 'read', $user );
+ $userCanRead = $permissionManager->quickUserCan( 'read', $user, $title );
// Avoid PHP 7.1 warning of passing $this by reference
$skinTemplate = $this;
}
// Checks if user can edit the current page if it exists or create it otherwise
- if ( $title->quickUserCan( 'edit', $user )
- && ( $title->exists() || $title->quickUserCan( 'create', $user ) )
+ if ( $permissionManager->quickUserCan( 'edit', $user, $title ) &&
+ ( $title->exists() ||
+ $permissionManager->quickUserCan( 'create', $user, $title ) )
) {
// Builds CSS class for talk page links
$isTalkClass = $isTalk ? ' istalk' : '';
'href' => $title->getLocalURL( 'action=history' ),
];
- if ( $title->quickUserCan( 'delete', $user ) ) {
+ if ( $permissionManager->quickUserCan( 'delete', $user, $title ) ) {
$content_navigation['actions']['delete'] = [
'class' => ( $onPage && $action == 'delete' ) ? 'selected' : false,
'text' => wfMessageFallback( "$skname-action-delete", 'delete' )
];
}
- if ( $title->quickUserCan( 'move', $user ) ) {
+ if ( $permissionManager->quickUserCan( 'move', $user, $title ) ) {
$moveTitle = SpecialPage::getTitleFor( 'Movepage', $title->getPrefixedDBkey() );
$content_navigation['actions']['move'] = [
'class' => $this->getTitle()->isSpecial( 'Movepage' ) ? 'selected' : false,
}
} else {
// article doesn't exist or is deleted
- if ( $title->quickUserCan( 'deletedhistory', $user ) ) {
+ if ( $permissionManager->quickUserCan( 'deletedhistory', $user, $title ) ) {
$n = $title->isDeleted();
if ( $n ) {
$undelTitle = SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedDBkey() );
// If the user can't undelete but can view deleted
// history show them a "View .. deleted" tab instead.
- $msgKey = $title->quickUserCan( 'undelete', $user ) ? 'undelete' : 'viewdeleted';
+ $msgKey = $permissionManager->quickUserCan( 'undelete',
+ $user, $title ) ? 'undelete' : 'viewdeleted';
$content_navigation['actions']['undelete'] = [
'class' => $this->getTitle()->isSpecial( 'Undelete' ) ? 'selected' : false,
'text' => wfMessageFallback( "$skname-action-$msgKey", "{$msgKey}_short" )
}
}
- if ( $title->quickUserCan( 'protect', $user ) && $title->getRestrictionTypes() &&
- MediaWikiServices::getInstance()->getPermissionManager()
- ->getNamespaceRestrictionLevels( $title->getNamespace(), $user ) !== [ '' ]
+ if ( $permissionManager->quickUserCan( 'protect', $user, $title ) &&
+ $title->getRestrictionTypes() &&
+ $permissionManager->getNamespaceRestrictionLevels( $title->getNamespace(),
+ $user ) !== [ '' ]
) {
$mode = $title->isProtected() ? 'unprotect' : 'protect';
$content_navigation['actions'][$mode] = [
}
// Checks if the user is logged in
- if ( $this->loggedin && MediaWikiServices::getInstance()
- ->getPermissionManager()
- ->userHasAllRights( $user, 'viewmywatchlist', 'editmywatchlist' )
+ if ( $this->loggedin && $permissionManager->userHasAllRights( $user,
+ 'viewmywatchlist', 'editmywatchlist' )
) {
/**
* The following actions use messages which, if made particular to
* @param object $result Result row
* @return string
*/
- private function makeWlhLink( $title, $result ) {
+ protected function makeWlhLink( $title, $result ) {
$wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() );
$label = $this->msg( 'nlinks' )->numParams( $result->value )->text();
return $this->getLinkRenderer()->makeLink( $wlh, $label );
}
$pager = new ContribsPager( $this->getContext(), [
'target' => $target,
- // Temporary, until newbie feature is fully removed from ContribsPager
- 'contribs' => 'user',
'namespace' => $this->opts['namespace'],
'tagfilter' => $this->opts['tagfilter'],
'start' => $this->opts['start'],
# If there were contributions, and it was a valid user or IP, show
# the appropriate "footer" message - WHOIS tools, etc.
- if ( $target != 'newbies' ) {
- $message = IP::isIPAddress( $target ) ?
- 'sp-contributions-footer-anon' :
- 'sp-contributions-footer';
-
- if ( !$this->msg( $message )->isDisabled() ) {
- $out->wrapWikiMsg(
- "<div class='mw-contributions-footer'>\n$1\n</div>",
- [ $message, $target ]
- );
- }
+ $message = IP::isIPAddress( $target ) ?
+ 'sp-contributions-footer-anon' :
+ 'sp-contributions-footer';
+
+ if ( !$this->msg( $message )->isDisabled() ) {
+ $out->wrapWikiMsg(
+ "<div class='mw-contributions-footer'>\n$1\n</div>",
+ [ $message, $target ]
+ );
}
}
}
if ( count( $err ) == 1 && isset( $err[0][0] ) && $err[0][0] == 'articleexists'
- && $newTitle->quickUserCan( 'delete', $user )
+ && MediaWikiServices::getInstance()->getPermissionManager()
+ ->quickUserCan( 'delete', $user, $newTitle )
) {
$out->wrapWikiMsg(
"<div class='warningbox'>\n$1\n</div>\n",
],
'user' => [
- 'type' => 'text',
+ 'class' => 'HTMLUserTextField',
'label-message' => 'newimages-user',
'name' => 'user',
],
$messageName = 'searchmenu-exists';
$linkClass = 'mw-search-exists';
} elseif ( ContentHandler::getForTitle( $title )->supportsDirectEditing()
- && $title->quickUserCan( 'create', $this->getUser() )
+ && MediaWikiServices::getInstance()->getPermissionManager()->quickUserCan( 'create',
+ $this->getUser(), $title )
) {
$messageName = 'searchmenu-new';
}
*/
private $target;
- /**
- * @var string Set to "newbie" to list contributions from the most recent 1% registered users.
- * $this->target is ignored then. Defaults to "users".
- */
- private $contribs;
-
/**
* @var string|int A single namespace number, or an empty string for all namespaces
*/
private $templateParser;
public function __construct( IContextSource $context, array $options ) {
- // Set ->target and ->contribs before calling parent::__construct() so
+ // Set ->target before calling parent::__construct() so
// parent can call $this->getIndexField() and get the right result. Set
// the rest too just to keep things simple.
$this->target = $options['target'] ?? '';
- $this->contribs = $options['contribs'] ?? 'users';
$this->namespace = $options['namespace'] ?? '';
$this->tagFilter = $options['tagfilter'] ?? false;
$this->nsInvert = $options['nsInvert'] ?? false;
* @return string
*/
private function getTargetTable() {
- if ( $this->contribs == 'newbie' ) {
- return 'revision';
- }
-
$user = User::newFromName( $this->target, false );
$ipRangeConds = $user->isAnon() ? $this->getIpRangeConds( $this->mDb, $this->target ) : null;
if ( $ipRangeConds ) {
];
// WARNING: Keep this in sync with getTargetTable()!
-
- if ( $this->contribs == 'newbie' ) {
- $max = $this->mDb->selectField( 'user', 'max(user_id)', '', __METHOD__ );
- $queryInfo['conds'][] = $revQuery['fields']['rev_user'] . ' >' . (int)( $max - $max / 100 );
- # ignore local groups with the bot right
- # @todo FIXME: Global groups may have 'bot' rights
- $groupsWithBotPermission = MediaWikiServices::getInstance()
- ->getPermissionManager()
- ->getGroupsWithPermission( 'bot' );
- if ( count( $groupsWithBotPermission ) ) {
- $queryInfo['tables'][] = 'user_groups';
- $queryInfo['conds'][] = 'ug_group IS NULL';
- $queryInfo['join_conds']['user_groups'] = [
- 'LEFT JOIN', [
- 'ug_user = ' . $revQuery['fields']['rev_user'],
- 'ug_group' => $groupsWithBotPermission,
- 'ug_expiry IS NULL OR ug_expiry >= ' .
- $this->mDb->addQuotes( $this->mDb->timestamp() )
- ]
- ];
- }
- // (T140537) Disallow looking too far in the past for 'newbies' queries. If the user requested
- // a timestamp offset far in the past such that there are no edits by users with user_ids in
- // the range, we would end up scanning all revisions from that offset until start of time.
- $queryInfo['conds'][] = 'rev_timestamp > ' .
- $this->mDb->addQuotes( $this->mDb->timestamp( wfTimestamp() - 30 * 24 * 60 * 60 ) );
+ $user = User::newFromName( $this->target, false );
+ $ipRangeConds = $user->isAnon() ? $this->getIpRangeConds( $this->mDb, $this->target ) : null;
+ if ( $ipRangeConds ) {
+ $queryInfo['tables'][] = 'ip_changes';
+ $queryInfo['join_conds']['ip_changes'] = [
+ 'LEFT JOIN', [ 'ipc_rev_id = rev_id' ]
+ ];
+ $queryInfo['conds'][] = $ipRangeConds;
} else {
- $user = User::newFromName( $this->target, false );
- $ipRangeConds = $user->isAnon() ? $this->getIpRangeConds( $this->mDb, $this->target ) : null;
- if ( $ipRangeConds ) {
- $queryInfo['tables'][] = 'ip_changes';
- $queryInfo['join_conds']['ip_changes'] = [
- 'LEFT JOIN', [ 'ipc_rev_id = rev_id' ]
- ];
- $queryInfo['conds'][] = $ipRangeConds;
+ // tables and joins are already handled by Revision::getQueryInfo()
+ $conds = ActorMigration::newMigration()->getWhere( $this->mDb, 'rev_user', $user );
+ $queryInfo['conds'][] = $conds['conds'];
+ // Force the appropriate index to avoid bad query plans (T189026)
+ if ( isset( $conds['orconds']['actor'] ) ) {
+ // @todo: This will need changing when revision_actor_temp goes away
+ $queryInfo['options']['USE INDEX']['temp_rev_user'] = 'actor_timestamp';
} else {
- // tables and joins are already handled by Revision::getQueryInfo()
- $conds = ActorMigration::newMigration()->getWhere( $this->mDb, 'rev_user', $user );
- $queryInfo['conds'][] = $conds['conds'];
- // Force the appropriate index to avoid bad query plans (T189026)
- if ( isset( $conds['orconds']['actor'] ) ) {
- // @todo: This will need changing when revision_actor_temp goes away
- $queryInfo['options']['USE INDEX']['temp_rev_user'] = 'actor_timestamp';
- } else {
- $queryInfo['options']['USE INDEX']['revision'] =
- isset( $conds['orconds']['userid'] ) ? 'user_timestamp' : 'usertext_timestamp';
- }
+ $queryInfo['options']['USE INDEX']['revision'] =
+ isset( $conds['orconds']['userid'] ) ? 'user_timestamp' : 'usertext_timestamp';
}
}
}
/**
- * @return string
+ * @deprecated since 1.34, redundant.
+ *
+ * @return string "users"
*/
public function getContribs() {
- return $this->contribs;
+ // Brought back for backwards compatibility, see T231540.
+ return 'users';
}
/**
}
if ( isset( $row->rev_id ) ) {
$this->mParentLens[$row->rev_id] = $row->rev_len;
- if ( $this->contribs === 'newbie' ) { // multiple users
- $batch->add( NS_USER, $row->user_name );
- $batch->add( NS_USER_TALK, $row->user_name );
- } elseif ( $isIpRange ) {
+ if ( $isIpRange ) {
// If this is an IP range, batch the IP's talk page
$batch->add( NS_USER_TALK, $row->rev_user_text );
}
$attribs = [];
$linkRenderer = $this->getLinkRenderer();
+ $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
$page = null;
// Create a title for the revision if possible
$topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
$classes[] = 'mw-contributions-current';
# Add rollback link
- if ( !$row->page_is_new && $page->quickUserCan( 'rollback', $user )
- && $page->quickUserCan( 'edit', $user )
+ if ( !$row->page_is_new &&
+ $permissionManager->quickUserCan( 'rollback', $user, $page ) &&
+ $permissionManager->quickUserCan( 'edit', $user, $page )
) {
$this->preventClickjacking();
$topmarktext .= ' ' . Linker::generateRollback( $rev, $this->getContext(),
$comment = $lang->getDirMark() . Linker::revComment( $rev, false, true, false );
$d = ChangesList::revDateLink( $rev, $user, $lang, $page );
- # Show user names for /newbies as there may be different users.
- # Note that only unprivileged users have rows with hidden user names excluded.
# When querying for an IP range, we want to always show user and user talk links.
$userlink = '';
- if ( ( $this->contribs == 'newbie' && !$rev->isDeleted( RevisionRecord::DELETED_USER ) )
- || $this->isQueryableRange( $this->target ) ) {
+ if ( $this->isQueryableRange( $this->target ) ) {
$userlink = ' <span class="mw-changeslist-separator"></span> '
. $lang->getDirMark()
. Linker::userLink( $rev->getUser(), $rev->getUserText() );
->getWhere( wfGetDB( DB_REPLICA ), 'img_user', User::newFromName( $user, false ) )['conds'];
}
- if ( $opts->getValue( 'newbies' ) ) {
- // newbie = most recent 1% of users
- $dbr = wfGetDB( DB_REPLICA );
- $max = $dbr->selectField( 'user', 'max(user_id)', '', __METHOD__ );
- $conds[] = $imgQuery['fields']['img_user'] . ' >' . (int)( $max - $max / 100 );
-
- // there's no point in looking for new user activity in a far past;
- // beyond a certain point, we'd just end up scanning the rest of the
- // table even though the users we're looking for didn't yet exist...
- // see T140537, (for ContribsPages, but similar to this)
- $conds[] = 'img_timestamp > ' .
- $dbr->addQuotes( $dbr->timestamp( wfTimestamp() - 30 * 24 * 60 * 60 ) );
- }
-
if ( !$opts->getValue( 'showbots' ) ) {
$groupsWithBotPermission = MediaWikiServices::getInstance()
->getPermissionManager()
$ext = FileBackend::extensionFromPath( $this->mVirtualTempPath );
// Get a 0-byte temp file to perform the concatenation at
$tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory()
- ->getTempFSFile( 'chunkedupload_', $ext );
+ ->newTempFSFile( 'chunkedupload_', $ext );
$tmpPath = false; // fail in concatenate()
if ( $tmpFile ) {
// keep alive with $this
/**
* Get a list of all available permissions.
*
- * @deprecated since 1.34, use MediaWikiServices::getInstance()->getPermissionManager()
- * ->getAllPermissions() instead
+ * @deprecated since 1.34, use PermissionManager::getAllPermissions() instead
*
* @return string[] Array of permission names
*/
"nocreate-loggedin": "You do not have permission to create new pages.",
"sectioneditnotsupported-title": "Section editing not supported",
"sectioneditnotsupported-text": "Section editing is not supported in this page.",
+ "modeleditnotsupported-title": "Editing not supported",
+ "modeleditnotsupported-text": "Editing is not supported for content model $1.",
"permissionserrors": "Permission error",
"permissionserrorstext": "You do not have permission to do that, for the following {{PLURAL:$1|reason|reasons}}:",
"permissionserrorstext-withaction": "You do not have permission to $2, for the following {{PLURAL:$1|reason|reasons}}:",
"content-model-json": "JSON",
"content-json-empty-object": "Empty object",
"content-json-empty-array": "Empty array",
+ "unsupported-content-model": "<strong>Warning:</strong> Content model $1 is not supported on this wiki.",
+ "unsupported-content-diff": "Diffs are not supported for content model $1.",
+ "unsupported-content-diff2": "Diffs between the content models $1 and $2 are not supported on this wiki.",
"deprecated-self-close-category": "Pages using invalid self-closed HTML tags",
"deprecated-self-close-category-desc": "The page contains invalid self-closed HTML tags, such as <code><b/></code> or <code><span/></code>. The behavior of these will change soon to be consistent with the HTML5 specification, so their use in wikitext is deprecated.",
"duplicate-args-warning": "<strong>Warning:</strong> [[:$1]] is calling [[:$2]] with more than one value for the \"$3\" parameter. Only the last value provided will be used.",
"wlheader-enotif": "Email notification is enabled.",
"wlheader-showupdated": "Pages that have been changed since you last visited them are shown in <strong>bold</strong>.",
"wlnote": "Below {{PLURAL:$1|is the last change|are the last <strong>$1</strong> changes}} in the last {{PLURAL:$2|hour|<strong>$2</strong> hours}}, as of $3, $4.",
- "wlshowlast": "Show last $1 hours $2 days",
"watchlist-hide": "Hide",
"watchlist-submit": "Show",
"wlshowtime": "Period of time to display:",
"img-lang-default": "(default language)",
"img-lang-info": "Render this image in $1. $2",
"img-lang-go": "Go",
- "ascending_abbrev": "asc",
- "descending_abbrev": "desc",
"table_pager_next": "Next page",
"table_pager_prev": "Previous page",
"table_pager_first": "First page",
"exif-scenetype-1": "See also:\n* {{msg-mw|Exif-scenetype}}\n* {{msg-mw|Exif-scenetype-1}}",
"exif-customrendered-0": "{{exif-qqq}}\n\nSee also:\n* {{msg-mw|Exif-customrendered}}\n* {{msg-mw|Exif-customrendered-0}}\n* {{msg-mw|Exif-customrendered-1}}",
"exif-customrendered-1": "{{exif-qqq}}\n\nSee also:\n* {{msg-mw|Exif-customrendered}}\n* {{msg-mw|Exif-customrendered-0}}\n* {{msg-mw|Exif-customrendered-1}}",
+ "exif-customrendered-2": "{{exif-qqq}}\n\nSee also:\n* {{msg-mw|Exif-customrendered}}",
+ "exif-customrendered-3": "{{exif-qqq}}\n\nSee also:\n* {{msg-mw|Exif-customrendered}}",
+ "exif-customrendered-4": "{{exif-qqq}}\n\nSee also:\n* {{msg-mw|Exif-customrendered}}",
+ "exif-customrendered-6": "{{exif-qqq}}\n\nSee also:\n* {{msg-mw|Exif-customrendered}}",
+ "exif-customrendered-7": "{{exif-qqq}}\n\nSee also:\n* {{msg-mw|Exif-customrendered}}",
+ "exif-customrendered-8": "{{exif-qqq}}\n\nSee also:\n* {{msg-mw|Exif-customrendered}}",
"exif-exposuremode-0": "{{exif-qqq}}\n{{Related|Exif-exposuremode}}",
"exif-exposuremode-1": "{{exif-qqq}}\n{{Related|Exif-exposuremode}}",
"exif-exposuremode-2": "{{exif-qqq}}\n\nA type of exposure mode shown as part of the metadata on image description pages. The Wikipedia article on [[w:Bracketing#Exposure_bracketing|bracketing]] says that 'auto bracket' is a camera exposure setting which automatically takes a series of pictures at slightly different light exposures.\n\n{{Related|Exif-exposuremode}}",
"nocreate-loggedin": "Used as error message.\n\nSee also:\n* {{msg-mw|Nocreatetext}}",
"sectioneditnotsupported-title": "Page title of special page, which presumably appears when someone tries to edit a section, and section editing is disabled. Explanation of section editing on [[meta:Help:Section_editing#Section_editing|meta]].",
"sectioneditnotsupported-text": "I think this is the text of an error message, which presumably appears when someone tries to edit a section, and section editing is disabled. Explanation of section editing on [[meta:Help:Section_editing#Section_editing|meta]].",
+ "modeleditnotsupported-title": "Page title used on the edit page when editing is not supported for the page's content model.",
+ "modeleditnotsupported-text": "Error message show on the edit page when editing is not supported for the page's content model..\n\nParameters:\n* $1 - the name of the content model.",
"permissionserrors": "Used as title of error message.\n\nSee also:\n* {{msg-mw|loginreqtitle}}\n{{Identical|Permission error}}",
"permissionserrorstext": "This message is \"without action\" version of {{msg-mw|Permissionserrorstext-withaction}}.\n\nParameters:\n* $1 - the number of reasons that were found why ''the action'' cannot be performed",
"permissionserrorstext-withaction": "This message is \"with action\" version of {{msg-mw|Permissionserrorstext}}.\n\nParameters:\n* $1 - the number of reasons that were found why the action cannot be performed\n* $2 - one of the action-* messages (for example {{msg-mw|action-edit}}) or other such messages tagged with {{tl|doc-action}} in their documentation\n\nPlease report at [[Support]] if you are unable to properly translate this message. Also see [[phab:T16246]] (now closed) for background.",
"content-model-json": "{{optional}}\nName for the JSON content model, used when decribing what type of content a page contains.\n\nThis message is substituted in:\n*{{msg-mw|Bad-target-model}}\n*{{msg-mw|Content-not-allowed-here}}\n{{identical|JSON}}",
"content-json-empty-object": "Used to represent an object with no properties on a JSON content model page.",
"content-json-empty-array": "Used to represent an array with no values on a JSON content model page.",
+ "unsupported-content-model": "Warning shown when trying to display content with an unknown model.\n\nParameters:\n* $1 - the technical name of the content model.",
+ "unsupported-content-diff": "Warning shown when trying to display a diff between content with a model that does not support diffing (perhaps because it's an unknown model).\n\nParameters:\n* $1 - the technical name of the model of the content",
+ "unsupported-content-diff2": "Warning shown when trying to display a diff between content that uses models that do not support diffing with each other.\n\nParameters:\n* $1 - the technical name of the model of the old content\n* $2 - the technical name of the model of the new content.",
"deprecated-self-close-category": "This message is used as a category name for a [[mw:Special:MyLanguage/Help:Tracking categories|tracking category]] where pages are placed automatically if they contain invalid self-closed HTML tags, such as <code><b/></code> or <code><span/></code>. The behavior of these will change soon to be consistent with the HTML5 specification, so their use in wikitext is deprecated.",
"deprecated-self-close-category-desc": "Invalid self-closed HTML tag category description. Shown on [[Special:TrackingCategories]].\n\nSee also:\n* {{msg-mw|deprecated-self-close-category}}",
"duplicate-args-warning": "If a page calls a template and specifies the same argument more than once, such as <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> or <code><nowiki>{{foo|bar|1=baz}}</nowiki></code>, this warning is displayed when previewing.\n\nParameters:\n* $1 - The calling page\n* $2 - The called template\n* $3 - The name of the duplicated argument",
"wlheader-enotif": "Message at the top of [[Special:Watchlist]], after {{msg-mw|watchlist-details}}. Has to be a full sentence.\n\nSee also:\n* {{msg-mw|Watchlist-options|fieldset}}\n* {{msg-mw|enotif reset|Submit button text}}",
"wlheader-showupdated": "Message at the top of [[Special:Watchlist]], after {{msg-mw|watchlist-details}}. Has to be a full sentence.",
"wlnote": "Used on [[Special:Watchlist]] when a maximum number of hours or days is specified.\n\nParameters:\n* $1 - the number of changes shown\n* $2 - the number of hours for which the changes are shown\n* $3 - a date alone\n* $4 - a time alone",
- "wlshowlast": "Appears on [[Special:Watchlist]]. Parameters:\n* $1 - a choice of different numbers of hours (\"1 | 2 | 6 | 12\")\n* $2 - a choice of different numbers of days (\"1 | 3 | 7\" and the maximum number of days available)\nClicking on your choice changes the list of changes you see (without changing the default in my preferences).",
"watchlist-hide": "Appears on [[Special:Watchlist]]. It is the first word on a new line with checkboxes to hide/unhide options\n{{Identical|Hide}}",
"watchlist-submit": "Label on the submit button in [[Special:Watchlist]]\n{{Identical|Show}}",
"wlshowtime": "Appears on [[Special:Watchlist]]. Label of a drop-down list used to specify the period of time to display in the watchlist. This period can be {{msg-mw|days}} or {{msg-mw|hours}}.",
"img-lang-default": "An option in the drop down of a translatable file. For example see [[:File:Gerrit patchset 25838 test.svg]].\n\nUsed when it cannot be determined what the default fallback language is.\n\nHowever it should be noted that most of the time, the content displayed for this option would be in English.\n{{Identical|Default language}}",
"img-lang-info": "Label for drop down box. Appears underneath the image on the image description page. See [[:File:Gerrit patchset 25838 test.svg]] for an example.\n\nParameters:\n* $1 - a drop down box with language options, uses the following messages:\n** {{msg-mw|Img-lang-default}}\n** {{msg-mw|Img-lang-opt}}. e.g. \"English (en)\", \"日本語 (ja)\"\n* $2 - a submit button, which uses the text from {{msg-mw|Img-lang-go}}",
"img-lang-go": "Go button for the language select for translatable files. See [[:File:Gerrit patchset 25838 test.svg]] for an example.\n\nSee also:\n* {{msg-mw|img-lang-info}}\n{{Identical|Go}}",
- "ascending_abbrev": "Abbreviation of ascending order.\nSee also:\n* {{msg-mw|Ascending abbrev}}\n* {{msg-mw|Descending abbrev}}",
- "descending_abbrev": "Abbreviation of descending order.\nSee also:\n* {{msg-mw|Ascending abbrev}}\n* {{msg-mw|Descending abbrev}}",
"table_pager_next": "Used as image button text of pager. See [[Support|example]] (the bottom of the page).\n{{Identical|Next page}}",
"table_pager_prev": "Used as image button text of pager. See [[Support|example]] (the bottom of the page).\n{{Identical|Previous page}}",
"table_pager_first": "Used as image button text of pager. See [[Support|example]] (the bottom of the page).\n{{Identical|First page}}",
// Respond to ResourceLoader request
$resourceLoader->respond( $context );
-Profiler::instance()->setTemplated( true );
+Profiler::instance()->setAllowOutput();
$mediawiki = new MediaWiki();
$mediawiki->doPostOutputShutdown( 'fast' );
+ $wgProfiler
+ [ 'threshold' => $wgProfileLimit ]
);
- $profiler->setTemplated( true );
+ $profiler->setAllowOutput();
Profiler::replaceStubInstance( $profiler );
}
DELETE {
?category ?x ?y
} WHERE {
+ ?category ?x ?y
VALUES ?category {
%s
}
} INSERT {
%s
} WHERE {
+ ?category ?x ?y
VALUES ?category {
%s
}
}
},
"eslint-utils": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz",
- "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==",
- "dev": true
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz",
+ "integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==",
+ "dev": true,
+ "requires": {
+ "eslint-visitor-keys": "^1.0.0"
+ }
},
"eslint-visitor-keys": {
"version": "1.0.0",
'mediawiki.pager.tablePager' => [
'styles' => 'resources/src/mediawiki.pager.tablePager/TablePager.less',
],
+ 'mediawiki.pulsatingdot' => [
+ 'styles' => [
+ 'resources/src/mediawiki.pulsatingdot/mediawiki.pulsatingdot.less',
+ ],
+ 'targets' => [ 'desktop', 'mobile' ],
+ ],
'mediawiki.searchSuggest' => [
'targets' => [ 'desktop', 'mobile' ],
'scripts' => 'resources/src/mediawiki.searchSuggest/searchSuggest.js',
'oojs-ui-core',
],
'messages' => [
- // Keep the uses message keys in sync with EditPage#setHeaders
+ // Keep these message keys in sync with EditPage#setHeaders
'creating',
'editconflict',
'editing',
);
},
+ // match prefix plus any combining characters to prevent ugly rendering (see T35242)
+ prefixPlusComboHighlight: function ( node, prefix ) {
+
+ // Equivalent to \p{Mark} (which is not currently available in JavaScript)
+ var comboMarks = '[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]';
+
+ $.highlightText.innerHighlight(
+ node,
+ new RegExp( '(^)' + mw.RegExp.escape( prefix ) + comboMarks + '*', 'i' )
+ );
+ },
+
// scans a node looking for the pattern and wraps a span around each match
innerHighlight: function ( node, pat ) {
var i, match, pos, spannode, middlebit, middleclone;
* @param {string} [options.method='splitAndHighlight'] Method of matching to use, one of:
* - 'splitAndHighlight': Split `matchString` on spaces, then match each word separately.
* - 'prefixHighlight': Match `matchString` at the beginning of text only.
+ * - 'prefixPlusComboHighlight': Match `matchString` plus any combining characters at
+ * the beginning of text only.
* @return {jQuery}
* @chainable
*/
}
if ( context.config.highlightInput ) {
- $result.highlightText( context.data.prevText, { method: 'prefixHighlight' } );
+ $result.highlightText( context.data.prevText, { method: 'prefixPlusComboHighlight' } );
}
// Widen results box if needed (new width is only calculated here, applied later).
--- /dev/null
+.mw-pulsating-dot {
+ &:before,
+ &:after {
+ content: '';
+ display: block;
+ position: absolute;
+ border-radius: 50%;
+ background-color: #36c;
+ }
+
+ &:before {
+ width: 36px;
+ height: 36px;
+ top: -18px;
+ left: -18px;
+ opacity: 0;
+ -webkit-animation: mw-pulsating-dot-pulse 3s ease-out;
+ -moz-animation: mw-pulsating-dot-pulse 3s ease-out;
+ animation: mw-pulsating-dot-pulse 3s ease-out;
+ -webkit-animation-iteration-count: infinite;
+ -moz-animation-iteration-count: infinite;
+ animation-iteration-count: infinite;
+ }
+
+ &:after {
+ width: 12px;
+ height: 12px;
+ top: -6px;
+ left: -6px;
+ }
+}
+
+.mw-pulsating-dot-pulse-frames() {
+ 0% {
+ transform: scale( 0 );
+ opacity: 0;
+ }
+
+ 25% {
+ transform: scale( 0 );
+ opacity: 0.1;
+ }
+
+ 50% {
+ transform: scale( 0.1 );
+ opacity: 0.3;
+ }
+
+ 75% {
+ transform: scale( 0.5 );
+ opacity: 0.5;
+ }
+
+ 100% {
+ transform: scale( 1 );
+ opacity: 0;
+ }
+}
+
+@-webkit-keyframes mw-pulsating-dot-pulse {
+ .mw-pulsating-dot-pulse-frames;
+}
+
+@-moz-keyframes mw-pulsating-dot-pulse {
+ .mw-pulsating-dot-pulse-frames;
+}
+
+@keyframes mw-pulsating-dot-pulse {
+ .mw-pulsating-dot-pulse-frames;
+}
// We have no way to display a translated placeholder for custom formats
placeholderDateFormat = '';
} else {
- // Messages: mw-widgets-dateinput-placeholder-day, mw-widgets-dateinput-placeholder-month
+ // The following messages are used here:
+ // * mw-widgets-dateinput-placeholder-day
+ // * mw-widgets-dateinput-placeholder-month
placeholderDateFormat = mw.msg( 'mw-widgets-dateinput-placeholder-' + config.precision );
}
// Whether the store is in use on this page.
enabled: null,
- // Modules whose string representation exceeds 100 kB are
- // ineligible for storage. See bug T66721.
- MODULE_SIZE_MAX: 100 * 1000,
+ // Modules whose serialised form exceeds 100 kB won't be stored (T66721).
+ MODULE_SIZE_MAX: 1e5,
// The contents of the store, mapping '[name]@[version]' keys
// to module implementations.
* @return {Object} Module store contents.
*/
toJSON: function () {
- return { items: mw.loader.store.items, vary: mw.loader.store.vary };
+ return {
+ items: mw.loader.store.items,
+ vary: mw.loader.store.vary,
+ // Store with 1e7 ms accuracy (1e4 seconds, or ~ 2.7 hours),
+ // which is enough for the purpose of expiring after ~ 30 days.
+ asOf: Math.ceil( Date.now() / 1e7 )
+ };
},
/**
this.enabled = true;
// If null, JSON.parse() will cast to string and re-parse, still null.
data = JSON.parse( raw );
- if ( data && typeof data.items === 'object' && data.vary === this.vary ) {
+ if ( data &&
+ typeof data.items === 'object' &&
+ data.vary === this.vary &&
+ // Only use if it's been less than 30 days since the data was written
+ // 30 days = 2,592,000 s = 2,592,000,000 ms = ± 259e7 ms
+ Date.now() < ( data.asOf * 1e7 ) + 259e7
+ ) {
+ // The data is not corrupt, matches our vary context, and has not expired.
this.items = data.items;
return;
}
//
// Please extend the regex instead of adding new ones!
// And add a test case to startup.test.js
- !ua.match( /MSIE 10|webOS\/1\.[0-4]|SymbianOS|NetFront|Opera Mini|S40OviBrowser|MeeGo|Android.+Glass|^Mozilla\/5\.0 .+ Gecko\/$|googleweblight|PLAYSTATION|PlayStation/ )
+ !ua.match( /MSIE 10|NetFront|Opera Mini|S40OviBrowser|MeeGo|Android.+Glass|^Mozilla\/5\.0 .+ Gecko\/$|googleweblight|PLAYSTATION|PlayStation/ )
);
}
# tests/phpunit/unit/includes
'BadFileLookupTest' => "$testDir/phpunit/unit/includes/BadFileLookupTest.php",
+ # tests/phpunit/unit/includes/filebackend
+ 'FileBackendGroupTestTrait' => "$testDir/phpunit/unit/includes/filebackend/FileBackendGroupTestTrait.php",
+
+ # tests/phpunit/unit/includes/language
+ 'LanguageFallbackTestTrait' => "$testDir/phpunit/unit/includes/language/LanguageFallbackTestTrait.php",
+
# tests/phpunit/unit/includes/libs/filebackend/fsfile
'TempFSFileTestTrait' => "$testDir/phpunit/unit/includes/libs/filebackend/fsfile/TempFSFileTestTrait.php",
<p><a rel="nofollow" class="external text" href="http://example.com">link</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external text" href="http://example.com" about="#mwt31" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[{{echo|http://example.com}} link]"}},"i":0}}]}'>link</a></p>
+<p><a rel="mw:ExtLink" href="http://example.com" about="#mwt31" typeof="mw:Transclusion" class="external text" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[{{echo|http://example.com}} link]"}},"i":0}}]}'>link</a></p>
!! end
!! test
<p><a rel="nofollow" class="external free" href="http://foo.com/a%7Cb">http://foo.com/a%7Cb</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="http://foo.com/a%7Cb" about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"http://foo.com/a&#124;b"}},"i":0}}]}'>http://foo.com/a%7Cb</a></p>
+<p><a rel="mw:ExtLink" href="http://foo.com/a%7Cb" about="#mwt1" typeof="mw:Transclusion" class="external free" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"http://foo.com/a&#124;b"}},"i":0}}]}'>http://foo.com/a%7Cb</a></p>
!! end
!! test
<p><a rel="nofollow" class="external text" href="http://example.org/index.php?title=Parser_test&action=edit">bar</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external text" href="http://example.org/index.php?title=Parser_test&action=edit" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[{{fullurl:{{FULLPAGENAME}}|action=edit}} bar]"}},"i":0}}]}'>bar</a></p>
+<p><a rel="mw:ExtLink" href="http://example.org/index.php?title=Parser_test&action=edit" typeof="mw:Transclusion" class="external text" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[{{fullurl:{{FULLPAGENAME}}|action=edit}} bar]"}},"i":0}}]}'>bar</a></p>
!! end
!! test
<dl><dt><a rel="nofollow" class="external free" href="news:alt.wikipedia.rox">news:alt.wikipedia.rox</a></dt>
<dd>This isn't even a real newsgroup!</dd></dl>
!! html/parsoid
-<dl><dt><a rel="mw:ExtLink" class="external free" href="news:alt.wikipedia.rox" data-parsoid='{"stx":"url"}'>news:alt.wikipedia.rox</a></dt><dd data-parsoid='{"stx":"row"}'>This isn't even a real newsgroup!</dd></dl>
+<dl><dt><a rel="mw:ExtLink" href="news:alt.wikipedia.rox" class="external free" data-parsoid='{"stx":"url"}'>news:alt.wikipedia.rox</a></dt><dd data-parsoid='{"stx":"row"}'>This isn't even a real newsgroup!</dd></dl>
!! end
!! test
!! end
# FIXME: Maybe get rid of this test?
-# From whitelist:
+# From old whitelist description:
# * The test is wrong, there are two colons where there should be :;
# * The PHP parser is wrong to close the <dl> after the <dt> containing the <ul>.
!! test
Numbered: <a rel="nofollow" class="external autonumber" href="http://example.com">[3]</a>
</p>
!! html/parsoid
-<p>Numbered: <a rel="mw:ExtLink" class="external autonumber" href="http://example.com"></a>
-Numbered: <a rel="mw:ExtLink" class="external autonumber" href="http://example.net"></a>
-Numbered: <a rel="mw:ExtLink" class="external autonumber" href="http://example.com"></a></p>
+<p>Numbered: <a rel="mw:ExtLink" href="http://example.com" class="external autonumber"></a>
+Numbered: <a rel="mw:ExtLink" href="http://example.net" class="external autonumber"></a>
+Numbered: <a rel="mw:ExtLink" href="http://example.com" class="external autonumber"></a></p>
!!end
!! test
<p><a rel="nofollow" class="external autonumber" href="http://example.com/1$2345">[1]</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external autonumber" href="http://example.com/1$2345"></a></p>
+<p><a rel="mw:ExtLink" href="http://example.com/1$2345" class="external autonumber"></a></p>
!!end
!! test
<p><a rel="nofollow" class="external free" href="http://example.com/1">http://example.com/1</a>[2345
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="http://example.com/1">http://example.com/1</a>[2345</p>
+<p><a rel="mw:ExtLink" href="http://example.com/1" class="external free">http://example.com/1</a>[2345</p>
!! end
!! test
<p><a rel="nofollow" class="external text" href="http://example.com/1">[2345</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external text" href="http://example.com/1">[2345</a></p>
+<p><a rel="mw:ExtLink" href="http://example.com/1" class="external text">[2345</a></p>
!!end
# parsoid adds a space before the link name
<p><a rel="nofollow" class="external autonumber" href="//example.com">[1]</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external autonumber" href="//example.com"></a></p>
+<p><a rel="mw:ExtLink" href="//example.com" class="external autonumber"></a></p>
!! end
!! test
</p><p><a href="http://en.wikipedia.org/wiki/Foo" class="extiw" title="wikipedia:Foo"><span>Bar</span></a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external autonumber" href="http://en.wikipedia.org/wiki/Foo"></a></p>
+<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/Foo" class="external autonumber"></a></p>
<p><a rel="mw:WikiLink/Interwiki" href="http://en.wikipedia.org/wiki/Foo" title="wikipedia:Foo">Bar</a></p>
<p><a rel="mw:WikiLink/Interwiki" href="http://en.wikipedia.org/wiki/Foo" title="wikipedia:Foo"><span>Bar</span></a></p>
!! end
<a rel="nofollow" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>,
-<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>;
-<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>\
-<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>.
-<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>:
-<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>!
-<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>?
-<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>)
-<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_(brackets)">http://example.com/url_with_(brackets)</a>
-(<a rel="mw:ExtLink" class="external free" href="http://example.com/url_without_brackets">http://example.com/url_without_brackets</a>)
-<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity&">http://example.com/url_with_entity&</a>
-<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity&">http://example.com/url_with_entity&</a>
-<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity&">http://example.com/url_with_entity&</a>
-<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&nbsp;","srcContent":"Â "}'>Â </span>
-<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&#xA0;","srcContent":"Â "}'>Â </span>
-<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&#160;","srcContent":"Â "}'>Â </span>
-<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&lt;","srcContent":"<"}'><</span>
-<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&#x3C;","srcContent":"<"}'><</span>
-<a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&#60;","srcContent":"<"}'><</span></p>
+<p><a rel="mw:ExtLink" href="http://example.com" class="external free">http://example.com</a>,
+<a rel="mw:ExtLink" href="http://example.com" class="external free">http://example.com</a>;
+<a rel="mw:ExtLink" href="http://example.com" class="external free">http://example.com</a>\
+<a rel="mw:ExtLink" href="http://example.com" class="external free">http://example.com</a>.
+<a rel="mw:ExtLink" href="http://example.com" class="external free">http://example.com</a>:
+<a rel="mw:ExtLink" href="http://example.com" class="external free">http://example.com</a>!
+<a rel="mw:ExtLink" href="http://example.com" class="external free">http://example.com</a>?
+<a rel="mw:ExtLink" href="http://example.com" class="external free">http://example.com</a>)
+<a rel="mw:ExtLink" href="http://example.com/url_with_(brackets)" class="external free">http://example.com/url_with_(brackets)</a>
+(<a rel="mw:ExtLink" href="http://example.com/url_without_brackets" class="external free">http://example.com/url_without_brackets</a>)
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity&" class="external free">http://example.com/url_with_entity&</a>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity&" class="external free">http://example.com/url_with_entity&</a>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity&" class="external free">http://example.com/url_with_entity&</a>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity" class="external free">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&nbsp;","srcContent":"Â "}'>Â </span>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity" class="external free">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&#xA0;","srcContent":"Â "}'>Â </span>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity" class="external free">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&#160;","srcContent":"Â "}'>Â </span>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity" class="external free">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&lt;","srcContent":"<"}'><</span>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity" class="external free">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&#x3C;","srcContent":"<"}'><</span>
+<a rel="mw:ExtLink" href="http://example.com/url_with_entity" class="external free">http://example.com/url_with_entity</a><span typeof="mw:Entity" data-parsoid='{"src":"&#60;","srcContent":"<"}'><</span></p>
!! end
!! test
<p><a rel="nofollow" class="external free" href="http://example.com/url_with_entity&amp">http://example.com/url_with_entity&amp</a>;
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="http://example.com/url_with_entity&amp">http://example.com/url_with_entity&amp</a>;</p>
+<p><a rel="mw:ExtLink" href="http://example.com/url_with_entity&amp" class="external free">http://example.com/url_with_entity&amp</a>;</p>
!! end
!! test
</p>
!! html/parsoid
<p><b>News:</b> Stuff here</p>
-<p><a rel="mw:ExtLink" class="external free" href="news:'a'b">news:'a'b</a><i>c</i>d e</p>
+<p><a rel="mw:ExtLink" href="news:'a'b" class="external free">news:'a'b</a><i>c</i>d e</p>
!! end
!! test
<p><a rel="nofollow" class="external text" href="http://+www.librarieswithoutborders.org">Libraries without borders</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external text" href="http://+www.librarieswithoutborders.org" data-parsoid='{"a":{"href":"http://+www.librarieswithoutborders.org"},"sa":{"href":"http://&#x20;www.librarieswithoutborders.org"}}'>Libraries without borders</a></p>
+<p><a rel="mw:ExtLink" href="http://+www.librarieswithoutborders.org" class="external text" data-parsoid='{"a":{"href":"http://+www.librarieswithoutborders.org"},"sa":{"href":"http://&#x20;www.librarieswithoutborders.org"}}'>Libraries without borders</a></p>
!! end
!! test
<p>URL in text: <a rel="nofollow" class="external text" href="http://example.com">http://example.com</a>
</p>
!! html/parsoid
-<p>URL in text: <a rel="mw:ExtLink" class="external text" href="http://example.com">http://example.com</a></p>
+<p>URL in text: <a rel="mw:ExtLink" href="http://example.com" class="external text">http://example.com</a></p>
!! end
!! test
<p>ja-style clickable images: <a rel="nofollow" class="external text" href="http://example.com"><img src="http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png" alt="Ncwikicol.png"/></a>
</p>
!! html/parsoid
-<p>ja-style clickable images: <a rel="mw:ExtLink" class="external text" href="http://example.com"><img src="http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png" alt="Ncwikicol.png" data-parsoid='{"type":"extlink"}'/></a></p>
+<p>ja-style clickable images: <a rel="mw:ExtLink" href="http://example.com" class="external text"><img src="http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png" alt="Ncwikicol.png" data-parsoid='{"type":"extlink"}'/></a></p>
!! end
!! test
<p>Old & use: <a rel="nofollow" class="external free" href="http://x&y">http://x&y</a>
</p>
!! html/parsoid
-<p>Old <span typeof="mw:Entity">&</span> use: <a rel="mw:ExtLink" class="external free" href="http://x&y">http://x&y</a></p>
+<p>Old <span typeof="mw:Entity">&</span> use: <a rel="mw:ExtLink" href="http://x&y" class="external free">http://x&y</a></p>
!! end
!! test
<p><a rel="nofollow" class="external free" href="http://example.com/?foo=bar">http://example.com/?foo=bar</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="http://example.com/?foo=bar">http://example.com/?foo=bar</a></p>
+<p><a rel="mw:ExtLink" href="http://example.com/?foo=bar" class="external free">http://example.com/?foo=bar</a></p>
!! end
##
<p>Old & use: <a rel="nofollow" class="external autonumber" href="http://x&y">[1]</a>
</p>
!! html/parsoid
-<p>Old <span typeof="mw:Entity">&</span> use: <a rel="mw:ExtLink" class="external autonumber" href="http://x&y"></a></p>
+<p>Old <span typeof="mw:Entity">&</span> use: <a rel="mw:ExtLink" href="http://x&y" class="external autonumber"></a></p>
!! end
# note that parsoid html is identical to [raw ampersand] case; so html2wt
<p>Old & use: <a rel="nofollow" class="external autonumber" href="http://x&y">[1]</a>
</p>
!! html/parsoid
-<p>Old <span typeof="mw:Entity">&</span> use: <a rel="mw:ExtLink" class="external autonumber" href="http://x&y"></a></p>
+<p>Old <span typeof="mw:Entity">&</span> use: <a rel="mw:ExtLink" href="http://x&y" class="external autonumber"></a></p>
!! end
!! test
<p><a rel="nofollow" class="external autonumber" href="http://example.com/?foo=bar">[1]</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external autonumber" href="http://example.com/?foo=bar"></a></p>
+<p><a rel="mw:ExtLink" href="http://example.com/?foo=bar" class="external autonumber"></a></p>
!! end
# note that parsoid html is identical to [raw equals] case; so html2wt
<p><a rel="nofollow" class="external autonumber" href="http://example.com/?foo=bar">[1]</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external autonumber" href="http://example.com/?foo=bar"></a></p>
+<p><a rel="mw:ExtLink" href="http://example.com/?foo=bar" class="external autonumber"></a></p>
!! end
# xxx parsoid strips the IDN character, so the round-trip tests will
<p><a rel="nofollow" class="external autonumber" href="http://example.com/">[1]</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external autonumber" href="http://example.com/"></a></p>
+<p><a rel="mw:ExtLink" href="http://example.com/" class="external autonumber"></a></p>
!! end
# FIXME: This test (the IDN characters in the text of a link) is an inconsistency.
<p><a rel="nofollow" class="external free" href="http://example.com/">http://example.com/</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="http://example.com/">http://example.com/</a></p>
+<p><a rel="mw:ExtLink" href="http://example.com/" class="external free">http://example.com/</a></p>
!! end
!! test
<p><a rel="nofollow" class="external autonumber" href="http://www.unausa.org/newindex.asp?place=http://www.unausa.org/programs/mun.asp">[1]</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external autonumber" href="http://www.unausa.org/newindex.asp?place=http://www.unausa.org/programs/mun.asp"></a></p>
+<p><a rel="mw:ExtLink" href="http://www.unausa.org/newindex.asp?place=http://www.unausa.org/programs/mun.asp" class="external autonumber"></a></p>
!! end
!! test
<p><a rel="nofollow" class="external free" href="http://www.example.com/">http://www.example.com/</a><b>html</b>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="http://www.example.com/" data-parsoid='{"stx":"url"}'>http://www.example.com/</a><b data-parsoid='{"stx":"html"}'>html</b></p>
+<p><a rel="mw:ExtLink" href="http://www.example.com/" class="external free" data-parsoid='{"stx":"url"}'>http://www.example.com/</a><b data-parsoid='{"stx":"html"}'>html</b></p>
!! end
!! test
</p><p><a rel="nofollow" class="external text" href="http://example.com">test </a><a href="/index.php?title=Wikilink&action=edit&redlink=1" class="new" title="Wikilink (page does not exist)">wikilink</a><a rel="nofollow" class="external text" href="http://example.com"> embedded in ext link</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external autonumber" href="http://example.com"></a><a rel="mw:WikiLink" href="./Wikilink" title="Wikilink">wikilink</a><span> embedded in ext link</span></p>
-<p><a rel="mw:ExtLink" class="external text" href="http://example.com">test </a><a rel="mw:WikiLink" href="./Wikilink" title="Wikilink">wikilink</a><span> embedded in ext link</span></p>
+<p><a rel="mw:ExtLink" href="http://example.com" class="external autonumber"></a><a rel="mw:WikiLink" href="./Wikilink" title="Wikilink">wikilink</a><span> embedded in ext link</span></p>
+<p><a rel="mw:ExtLink" href="http://example.com" class="external text">test </a><a rel="mw:WikiLink" href="./Wikilink" title="Wikilink">wikilink</a><span> embedded in ext link</span></p>
!! end
!! test
</p><p>{{echo|[[Foo}}
</p>
!! html/parsoid
-<p>[<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a> x</p>
-<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[http://example.com x"}},"i":0}}]}'>[<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a> x</p>
+<p>[<a rel="mw:ExtLink" href="http://example.com" class="external free">http://example.com</a> x</p>
+<p typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[http://example.com x"}},"i":0}}]}'>[<a rel="mw:ExtLink" href="http://example.com" class="external free">http://example.com</a> x</p>
<p>[[Foo</p>
<p>{{echo|[[Foo}}</p>
!! end
<p>[[Foo|<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"]]"}},"i":0}}]}'>]]</span></p>
!! end
+!! article
+Template:pipe page
+!! text
+Main|Page
+!! endarticle
+
+## FIXME: Parsoid doesn't support this and may never. See T226523
+!! test
+Template returning pipe used in wikilink target
+!! wikitext
+[[{{pipe page}}]]
+!! html/php+tidy
+<p><a href="/index.php?title=Main&action=edit&redlink=1" class="new" title="Main (page does not exist)">Page</a>
+</p>
+!! html/parsoid
+<p>[[<span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"pipe page","href":"./Template:Pipe_page"},"params":{},"i":0}}]}'>Main|Page</span>]]</p>
+!! end
+
+# Italic/link nesting is changed in this test, but the rendered result is the
+# same. Currently the result is actually an improvement over the MediaWiki
+# output.
!! test
T4702: Mismatched <i>, <b> and <a> tags are invalid
!! wikitext
''Something [http://example.com in italic'']
''Something [http://example.com mixed''''', even bold]'''
'''''Now [http://example.com both''''']
-!! html
+!! html/php
<p><a rel="nofollow" class="external text" href="http://example.com"><i>text</i></a>
<a rel="nofollow" class="external text" href="http://example.com"><b>text</b></a>
<i>Something </i><a rel="nofollow" class="external text" href="http://example.com"><i>in italic</i></a>
<i>Something </i><a rel="nofollow" class="external text" href="http://example.com"><i>mixed</i><b>, even bold</b></a>
<i><b>Now </b></i><a rel="nofollow" class="external text" href="http://example.com"><i><b>both</b></i></a>
</p>
+!! html/parsoid
+<p><i data-parsoid='{"autoInsertedEnd":true}'><a rel="mw:ExtLink" href="http://example.com" class="external text">text<i data-parsoid='{"autoInsertedEnd":true}'></i></a></i>
+<a rel="mw:ExtLink" href="http://example.com" class="external text"><b data-parsoid='{"autoInsertedEnd":true}'>text</b></a><b data-parsoid='{"autoInsertedEnd":true}'></b>
+<i data-parsoid='{"autoInsertedEnd":true}'>Something <a rel="mw:ExtLink" href="http://example.com" class="external text">in italic<i data-parsoid='{"autoInsertedEnd":true}'></i></a></i>
+<i>Something <a rel="mw:ExtLink" href="http://example.com" class="external text">mixed<b data-parsoid='{"autoInsertedEnd":true}'><i data-parsoid='{"autoInsertedEnd":true}'>, even bold</i></b></a>'</i>
+<b data-parsoid='{"autoInsertedEnd":true}'><i data-parsoid='{"autoInsertedEnd":true}'>Now <a rel="mw:ExtLink" href="http://example.com" class="external text">both<b data-parsoid='{"autoInsertedEnd":true}'><i data-parsoid='{"autoInsertedEnd":true}'></i></b></a></i></b></p>
!! end
<p><a rel="nofollow" class="external free" href="http://www.example.com/?title=AT%26T">http://www.example.com/?title=AT%26T</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="http://www.example.com/?title=AT%26T">http://www.example.com/?title=AT%26T</a></p>
+<p><a rel="mw:ExtLink" href="http://www.example.com/?title=AT%26T" class="external free">http://www.example.com/?title=AT%26T</a></p>
!! end
# According to https://www.w3.org/TR/2011/WD-html5-20110525/Overview.html#parsing-urls a plain
<p><a rel="nofollow" class="external free" href="http://www.example.com/?title=100%25_Bran">http://www.example.com/?title=100%25_Bran</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="http://www.example.com/?title=100%25_Bran">http://www.example.com/?title=100%25_Bran</a></p>
+<p><a rel="mw:ExtLink" href="http://www.example.com/?title=100%25_Bran" class="external free">http://www.example.com/?title=100%25_Bran</a></p>
!! end
!! test
<p><a rel="nofollow" class="external free" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">http://www.example.com/?title=Ben-Hur_%281959_film%29</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">http://www.example.com/?title=Ben-Hur_%281959_film%29</a></p>
+<p><a rel="mw:ExtLink" href="http://www.example.com/?title=Ben-Hur_%281959_film%29" class="external free">http://www.example.com/?title=Ben-Hur_%281959_film%29</a></p>
!! end
<p><a rel="nofollow" class="external autonumber" href="http://www.example.com/?title=AT%26T">[1]</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external autonumber" href="http://www.example.com/?title=AT%26T"></a></p>
+<p><a rel="mw:ExtLink" href="http://www.example.com/?title=AT%26T" class="external autonumber"></a></p>
!! end
!! test
<p><a rel="nofollow" class="external autonumber" href="http://www.example.com/?title=100%25_Bran">[1]</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external autonumber" href="http://www.example.com/?title=100%25_Bran"></a></p>
+<p><a rel="mw:ExtLink" href="http://www.example.com/?title=100%25_Bran" class="external autonumber"></a></p>
!! end
!! test
<p><a rel="nofollow" class="external autonumber" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">[1]</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external autonumber" href="http://www.example.com/?title=Ben-Hur_%281959_film%29"></a></p>
+<p><a rel="mw:ExtLink" href="http://www.example.com/?title=Ben-Hur_%281959_film%29" class="external autonumber"></a></p>
!! end
<p><a rel="nofollow" class="external text" href="http://www.example.com/?title=AT%26T">link</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external text" href="http://www.example.com/?title=AT%26T">link</a></p>
+<p><a rel="mw:ExtLink" href="http://www.example.com/?title=AT%26T" class="external text">link</a></p>
!! end
!! test
<p><a rel="nofollow" class="external text" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">link</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external text" href="http://www.example.com/?title=Ben-Hur_%281959_film%29">link</a></p>
+<p><a rel="mw:ExtLink" href="http://www.example.com/?title=Ben-Hur_%281959_film%29" class="external text">link</a></p>
!! end
!! test
</p><p><a rel="nofollow" class="external text" href="//foo.org/bar.">bang</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external text" href="//foo.org/bar#baz.">bang</a></p>
-<p><a rel="mw:ExtLink" class="external text" href="//foo.org/bar.">bang</a></p>
+<p><a rel="mw:ExtLink" href="//foo.org/bar#baz." class="external text">bang</a></p>
+<p><a rel="mw:ExtLink" href="//foo.org/bar." class="external text">bang</a></p>
!! end
!! test
</p><p><a rel="nofollow" class="external text" href="//foo.org/bar'baz">bang</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external autonumber" href="//foo.org/bar'baz"></a></p>
-<p><a rel="mw:ExtLink" class="external text" href="//foo.org/bar'baz">bang</a></p>
+<p><a rel="mw:ExtLink" href="//foo.org/bar'baz" class="external autonumber"></a></p>
+<p><a rel="mw:ExtLink" href="//foo.org/bar'baz" class="external text">bang</a></p>
!! end
!! test
<p><a rel="nofollow" class="external text" href="http://www.musee-picasso.fr/pages/page_id18528_u1l2.htm"><i>La muerte de Casagemas</i> (1901) en el sitio de </a><a href="/index.php?title=Museo_Picasso_(Par%C3%ADs)&action=edit&redlink=1" class="new" title="Museo Picasso (ParÃs) (page does not exist)">Museo Picasso</a>.
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external text" href="http://www.musee-picasso.fr/pages/page_id18528_u1l2.htm"><i>La muerte de Casagemas</i> (1901) en el sitio de </a><a rel="mw:WikiLink" href="./Museo_Picasso_(ParÃs)" title="Museo Picasso (ParÃs)">Museo Picasso</a><span>.</span></p>
+<p><a rel="mw:ExtLink" href="http://www.musee-picasso.fr/pages/page_id18528_u1l2.htm" class="external text"><i>La muerte de Casagemas</i> (1901) en el sitio de </a><a rel="mw:WikiLink" href="./Museo_Picasso_(ParÃs)" title="Museo Picasso (ParÃs)">Museo Picasso</a><span>.</span></p>
!! end
!! test
<p><a rel="nofollow" class="external text" href="http://www.google.com">Google </a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external text" href="http://www.google.com">Google <!-- comment --></a></p>
+<p><a rel="mw:ExtLink" href="http://www.google.com" class="external text">Google <!-- comment --></a></p>
!! end
!! test
<p><a rel="nofollow" class="external text" href="http://192.168.0.1">Link</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external text" href="http://192.168.0.1">Link</a></p>
+<p><a rel="mw:ExtLink" href="http://192.168.0.1" class="external text">Link</a></p>
!! end
!! test
</p><p><a rel="nofollow" class="external free" href="http://example.com/index.php?foozoid%5B%5D=bar">http://example.com/index.php?foozoid%5B%5D=bar</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="http://example.com/index.php?foozoid%5B%5D=bar">http://example.com/index.php?foozoid%5B%5D=bar</a></p>
+<p><a rel="mw:ExtLink" href="http://example.com/index.php?foozoid%5B%5D=bar" class="external free">http://example.com/index.php?foozoid%5B%5D=bar</a></p>
-<p><a rel="mw:ExtLink" class="external free" href="http://example.com/index.php?foozoid%5B%5D=bar" data-parsoid='{"stx":"url","a":{"href":"http://example.com/index.php?foozoid%5B%5D=bar"},"sa":{"href":"http://example.com/index.php?foozoid&#x5B;&#x5D;=bar"}}'>http://example.com/index.php?foozoid%5B%5D=bar</a></p>
+<p><a rel="mw:ExtLink" href="http://example.com/index.php?foozoid%5B%5D=bar" class="external free" data-parsoid='{"stx":"url","a":{"href":"http://example.com/index.php?foozoid%5B%5D=bar"},"sa":{"href":"http://example.com/index.php?foozoid&#x5B;&#x5D;=bar"}}'>http://example.com/index.php?foozoid%5B%5D=bar</a></p>
!! end
!! test
<li><a rel="nofollow" class="external free" href="http://[::FFFF:129.144.52.38]:80/index.html">http://[::FFFF:129.144.52.38]:80/index.html</a></li>
<li><a rel="nofollow" class="external free" href="http://[2010:836B:4179::836B:4179]">http://[2010:836B:4179::836B:4179]</a></li></ul>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="http://[2404:130:0:1000::187:2]/index.php">http://[2404:130:0:1000::187:2]/index.php</a></p>
+<p><a rel="mw:ExtLink" href="http://[2404:130:0:1000::187:2]/index.php" class="external free">http://[2404:130:0:1000::187:2]/index.php</a></p>
<p>Examples from <a href="https://tools.ietf.org/html/rfc2373" rel="mw:ExtLink" class="external mw-magiclink">RFC 2373</a>, section 2.2:</p>
-<ul><li><a rel="mw:ExtLink" class="external free" href="http://[1080::8:800:200C:417A]/unicast">http://[1080::8:800:200C:417A]/unicast</a></li>
-<li><a rel="mw:ExtLink" class="external free" href="http://[FF01::101]/multicast">http://[FF01::101]/multicast</a></li>
-<li><a rel="mw:ExtLink" class="external free" href="http://[::1]/loopback">http://[::1]/loopback</a></li>
-<li><a rel="mw:ExtLink" class="external free" href="http://[::]/unspecified">http://[::]/unspecified</a></li>
-<li><a rel="mw:ExtLink" class="external free" href="http://[::13.1.68.3]/ipv4compat">http://[::13.1.68.3]/ipv4compat</a></li>
-<li><a rel="mw:ExtLink" class="external free" href="http://[::FFFF:129.144.52.38]/ipv4compat">http://[::FFFF:129.144.52.38]/ipv4compat</a></li></ul>
+<ul><li><a rel="mw:ExtLink" href="http://[1080::8:800:200C:417A]/unicast" class="external free">http://[1080::8:800:200C:417A]/unicast</a></li>
+<li><a rel="mw:ExtLink" href="http://[FF01::101]/multicast" class="external free">http://[FF01::101]/multicast</a></li>
+<li><a rel="mw:ExtLink" href="http://[::1]/loopback" class="external free">http://[::1]/loopback</a></li>
+<li><a rel="mw:ExtLink" href="http://[::]/unspecified" class="external free">http://[::]/unspecified</a></li>
+<li><a rel="mw:ExtLink" href="http://[::13.1.68.3]/ipv4compat" class="external free">http://[::13.1.68.3]/ipv4compat</a></li>
+<li><a rel="mw:ExtLink" href="http://[::FFFF:129.144.52.38]/ipv4compat" class="external free">http://[::FFFF:129.144.52.38]/ipv4compat</a></li></ul>
<p>Examples from <a href="https://tools.ietf.org/html/rfc2732" rel="mw:ExtLink" class="external mw-magiclink">RFC 2732</a>, section 2:</p>
-<ul><li><a rel="mw:ExtLink" class="external free" href="http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html">http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html</a></li>
-<li><a rel="mw:ExtLink" class="external free" href="http://[1080:0:0:0:8:800:200C:417A]/index.html">http://[1080:0:0:0:8:800:200C:417A]/index.html</a></li>
-<li><a rel="mw:ExtLink" class="external free" href="http://[3ffe:2a00:100:7031::1]">http://[3ffe:2a00:100:7031::1]</a></li>
-<li><a rel="mw:ExtLink" class="external free" href="http://[1080::8:800:200C:417A]/foo">http://[1080::8:800:200C:417A]/foo</a></li>
-<li><a rel="mw:ExtLink" class="external free" href="http://[::192.9.5.5]/ipng">http://[::192.9.5.5]/ipng</a></li>
-<li><a rel="mw:ExtLink" class="external free" href="http://[::FFFF:129.144.52.38]:80/index.html">http://[::FFFF:129.144.52.38]:80/index.html</a></li>
-<li><a rel="mw:ExtLink" class="external free" href="http://[2010:836B:4179::836B:4179]">http://[2010:836B:4179::836B:4179]</a></li></ul>
+<ul><li><a rel="mw:ExtLink" href="http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html" class="external free">http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html</a></li>
+<li><a rel="mw:ExtLink" href="http://[1080:0:0:0:8:800:200C:417A]/index.html" class="external free">http://[1080:0:0:0:8:800:200C:417A]/index.html</a></li>
+<li><a rel="mw:ExtLink" href="http://[3ffe:2a00:100:7031::1]" class="external free">http://[3ffe:2a00:100:7031::1]</a></li>
+<li><a rel="mw:ExtLink" href="http://[1080::8:800:200C:417A]/foo" class="external free">http://[1080::8:800:200C:417A]/foo</a></li>
+<li><a rel="mw:ExtLink" href="http://[::192.9.5.5]/ipng" class="external free">http://[::192.9.5.5]/ipng</a></li>
+<li><a rel="mw:ExtLink" href="http://[::FFFF:129.144.52.38]:80/index.html" class="external free">http://[::FFFF:129.144.52.38]:80/index.html</a></li>
+<li><a rel="mw:ExtLink" href="http://[2010:836B:4179::836B:4179]" class="external free">http://[2010:836B:4179::836B:4179]</a></li></ul>
!! end
!! test
<li><a rel="nofollow" class="external text" href="http://[::FFFF:129.144.52.38]:80/index.html">6</a></li>
<li><a rel="nofollow" class="external text" href="http://[2010:836B:4179::836B:4179]">7</a></li></ul>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external text" href="http://[2404:130:0:1000::187:2]/index.php">test</a></p>
+<p><a rel="mw:ExtLink" href="http://[2404:130:0:1000::187:2]/index.php" class="external text">test</a></p>
<p>Examples from <a href="https://tools.ietf.org/html/rfc2373" rel="mw:ExtLink" class="external mw-magiclink">RFC 2373</a>, section 2.2:</p>
-<ul><li><a rel="mw:ExtLink" class="external text" href="http://[1080::8:800:200C:417A]">unicast</a></li>
-<li><a rel="mw:ExtLink" class="external text" href="http://[FF01::101]">multicast</a></li>
-<li><a rel="mw:ExtLink" class="external text" href="http://[::1]/">loopback</a></li>
-<li><a rel="mw:ExtLink" class="external text" href="http://[::]">unspecified</a></li>
-<li><a rel="mw:ExtLink" class="external text" href="http://[::13.1.68.3]">ipv4compat</a></li>
-<li><a rel="mw:ExtLink" class="external text" href="http://[::FFFF:129.144.52.38]">ipv4compat</a></li></ul>
+<ul><li><a rel="mw:ExtLink" href="http://[1080::8:800:200C:417A]" class="external text">unicast</a></li>
+<li><a rel="mw:ExtLink" href="http://[FF01::101]" class="external text">multicast</a></li>
+<li><a rel="mw:ExtLink" href="http://[::1]/" class="external text">loopback</a></li>
+<li><a rel="mw:ExtLink" href="http://[::]" class="external text">unspecified</a></li>
+<li><a rel="mw:ExtLink" href="http://[::13.1.68.3]" class="external text">ipv4compat</a></li>
+<li><a rel="mw:ExtLink" href="http://[::FFFF:129.144.52.38]" class="external text">ipv4compat</a></li></ul>
<p>Examples from <a href="https://tools.ietf.org/html/rfc2732" rel="mw:ExtLink" class="external mw-magiclink">RFC 2732</a>, section 2:</p>
-<ul><li><a rel="mw:ExtLink" class="external text" href="http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html">1</a></li>
-<li><a rel="mw:ExtLink" class="external text" href="http://[1080:0:0:0:8:800:200C:417A]/index.html">2</a></li>
-<li><a rel="mw:ExtLink" class="external text" href="http://[3ffe:2a00:100:7031::1]">3</a></li>
-<li><a rel="mw:ExtLink" class="external text" href="http://[1080::8:800:200C:417A]/foo">4</a></li>
-<li><a rel="mw:ExtLink" class="external text" href="http://[::192.9.5.5]/ipng">5</a></li>
-<li><a rel="mw:ExtLink" class="external text" href="http://[::FFFF:129.144.52.38]:80/index.html">6</a></li>
-<li><a rel="mw:ExtLink" class="external text" href="http://[2010:836B:4179::836B:4179]">7</a></li></ul>
+<ul><li><a rel="mw:ExtLink" href="http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html" class="external text">1</a></li>
+<li><a rel="mw:ExtLink" href="http://[1080:0:0:0:8:800:200C:417A]/index.html" class="external text">2</a></li>
+<li><a rel="mw:ExtLink" href="http://[3ffe:2a00:100:7031::1]" class="external text">3</a></li>
+<li><a rel="mw:ExtLink" href="http://[1080::8:800:200C:417A]/foo" class="external text">4</a></li>
+<li><a rel="mw:ExtLink" href="http://[::192.9.5.5]/ipng" class="external text">5</a></li>
+<li><a rel="mw:ExtLink" href="http://[::FFFF:129.144.52.38]:80/index.html" class="external text">6</a></li>
+<li><a rel="mw:ExtLink" href="http://[2010:836B:4179::836B:4179]" class="external text">7</a></li></ul>
!! end
!! test
[<span about="#mwt22" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo"}},"i":0}}]}'>foo</span>l's] errand
[<span about="#mwt23" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo"}},"i":0}}]}'>foo</span>l's errand]
[url=<span about="#mwt24" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo"}},"i":0}}]}'>foo</span>]
-[url=<a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>]
+[url=<a rel="mw:ExtLink" href="http://example.com" class="external free">http://example.com</a>]
[http:// bare protocols don't count]</p>
!! end
<p><a rel="nofollow" class="external text" href="https://github.com/search?l=&q=ResourceLoader+%40wikimedia">Search</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external text" href="https://github.com/search?l=&q=ResourceLoader+%40wikimedia">Search</a></p>
+<p><a rel="mw:ExtLink" href="https://github.com/search?l=&q=ResourceLoader+%40wikimedia" class="external text">Search</a></p>
!! end
!! test
<p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a></p>
+<p><a rel="mw:ExtLink" href="http://example.com" class="external free">http://example.com</a></p>
!! end
!! test
</p><p><a rel="nofollow" class="external text" href="http://example.com)">foo</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a>)</p>
-<p><a rel="mw:ExtLink" class="external free" href="http://example.com/test">http://example.com/test</a>)</p>
-<p><a rel="mw:ExtLink" class="external free" href="http://example.com/(test)">http://example.com/(test)</a></p>
-<p><a rel="mw:ExtLink" class="external free" href="http://example.com/((test)">http://example.com/((test)</a></p>
-<p>(<a rel="mw:ExtLink" class="external free" href="http://example.com/(test))">http://example.com/(test))</a></p>
-<p>(<a rel="mw:ExtLink" class="external free" href="http://example.com/(test)))))">http://example.com/(test)))))</a></p>
-<p><a rel="mw:ExtLink" class="external free" href="http://example.com/a)b">http://example.com/a)b</a></p>
-<p><a rel="mw:ExtLink" class="external text" href="http://example.com)">foo</a></p>
+<p><a rel="mw:ExtLink" href="http://example.com" class="external free">http://example.com</a>)</p>
+<p><a rel="mw:ExtLink" href="http://example.com/test" class="external free">http://example.com/test</a>)</p>
+<p><a rel="mw:ExtLink" href="http://example.com/(test)" class="external free">http://example.com/(test)</a></p>
+<p><a rel="mw:ExtLink" href="http://example.com/((test)" class="external free">http://example.com/((test)</a></p>
+<p>(<a rel="mw:ExtLink" href="http://example.com/(test))" class="external free">http://example.com/(test))</a></p>
+<p>(<a rel="mw:ExtLink" href="http://example.com/(test)))))" class="external free">http://example.com/(test)))))</a></p>
+<p><a rel="mw:ExtLink" href="http://example.com/a)b" class="external free">http://example.com/a)b</a></p>
+<p><a rel="mw:ExtLink" href="http://example.com)" class="external text">foo</a></p>
!! end
!! test
</p><p>(<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>)
</p>
!! html/parsoid
-<p>(<a typeof="mw:ExpandedAttrs" about="#mwt2" rel="mw:ExtLink" class="external free" href="http://example.com/hi" data-parsoid='{"stx":"url","a":{"href":"http://example.com/hi"},"sa":{"href":"http://example.com/{{echo|hi}}"}}' data-mw='{"attribs":[[{"txt":"href"},{"html":"http://example.com/<span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid='{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[20,31,null,null]}' data-mw='{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"hi\"}},\"i\":0}}]}'>hi</span>"}]]}'>http://example.com/hi</a>)</p>
+<p>(<a typeof="mw:ExpandedAttrs" about="#mwt2" rel="mw:ExtLink" href="http://example.com/hi" class="external free" data-parsoid='{"stx":"url","a":{"href":"http://example.com/hi"},"sa":{"href":"http://example.com/{{echo|hi}}"}}' data-mw='{"attribs":[[{"txt":"href"},{"html":"http://example.com/<span about=\"#mwt1\" typeof=\"mw:Transclusion\" data-parsoid='{\"pi\":[[{\"k\":\"1\"}]],\"dsr\":[20,31,null,null]}' data-mw='{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"hi\"}},\"i\":0}}]}'>hi</span>"}]]}'>http://example.com/hi</a>)</p>
-<p>(<a rel="mw:ExtLink" class="external free" href="http://example.com" data-parsoid='{"stx":"url","a":{"href":"http://example.com"},"sa":{"href":"http://example.com<!-- hi -->"}}'>http://example.com</a>)</p>
+<p>(<a rel="mw:ExtLink" href="http://example.com" class="external free" data-parsoid='{"stx":"url","a":{"href":"http://example.com"},"sa":{"href":"http://example.com<!-- hi -->"}}'>http://example.com</a>)</p>
!! end
!! test
</td></tr></table>
!!end
+# Differences between Parsoid and PHP re: trailing whitespace in a
+# table cell.
!! test
Table rowspan
!! wikitext
|Cell 1, row 2
|Cell 3, row 2
|}
-!! html
+!! html/php
<table border="1">
<tr>
<td>Cell 1, row 1
</td>
<td>Cell 3, row 2
</td></tr></table>
+!! html/parsoid
+<table border="1">
+<tbody><tr data-parsoid='{"autoInsertedStart":true}'><td>Cell 1, row 1</td>
+<td rowspan="2">Cell 2, row 1 (and 2)</td>
+<td>Cell 3, row 1</td></tr>
+<tr data-parsoid='{"startTagSrc":"|-"}'>
+<td>Cell 1, row 2</td>
+<td>Cell 3, row 2</td></tr>
+</tbody></table>
!! end
!! test
!! html/parsoid
<table><tbody>
<tr>
-<td data-parsoid='{"startTagSrc":"| ","attrSepSrc":"|","autoInsertedEnd":true}'>[<a rel="mw:ExtLink" class="external free" href="ftp://%7Cx" data-parsoid='{"stx":"url","a":{"href":"ftp://%7Cx"},"sa":{"href":"ftp://|x"}}'>ftp://%7Cx</a></td><td data-parsoid='{"stx":"row","autoInsertedEnd":true}'>]" onmouseover="alert(document.cookie)">test</td></tr></tbody></table>
+<td data-parsoid='{"startTagSrc":"| ","attrSepSrc":"|","autoInsertedEnd":true}'>[<a rel="mw:ExtLink" href="ftp://%7Cx" class="external free" data-parsoid='{"stx":"url","a":{"href":"ftp://%7Cx"},"sa":{"href":"ftp://|x"}}'>ftp://%7Cx</a></td><td data-parsoid='{"stx":"row","autoInsertedEnd":true}'>]" onmouseover="alert(document.cookie)">test</td></tr></tbody></table>
!! end
!! test
</p>
!! end
+# The PHP parser strips the hash fragment for non-existent pages, but
+# Parsoid does not. (T227693)
!! test
Broken link with fragment
!! wikitext
[[Zigzagzogzagzig#zug]]
-!! html
+!! html/php
<p><a href="/index.php?title=Zigzagzogzagzig&action=edit&redlink=1" class="new" title="Zigzagzogzagzig (page does not exist)">Zigzagzogzagzig#zug</a>
</p>
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="./Zigzagzogzagzig#zug" title="Zigzagzogzagzig" data-parsoid="{"tsr":[0,23],"src":"[[Zigzagzogzagzig#zug]]","bsp":[0,23],"stx":"simple"}">Zigzagzogzagzig#zug</a></p>
!! end
!! test
</p>
!! end
+# Parsoid does not strip fragment from red links: T227693
!! test
Nonexistent special page link with fragment
!! wikitext
[[Special:ThisNameWillHopefullyNeverBeUsed#anchor]]
-!! html
+!! html/php
<p><a href="/wiki/Special:ThisNameWillHopefullyNeverBeUsed" class="new" title="Special:ThisNameWillHopefullyNeverBeUsed (page does not exist)">Special:ThisNameWillHopefullyNeverBeUsed#anchor</a>
</p>
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="./Special:ThisNameWillHopefullyNeverBeUsed#anchor" title="Special:ThisNameWillHopefullyNeverBeUsed">Special:ThisNameWillHopefullyNeverBeUsed#anchor</a></p>
!! end
!! test
<p>[<a rel="nofollow" class="external autonumber" href="http://www.example.com">[1]</a>]
</p>
!! html/parsoid
-<p>[<a rel="mw:ExtLink" class="external autonumber" href="http://www.example.com"></a>]</p>
+<p>[<a rel="mw:ExtLink" href="http://www.example.com" class="external autonumber"></a>]</p>
!! end
!! test
<p>[<a rel="nofollow" class="external autonumber" href="//www.example.com">[1]</a>]
</p>
!! html/parsoid
-<p>[<a rel="mw:ExtLink" class="external autonumber" href="//www.example.com"></a>]</p>
+<p>[<a rel="mw:ExtLink" href="//www.example.com" class="external autonumber"></a>]</p>
!! end
!! test
<p>Piped link to URL: [<a rel="nofollow" class="external text" href="http://www.example.com%7Can">example URL</a>]
</p>
!! html/parsoid
-<p>Piped link to URL: [<a rel="mw:ExtLink" class="external text" href="http://www.example.com%7Can" data-parsoid='{"a":{"href":"http://www.example.com%7Can"},"sa":{"href":"http://www.example.com|an"}}'>example URL</a>]</p>
+<p>Piped link to URL: [<a rel="mw:ExtLink" href="http://www.example.com%7Can" class="external text" data-parsoid='{"a":{"href":"http://www.example.com%7Can"},"sa":{"href":"http://www.example.com|an"}}'>example URL</a>]</p>
!! end
!! test
</p><p>[<a rel="nofollow" class="external free" href="http://www.example.com">http://www.example.com</a>
</p>
!! html/parsoid
-<p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[http://www.example.com "},"2":{"wt":"123]"}},"i":0}}]}'>[<a rel="mw:ExtLink" class="external free" href="http://www.example.com">http://www.example.com</a> </p>
+<p about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[http://www.example.com "},"2":{"wt":"123]"}},"i":0}}]}'>[<a rel="mw:ExtLink" href="http://www.example.com" class="external free">http://www.example.com</a> </p>
-<p about="#mwt2" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[[http://www.example.com |123]]"}},"i":0}}]}'>[<a rel="mw:ExtLink" class="external text" href="http://www.example.com">|123</a>]</p>
+<p about="#mwt2" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[[http://www.example.com |123]]"}},"i":0}}]}'>[<a rel="mw:ExtLink" href="http://www.example.com" class="external text">|123</a>]</p>
-<p>{{echo|[<a rel="mw:ExtLink" class="external text" href="http://www.example.com" data-parsoid='{"targetOff":114,"contentOffsets":[114,118],"dsr":[90,119,24,1]}'>|123</a>}}</p>
+<p>{{echo|[<a rel="mw:ExtLink" href="http://www.example.com" class="external text" data-parsoid='{"targetOff":114,"contentOffsets":[114,118],"dsr":[90,119,24,1]}'>|123</a>}}</p>
-<p about="#mwt3" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[http://www.example.com "},"2":{"wt":"123]]"}},"i":0}}]}'>[<a rel="mw:ExtLink" class="external free" href="http://www.example.com">http://www.example.com</a> </p>
+<p about="#mwt3" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"[http://www.example.com "},"2":{"wt":"123]]"}},"i":0}}]}'>[<a rel="mw:ExtLink" href="http://www.example.com" class="external free">http://www.example.com</a> </p>
!! end
!! test
<p><a rel="mw:WikiLink/Interwiki" href="http://www.usemod.com/cgi-bin/mb.pl?ok" title="meatball:ok">meatball:ok</a>
<a rel="mw:WikiLink/Interwiki" href="http://www.usemod.com/cgi-bin/mb.pl?ok#foo" title="meatball:ok">ok with fragment</a>
<a rel="mw:WikiLink/Interwiki" href="http://www.usemod.com/cgi-bin/mb.pl?ok_as_well%3F" title="meatball:ok as well?">ok ending with ? mark</a>
-<a rel="mw:ExtLink" class="external text" href="http://de.wikipedia.org/wiki/Foo?action=history">has query</a>
-<a rel="mw:ExtLink" class="external text" href="http://de.wikipedia.org/wiki/#foo">is just fragment</a></p>
+<a rel="mw:ExtLink" href="http://de.wikipedia.org/wiki/Foo?action=history" class="external text">has query</a>
+<a rel="mw:ExtLink" href="http://de.wikipedia.org/wiki/#foo" class="external text">is just fragment</a></p>
!! end
!! test
</p>
!! html/parsoid
<p>X<a rel="mw:WikiLink" href="./Special:BookSources/0978739256" title="Special:BookSources/0978739256">foo</a></p>
-<p>X<a rel="mw:ExtLink" class="external text" href="https://tools.ietf.org/html/rfc1234">foo</a></p>
+<p>X<a rel="mw:ExtLink" href="https://tools.ietf.org/html/rfc1234" class="external text">foo</a></p>
!! end
!! test
<p><span typeof="mw:Transclusion" about="#mwt1" data-mw='{"parts":[{"template":{"target":{"wt":"[[Main Page]]"},"params":{},"i":0}}]}'>{{</span><a rel="mw:WikiLink" href="./Main_Page" about="#mwt1">Main Page</a><span about="#mwt1">}}</span></p>
!! end
+# The html2html output of this test is currently failing
+# because the html2wt output is broken; see
+# https://phabricator.wikimedia.org/T220018#5123777 for a discussion.
+# Not (yet) including html2wt as a test mode because there are
+# a couple of different correct ways this could be <nowiki>'ed.
!! test
Template with just whitespace in it, T70421
!! wikitext
{{echo|{{ }}}}
+!! options
+parsoid=wt2html,html2html
+!! html/php+tidy
+<p>{{ }}
+</p>
!! html/parsoid
<p about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"pi":[[{"k":"1"}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"{{ }}"}},"i":0}}]}'>{{ }}</p>
!! end
+# This is currently the wikitext output of html2wt on the above test
+# case; note that it is broken! Adding a <nowiki> around the closing
+# brace changes how the open braces associate, breaking the outer
+# {{echo}} template invocation. *However* this "broken" wikitext
+# exposed a useful tokenizer bug (T221384) in how the broken_template
+# rule was being backtracked into, so it's a useful test case even
+# if/when the above test case gets its html2wt output fixed.
+!! test
+Template with just whitespace (bad template brace matching)
+!! options
+parsoid=wt2html
+!! wikitext
+{{echo|{{ }<nowiki>}</nowiki>}}
+!! html/php+tidy
+<p>{{echo|{{ }}}}
+</p>
+!! html/parsoid
+<p>{{echo|{{ }<span typeof="mw:Nowiki">}</span>}}</p>
+!! end
+
!! article
Template:test
!! text
Template infinite loop
!! wikitext
{{loop1}}
-!! html
+!! html/php
<p><span class="error">Template loop detected: <a href="/wiki/Template:Loop1" title="Template:Loop1">Template:Loop1</a></span>
</p>
+!! html/parsoid
+<p><span class="error" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"loop1","href":"./Template:Loop1"},"params":{},"i":0}}]}'>Template loop detected: <a rel="mw:WikiLink" href="./Template:Loop1" title="Template:Loop1">Template:Loop1</a></span></p>
!! end
!! test
<td>hi
</td></tr></tbody></table>
!! html/parsoid
-<p about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"stx":"html","autoInsertedEnd":true,"pi":[[{"k":"1"}],[{"k":"1"}],[{"k":"1"}]],"firstWikitextNode":"table"}' data-mw='{"parts":["{|",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"\n<p>ha</p>"}},"i":0}},"\n","{|",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"\n<p>ho</p>"}},"i":1}},"\n",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"{{!}}hi"}},"i":2}},"\n|}"]}'>ha</p><table about="#mwt1" typeof="mw:ExpandedAttrs" data-mw='{"attribs":[[{"txt":"","html":""},{"html":""}]]}'>
+<p about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"stx":"html","autoInsertedEnd":true,"pi":[[{"k":"1"}],[{"k":"1"}],[{"k":"1"}]],"firstWikitextNode":"TABLE"}' data-mw='{"parts":["{|",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"\n<p>ha</p>"}},"i":0}},"\n","{|",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"\n<p>ho</p>"}},"i":1}},"\n",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"{{!}}hi"}},"i":2}},"\n|}"]}'>ha</p><table about="#mwt1" typeof="mw:ExpandedAttrs" data-mw='{"attribs":[[{"txt":"","html":""},{"html":""}]]}'>
</table><p about="#mwt1">ho</p><table about="#mwt1" typeof="mw:ExpandedAttrs" data-mw='{"attribs":[[{"txt":"","html":""},{"html":""}]]}'>
<onlyinclude> being included
!! wikitext
{{Includes2}}
-!! html
+!! html/php+tidy
<p>Foo
</p>
+!! html/parsoid
+<p><meta typeof="mw:Transclusion mw:Includes/OnlyInclude" about="#mwt1" data-mw='{"parts":[{"template":{"target":{"wt":"Includes2","href":"./Template:Includes2"},"params":{},"i":0}}]}'/><span about="#mwt1">Foo</span><meta typeof="mw:Includes/OnlyInclude/End" about="#mwt1"/></p>
!! end
<onlyinclude> and <includeonly> being included
!! wikitext
{{Includes3}}
-!! html
+!! html/php+tidy
<p>Foo
</p>
+!! html/parsoid
+<p><meta typeof="mw:Transclusion mw:Includes/OnlyInclude" about="#mwt1" data-mw='{"parts":[{"template":{"target":{"wt":"Includes3","href":"./Template:Includes3"},"params":{},"i":0}}]}'/><span about="#mwt1">Foo</span><meta typeof="mw:Includes/OnlyInclude/End" about="#mwt1"/></p>
!! end
# FIXME: Parsoid's markup for this is quite ugly.
Un-closed <noinclude>
!! wikitext
<noinclude>
-!! html
+!! html/php+tidy
+!! html/parsoid
+<meta typeof="mw:Includes/NoInclude" data-parsoid='{"src":"<noinclude>"}'/>
+!! end
+
+!! test
+Empty <noinclude>
+!! wikitext
+Hello<noinclude></noinclude>!
+!! html/php+tidy
+<p>Hello!
+</p>
+!! html/parsoid
+<p>Hello<meta typeof="mw:Includes/NoInclude" data-parsoid='{"src":"<noinclude>"}'/><meta typeof="mw:Includes/NoInclude/End" data-parsoid='{"src":"</noinclude>"}'/>!</p>
!! end
!! test
## will normalize the include directives to serialize on their own line.
## Selser will take care of preserving formatting in scenarios where they
## intermingled with other wikitext.
+## This test also triggered T223411 during Parsoid-PHP porting.
!! test
Includes and comments at SOL
!! options
<li>{{echo|<a rel="nofollow" class="external text" href="http://example.com/-{foo">Breaks template, however</a>}}</li></ul>
!! html/parsoid
<ul>
-<li><a rel="mw:ExtLink" class="external text" href="http://example.com/-{foo">Example in URL</a></li>
-<li><a rel="mw:ExtLink" class="external text" href="http://example.com">Example in -{link} description</a></li>
-<li>{{echo|<a rel="mw:ExtLink" class="external text" href="http://example.com/-{foo">Breaks template, however</a>}}</li>
+<li><a rel="mw:ExtLink" href="http://example.com/-{foo" class="external text">Example in URL</a></li>
+<li><a rel="mw:ExtLink" href="http://example.com" class="external text">Example in -{link} description</a></li>
+<li>{{echo|<a rel="mw:ExtLink" href="http://example.com/-{foo" class="external text">Breaks template, however</a>}}</li>
</ul>
!! end
<p><span about="#mwt2" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"hi<ref>[[ho|{{echo|hi}}]]</ref>"}},"i":0}}]}'>hi</span><sup about="#mwt2" class="mw-ref" id="cite_ref-1" rel="dc:references" typeof="mw:Extension/ref" data-mw='{"name":"ref","attrs":{},"body":{"id":"mw-reference-text-cite_note-1"}}'><a href="./Main_Page#cite_note-1" style="counter-reset: mw-Ref 1;"><span class="mw-reflink-text">[1]</span></a></sup>
<span about="#mwt8" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"hi<ref>[http://test.com?q={{echo|ho}}]</ref>"}},"i":0}}]}'>hi</span><sup about="#mwt8" class="mw-ref" id="cite_ref-2" rel="dc:references" typeof="mw:Extension/ref" data-mw='{"name":"ref","attrs":{},"body":{"id":"mw-reference-text-cite_note-2"}}'><a href="./Main_Page#cite_note-2" style="counter-reset: mw-Ref 2;"><span class="mw-reflink-text">[2]</span></a></sup>
<span about="#mwt13" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"hi<ref>-{ho|{{echo|hi}}}-</ref>"}},"i":0}}]}'>hi</span><sup about="#mwt13" class="mw-ref" id="cite_ref-3" rel="dc:references" typeof="mw:Extension/ref" data-mw='{"name":"ref","attrs":{},"body":{"id":"mw-reference-text-cite_note-3"}}'><a href="./Main_Page#cite_note-3" style="counter-reset: mw-Ref 3;"><span class="mw-reflink-text">[3]</span></a></sup></p>
-<ol class="mw-references references" typeof="mw:Extension/references" about="#mwt17" data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-1" id="cite_note-1"><a href="./Main_Page#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text"><a rel="mw:WikiLink" href="./Ho" title="Ho">hi</a></span></li><li about="#cite_note-2" id="cite_note-2"><a href="./Main_Page#cite_ref-2" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-2" class="mw-reference-text"><a rel="mw:ExtLink" class="external autonumber" href="http://test.com?q=ho"></a></span></li><li about="#cite_note-3" id="cite_note-3"><a href="./Main_Page#cite_ref-3" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-3" class="mw-reference-text"><span typeof="mw:LanguageVariant" data-mw-variant='{"filter":{"l":["ho"],"t":"hi"}}'></span></span></li></ol>
+<ol class="mw-references references" typeof="mw:Extension/references" about="#mwt17" data-mw='{"name":"references","attrs":{}}'><li about="#cite_note-1" id="cite_note-1"><a href="./Main_Page#cite_ref-1" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-1" class="mw-reference-text"><a rel="mw:WikiLink" href="./Ho" title="Ho">hi</a></span></li><li about="#cite_note-2" id="cite_note-2"><a href="./Main_Page#cite_ref-2" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-2" class="mw-reference-text"><a rel="mw:ExtLink" href="http://test.com?q=ho" class="external autonumber"></a></span></li><li about="#cite_note-3" id="cite_note-3"><a href="./Main_Page#cite_ref-3" rel="mw:referencedBy"><span class="mw-linkback-text">↑ </span></a> <span id="mw-reference-text-cite_note-3" class="mw-reference-text"><span typeof="mw:LanguageVariant" data-mw-variant='{"filter":{"l":["ho"],"t":"hi"}}'></span></span></li></ol>
!! end
###
!! html/php
<div class="thumb tright"><div class="thumbinner" style="width:222px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" decoding="async" width="220" height="25" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a></div></div></div>
!! html/parsoid
-<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a></figcaption></figure>
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a rel="mw:ExtLink" href="http://example.com" class="external free">http://example.com</a></figcaption></figure>
!! end
!! test
!! html/php
<div class="thumb tright"><div class="thumbinner" style="width:222px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Alteration" src="http://example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" decoding="async" width="220" height="25" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/330px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/440px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a></div></div></div>
!! html/parsoid
-<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img alt="Alteration" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a></figcaption></figure>
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img alt="Alteration" resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a rel="mw:ExtLink" href="http://example.com" class="external free">http://example.com</a></figcaption></figure>
!! end
!! test
!! html/php
<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" decoding="async" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>Please <a rel="nofollow" class="external free" href="mailto:nobody@example.com">mailto:nobody@example.com</a></div></div></div>
!! html/parsoid
-<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption>Please <a rel="mw:ExtLink" class="external free" href="mailto:nobody@example.com">mailto:nobody@example.com</a></figcaption></figure>
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption>Please <a rel="mw:ExtLink" href="mailto:nobody@example.com" class="external free">mailto:nobody@example.com</a></figcaption></figure>
!! end
# Pending resolution to T2368
!! html/php
<div class="thumb tright"><div class="thumbinner" style="width:202px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" decoding="async" width="200" height="23" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/300px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/400px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>This caption has <a rel="nofollow" class="external text" href="irc://example.net">irc</a> and <a rel="nofollow" class="external text" href="https://example.com">Secure</a> ext links in it.</div></div></div>
!! html/parsoid
-<figure typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="23" width="200"/></a><figcaption>This caption has <a rel="mw:ExtLink" class="external text" href="irc://example.net">irc</a> and <a rel="mw:ExtLink" class="external text" href="https://example.com">Secure</a> ext links in it.</figcaption></figure>
+<figure typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="23" width="200"/></a><figcaption>This caption has <a rel="mw:ExtLink" href="irc://example.net" class="external text">irc</a> and <a rel="mw:ExtLink" href="https://example.com" class="external text">Secure</a> ext links in it.</figcaption></figure>
!! end
!! test
[[.]]
[[..]]
[[foo././bar]]
-[[foo<a rel="mw:ExtLink" class="external autonumber" href="http://example.com"></a>xyz]]</p>
+[[foo<a rel="mw:ExtLink" href="http://example.com" class="external autonumber"></a>xyz]]</p>
<p>[[<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"./../foo"}},"i":0}}]}'>./../foo</span>|bar]]
[[<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"foo/."}},"i":0}}]}'>foo/.</span>|bar]]
<link rel="mw:PageProp/Category" href="./Category:MediaWiki_User's_Guide#MediaWiki%20User's%20Guide" data-parsoid='{"stx":"piped","a":{"href":"./Category:MediaWiki_User's_Guide"},"sa":{"href":"Category:MediaWiki User's Guide"}}'/>
!! end
+!! test
+Category with template-generated sort key
+!! options
+cat
+!! wikitext
+[[Category:MediaWiki User's Guide|MediaWiki {{echo|Foo}} Guide]]
+!! html/php
+cat=MediaWiki_User's_Guide sort=MediaWiki Foo Guide
+!! html/parsoid
+<link rel="mw:PageProp/Category" href="./Category:MediaWiki_User's_Guide#MediaWiki%20Foo%20Guide" typeof="mw:ExpandedAttrs" data-mw='{"attribs":[[{"txt":"mw:sortKey"},{"html":"MediaWiki <span typeof=\"mw:Transclusion\" data-mw='{\"parts\":[{\"template\":{\"target\":{\"wt\":\"echo\",\"href\":\"./Template:Echo\"},\"params\":{\"1\":{\"wt\":\"Foo\"}},\"i\":0}}]}'>Foo</span> Guide"}]]}'/>
+!! end
+
!! test
Category with empty sort key
!! options
<p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a> <a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" decoding="async" width="1941" height="220" /></a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a> <figure-inline class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure-inline></p>
+<p><a rel="mw:ExtLink" href="http://example.com" class="external free">http://example.com</a> <figure-inline class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure-inline></p>
!!end
# Parsoid doesn't wt2wt this cleanly because it adds <nowiki>s.
<p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" decoding="async" width="1941" height="220" /></a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a><figure-inline class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure-inline></p>
+<p><a rel="mw:ExtLink" href="http://example.com" class="external free">http://example.com</a><figure-inline class="mw-default-size" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941"/></a></figure-inline></p>
!!end
!! test
</p>
!! end
+!! test
+Text with HTML5 semicolon-less entity (should not decode)
+!! wikitext
+&amp;
+!! html/php+tidy
+<p>&ampamp;
+</p>
+!! html/parsoid
+<p>&ampamp;</p>
+!! end
+
!! test
HTML5 tags
!! wikitext
</p><p><a rel="nofollow" class="external free" href="mailto:inline@mail.tld">mailto:inline@mail.tld</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external autonumber" href="http://first/"></a> <a rel="mw:ExtLink" class="external autonumber" href="http://second"></a> <a rel="mw:ExtLink" class="external autonumber" href="ftp://ftp"></a></p>
-<p><a rel="mw:ExtLink" class="external free" href="ftp://inlineftp">ftp://inlineftp</a></p>
-<p><a rel="mw:ExtLink" class="external text" href="mailto:enclosed@mail.tld">With target</a></p>
-<p><a rel="mw:ExtLink" class="external autonumber" href="mailto:enclosed@mail.tld"></a></p>
-<p><a rel="mw:ExtLink" class="external free" href="mailto:inline@mail.tld">mailto:inline@mail.tld</a></p>
+<p><a rel="mw:ExtLink" href="http://first/" class="external autonumber"></a> <a rel="mw:ExtLink" href="http://second" class="external autonumber"></a> <a rel="mw:ExtLink" href="ftp://ftp" class="external autonumber"></a></p>
+<p><a rel="mw:ExtLink" href="ftp://inlineftp" class="external free">ftp://inlineftp</a></p>
+<p><a rel="mw:ExtLink" href="mailto:enclosed@mail.tld" class="external text">With target</a></p>
+<p><a rel="mw:ExtLink" href="mailto:enclosed@mail.tld" class="external autonumber"></a></p>
+<p><a rel="mw:ExtLink" href="mailto:inline@mail.tld" class="external free">mailto:inline@mail.tld</a></p>
!! end
</div>
!! html/parsoid
<h2 id="onmouseover="><span id="onmouseover.3D" typeof="mw:FallbackId"></span>onmouseover=</h2>
-<p><a rel="mw:ExtLink" class="external free" href="http://__TOC__" data-parsoid='{"stx":"url"}'>http://__TOC__</a></p>
+<p><a rel="mw:ExtLink" href="http://__TOC__" class="external free" data-parsoid='{"stx":"url"}'>http://__TOC__</a></p>
!! end
!! test
http://===r:::https://b
{|
-!! html
+!! html/php
<p><a rel="nofollow" class="external free" href="http://===r:::https://b">http://===r:::https://b</a>
</p>
<table>
<tr><td></td></tr>
</table>
+!! html/parsoid
+<p><a rel="mw:ExtLink" href="http://===r:::https://b" class="external free" data-parsoid='{"stx":"url"}'>http://===r:::https://b</a></p>
+
+<table data-parsoid='{"autoInsertedEnd":true}'></table>
+!! end
+
+# The above 'Parser24' fuzz test exposed a tokenizer bug (T221384);
+# this is a minimized version of the above test to catch regressions.
+!! test
+Fuzz testing: Parser24 (minimized)
+!! options
+parsoid=wt2html
+!! wikitext
+{{<u {{{{[[Sx-->}}
+!! html/php+tidy
+<p>{{<u>}}
+</u></p>
+!! html/parsoid
+<p>{{<u data-parsoid='{"stx":"html","a":{"{{{{[[Sx--":null},"sa":{"{{{{[[Sx--":""},"autoInsertedEnd":true}'>}}</u></p>
!! end
## Remex doesn't account for fostered content.
<p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a> junk
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a> <span typeof="mw:Nowiki">junk</span></p>
+<p><a rel="mw:ExtLink" href="http://example.com" class="external free" data-parsoid='{"stx":"url"}'>http://example.com</a> <span typeof="mw:Nowiki">junk</span></p>
!! end
!!test
<p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>junk
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a><span typeof="mw:Nowiki">junk</span></p>
+<p><a rel="mw:ExtLink" href="http://example.com" class="external free" data-parsoid='{"stx":"url"}'>http://example.com</a><span typeof="mw:Nowiki">junk</span></p>
!! end
!! test
!! html/php+tidy
<p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a></p><pre>junk</pre>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="http://example.com" data-parsoid='{"stx":"url"}'>http://example.com</a></p><pre typeof="mw:Extension/pre" about="#mwt2" data-mw='{"name":"pre","attrs":{},"body":{"extsrc":"junk"}}'>junk</pre>
+<p><a rel="mw:ExtLink" href="http://example.com" class="external free" data-parsoid='{"stx":"url"}'>http://example.com</a></p><pre typeof="mw:Extension/pre" about="#mwt2" data-mw='{"name":"pre","attrs":{},"body":{"extsrc":"junk"}}'>junk</pre>
!! end
!! test
<pre dir=" "></pre>
!! html/parsoid
<pre dir="
-" typeof="mw:Extension/pre" about="#mwt2"data-mw='{"name":"pre","attrs":{"dir":"\n"},"body":{"extsrc":""}}'></pre>
+" typeof="mw:Extension/pre" about="#mwt2" data-mw='{"name":"pre","attrs":{"dir":""},"body":{"extsrc":""}}'></pre>
!! end
!! test
!! html/php
<ul><li><a rel="nofollow" class="external free" href="irc://%0Aa">irc://%0Aa</a></li></ul>
!! html/parsoid
-<ul><li><a rel="mw:ExtLink" class="external free" href="irc://%0Aa" data-parsoid='{"stx":"url","a":{"href":"irc://%0Aa"},"sa":{"href":"irc://&#x0A;a"}}'>irc://%0Aa</a></li></ul>
+<ul><li><a rel="mw:ExtLink" href="irc://%0Aa" class="external free" data-parsoid='{"stx":"url","a":{"href":"irc://%0Aa"},"sa":{"href":"irc://&#x0A;a"}}'>irc://%0Aa</a></li></ul>
!! end
!! test
!! html/php
<ul><li><a rel="nofollow" class="external free" href="irc://%0Aa">irc://%0Aa</a></li></ul>
!! html/parsoid
-<ul><li><a rel="mw:ExtLink" class="external free" href="irc://%0Aa">irc://%0Aa</a></li></ul>
+<ul><li><a rel="mw:ExtLink" href="irc://%0Aa" class="external free">irc://%0Aa</a></li></ul>
!! end
# The PHP parser strips the empty tags out for giggles; parsoid doesn't.
<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}]}'><a href="./Special:FilePath/Image2.gif"><span resource="./File:Image2.gif" data-width="120" data-height="120">File:Image2.gif</span></a></figure-inline></div><div class="gallerytext"></div></li>
<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}]}'><a href="./Special:FilePath/Image3"><span resource="./File:Image3" data-width="120" data-height="120">File:Image3</span></a></figure-inline></div><div class="gallerytext"></div></li>
<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}]}'><a href="./Special:FilePath/Image4"><span resource="./File:Image4" data-width="300">File:Image4</span></a></figure-inline></div><div class="gallerytext"></div></li>
-<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}]}'><a href="./Special:FilePath/Image5.svg"><span resource="./File:Image5.svg" data-width="120" data-height="120">File:Image5.svg</span></a></figure-inline></div><div class="gallerytext"> <a rel="mw:ExtLink" class="external free" href="http://///////">http://///////</a></div></li>
+<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}]}'><a href="./Special:FilePath/Image5.svg"><span resource="./File:Image5.svg" data-width="120" data-height="120">File:Image5.svg</span></a></figure-inline></div><div class="gallerytext"> <a rel="mw:ExtLink" href="http://///////" class="external free">http://///////</a></div></li>
<li class="gallerybox" style="width: 155px;"><div class="thumb" style="width: 150px; height: 150px;"><figure-inline typeof="mw:Error mw:Image" data-mw='{"errors":[{"key":"apierror-filedoesnotexist","message":"This image does not exist."}]}'><a href="./Special:FilePath/*_image6"><span resource="./File:*_image6" data-width="120" data-height="120">File:* image6</span></a></figure-inline></div><div class="gallerytext"></div></li>
</ul>
!! end
</ul>
!! end
+!! test
+Gallery in nolines mode
+!! wikitext
+<gallery mode="nolines" showfilenames="yes" caption="No Lines!">
+File:Foobar.jpg|foo
+</gallery>
+!! html/php
+<ul class="gallery mw-gallery-nolines">
+ <li class='gallerycaption'>No Lines!</li>
+ <li class="gallerybox" style="width: 125px"><div style="width: 125px">
+ <div class="thumb" style="width: 120px;"><div style="margin:0px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" decoding="async" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div>
+ <div class="gallerytext">
+<p>foo
+</p>
+ </div>
+ </div></li>
+</ul>
+!! html/parsoid
+<ul class="gallery mw-gallery-nolines" typeof="mw:Extension/gallery" about="#mwt3" data-mw='{"name":"gallery","attrs":{"mode":"nolines","showfilenames":"yes"},"body":{}}'>
+<li class="gallerycaption">No Lines!</li>
+<li class="gallerybox" style="width: 125px;"><div class="thumb" style="width: 120px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="14" width="120"/></a></figure-inline></div><div class="gallerytext">foo</div></li>
+</ul>
+!! end
+
!! test
Gallery in slideshow mode
!! wikitext
</ul>
!! html/parsoid
<ul class="gallery mw-gallery-packed" typeof="mw:Extension/gallery" about="#mwt3" data-parsoid='{"dsr":[0,50,23,10]}' data-mw='{"name":"gallery","attrs":{"mode":"packed"},"body":{}}'>
-<li class="gallerybox" style="width: 1061px;"><div class="thumb" style="width: 1059px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/1589px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="120" width="1059"/></a></figure-inline></div><div class="gallerytext"></div></li>
+<li class="gallerybox" style="width: 1061.3333333333333px;"><div class="thumb" style="width: 1059.3333333333333px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/1589px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="120" width="1060"/></a></figure-inline></div><div class="gallerytext"></div></li>
+</ul>
+!! end
+
+!! test
+Gallery in packed-overlay mode
+!! wikitext
+<gallery mode="packed-overlay" showfilenames="yes" caption="Packed Overlay!">
+File:Foobar.jpg|foo
+</gallery>
+!! html/php
+<ul class="gallery mw-gallery-packed-overlay">
+ <li class='gallerycaption'>Packed Overlay!</li>
+ <li class="gallerybox" style="width: 1061.3333333333px"><div style="width: 1061.3333333333px">
+ <div class="thumb" style="width: 1059.3333333333px;"><div style="margin:0px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/1589px-Foobar.jpg" decoding="async" width="1060" height="120" srcset="http://example.com/images/3/3a/Foobar.jpg 1.5x" /></a></div></div>
+ <div class="gallerytextwrapper" style="width: 1040px"><div class="gallerytext">
+<p>foo
+</p>
+ </div></div>
+ </div></li>
+</ul>
+!! html/parsoid
+<ul class="gallery mw-gallery-packed-overlay" typeof="mw:Extension/gallery" about="#mwt3" data-mw='{"name":"gallery","attrs":{"mode":"packed-overlay","showfilenames":"yes"},"body":{}}'>
+<li class="gallerycaption">Packed Overlay!</li>
+<li class="gallerybox" style="width: 1061.3333333333333px;"><div class="thumb" style="width: 1059.3333333333333px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/1589px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="120" width="1060"/></a></figure-inline></div><div class="gallerytextwrapper" style="width: 1040px;"><div class="gallerytext">foo</div></div></li>
+</ul>
+!! end
+
+!! test
+Gallery in packed-hover mode
+!! wikitext
+<gallery mode="packed-hover" showfilenames="yes" caption="Packed Hover!">
+File:Foobar.jpg|foo
+</gallery>
+!! html/php
+<ul class="gallery mw-gallery-packed-hover">
+ <li class='gallerycaption'>Packed Hover!</li>
+ <li class="gallerybox" style="width: 1061.3333333333px"><div style="width: 1061.3333333333px">
+ <div class="thumb" style="width: 1059.3333333333px;"><div style="margin:0px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/1589px-Foobar.jpg" decoding="async" width="1060" height="120" srcset="http://example.com/images/3/3a/Foobar.jpg 1.5x" /></a></div></div>
+ <div class="gallerytextwrapper" style="width: 1040px"><div class="gallerytext">
+<p>foo
+</p>
+ </div></div>
+ </div></li>
+</ul>
+!! html/parsoid
+<ul class="gallery mw-gallery-packed-hover" typeof="mw:Extension/gallery" about="#mwt3" data-mw='{"name":"gallery","attrs":{"mode":"packed-hover","showfilenames":"yes"},"body":{}}'>
+<li class="gallerycaption">Packed Hover!</li>
+<li class="gallerybox" style="width: 1061.3333333333333px;"><div class="thumb" style="width: 1059.3333333333333px;"><figure-inline typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/1589px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="120" width="1060"/></a></figure-inline></div><div class="gallerytextwrapper" style="width: 1040px;"><div class="gallerytext">foo</div></div></li>
</ul>
!! end
# See: https://www.w3.org/TR/html5/syntax.html#character-references
# Note that U+000C (form feed) is not a valid XML character, so
# it is banned even though allowed in HTML5.
+# Note there are also weird legacy numeric entities which are mapped
+# elsewhere; see T113194
!! test
-Illegal character references (T106578)
+Illegal character references (T106578, T113194)
+!! options
+parsoid={ "modes": ["wt2html","html2html"], "normalizePhp": true }
!! wikitext
; Null: �
; FF: 
; CR: 
; Control (low): 
; Control (high):  Ÿ
+; Unsupported legacy: € ‚ ƒ – Ÿ
; Surrogate: ��
; This is an okay astral character: 💩
!! html+tidy
<dd>&#8;</dd>
<dt>Control (high)</dt>
<dd>&#x7F; &#x9F;</dd>
+<dt>Unsupported legacy</dt>
+<dd>&#128; &#130; &#131; &#150; &#159;</dd>
<dt>Surrogate</dt>
<dd>&#xD83D;&#xDCA9;</dd>
<dt>This is an okay astral character</dt>
<p><abbr>(fr)</abbr> <a href="/wiki/Special:BookSources/2753300917" class="internal mw-magiclink-isbn">ISBN 2753300917</a> <a rel="nofollow" class="external text" href="http://www.example.com">example.com</a>
</p>
!! html/parsoid
-<p><abbr data-parsoid='{"stx":"html"}'>(fr)</abbr> <a href="./Special:BookSources/2753300917" rel="mw:WikiLink" data-parsoid='{"stx":"magiclink"}'>ISBN 2753300917</a> <a rel="mw:ExtLink" class="external text" href="http://www.example.com">example.com</a></p>
+<p><abbr data-parsoid='{"stx":"html"}'>(fr)</abbr> <a href="./Special:BookSources/2753300917" rel="mw:WikiLink" data-parsoid='{"stx":"magiclink"}'>ISBN 2753300917</a> <a rel="mw:ExtLink" href="http://www.example.com" class="external text">example.com</a></p>
!! end
!! test
!! html/php
<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" decoding="async" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>An <a rel="nofollow" class="external text" href="http://test/?param1=%7Cleft%7C&param2=%7Cx">external</a> URL</div></div></div>
!! html/parsoid
-<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption>An <a rel="mw:ExtLink" class="external text" href="http://test/?param1=%7Cleft%7C&param2=%7Cx" data-parsoid='{"a":{"href":"http://test/?param1=%7Cleft%7C&param2=%7Cx"},"sa":{"href":"http://test/?param1=|left|&param2=|x"}}'>external</a> URL</figcaption></figure>
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption>An <a rel="mw:ExtLink" href="http://test/?param1=%7Cleft%7C&param2=%7Cx" class="external text" data-parsoid='{"a":{"href":"http://test/?param1=%7Cleft%7C&param2=%7Cx"},"sa":{"href":"http://test/?param1=|left|&param2=|x"}}'>external</a> URL</figcaption></figure>
!! end
!! test
<p>Nested: Hello Hong Kong!
</p>
!! html/parsoid
-<p>Nested: <span typeof="mw:LanguageVariant" data-parsoid='{"tSp":[7]}' data-mw-variant='{"twoway":[{"l":"zh-hans","t":"Hi <span typeof=\"mw:LanguageVariant\" data-mw-variant='{\"twoway\":[{\"l\":\"zh-cn\",\"t\":\"China\"},{\"l\":\"zh-sg\",\"t\":\"Singapore\"}]}' data-parsoid='{\"fl\":[],\"tSp\":[7],\"dsr\":[21,53,null,2]}'></span>"},{"l":"zh-hant","t":"Hello <span typeof=\"mw:LanguageVariant\" data-mw-variant='{\"twoway\":[{\"l\":\"zh-tw\",\"t\":\"Taiwan\"},{\"l\":\"zh-hk\",\"t\":\"H&lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&apos;{\\\"disabled\\\":{\\\"t\\\":\\\"ong\\\"}}&apos; data-parsoid=&apos;{\\\"fl\\\":[],\\\"dsr\\\":[90,97,null,2]}&apos;>&lt;/span> K&lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&apos;{\\\"disabled\\\":{\\\"t\\\":\\\"\\\"}}&apos; data-parsoid=&apos;{\\\"fl\\\":[],\\\"dsr\\\":[99,103,null,2]}&apos;>&lt;/span>ong\"}]}' data-parsoid='{\"fl\":[],\"tSp\":[7],\"dsr\":[68,109,null,2]}'></span>"}]}'></span>!</p>
+<p>Nested: <span typeof="mw:LanguageVariant" data-parsoid='{"tSp":[7]}' data-mw-variant='{"twoway":[{"l":"zh-hans","t":"Hi <span typeof=\"mw:LanguageVariant\" data-mw-variant='{\"twoway\":[{\"l\":\"zh-cn\",\"t\":\"China\"},{\"l\":\"zh-sg\",\"t\":\"Singapore\"}]}' data-parsoid='{\"fl\":[],\"tSp\":[7],\"dsr\":[21,53,2,2]}'></span>"},{"l":"zh-hant","t":"Hello <span typeof=\"mw:LanguageVariant\" data-mw-variant='{\"twoway\":[{\"l\":\"zh-tw\",\"t\":\"Taiwan\"},{\"l\":\"zh-hk\",\"t\":\"H&lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&apos;{\\\"disabled\\\":{\\\"t\\\":\\\"ong\\\"}}&apos; data-parsoid=&apos;{\\\"fl\\\":[],\\\"dsr\\\":[90,97,2,2]}&apos;>&lt;/span> K&lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&apos;{\\\"disabled\\\":{\\\"t\\\":\\\"\\\"}}&apos; data-parsoid=&apos;{\\\"fl\\\":[],\\\"dsr\\\":[99,103,2,2]}&apos;>&lt;/span>ong\"}]}' data-parsoid='{\"fl\":[],\"tSp\":[7],\"dsr\":[68,109,2,2]}'></span>"}]}'></span>!</p>
!! end
!! test
<p><span title="X">A</span>
</p>
!! html/parsoid
-<p><span typeof="mw:LanguageVariant" data-mw-variant='{"filter":{"l":["zh","zh-hans","zh-hant"],"t":"<span title=\"\" about=\"#mwt1\" typeof=\"mw:ExpandedAttrs\" data-parsoid='{\"stx\":\"html\",\"a\":{\"title\":\"\"},\"sa\":{\"title\":\"-{X}-\"},\"dsr\":[21,49,20,7]}' data-mw='{\"attribs\":[[{\"txt\":\"title\"},{\"html\":\"&lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&apos;{\\\"disabled\\\":{\\\"t\\\":\\\"X\\\"}}&apos; data-parsoid=&apos;{\\\"fl\\\":[],\\\"dsr\\\":[34,39,null,2]}&apos;>&lt;/span>\"}]]}'>A</span>"}}'></span></p>
+<p><span typeof="mw:LanguageVariant" data-mw-variant='{"filter":{"l":["zh","zh-hans","zh-hant"],"t":"<span title=\"\" about=\"#mwt1\" typeof=\"mw:ExpandedAttrs\" data-parsoid='{\"stx\":\"html\",\"a\":{\"title\":\"\"},\"sa\":{\"title\":\"-{X}-\"},\"dsr\":[21,49,20,7]}' data-mw='{\"attribs\":[[{\"txt\":\"title\"},{\"html\":\"&lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&apos;{\\\"disabled\\\":{\\\"t\\\":\\\"X\\\"}}&apos; data-parsoid=&apos;{\\\"fl\\\":[],\\\"dsr\\\":[34,39,2,2]}&apos;>&lt;/span>\"}]]}'>A</span>"}}'></span></p>
!! end
!! test
<p><span title="X">A</span>
</p>
!! html/parsoid
-<p><span typeof="mw:LanguageVariant" data-mw-variant='{"disabled":{"t":"<span title=\"\" about=\"#mwt1\" typeof=\"mw:ExpandedAttrs\" data-parsoid='{\"stx\":\"html\",\"a\":{\"title\":\"\"},\"sa\":{\"title\":\"-{X}-\"},\"dsr\":[2,30,20,7]}' data-mw='{\"attribs\":[[{\"txt\":\"title\"},{\"html\":\"&lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&apos;{\\\"disabled\\\":{\\\"t\\\":\\\"X\\\"}}&apos; data-parsoid=&apos;{\\\"fl\\\":[],\\\"dsr\\\":[15,20,null,2]}&apos;>&lt;/span>\"}]]}'>A</span>"}}'></span></p>
+<p><span typeof="mw:LanguageVariant" data-mw-variant='{"disabled":{"t":"<span title=\"\" about=\"#mwt1\" typeof=\"mw:ExpandedAttrs\" data-parsoid='{\"stx\":\"html\",\"a\":{\"title\":\"\"},\"sa\":{\"title\":\"-{X}-\"},\"dsr\":[2,30,20,7]}' data-mw='{\"attribs\":[[{\"txt\":\"title\"},{\"html\":\"&lt;span typeof=\\\"mw:LanguageVariant\\\" data-mw-variant=&apos;{\\\"disabled\\\":{\\\"t\\\":\\\"X\\\"}}&apos; data-parsoid=&apos;{\\\"fl\\\":[],\\\"dsr\\\":[15,20,2,2]}&apos;>&lt;/span>\"}]]}'>A</span>"}}'></span></p>
!! end
# Parsoid and PHP disagree on how to parse this example: Parsoid
<a rel="nofollow" class="external text" href="//www.google.com">www.гоогле.цом</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="http://www.google.com">http://www.google.com</a>
-<a rel="mw:ExtLink" class="external free" href="gopher://www.google.com">gopher://www.google.com</a>
-<a rel="mw:ExtLink" class="external text" href="http://www.google.com">http://www.google.com</a>
-<a rel="mw:ExtLink" class="external text" href="gopher://www.google.com">gopher://www.google.com</a>
-<a rel="mw:ExtLink" class="external text" href="https://www.google.com">irc://www.google.com</a>
-<a rel="mw:ExtLink" class="external text" href="ftp://www.google.com">www.google.com/ftp://dir</a>
-<a rel="mw:ExtLink" class="external text" href="//www.google.com">www.google.com</a></p>
+<p><a rel="mw:ExtLink" href="http://www.google.com" class="external free">http://www.google.com</a>
+<a rel="mw:ExtLink" href="gopher://www.google.com" class="external free">gopher://www.google.com</a>
+<a rel="mw:ExtLink" href="http://www.google.com" class="external text">http://www.google.com</a>
+<a rel="mw:ExtLink" href="gopher://www.google.com" class="external text">gopher://www.google.com</a>
+<a rel="mw:ExtLink" href="https://www.google.com" class="external text">irc://www.google.com</a>
+<a rel="mw:ExtLink" href="ftp://www.google.com" class="external text">www.google.com/ftp://dir</a>
+<a rel="mw:ExtLink" href="//www.google.com" class="external text">www.google.com</a></p>
!! end
!! test
<p><a rel="nofollow" class="external autonumber" href="http://en.wikipedia.org/">[Û±]</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external autonumber" href="http://en.wikipedia.org/"></a></p>
+<p><a rel="mw:ExtLink" href="http://en.wikipedia.org/" class="external autonumber"></a></p>
!! end
!! test
<p><a rel="nofollow" class="external autonumber" href="HttP://MediaWiki.Org/">[1]</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external autonumber" href="HttP://MediaWiki.Org/"></a></p>
+<p><a rel="mw:ExtLink" href="HttP://MediaWiki.Org/" class="external autonumber"></a></p>
!! end
!!test
<p><a rel="nofollow" class="external free" href="HttP://MediaWiki.Org/">HttP://MediaWiki.Org/</a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external free" href="HttP://MediaWiki.Org/">HttP://MediaWiki.Org/</a></p>
+<p><a rel="mw:ExtLink" href="HttP://MediaWiki.Org/" class="external free">HttP://MediaWiki.Org/</a></p>
!! end
!!test
<b><small><figure class="mw-halign-right" typeof="mw:Image"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/300px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="34" width="300"/></a><figcaption></figcaption></figure></small></b>
!! end
+## Just a regression test
+!! test
+Wikilink with only closing tag in target
+!! options
+parsoid=wt2html
+!! wikitext
+[[Test|</span>]]
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="./Test" title="Test"></a></p>
+!! end
+
#### ----------------------------------------------------------------
#### Parsoid-only testing of Parsoid's impl of LST
#### Not implemented yet, see
!!end
!! test
-Don't block XML namespace declaration
+T72867: Don't block XML namespace declaration
!! wikitext
<span xmlns:dct="http://purl.org/dc/terms/" property="dct:title">MediaWiki</span>
!! html/php
[[es:Toxine_bactérienne]]
!! end
+# Regression test for T219023
+!! test
+Emit simple non-piped link where possible
+!! options
+parsoid=html2wt
+!! html/parsoid
+<a rel='mw:WikiLink' href='./VisualEditor'>VisualEditor</a>
+<a rel='mw:WikiLink' href='./VisualEditor'>visualEditor</a>
+<a rel='mw:WikiLink' href='./VisualEditor link'>VisualEditor link</a>
+<a rel='mw:WikiLink' href='./VisualEditor link'>visualEditor link</a>
+!! wikitext
+[[VisualEditor]]
+[[visualEditor]]
+[[VisualEditor link]]
+[[visualEditor link]]
+!! end
+
!! test
Image: Modifying size of an image (1)
!! options
!! wikitext
x<nowiki/>http://cscott.net<nowiki/>x
!! html/parsoid
-<p>x<a rel="mw:ExtLink" class="external free" href="http://cscott.net">http://cscott.net</a>x</p>
+<p>x<a rel="mw:ExtLink" href="http://cscott.net" class="external free">http://cscott.net</a>x</p>
!! end
# this is the "easy" test because it leaves in place all the
http://example.com <nowiki>http://example.com</nowiki> is not a link.
!! end
+!! test
+WTS of an autolink surrounded by square brackets (T220018)
+!! options
+parsoid=html2wt
+!! html/parsoid
+<p>[<a rel="mw:ExtLink" href="http://example.com">http://example.com</a>]</p>
+!! wikitext
+<nowiki>[</nowiki>http://example.com]
+!! end
+
+!! test
+WTS of edited autolink surrounded by square brackets (T220018)
+!! options
+parsoid={
+ "modes": ["wt2wt"],
+ "changes": [
+ [ "a", "before", "[" ],
+ [ "a", "after", "]" ]
+ ]
+}
+!! wikitext
+http://example.com
+!! wikitext/edited
+<nowiki>[</nowiki>http://example.com]
+!! end
+
+!! test
+WTS of an external link surrounded by square brackets (T220018)
+!! options
+parsoid=html2wt
+!! html/parsoid
+<p>[<a rel="mw:ExtLink" href="http://example.com">foo</a>]</p>
+!! wikitext
+<nowiki>[</nowiki>[http://example.com foo]]
+!! end
+
+!! test
+WTS of edited external link surrounded by square brackets (T220018)
+!! options
+parsoid={
+ "modes": ["wt2wt"],
+ "changes": [
+ [ "a", "before", "[" ],
+ [ "a", "after", "]" ]
+ ]
+}
+!! wikitext
+[http://example.com foo]
+!! wikitext/edited
+<nowiki>[</nowiki>[http://example.com foo]]
+!! end
+
!! test
Magic links inside links (not autolinked)
!! wikitext
<a rel="mw:WikiLink" href="./Foo" title="Foo">PMID 1234</a>
<a rel="mw:WikiLink" href="./Foo" title="Foo">ISBN 123456789x</a></p>
-<p><a rel="mw:ExtLink" class="external text" href="http://foo.com">http://example.com</a>
-<a rel="mw:ExtLink" class="external text" href="http://foo.com">RFC 1234</a>
-<a rel="mw:ExtLink" class="external text" href="http://foo.com">PMID 1234</a>
-<a rel="mw:ExtLink" class="external text" href="http://foo.com">ISBN 123456789x</a></p>
+<p><a rel="mw:ExtLink" href="http://foo.com" class="external text">http://example.com</a>
+<a rel="mw:ExtLink" href="http://foo.com" class="external text">RFC 1234</a>
+<a rel="mw:ExtLink" href="http://foo.com" class="external text">PMID 1234</a>
+<a rel="mw:ExtLink" href="http://foo.com" class="external text">ISBN 123456789x</a></p>
!! end
!! test
<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" decoding="async" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div><a class="external mw-magiclink-pmid" rel="nofollow" href="//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract">PMID 1234</a></div></div></div>
<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" decoding="async" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div><a href="/wiki/Special:BookSources/123456789X" class="internal mw-magiclink-isbn">ISBN 123456789x</a></div></div></div>
!! html/parsoid
-<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a rel="mw:ExtLink" class="external free" href="http://example.com">http://example.com</a></figcaption></figure>
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a rel="mw:ExtLink" href="http://example.com" class="external free">http://example.com</a></figcaption></figure>
<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a href="https://tools.ietf.org/html/rfc1234" rel="mw:ExtLink" class="external mw-magiclink">RFC 1234</a></figcaption></figure>
<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a href="//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract" rel="mw:ExtLink" class="external mw-magiclink">PMID 1234</a></figcaption></figure>
<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><a href="./Special:BookSources/123456789X" rel="mw:WikiLink">ISBN 123456789x</a></figcaption></figure>
!! wikitext
{{echo|hi}}[http://example.com [[ho]]]
!! html/parsoid
-<p><span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"hi"}},"i":0}}]}'>hi</span><a rel="mw:ExtLink" class="external autonumber" href="http://example.com"></a><a rel="mw:WikiLink" href="./Ho" title="Ho" data-parsoid='{"misnested":true}'>ho</a></p>
+<p><span about="#mwt1" typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"hi"}},"i":0}}]}'>hi</span><a rel="mw:ExtLink" href="http://example.com" class="external autonumber"></a><a rel="mw:WikiLink" href="./Ho" title="Ho" data-parsoid='{"misnested":true}'>ho</a></p>
!! end
!! test
!! options
parsoid=html2wt
!! html/parsoid
-<span about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"pi":[[{"k":"1"}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"a"}},"i":0}}]}'>a</span><table about="#mwt2" typeof="mw:Transclusion mw:ExpandedAttrs" data-parsoid='{"a":{"{{echo|c\n{{!}}d\n}}":null},"sa":{"{{echo|c\n{{!}}d\n}}":""},"firstWikitextNode":"table","pi":[[{"k":"1"}]]}' data-mw='{"parts":["{|",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"c\n{{!}}d\n"}},"i":0}},"\n|}"]}'>
+<span about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"pi":[[{"k":"1"}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"a"}},"i":0}}]}'>a</span><table about="#mwt2" typeof="mw:Transclusion mw:ExpandedAttrs" data-parsoid='{"a":{"{{echo|c\n{{!}}d\n}}":null},"sa":{"{{echo|c\n{{!}}d\n}}":""},"firstWikitextNode":"TABLE","pi":[[{"k":"1"}]]}' data-mw='{"parts":["{|",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"c\n{{!}}d\n"}},"i":0}},"\n|}"]}'>
<tbody><tr><td>d
</td></tr>
</tbody></table>
</p>
<a rel="nofollow" class="external text" href="http://www.google.com"></a><div class="thumb tright"><a rel="nofollow" class="external text" href="http://www.google.com"></a><div class="thumbinner" style="width:182px;"><a rel="nofollow" class="external text" href="http://www.google.com"></a><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" decoding="async" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"></a></div>123</div></div></div>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external text" href="http://www.google.com" data-parsoid='{"targetOff":23,"contentOffsets":[23,46]}'><figure-inline class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"123"}]}' data-mw='{"caption":"123"}'></figure-inline></a><a href="./File:Foobar.jpg" data-parsoid='{"misnested":true}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"},"misnested":true}'/></a></p>
+<p><a rel="mw:ExtLink" href="http://www.google.com" class="external text" data-parsoid='{"targetOff":23,"contentOffsets":[23,46]}'><figure-inline class="mw-default-size" typeof="mw:Image" data-parsoid='{"optList":[{"ck":"caption","ak":"123"}]}' data-mw='{"caption":"123"}'></figure-inline></a><a href="./File:Foobar.jpg" data-parsoid='{"misnested":true}'><img resource="./File:Foobar.jpg" src="//example.com/images/3/3a/Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="220" width="1941" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"220","width":"1941"},"sa":{"resource":"File:Foobar.jpg"},"misnested":true}'/></a></p>
-<a rel="mw:ExtLink" class="external autonumber" href="http://www.google.com" data-parsoid='{"targetOff":72,"contentOffsets":[72,101]}'></a><figure class="mw-default-size" typeof="mw:Image/Thumb" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"caption","ak":"123"}]}'><a rel="mw:ExtLink" class="external autonumber" href="http://www.google.com" data-parsoid='{"targetOff":72,"contentOffsets":[72,101]}'></a><a href="./File:Foobar.jpg" data-parsoid='{"misnested":true}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"resource":"File:Foobar.jpg"},"misnested":true}'/></a><figcaption data-parsoid='{"misnested":true}'>123</figcaption></figure>
+<a rel="mw:ExtLink" href="http://www.google.com" class="external autonumber" data-parsoid='{"targetOff":72,"contentOffsets":[72,101]}'></a><figure class="mw-default-size" typeof="mw:Image/Thumb" data-parsoid='{"optList":[{"ck":"thumbnail","ak":"thumb"},{"ck":"caption","ak":"123"}]}'><a rel="mw:ExtLink" href="http://www.google.com" class="external autonumber" data-parsoid='{"targetOff":72,"contentOffsets":[72,101]}'></a><a href="./File:Foobar.jpg" data-parsoid='{"misnested":true}'><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220" data-parsoid='{"a":{"resource":"./File:Foobar.jpg","height":"25","width":"220"},"sa":{"resource":"File:Foobar.jpg"},"misnested":true}'/></a><figcaption data-parsoid='{"misnested":true}'>123</figcaption></figure>
!! end
# --------------------------------------------
{{echo|foo}}
!! end
+!! test
+Only html p-tag is strong indent pre suppressing
+!! options
+parsoid=html2wt
+!! html/parsoid
+<p>test2<span>
+ test3
+</span></p>
+!! wikitext
+test2<span>
+<nowiki> </nowiki>test3
+</span>
+!! end
+
# -----------------------------------------------------------------
# End of section for Parsoid-only html2wt tests for serialization
# of new content
<a rel="nofollow" class="external text" href="http://example.com/">link <span title="title with [brackets]">span</span></a>
</p>
!! html/parsoid
-<p><a rel="mw:ExtLink" class="external text" href="http://example.com/">link <span title="title with [brackets]">span</span></a>
-<a rel="mw:ExtLink" class="external text" href="http://example.com/">link <span title="title with [brackets]" data-parsoid='{"stx":"html","a":{"title":"title with [brackets]"},"sa":{"title":"title with &#91;brackets&#93;"}}'>span</span></a></p>
+<p><a rel="mw:ExtLink" href="http://example.com/" class="external text">link <span title="title with [brackets]">span</span></a>
+<a rel="mw:ExtLink" href="http://example.com/" class="external text">link <span title="title with [brackets]" data-parsoid='{"stx":"html","a":{"title":"title with [brackets]"},"sa":{"title":"title with &#91;brackets&#93;"}}'>span</span></a></p>
!! end
!! test
*foo
footer
!! end
+
+!! test
+Ensure disambiguation links are marked properly
+!! options
+parsoid=wt2html
+!! wikitext
+[[Disambiguation]]
+!! html/parsoid
+<p><a rel="mw:WikiLink" href="./Disambiguation" title="Disambiguation" class="mw-disambig">Disambiguation</a></p>
+!! end
$this->tmpFiles = array_merge( $this->tmpFiles, (array)$files );
}
- // @todo Make const when we no longer support HHVM (T192166)
- private static $namespaceAffectingSettings = [
- 'wgAllowImageMoving',
- 'wgCanonicalNamespaceNames',
- 'wgCapitalLinkOverrides',
- 'wgCapitalLinks',
- 'wgContentNamespaces',
- 'wgExtensionMessagesFiles',
- 'wgExtensionNamespaces',
- 'wgExtraNamespaces',
- 'wgExtraSignatureNamespaces',
- 'wgNamespaceContentModels',
- 'wgNamespaceProtection',
- 'wgNamespacesWithSubpages',
- 'wgNonincludableNamespaces',
- 'wgRestrictionLevels',
- ];
-
protected function tearDown() {
global $wgRequest, $wgSQLMode;
foreach ( $this->iniSettings as $name => $value ) {
ini_set( $name, $value );
}
- if (
- array_intersect( self::$namespaceAffectingSettings, array_keys( $this->mwGlobals ) ) ||
- array_intersect( self::$namespaceAffectingSettings, $this->mwGlobalsToUnset )
- ) {
- $this->resetNamespaces();
- }
$this->mwGlobals = [];
$this->mwGlobalsToUnset = [];
$this->restoreLoggers();
);
if ( $name === 'ContentLanguage' ) {
- $this->doSetMwGlobals( [ 'wgContLang' => $this->localServices->getContentLanguage() ] );
+ $this->setMwGlobals( [ 'wgContLang' => $this->localServices->getContentLanguage() ] );
}
}
* The key is added to the array of globals that will be reset afterwards
* in the tearDown().
*
- * It may be necessary to call resetServices() to allow any changed configuration variables
- * to take effect on services that get initialized based on these variables.
- *
* @par Example
* @code
* protected function setUp() {
* @param mixed|null $value Value to set the global to (ignored
* if an array is given as first argument).
*
- * @note To allow changes to global variables to take effect on global service instances,
- * call resetServices().
+ * @note This will call resetServices().
*
* @since 1.21
*/
$pairs = [ $pairs => $value ];
}
- if ( isset( $pairs['wgContLang'] ) ) {
- throw new MWException(
- 'No setting $wgContLang, use setContentLang() or setService( \'ContentLanguage\' )'
- );
- }
-
- $this->doSetMwGlobals( $pairs, $value );
- }
-
- /**
- * An internal method that allows setService() to set globals that tests are not supposed to
- * touch.
- */
- private function doSetMwGlobals( $pairs, $value = null ) {
$this->doStashMwGlobals( array_keys( $pairs ) );
foreach ( $pairs as $key => $value ) {
$GLOBALS[$key] = $value;
}
- if ( array_intersect( self::$namespaceAffectingSettings, array_keys( $pairs ) ) ) {
- $this->resetNamespaces();
- }
+ $this->resetServices();
}
/**
ini_set( $name, $value );
}
- /**
- * Must be called whenever namespaces are changed, e.g., $wgExtraNamespaces is altered.
- * Otherwise old namespace data will lurk and cause bugs.
- */
- private function resetNamespaces() {
- if ( !$this->localServices ) {
- throw new Exception( __METHOD__ . ' must be called after MediaWikiTestCase::run()' );
- }
-
- if ( $this->localServices !== MediaWikiServices::getInstance() ) {
- throw new Exception( __METHOD__ . ' will not work because the global MediaWikiServices '
- . 'instance has been replaced by test code.' );
- }
-
- Language::clearCaches();
- }
-
/**
* Check if we can back up a value by performing a shallow copy.
* Values which fail this test are copied recursively.
* Useful for setting some entries in a configuration array, instead of
* setting the entire array.
*
- * It may be necessary to call resetServices() to allow any changed configuration variables
- * to take effect on services that get initialized based on these variables.
- *
* @param string $name The name of the global, as in wgFooBar
* @param array $values The array containing the entries to set in that global
*
* @throws MWException If the designated global is not an array.
*
- * @note To allow changes to global variables to take effect on global service instances,
- * call resetServices().
+ * @note This will call resetServices().
*
* @since 1.21
*/
}
self::resetGlobalParser();
+ Language::clearCaches();
}
/**
*/
public function setContentLang( $lang ) {
if ( $lang instanceof Language ) {
- $this->setMwGlobals( 'wgLanguageCode', $lang->getCode() );
// Set to the exact object requested
$this->setService( 'ContentLanguage', $lang );
+ $this->setMwGlobals( 'wgLanguageCode', $lang->getCode() );
} else {
$this->setMwGlobals( 'wgLanguageCode', $lang );
- // Let the service handler make up the object. Avoid calling setService(), because if
- // we do, overrideMwServices() will complain if it's called later on.
- $services = MediaWikiServices::getInstance();
- $services->resetServiceForTesting( 'ContentLanguage' );
- $this->doSetMwGlobals( [ 'wgContLang' => $services->getContentLanguage() ] );
}
}
* or three values to set a single permission, like
* $this->setGroupPermissions( '*', 'read', false );
*
+ * @note This will call resetServices().
+ *
* @since 1.31
* @param array|string $newPerms Either an array of permissions to change,
* in which case the next two parameters are ignored; or a single string
}
$this->setMwGlobals( 'wgGroupPermissions', $newPermissions );
-
- // Reset services so they pick up the new permissions.
- // Resetting just PermissionManager is not sufficient, since other services may
- // have the old instance of PermissionManager injected.
- $this->resetServices();
}
/**
/**
* Create a temporary hook handler which will be reset by tearDown.
* This replaces other handlers for the same hook.
+ *
+ * @note This will call resetServices().
+ *
* @param string $hookName Hook name
* @param mixed $handler Value suitable for a hook handler
* @since 1.28
...array_map( [ $this, 'matches' ], $values )
) );
}
+
+ /**
+ * Return a PHPUnit mock that is expected to never have any methods called on it.
+ *
+ * @param string $type
+ * @return object
+ */
+ protected function createNoOpMock( $type ) {
+ $mock = $this->createMock( $type );
+ $mock->expects( $this->never() )->method( $this->anything() );
+ return $mock;
+ }
}
global $wgHooks;
$wgHooks[$hookName] = [ $handler ];
}
+
+ protected function getMockMessage( $text, ...$params ) {
+ if ( isset( $params[0] ) && is_array( $params[0] ) ) {
+ $params = $params[0];
+ }
+
+ $msg = $this->getMockBuilder( Message::class )
+ ->disableOriginalConstructor()
+ ->setMethods( [] )
+ ->getMock();
+
+ $msg->method( 'toString' )->willReturn( $text );
+ $msg->method( '__toString' )->willReturn( $text );
+ $msg->method( 'text' )->willReturn( $text );
+ $msg->method( 'parse' )->willReturn( $text );
+ $msg->method( 'plain' )->willReturn( $text );
+ $msg->method( 'parseAsBlock' )->willReturn( $text );
+ $msg->method( 'escaped' )->willReturn( $text );
+
+ $msg->method( 'title' )->willReturn( $msg );
+ $msg->method( 'inLanguage' )->willReturn( $msg );
+ $msg->method( 'inContentLanguage' )->willReturn( $msg );
+ $msg->method( 'useDatabase' )->willReturn( $msg );
+ $msg->method( 'setContext' )->willReturn( $msg );
+
+ $msg->method( 'exists' )->willReturn( true );
+ $msg->method( 'content' )->willReturn( new MessageContent( $msg ) );
+
+ return $msg;
+ }
}
DELETE {
?category ?x ?y
} WHERE {
+ ?category ?x ?y
VALUES ?category {
<http://acme.test/wiki/Category:Test> <http://acme.test/wiki/Category:Test_2>
}
DELETE {
?category ?x ?y
} WHERE {
+ ?category ?x ?y
VALUES ?category {
<http://acme.test/wiki/Category:Changed_category>
}
DELETE {
?category ?x ?y
} WHERE {
+ ?category ?x ?y
VALUES ?category {
<http://acme.test/wiki/Category:Test> <http://acme.test/wiki/Category:MovedTo> <http://acme.test/wiki/Category:Test_2> <http://acme.test/wiki/Category:Test_3> <http://acme.test/wiki/Category:Test_4>
}
// for User::getActorId()
'wgActorTableSchemaMigrationStage' => $stage
] );
- $this->overrideMwServices();
$user = $this->getMutableTestUser()->getUser();
$userIdentity = $this->getMock( UserIdentity::class );
// Note, there are some obscure globals which
// could affect the results which aren't included above.
- $this->overrideMwServices();
$context = RequestContext::getMain();
$resp = $context->getRequest()->response();
$conf = $context->getConfig();
fwrite( $f, 'Message' );
fclose( $f );
- // Reset the service to avoid cached results
- $this->overrideMwServices();
-
$this->assertTrue( wfReadOnly() );
$this->assertTrue( wfReadOnly() ); # Check cached
}
$this->setMwGlobals( [
'wgReadOnly' => 'reason'
] );
- $this->overrideMwServices();
$this->assertSame( 'reason', wfReadOnlyReason() );
}
] );
$this->setLogger( 'wfDebug', new LegacyLogger( 'wfDebug' ) );
+ unlink( $debugLogFile );
wfDebug( "This is a normal string" );
$this->assertEquals( "This is a normal string\n", file_get_contents( $debugLogFile ) );
- unlink( $debugLogFile );
+ unlink( $debugLogFile );
wfDebug( "This is nöt an ASCII string" );
$this->assertEquals( "This is nöt an ASCII string\n", file_get_contents( $debugLogFile ) );
- unlink( $debugLogFile );
+ unlink( $debugLogFile );
wfDebug( "\00305This has böth UTF and control chars\003" );
$this->assertEquals(
" 05This has böth UTF and control chars \n",
file_get_contents( $debugLogFile )
);
- unlink( $debugLogFile );
+ unlink( $debugLogFile );
wfDebugMem();
$this->assertGreaterThan(
1000,
preg_replace( '/\D/', '', file_get_contents( $debugLogFile ) )
);
- unlink( $debugLogFile );
+ unlink( $debugLogFile );
wfDebugMem( true );
$this->assertGreaterThan(
1000000,
preg_replace( '/\D/', '', file_get_contents( $debugLogFile ) )
);
+
unlink( $debugLogFile );
}
// Don't try to fetch the files from Commons or anything, please
$this->setMwGlobals( 'wgForeignFileRepos', [] );
- // We need to reset services immediately so that editPage() doesn't use the old RepoGroup
- // and hit the network
- $this->resetServices();
// XXX How do we get file redirects to work?
$this->editPage( 'File:Redirect to bad.jpg', '#REDIRECT [[Bad.jpg]]' );
*/
public function testRawHtmlInMsg() {
$this->setMwGlobals( 'wgRawHtml', true );
- // We have to reset the core hook registration.
- // to register the html hook
- $this->overrideMwServices();
$msg = new RawMessage( '<html><script>alert("xss")</script></html>' );
$txt = '<span class="error"><html> tags cannot be' .
* @group Database
*/
class MovePageTest extends MediaWikiTestCase {
- /**
- * @param string $class
- * @return object A mock that throws on any method call
- */
- private function getNoOpMock( $class ) {
- $mock = $this->createMock( $class );
- $mock->expects( $this->never() )->method( $this->anythingBut( '__destruct' ) );
- return $mock;
- }
-
/**
* The only files that exist are 'File:Existent.jpg', 'File:Existent2.jpg', and
* 'File:Existent-file-no-page.jpg'. Calling unexpected methods causes a test failure.
private function newMovePage( $old, $new, array $params = [] ) : MovePage {
$mockLB = $this->createMock( LoadBalancer::class );
$mockLB->method( 'getConnection' )
- ->willReturn( $params['db'] ?? $this->getNoOpMock( IDatabase::class ) );
+ ->willReturn( $params['db'] ?? $this->createNoOpMock( IDatabase::class ) );
$mockLB->expects( $this->never() )
->method( $this->anythingBut( 'getConnection', '__destruct' ) );
),
$mockLB,
$params['nsInfo'] ?? $mockNsInfo,
- $params['wiStore'] ?? $this->getNoOpMock( WatchedItemStore::class ),
- $params['permMgr'] ?? $this->getNoOpMock( PermissionManager::class ),
+ $params['wiStore'] ?? $this->createNoOpMock( WatchedItemStore::class ),
+ $params['permMgr'] ?? $this->createNoOpMock( PermissionManager::class ),
$params['repoGroup'] ?? $this->getMockRepoGroup()
);
}
foreach ( $extraOptions as $key => $val ) {
$this->setMwGlobals( "wg$key", $val );
}
- $this->overrideMwServices();
$this->setService( 'RepoGroup', $this->getMockRepoGroup() );
// This returns true instead of an array if there are no errors
$this->hideDeprecated( 'Title::isValidMoveOperation' );
$this->user = $this->userUser;
}
-
- $this->resetServices();
}
public function tearDown() {
} else {
$this->user = $this->altUser;
}
- $this->resetServices();
}
/**
global $wgGroupPermissions;
$old = $wgGroupPermissions;
- $wgGroupPermissions = [];
- $this->resetServices();
+ $this->setMwGlobals( 'wgGroupPermissions', [] );
$this->assertEquals( $check[$action][1],
MediaWikiServices::getInstance()->getPermissionManager()
$this->assertEquals( $check[$action][1],
MediaWikiServices::getInstance()->getPermissionManager()
->getPermissionErrors( $action, $this->user, $this->title, 'secure' ) );
- $wgGroupPermissions = $old;
- $this->resetServices();
+ $this->setMwGlobals( 'wgGroupPermissions', $old );
$this->overrideUserPermissions( $this->user, $action );
$this->assertEquals( $check[$action][2],
->userCan( $action, $this->user, $this->title, true ) );
$this->assertEquals( $check[$action][3],
MediaWikiServices::getInstance()->getPermissionManager()
- ->userCan( $action, $this->user, $this->title,
- PermissionManager::RIGOR_QUICK ) );
+ ->quickUserCan( $action, $this->user, $this->title ) );
# count( User::getGroupsWithPermissions( $action ) ) < 1
}
}
protected function runGroupPermissions( $perm, $action, $result, $result2 = null ) {
- global $wgGroupPermissions;
-
if ( $result2 === null ) {
$result2 = $result;
}
- $wgGroupPermissions['autoconfirmed']['move'] = false;
- $wgGroupPermissions['user']['move'] = false;
- $this->resetServices();
+ $this->setGroupPermissions( 'autoconfirmed', 'move', false );
+ $this->setGroupPermissions( 'user', 'move', false );
$this->overrideUserPermissions( $this->user, $perm );
$res = MediaWikiServices::getInstance()->getPermissionManager()
->getPermissionErrors( $action, $this->user, $this->title );
$this->assertEquals( $result, $res );
- $wgGroupPermissions['autoconfirmed']['move'] = true;
- $wgGroupPermissions['user']['move'] = false;
- $this->resetServices();
+ $this->setGroupPermissions( 'autoconfirmed', 'move', true );
+ $this->setGroupPermissions( 'user', 'move', false );
$this->overrideUserPermissions( $this->user, $perm );
$res = MediaWikiServices::getInstance()->getPermissionManager()
->getPermissionErrors( $action, $this->user, $this->title );
$this->assertEquals( $result2, $res );
- $wgGroupPermissions['autoconfirmed']['move'] = true;
- $wgGroupPermissions['user']['move'] = true;
- $this->resetServices();
+ $this->setGroupPermissions( 'autoconfirmed', 'move', true );
+ $this->setGroupPermissions( 'user', 'move', true );
$this->overrideUserPermissions( $this->user, $perm );
$res = MediaWikiServices::getInstance()->getPermissionManager()
->getPermissionErrors( $action, $this->user, $this->title );
$this->assertEquals( $result2, $res );
- $wgGroupPermissions['autoconfirmed']['move'] = false;
- $wgGroupPermissions['user']['move'] = true;
- $this->resetServices();
+ $this->setGroupPermissions( 'autoconfirmed', 'move', false );
+ $this->setGroupPermissions( 'user', 'move', true );
$this->overrideUserPermissions( $this->user, $perm );
$res = MediaWikiServices::getInstance()->getPermissionManager()
->getPermissionErrors( $action, $this->user, $this->title );
$this->assertEquals( true,
MediaWikiServices::getInstance()->getPermissionManager()
- ->userCan( 'edit', $this->user, $this->title, PermissionManager::RIGOR_QUICK ) );
+ ->quickUserCan( 'edit', $this->user, $this->title ) );
$this->title->mRestrictions = [ "edit" => [ 'bogus', "sysop", "protect", "" ],
"bogus" => [ 'bogus', "sysop", "protect", "" ] ];
$this->assertEquals( false,
MediaWikiServices::getInstance()->getPermissionManager()
- ->userCan( 'bogus', $this->user, $this->title, PermissionManager::RIGOR_QUICK ) );
+ ->quickUserCan( 'bogus', $this->user, $this->title ) );
$this->assertEquals( false,
- MediaWikiServices::getInstance()->getPermissionManager()->userCan(
- 'edit', $this->user, $this->title, PermissionManager::RIGOR_QUICK ) );
+ MediaWikiServices::getInstance()->getPermissionManager()->quickUserCan(
+ 'edit', $this->user, $this->title ) );
$this->assertEquals( [ [ 'badaccess-group0' ],
[ 'protectedpagetext', 'bogus', 'bogus' ],
$this->overrideUserPermissions( $this->user, [ "edit", "editprotected" ] );
$this->assertEquals( false,
- MediaWikiServices::getInstance()->getPermissionManager()->userCan(
- 'bogus', $this->user, $this->title, PermissionManager::RIGOR_QUICK ) );
+ MediaWikiServices::getInstance()->getPermissionManager()->quickUserCan(
+ 'bogus', $this->user, $this->title ) );
$this->assertEquals( false,
- MediaWikiServices::getInstance()->getPermissionManager()->userCan(
- 'edit', $this->user, $this->title, PermissionManager::RIGOR_QUICK ) );
+ MediaWikiServices::getInstance()->getPermissionManager()->quickUserCan(
+ 'edit', $this->user, $this->title ) );
$this->assertEquals( [ [ 'badaccess-group0' ],
[ 'protectedpagetext', 'bogus', 'bogus' ],
->getPermissionErrors( 'edit', $this->user, $this->title ) );
$this->setMwGlobals( 'wgEmailConfirmToEdit', false );
- $this->resetServices();
$this->overrideUserPermissions( $this->user, [
'createpage',
'edit',
->userCan( 'move-target', $this->user, $this->title ) );
// quickUserCan should ignore user blocks
$this->assertEquals( true, MediaWikiServices::getInstance()->getPermissionManager()
- ->userCan( 'move-target', $this->user, $this->title,
- PermissionManager::RIGOR_QUICK ) );
+ ->quickUserCan( 'move-target', $this->user, $this->title ) );
global $wgLocalTZoffset;
$wgLocalTZoffset = -60;
$rights = array_diff( $rights, [ 'writetest' ] );
} );
- $this->resetServices();
$rights = MediaWikiServices::getInstance()
->getPermissionManager()
->getUserPermissions( $user );
'wgRestrictionLevels' => [ '', 'autoconfirmed', 'sysop' ],
'wgAutopromote' => []
] );
- $this->resetServices();
$user = is_null( $userGroups ) ? null : $this->getTestUser( $userGroups )->getUser();
$this->assertSame( $expected, MediaWikiServices::getInstance()
->getPermissionManager()
->getNamespaceRestrictionLevels( $ns, $user ) );
}
+
+ /**
+ * @covers \MediaWiki\Permissions\PermissionManager::getAllPermissions
+ */
+ public function testGetAllPermissions() {
+ $this->setMwGlobals( [
+ 'wgAvailableRights' => [ 'test_right' ]
+ ] );
+ $this->resetServices();
+ $this->assertContains(
+ 'test_right',
+ MediaWikiServices::getInstance()
+ ->getPermissionManager()
+ ->getAllPermissions()
+ );
+ }
+
+ /**
+ * @covers \MediaWiki\Permissions\PermissionManager::getRightsCacheKey
+ * @throws \Exception
+ */
+ public function testAnonPermissionsNotClash() {
+ $user1 = User::newFromName( 'User1' );
+ $user2 = User::newFromName( 'User2' );
+ $pm = MediaWikiServices::getInstance()->getPermissionManager();
+ $pm->overrideUserRightsForTesting( $user2, [] );
+ $this->assertNotSame( $pm->getUserPermissions( $user1 ), $pm->getUserPermissions( $user2 ) );
+ }
+
+ /**
+ * @covers \MediaWiki\Permissions\PermissionManager::getRightsCacheKey
+ */
+ public function testAnonPermissionsNotClashOneRegistered() {
+ $user1 = User::newFromName( 'User1' );
+ $user2 = $this->getTestSysop()->getUser();
+ $pm = MediaWikiServices::getInstance()->getPermissionManager();
+ $this->assertNotSame( $pm->getUserPermissions( $user1 ), $pm->getUserPermissions( $user2 ) );
+ }
}
$this->originalHandlers = TestingAccessWrapper::newFromClass( Hooks::class )->handlers;
TestingAccessWrapper::newFromClass( Hooks::class )->handlers = [];
-
- $this->overrideMwServices();
}
public function tearDown() {
$user = User::newFromName( 'Test user' );
// 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 ) {
*/
public function testRevisionSelectFields( $migrationStageSettings, $expected ) {
$this->setMwGlobals( $migrationStageSettings );
- $this->overrideMwServices();
$this->hideDeprecated( 'Revision::selectFields' );
$this->assertArrayEqualsIgnoringIntKeyOrder( $expected, Revision::selectFields() );
*/
public function testRevisionSelectArchiveFields( $migrationStageSettings, $expected ) {
$this->setMwGlobals( $migrationStageSettings );
- $this->overrideMwServices();
$this->hideDeprecated( 'Revision::selectArchiveFields' );
$this->assertArrayEqualsIgnoringIntKeyOrder( $expected, Revision::selectArchiveFields() );
public function testRevisionUserJoinCond() {
$this->hideDeprecated( 'Revision::userJoinCond' );
$this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_OLD );
- $this->overrideMwServices();
$this->assertEquals(
[ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
Revision::userJoinCond()
*/
public function testRevisionGetArchiveQueryInfo( $migrationStageSettings, $expected ) {
$this->setMwGlobals( $migrationStageSettings );
- $this->overrideMwServices();
$queryInfo = Revision::getArchiveQueryInfo();
$this->assertQueryInfoEquals( $expected, $queryInfo );
*/
public function testRevisionGetQueryInfo( $migrationStageSettings, $options, $expected ) {
$this->setMwGlobals( $migrationStageSettings );
- $this->overrideMwServices();
$queryInfo = Revision::getQueryInfo( $options );
$this->assertQueryInfoEquals( $expected, $queryInfo );
*/
public function testRevisionStoreGetQueryInfo( $migrationStageSettings, $options, $expected ) {
$this->setMwGlobals( $migrationStageSettings );
- $this->overrideMwServices();
$store = MediaWikiServices::getInstance()->getRevisionStore();
$expected
) {
$this->setMwGlobals( $migrationStageSettings );
- $this->overrideMwServices();
$store = MediaWikiServices::getInstance()->getRevisionStore();
*/
public function testRevisionStoreGetArchiveQueryInfo( $migrationStageSettings, $expected ) {
$this->setMwGlobals( $migrationStageSettings );
- $this->overrideMwServices();
$store = MediaWikiServices::getInstance()->getRevisionStore();
'wgContentHandlerUseDB' => $this->getContentHandlerUseDB(),
'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
] );
-
- $this->overrideMwServices();
}
protected function addCoreDBData() {
$title = $this->getTestPageTitle();
$rev = $this->getRevisionRecordFromDetailsArray( $revDetails );
- $this->overrideMwServices();
$store = MediaWikiServices::getInstance()->getRevisionStore();
$return = $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) );
'user' => true,
];
- $this->overrideMwServices();
$store = MediaWikiServices::getInstance()->getRevisionStore();
// Insert the first revision
* @covers \MediaWiki\Revision\RevisionStore::findSlotContentId
*/
public function testNewNullRevision( Title $title, $revDetails, $comment, $minor = false ) {
- $this->overrideMwServices();
-
$user = TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser();
$page = WikiPage::factory( $title );
*/
public function testNewRevisionFromRow_anonEdit_legacyEncoding() {
$this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' );
- $this->overrideMwServices();
$page = $this->getTestPage();
$text = __METHOD__ . 'a-ä';
/** @var Revision $rev */
*/
public function testNewRevisionFromArchiveRow_legacyEncoding() {
$this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' );
- $this->overrideMwServices();
$store = MediaWikiServices::getInstance()->getRevisionStore();
$title = Title::newFromText( __METHOD__ );
$text = __METHOD__ . '-bä';
public function testGetKnownCurrentRevision_userNameChange() {
global $wgActorTableSchemaMigrationStage;
- $this->overrideMwServices();
$cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
$this->setService( 'MainWANObjectCache', $cache );
* @covers \MediaWiki\Revision\RevisionStore::getKnownCurrentRevision
*/
public function testGetKnownCurrentRevision_revDelete() {
- $this->overrideMwServices();
$cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
$this->setService( 'MainWANObjectCache', $cache );
'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
] );
- $this->overrideMwServices();
-
if ( !$this->testPage ) {
/**
* We have to create a new page for each subclass as the page creation may result
* @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
*/
public function testConstructFromRowWithBadPageId() {
- $this->overrideMwServices();
Wikimedia\suppressWarnings();
$rev = new Revision( (object)[
'rev_page' => 77777777,
*/
public function testLoadFromTitle() {
$this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_NEW );
- $this->overrideMwServices();
$title = $this->getMockTitle();
$conditions = [
use InvalidArgumentException;
use Language;
use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\BlobAccessException;
use MediaWiki\Storage\SqlBlobStore;
use MediaWikiTestCase;
use stdClass;
}
/**
+ * @param string $blob
* @dataProvider provideBlobs
* @covers \MediaWiki\Storage\SqlBlobStore::storeBlob
* @covers \MediaWiki\Storage\SqlBlobStore::getBlob
$this->assertSame( $blob, $store->getBlob( $address ) );
}
+ /**
+ * @covers \MediaWiki\Storage\SqlBlobStore::storeBlob
+ * @covers \MediaWiki\Storage\SqlBlobStore::getBlobBatch
+ */
+ public function testSimpleStorageGetBlobBatchSimpleEmpty() {
+ $store = $this->getBlobStore();
+ $this->assertArrayEquals(
+ [],
+ $store->getBlobBatch( [] )->getValue()
+ );
+ }
+
+ /**
+ * @param string $blob
+ * @dataProvider provideBlobs
+ * @covers \MediaWiki\Storage\SqlBlobStore::storeBlob
+ * @covers \MediaWiki\Storage\SqlBlobStore::getBlobBatch
+ */
+ public function testSimpleStorageGetBlobBatchSimpleRoundtrip( $blob ) {
+ $store = $this->getBlobStore();
+ $addresses = [
+ $store->storeBlob( $blob ),
+ $store->storeBlob( $blob . '1' )
+ ];
+ $this->assertArrayEquals(
+ array_combine( $addresses, [ $blob, $blob . '1' ] ),
+ $store->getBlobBatch( $addresses )->getValue()
+ );
+ }
+
+ /**
+ * @covers \MediaWiki\Storage\SqlBlobStore::getBlob
+ */
+ public function testSimpleStorageNonExistentBlob() {
+ $this->setExpectedException( BlobAccessException::class );
+ $store = $this->getBlobStore();
+ $store->getBlob( 'tt:this_will_not_exist' );
+ }
+
+ /**
+ * @covers \MediaWiki\Storage\SqlBlobStore::getBlobBatch
+ */
+ public function testSimpleStorageNonExistentBlobBatch() {
+ $store = $this->getBlobStore();
+ $result = $store->getBlobBatch( [ 'tt:this_will_not_exist', 'tt:1000', 'bla:1001' ] );
+ $this->assertSame(
+ [
+ 'tt:this_will_not_exist' => null,
+ 'tt:1000' => null,
+ 'bla:1001' => null
+ ],
+ $result->getValue()
+ );
+ $this->assertSame( [
+ [
+ 'type' => 'warning',
+ 'message' => 'internalerror',
+ 'params' => [
+ 'Bad blob address: tt:this_will_not_exist'
+ ]
+ ],
+ [
+ 'type' => 'warning',
+ 'message' => 'internalerror',
+ 'params' => [
+ 'Unknown blob address schema: bla'
+ ]
+ ],
+ [
+ 'type' => 'warning',
+ 'message' => 'internalerror',
+ 'params' => [
+ 'Unable to fetch blob at tt:1000'
+ ]
+ ]
+ ], $result->getErrors() );
+ }
+
+ /**
+ * @covers \MediaWiki\Storage\SqlBlobStore::getBlobBatch
+ */
+ public function testSimpleStoragePartialNonExistentBlobBatch() {
+ $store = $this->getBlobStore();
+ $address = $store->storeBlob( 'test_data' );
+ $result = $store->getBlobBatch( [ $address, 'tt:this_will_not_exist_too' ] );
+ $this->assertSame(
+ [
+ $address => 'test_data',
+ 'tt:this_will_not_exist_too' => null
+ ],
+ $result->getValue()
+ );
+ $this->assertSame( [
+ [
+ 'type' => 'warning',
+ 'message' => 'internalerror',
+ 'params' => [
+ 'Bad blob address: tt:this_will_not_exist_too'
+ ]
+ ],
+ ], $result->getErrors() );
+ }
+
/**
* @dataProvider provideBlobs
* @covers \MediaWiki\Storage\SqlBlobStore::storeBlob
$this->user = $this->userUser;
}
- $this->resetServices();
}
protected function setTitle( $ns, $title = "Main_Page" ) {
global $wgGroupPermissions;
$old = $wgGroupPermissions;
- $wgGroupPermissions = [];
-
- $this->resetServices();
+ $this->setMwGlobals( 'wgGroupPermissions', [] );
$this->assertEquals( $check[$action][1],
$this->title->getUserPermissionsErrors( $action, $this->user, true ) );
$this->assertEquals( $check[$action][1],
$this->title->getUserPermissionsErrors( $action, $this->user, 'secure' ) );
- $wgGroupPermissions = $old;
- $this->resetServices();
+ $this->setMwGlobals( 'wgGroupPermissions', $old );
$this->overrideUserPermissions( $this->user, $action );
$this->assertEquals( $check[$action][2],
}
protected function runGroupPermissions( $action, $result, $result2 = null ) {
- global $wgGroupPermissions;
-
if ( $result2 === null ) {
$result2 = $result;
}
$userPermsOverrides = MediaWikiServices::getInstance()->getPermissionManager()
->getUserPermissions( $this->user );
- $wgGroupPermissions['autoconfirmed']['move'] = false;
- $wgGroupPermissions['user']['move'] = false;
- $this->resetServices();
+ $this->setGroupPermissions( 'autoconfirmed', 'move', false );
+ $this->setGroupPermissions( 'user', 'move', false );
$this->overrideUserPermissions( $this->user, $userPermsOverrides );
$res = $this->title->getUserPermissionsErrors( $action, $this->user );
$this->assertEquals( $result, $res );
- $wgGroupPermissions['autoconfirmed']['move'] = true;
- $wgGroupPermissions['user']['move'] = false;
- $this->resetServices();
+ $this->setGroupPermissions( 'autoconfirmed', 'move', true );
+ $this->setGroupPermissions( 'user', 'move', false );
$this->overrideUserPermissions( $this->user, $userPermsOverrides );
$res = $this->title->getUserPermissionsErrors( $action, $this->user );
$this->assertEquals( $result2, $res );
- $wgGroupPermissions['autoconfirmed']['move'] = true;
- $wgGroupPermissions['user']['move'] = true;
- $this->resetServices();
+ $this->setGroupPermissions( 'autoconfirmed', 'move', true );
+ $this->setGroupPermissions( 'user', 'move', true );
$this->overrideUserPermissions( $this->user, $userPermsOverrides );
$res = $this->title->getUserPermissionsErrors( $action, $this->user );
$this->assertEquals( $result2, $res );
- $wgGroupPermissions['autoconfirmed']['move'] = false;
- $wgGroupPermissions['user']['move'] = true;
- $this->resetServices();
+ $this->setGroupPermissions( 'autoconfirmed', 'move', false );
+ $this->setGroupPermissions( 'user', 'move', true );
$this->overrideUserPermissions( $this->user, $userPermsOverrides );
$res = $this->title->getUserPermissionsErrors( $action, $this->user );
$this->assertEquals( $result2, $res );
'wgEmailAuthentication' => true,
'wgBlockDisablesLogin' => false,
] );
- $this->resetServices();
$this->overrideUserPermissions(
$this->user,
$this->title->getUserPermissionsErrors( 'edit', $this->user ) );
$this->setMwGlobals( 'wgEmailConfirmToEdit', false );
- $this->resetServices();
$this->overrideUserPermissions(
$this->user,
[ 'createpage', 'edit', 'move', 'rollback', 'patrol', 'upload', 'purge' ]
],
],
] );
- $this->resetServices();
$now = time();
$this->user->mBlockedby = $this->user->getName();
]
]
] );
-
- // Reset services since we modified $wgLocalInterwikis
- $this->overrideMwServices();
}
/**
$this->setMwGlobals( 'wgRevokePermissions',
[ 'user' => [ 'applychangetags' => true ] ] );
- $this->resetServices();
-
$this->doBlock( [ 'tags' => 'custom tag' ] );
}
$this->mergeMwGlobalArrayValue( 'wgGroupPermissions',
[ 'sysop' => $newPermissions ] );
- $this->resetServices();
$res = $this->doBlock( [ 'hidename' => '' ] );
$dbw = wfGetDB( DB_MASTER );
$this->setMwGlobals( 'wgRevokePermissions',
[ 'sysop' => [ 'blockemail' => true ] ] );
- $this->resetServices();
-
$this->doBlock( [ 'noemail' => '' ] );
}
ChangeTags::defineTag( 'custom tag' );
$this->setMwGlobals( 'wgRevokePermissions',
[ 'user' => [ 'applychangetags' => true ] ] );
- $this->resetServices();
$this->editPage( $name, 'Some text' );
$this->tablesUsed,
[ 'change_tag', 'change_tag_def', 'logging' ]
);
- $this->resetServices();
}
public function testEdit() {
ChangeTags::defineTag( 'custom tag' );
$this->setMwGlobals( 'wgRevokePermissions',
[ 'user' => [ 'applychangetags' => true ] ] );
- // Supply services with updated globals
- $this->resetServices();
try {
$this->doApiRequestWithToken( [
$this->setMwGlobals( 'wgRevokePermissions',
[ 'user' => [ 'upload' => true ] ] );
- // Supply services with updated globals
- $this->resetServices();
$this->doApiRequestWithToken( [
'action' => 'edit',
'The content you supplied exceeds the article size limit of 1 kilobyte.' );
$this->setMwGlobals( 'wgMaxArticleSize', 1 );
- // Supply services with updated globals
- $this->resetServices();
$text = str_repeat( '!', 1025 );
'The action you have requested is limited to users in the group: ' );
$this->setMwGlobals( 'wgRevokePermissions', [ '*' => [ 'edit' => true ] ] );
- // Supply services with updated globals
- $this->resetServices();
$this->doApiRequestWithToken( [
'action' => 'edit',
$this->setMwGlobals( 'wgRevokePermissions',
[ 'user' => [ 'editcontentmodel' => true ] ] );
- // Supply services with updated globals
- $this->resetServices();
$this->doApiRequestWithToken( [
'action' => 'edit',
public function testSetCacheModeUnrecognized() {
$api = new ApiMain();
$api->setCacheMode( 'unrecognized' );
- $this->resetServices();
$this->assertSame(
'private',
TestingAccessWrapper::newFromObject( $api )->mCacheMode,
$name = ucfirst( __FUNCTION__ );
$this->mergeMwGlobalArrayValue( 'wgNamespacesWithSubpages', [ NS_MAIN => true ] );
- $this->resetServices();
$pages = [ $name, "$name/1", "$name/2", "Talk:$name", "Talk:$name/1", "Talk:$name/3" ];
$ids = [];
$this->mUserMock->method( 'getOptions' )
->willReturn( [] );
+ // DefaultPreferencesFactory calls a ton of user methods, but we still want to list all of
+ // them in case bugs are caused by unexpected things returning null that shouldn't.
+ $this->mUserMock->expects( $this->never() )->method( $this->anythingBut(
+ 'getEffectiveGroups', 'getOptionKinds', 'getInstanceForUpdate', 'getOptions', 'getId',
+ 'isAnon', 'getRequest', 'isLoggedIn', 'getName', 'getGroupMemberships', 'getEditCount',
+ 'getRegistration', 'isAllowed', 'getRealName', 'getOption', 'getStubThreshold',
+ 'getBoolOption', 'getEmail', 'getDatePreference', 'useRCPatrol', 'useNPPatrol',
+ 'setOption', 'saveSettings', 'resetOptions', 'isRegistered'
+ ) );
+
// Create a new context
$this->mContext = new DerivativeContext( new RequestContext() );
$this->mContext->getContext()->setTitle( Title::newFromText( 'Test' ) );
private function executeQuery( $request ) {
$this->mContext->setRequest( new FauxRequest( $request, true, $this->mSession ) );
+ $this->mUserMock->method( 'getRequest' )->willReturn( $this->mContext->getRequest() );
+
$this->mTested->execute();
return $this->mTested->getResult()->getResultData( null, [ 'Strip' => 'all' ] );
$this->setMwGlobals( 'wgExtraInterlanguageLinkPrefixes', [ 'madeuplanguage' ] );
$this->tablesUsed[] = 'interwiki';
- $this->resetServices();
}
/**
$this->setExpectedApiException( 'apierror-siteinfo-includealldenied' );
}
- $mockLB = $this->getMockBuilder( LoadBalancer::class )
- ->disableOriginalConstructor()
- ->setMethods( [ 'getMaxLag', 'getLagTimes', 'getServerName', '__destruct' ] )
- ->getMock();
+ $mockLB = $this->createMock( LoadBalancer::class );
$mockLB->method( 'getMaxLag' )->willReturn( [ null, 7, 1 ] );
$mockLB->method( 'getLagTimes' )->willReturn( [ 5, 7 ] );
$mockLB->method( 'getServerName' )->will( $this->returnValueMap( [
[ 0, 'apple' ], [ 1, 'carrot' ]
] ) );
+ $mockLB->method( 'getLocalDomainID' )->willReturn( 'testdomain' );
+ $mockLB->expects( $this->never() )->method( $this->anythingBut(
+ 'getMaxLag', 'getLagTimes', 'getServerName', 'getLocalDomainID', '__destruct'
+ ) );
$this->setService( 'DBLoadBalancer', $mockLB );
$this->setMwGlobals( 'wgShowHostnames', $showHostnames );
if ( $remove ) {
$this->mergeMwGlobalArrayValue( 'wgRemoveGroups', [ 'bureaucrat' => $remove ] );
}
-
- $this->resetServices();
}
/**
ChangeTags::defineTag( 'custom tag' );
$this->setGroupPermissions( 'user', 'applychangetags', false );
- $this->resetServices();
$this->doFailedRightsChange(
'You do not have permission to apply change tags along with your changes.',
global $wgActorTableSchemaMigrationStage;
$reset = new \Wikimedia\ScopedCallback( function ( $v ) {
- global $wgActorTableSchemaMigrationStage;
- $wgActorTableSchemaMigrationStage = $v;
- $this->overrideMwServices();
+ $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', $v );
}, [ $wgActorTableSchemaMigrationStage ] );
// Needs to WRITE_BOTH so READ_OLD tests below work. READ mode here doesn't really matter.
- $wgActorTableSchemaMigrationStage = SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW;
- $this->overrideMwServices();
+ $this->setMwGlobals( 'wgActorTableSchemaMigrationStage',
+ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW );
$users = [
User::newFromName( '192.168.2.2', false ),
$this->markTestSkippedIfDbType( 'sqlite' );
$this->setMwGlobals( 'wgActorTableSchemaMigrationStage', $stage );
- $this->overrideMwServices();
if ( isset( $params['ucuserids'] ) ) {
$params['ucuserids'] = implode( '|', array_map( 'User::idFromName', $params['ucuserids'] ) );
*/
public function testInterwikiUser( $stage ) {
$this->setMwGlobals( 'wgActorTableSchemaMigrationStage', $stage );
- $this->overrideMwServices();
$params = [
'action' => 'query',
],
'wgProxyWhitelist' => [],
] );
- $this->resetServices();
$status = $this->manager->checkAccountCreatePermissions( new \User );
$this->assertFalse( $status->isOK() );
$this->assertTrue( $status->hasMessage( 'sorbs_create_account_reason' ) );
$this->setMwGlobals( 'wgProxyWhitelist', [ '127.0.0.1' ] );
- $this->resetServices();
$status = $this->manager->checkAccountCreatePermissions( new \User );
$this->assertTrue( $status->isGood() );
}
$this->assertSame( AuthenticationResponse::FAIL, $ret->status );
$this->assertSame( 'noname', $ret->message->getKey() );
+ $this->hook( 'LocalUserCreated', $this->never() );
$readOnlyMode = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
$readOnlyMode->setReason( 'Because' );
- $this->hook( 'LocalUserCreated', $this->never() );
$userReq->username = self::usernameForCreation();
$ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
$this->unhook( 'LocalUserCreated' );
$session, $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
);
+ $this->hook( 'LocalUserCreated', $this->never() );
$this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
[ 'username' => $creator->getName() ] + $session );
$readOnlyMode = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
$readOnlyMode->setReason( 'Because' );
- $this->hook( 'LocalUserCreated', $this->never() );
$ret = $this->manager->continueAccountCreation( [] );
$this->unhook( 'LocalUserCreated' );
$this->assertSame( AuthenticationResponse::FAIL, $ret->status );
$this->mergeMwGlobalArrayValue( 'wgObjectCaches',
[ __METHOD__ => [ 'class' => 'HashBagOStuff' ] ] );
$this->setMwGlobals( [ 'wgMainCacheType' => __METHOD__ ] );
- // Supply services with updated globals
- $this->resetServices();
// Set up lots of mocks...
$mocks = [];
// Wiki is read-only
$session->clear();
+ $this->hook( 'LocalUserCreated', $this->never() );
$readOnlyMode = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
$readOnlyMode->setReason( 'Because' );
$user = \User::newFromName( $username );
- $this->hook( 'LocalUserCreated', $this->never() );
$ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
$this->unhook( 'LocalUserCreated' );
$this->assertEquals( \Status::newFatal( wfMessage( 'readonlytext', 'Because' ) ), $ret );
private function getBlockManagerConstructorArgs( $overrideConfig ) {
$blockManagerConfig = array_merge( $this->blockManagerConfig, $overrideConfig );
$this->setMwGlobals( $blockManagerConfig );
- $this->overrideMwServices();
return [
new LoggedServiceOptions(
self::$serviceOptionsAccessLog,
// let's choose e.g. German (de)
$this->setUserLang( 'de' );
$this->setContentLang( 'de' );
- $this->resetServices();
}
function addDBDataOnce() {
]
]
] );
- $this->overrideMwServices();
$messageCache = MessageCache::singleton();
$messageCache->enable();
--- /dev/null
+<?php
+
+use MediaWiki\Revision\SlotRecord;
+use MediaWiki\Revision\SlotRenderingProvider;
+
+/**
+ * @group ContentHandler
+ */
+class UnknownContentHandlerTest extends MediaWikiLangTestCase {
+ /**
+ * @covers UnknownContentHandler::supportsDirectEditing
+ */
+ public function testSupportsDirectEditing() {
+ $handler = new UnknownContentHandler( 'horkyporky' );
+ $this->assertFalse( $handler->supportsDirectEditing(), 'direct editing supported' );
+ }
+
+ /**
+ * @covers UnknownContentHandler::serializeContent
+ */
+ public function testSerializeContent() {
+ $handler = new UnknownContentHandler( 'horkyporky' );
+ $content = new UnknownContent( 'hello world', 'horkyporky' );
+
+ $this->assertEquals( 'hello world', $handler->serializeContent( $content ) );
+ $this->assertEquals(
+ 'hello world',
+ $handler->serializeContent( $content, 'application/horkyporky' )
+ );
+ }
+
+ /**
+ * @covers UnknownContentHandler::unserializeContent
+ */
+ public function testUnserializeContent() {
+ $handler = new UnknownContentHandler( 'horkyporky' );
+ $content = $handler->unserializeContent( 'hello world' );
+ $this->assertEquals( 'hello world', $content->getData() );
+
+ $content = $handler->unserializeContent( 'hello world', 'application/horkyporky' );
+ $this->assertEquals( 'hello world', $content->getData() );
+ }
+
+ /**
+ * @covers UnknownContentHandler::makeEmptyContent
+ */
+ public function testMakeEmptyContent() {
+ $handler = new UnknownContentHandler( 'horkyporky' );
+ $content = $handler->makeEmptyContent();
+
+ $this->assertTrue( $content->isEmpty() );
+ $this->assertEquals( '', $content->getData() );
+ }
+
+ public static function dataIsSupportedFormat() {
+ return [
+ [ null, true ],
+ [ 'application/octet-stream', true ],
+ [ 'unknown/unknown', true ],
+ [ 'text/plain', false ],
+ [ 99887766, false ],
+ ];
+ }
+
+ /**
+ * @dataProvider dataIsSupportedFormat
+ * @covers UnknownContentHandler::isSupportedFormat
+ */
+ public function testIsSupportedFormat( $format, $supported ) {
+ $handler = new UnknownContentHandler( 'horkyporky' );
+ $this->assertEquals( $supported, $handler->isSupportedFormat( $format ) );
+ }
+
+ /**
+ * @covers ContentHandler::getSecondaryDataUpdates
+ */
+ public function testGetSecondaryDataUpdates() {
+ $title = Title::newFromText( 'Somefile.jpg', NS_FILE );
+ $content = new UnknownContent( '', 'horkyporky' );
+
+ /** @var SlotRenderingProvider $srp */
+ $srp = $this->getMock( SlotRenderingProvider::class );
+
+ $handler = new UnknownContentHandler( 'horkyporky' );
+ $updates = $handler->getSecondaryDataUpdates( $title, $content, SlotRecord::MAIN, $srp );
+
+ $this->assertEquals( [], $updates );
+ }
+
+ /**
+ * @covers ContentHandler::getDeletionUpdates
+ */
+ public function testGetDeletionUpdates() {
+ $title = Title::newFromText( 'Somefile.jpg', NS_FILE );
+
+ $handler = new UnknownContentHandler( 'horkyporky' );
+ $updates = $handler->getDeletionUpdates( $title, SlotRecord::MAIN );
+
+ $this->assertEquals( [], $updates );
+ }
+
+ /**
+ * @covers ContentHandler::getDeletionUpdates
+ */
+ public function testGetSlotDiffRenderer() {
+ $context = new RequestContext();
+ $context->setRequest( new FauxRequest() );
+
+ $handler = new UnknownContentHandler( 'horkyporky' );
+ $slotDiffRenderer = $handler->getSlotDiffRenderer( $context );
+
+ $oldContent = $handler->unserializeContent( 'Foo' );
+ $newContent = $handler->unserializeContent( 'Foo bar' );
+
+ $diff = $slotDiffRenderer->getDiff( $oldContent, $newContent );
+ $this->assertNotEmpty( $diff );
+ }
+
+}
--- /dev/null
+<?php
+
+/**
+ * @group ContentHandler
+ */
+class UnknownContentTest extends MediaWikiLangTestCase {
+
+ /**
+ * @param string $data
+ * @return UnknownContent
+ */
+ public function newContent( $data, $type = 'xyzzy' ) {
+ return new UnknownContent( $data, $type );
+ }
+
+ /**
+ * @covers UnknownContent::getParserOutput
+ */
+ public function testGetParserOutput() {
+ $this->setUserLang( 'en' );
+ $this->setContentLang( 'qqx' );
+
+ $title = Title::newFromText( 'Test' );
+ $content = $this->newContent( 'Horkyporky' );
+
+ $po = $content->getParserOutput( $title );
+ $html = $po->getText();
+ $html = preg_replace( '#<!--.*?-->#sm', '', $html ); // strip comments
+
+ $this->assertNotContains( 'Horkyporky', $html );
+ $this->assertNotContains( '(unsupported-content-model)', $html );
+ }
+
+ /**
+ * @covers UnknownContent::preSaveTransform
+ */
+ public function testPreSaveTransform() {
+ $title = Title::newFromText( 'Test' );
+ $user = $this->getTestUser()->getUser();
+ $content = $this->newContent( 'Horkyporky ~~~' );
+
+ $options = new ParserOptions();
+
+ $this->assertSame( $content, $content->preSaveTransform( $title, $user, $options ) );
+ }
+
+ /**
+ * @covers UnknownContent::preloadTransform
+ */
+ public function testPreloadTransform() {
+ $title = Title::newFromText( 'Test' );
+ $content = $this->newContent( 'Horkyporky ~~~' );
+
+ $options = new ParserOptions();
+
+ $this->assertSame( $content, $content->preloadTransform( $title, $options ) );
+ }
+
+ /**
+ * @covers UnknownContent::getRedirectTarget
+ */
+ public function testGetRedirectTarget() {
+ $content = $this->newContent( '#REDIRECT [[Horkyporky]]' );
+ $this->assertNull( $content->getRedirectTarget() );
+ }
+
+ /**
+ * @covers UnknownContent::isRedirect
+ */
+ public function testIsRedirect() {
+ $content = $this->newContent( '#REDIRECT [[Horkyporky]]' );
+ $this->assertFalse( $content->isRedirect() );
+ }
+
+ /**
+ * @covers UnknownContent::isCountable
+ */
+ public function testIsCountable() {
+ $content = $this->newContent( '[[Horkyporky]]' );
+ $this->assertFalse( $content->isCountable( true ) );
+ }
+
+ /**
+ * @covers UnknownContent::getTextForSummary
+ */
+ public function testGetTextForSummary() {
+ $content = $this->newContent( 'Horkyporky' );
+ $this->assertSame( '', $content->getTextForSummary() );
+ }
+
+ /**
+ * @covers UnknownContent::getTextForSearchIndex
+ */
+ public function testGetTextForSearchIndex() {
+ $content = $this->newContent( 'Horkyporky' );
+ $this->assertSame( '', $content->getTextForSearchIndex() );
+ }
+
+ /**
+ * @covers UnknownContent::copy
+ */
+ public function testCopy() {
+ $content = $this->newContent( 'hello world.' );
+ $copy = $content->copy();
+
+ $this->assertSame( $content, $copy );
+ }
+
+ /**
+ * @covers UnknownContent::getSize
+ */
+ public function testGetSize() {
+ $content = $this->newContent( 'hello world.' );
+
+ $this->assertEquals( 12, $content->getSize() );
+ }
+
+ /**
+ * @covers UnknownContent::getData
+ */
+ public function testGetData() {
+ $content = $this->newContent( 'hello world.' );
+
+ $this->assertEquals( 'hello world.', $content->getData() );
+ }
+
+ /**
+ * @covers UnknownContent::getNativeData
+ */
+ public function testGetNativeData() {
+ $content = $this->newContent( 'hello world.' );
+
+ $this->assertEquals( 'hello world.', $content->getNativeData() );
+ }
+
+ /**
+ * @covers UnknownContent::getWikitextForTransclusion
+ */
+ public function testGetWikitextForTransclusion() {
+ $content = $this->newContent( 'hello world.' );
+
+ $this->assertEquals( '', $content->getWikitextForTransclusion() );
+ }
+
+ /**
+ * @covers UnknownContent::getModel
+ */
+ public function testGetModel() {
+ $content = $this->newContent( "hello world.", 'horkyporky' );
+
+ $this->assertEquals( 'horkyporky', $content->getModel() );
+ }
+
+ /**
+ * @covers UnknownContent::getContentHandler
+ */
+ public function testGetContentHandler() {
+ $this->mergeMwGlobalArrayValue(
+ 'wgContentHandlers',
+ [ 'horkyporky' => 'UnknownContentHandler' ]
+ );
+
+ $content = $this->newContent( "hello world.", 'horkyporky' );
+
+ $this->assertInstanceOf( UnknownContentHandler::class, $content->getContentHandler() );
+ $this->assertEquals( 'horkyporky', $content->getContentHandler()->getModelID() );
+ }
+
+ public static function dataIsEmpty() {
+ return [
+ [ '', true ],
+ [ ' ', false ],
+ [ '0', false ],
+ [ 'hallo welt.', false ],
+ ];
+ }
+
+ /**
+ * @dataProvider dataIsEmpty
+ * @covers UnknownContent::isEmpty
+ */
+ public function testIsEmpty( $text, $empty ) {
+ $content = $this->newContent( $text );
+
+ $this->assertEquals( $empty, $content->isEmpty() );
+ }
+
+ public function provideEquals() {
+ return [
+ [ new UnknownContent( "hallo", 'horky' ), null, false ],
+ [ new UnknownContent( "hallo", 'horky' ), new UnknownContent( "hallo", 'horky' ), true ],
+ [ new UnknownContent( "hallo", 'horky' ), new UnknownContent( "hallo", 'xyzzy' ), false ],
+ [ new UnknownContent( "hallo", 'horky' ), new JavaScriptContent( "hallo" ), false ],
+ [ new UnknownContent( "hallo", 'horky' ), new WikitextContent( "hallo" ), false ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideEquals
+ * @covers UnknownContent::equals
+ */
+ public function testEquals( Content $a, Content $b = null, $equal = false ) {
+ $this->assertEquals( $equal, $a->equals( $b ) );
+ }
+
+ public static function provideConvert() {
+ return [
+ [ // #0
+ 'Hallo Welt',
+ CONTENT_MODEL_WIKITEXT,
+ 'lossless',
+ 'Hallo Welt'
+ ],
+ [ // #1
+ 'Hallo Welt',
+ CONTENT_MODEL_WIKITEXT,
+ 'lossless',
+ 'Hallo Welt'
+ ],
+ [ // #1
+ 'Hallo Welt',
+ CONTENT_MODEL_CSS,
+ 'lossless',
+ 'Hallo Welt'
+ ],
+ [ // #1
+ 'Hallo Welt',
+ CONTENT_MODEL_JAVASCRIPT,
+ 'lossless',
+ 'Hallo Welt'
+ ],
+ ];
+ }
+
+ /**
+ * @covers UnknownContent::convert
+ */
+ public function testConvert() {
+ $content = $this->newContent( 'More horkyporky?' );
+
+ $this->assertFalse( $content->convert( CONTENT_MODEL_TEXT ) );
+ }
+
+ /**
+ * @covers UnknownContent::__construct
+ * @covers UnknownContentHandler::serializeContent
+ */
+ public function testSerialize() {
+ $this->mergeMwGlobalArrayValue(
+ 'wgContentHandlers',
+ [ 'horkyporky' => 'UnknownContentHandler' ]
+ );
+
+ $content = $this->newContent( 'Hörkypörky', 'horkyporky' );
+
+ $this->assertSame( 'Hörkypörky', $content->serialize() );
+ }
+
+}
* @dataProvider provideGenerateContentDiffBody
*/
public function testGenerateContentDiffBody(
- Content $oldContent, Content $newContent, $expectedDiff
+ array $oldContentArgs, array $newContentArgs, $expectedDiff
) {
+ $this->mergeMwGlobalArrayValue( 'wgContentHandlers', [
+ 'testing-nontext' => DummyNonTextContentHandler::class,
+ ] );
+ $oldContent = ContentHandler::makeContent( ...$oldContentArgs );
+ $newContent = ContentHandler::makeContent( ...$newContentArgs );
+
// Set $wgExternalDiffEngine to something bogus to try to force use of
// the PHP engine rather than wikidiff2.
$this->setMwGlobals( [
$this->assertSame( $expectedDiff, $this->getPlainDiff( $diff ) );
}
- public function provideGenerateContentDiffBody() {
- $this->mergeMwGlobalArrayValue( 'wgContentHandlers', [
- 'testing-nontext' => DummyNonTextContentHandler::class,
- ] );
- $content1 = ContentHandler::makeContent( 'xxx', null, CONTENT_MODEL_TEXT );
- $content2 = ContentHandler::makeContent( 'yyy', null, CONTENT_MODEL_TEXT );
+ public static function provideGenerateContentDiffBody() {
+ $content1 = [ 'xxx', null, CONTENT_MODEL_TEXT ];
+ $content2 = [ 'yyy', null, CONTENT_MODEL_TEXT ];
return [
'self-diff' => [ $content1, $content1, '' ],
/**
* @dataProvider provideGetDiff
- * @param Content|null $oldContent
- * @param Content|null $newContent
+ * @param array|null $oldContentArgs To pass to makeContent() (if not null)
+ * @param array|null $newContentArgs
* @param string|Exception $expectedResult
* @throws Exception
*/
public function testGetDiff(
- Content $oldContent = null, Content $newContent = null, $expectedResult
+ array $oldContentArgs = null, array $newContentArgs = null, $expectedResult
) {
+ $this->mergeMwGlobalArrayValue( 'wgContentHandlers', [
+ 'testing' => DummyContentHandlerForTesting::class,
+ 'testing-nontext' => DummyNonTextContentHandler::class,
+ ] );
+
+ $oldContent = $oldContentArgs ? self::makeContent( ...$oldContentArgs ) : null;
+ $newContent = $newContentArgs ? self::makeContent( ...$newContentArgs ) : null;
+
if ( $expectedResult instanceof Exception ) {
$this->setExpectedException( get_class( $expectedResult ), $expectedResult->getMessage() );
}
$this->assertSame( $expectedResult, $plainDiff );
}
- public function provideGetDiff() {
- $this->mergeMwGlobalArrayValue( 'wgContentHandlers', [
- 'testing' => DummyContentHandlerForTesting::class,
- 'testing-nontext' => DummyNonTextContentHandler::class,
- ] );
-
+ public static function provideGetDiff() {
return [
'same text' => [
- $this->makeContent( "aaa\nbbb\nccc" ),
- $this->makeContent( "aaa\nbbb\nccc" ),
+ [ "aaa\nbbb\nccc" ],
+ [ "aaa\nbbb\nccc" ],
"",
],
'different text' => [
- $this->makeContent( "aaa\nbbb\nccc" ),
- $this->makeContent( "aaa\nxxx\nccc" ),
+ [ "aaa\nbbb\nccc" ],
+ [ "aaa\nxxx\nccc" ],
" aaa aaa\n-bbb+xxx\n ccc ccc",
],
'no right content' => [
- $this->makeContent( "aaa\nbbb\nccc" ),
+ [ "aaa\nbbb\nccc" ],
null,
"-aaa+ \n-bbb \n-ccc ",
],
'no left content' => [
null,
- $this->makeContent( "aaa\nbbb\nccc" ),
+ [ "aaa\nbbb\nccc" ],
"- +aaa\n +bbb\n +ccc",
],
'no content' => [
new InvalidArgumentException( '$oldContent and $newContent cannot both be null' ),
],
'non-text left content' => [
- $this->makeContent( '', 'testing-nontext' ),
- $this->makeContent( "aaa\nbbb\nccc" ),
+ [ '', 'testing-nontext' ],
+ [ "aaa\nbbb\nccc" ],
new ParameterTypeException( '$oldContent', 'TextContent|null' ),
],
'non-text right content' => [
- $this->makeContent( "aaa\nbbb\nccc" ),
- $this->makeContent( '', 'testing-nontext' ),
+ [ "aaa\nbbb\nccc" ],
+ [ '', 'testing-nontext' ],
new ParameterTypeException( '$newContent', 'TextContent|null' ),
],
];
* @param string $model
* @return null|TextContent
*/
- private function makeContent( $str, $model = CONTENT_MODEL_TEXT ) {
+ private static function makeContent( $str, $model = CONTENT_MODEL_TEXT ) {
return ContentHandler::makeContent( $str, null, $model );
}
--- /dev/null
+<?php
+
+/**
+ * @covers UnsupportedSlotDiffRenderer
+ */
+class UnsupportedSlotDiffRendererTest extends MediaWikiTestCase {
+
+ public function provideDiff() {
+ $oldContent = new TextContent( 'Kittens' );
+ $newContent = new TextContent( 'Goats' );
+ $badContent = new UnknownContent( 'Dragons', 'xyzzy' );
+
+ yield [ '(unsupported-content-diff)', $oldContent, null ];
+ yield [ '(unsupported-content-diff)', null, $newContent ];
+ yield [ '(unsupported-content-diff)', $oldContent, $newContent ];
+ yield [ '(unsupported-content-diff2)', $badContent, $newContent ];
+ yield [ '(unsupported-content-diff2)', $oldContent, $badContent ];
+ yield [ '(unsupported-content-diff)', null, $badContent ];
+ yield [ '(unsupported-content-diff)', $badContent, null ];
+ }
+
+ /**
+ * @dataProvider provideDiff
+ */
+ public function testDiff( $expected, $oldContent, $newContent ) {
+ $this->mergeMwGlobalArrayValue(
+ 'wgContentHandlers',
+ [ 'xyzzy' => 'UnknownContentHandler' ]
+ );
+
+ $localizer = $this->getMock( MessageLocalizer::class );
+
+ $localizer->method( 'msg' )
+ ->willReturnCallback( function ( $key, ...$params ) {
+ return new RawMessage( "($key)", $params );
+ } );
+
+ $sdr = new UnsupportedSlotDiffRenderer( $localizer );
+ $this->assertContains( $expected, $sdr->getDiff( $oldContent, $newContent ) );
+ }
+
+}
--- /dev/null
+<?php
+
+use MediaWiki\MediaWikiServices;
+
+/**
+ * @coversDefaultClass FileBackendGroup
+ * @covers ::singleton
+ * @covers ::destroySingleton
+ */
+class FileBackendGroupIntegrationTest extends MediaWikiIntegrationTestCase {
+ use FileBackendGroupTestTrait;
+
+ private static function getWikiID() {
+ return wfWikiID();
+ }
+
+ private function getLockManagerGroupFactory() {
+ return MediaWikiServices::getInstance()->getLockManagerGroupFactory();
+ }
+
+ private function newObj( array $options = [] ) : FileBackendGroup {
+ $globals = [ 'DirectoryMode', 'FileBackends', 'ForeignFileRepos', 'LocalFileRepo' ];
+ foreach ( $globals as $global ) {
+ $this->setMwGlobals(
+ "wg$global", $options[$global] ?? self::getDefaultOptions()[$global] );
+ }
+
+ $serviceMembers = [
+ 'configuredROMode' => 'ConfiguredReadOnlyMode',
+ 'srvCache' => 'LocalServerObjectCache',
+ 'wanCache' => 'MainWANObjectCache',
+ 'mimeAnalyzer' => 'MimeAnalyzer',
+ 'lmgFactory' => 'LockManagerGroupFactory',
+ 'tmpFileFactory' => 'TempFSFileFactory',
+ ];
+
+ foreach ( $serviceMembers as $key => $name ) {
+ if ( isset( $options[$key] ) ) {
+ $this->setService( $name, $options[$key] );
+ }
+ }
+
+ $this->assertEmpty(
+ array_diff( array_keys( $options ), $globals, array_keys( $serviceMembers ) ) );
+
+ $this->resetServices();
+ FileBackendGroup::destroySingleton();
+
+ $services = MediaWikiServices::getInstance();
+
+ foreach ( $serviceMembers as $key => $name ) {
+ if ( $key === 'srvCache' ) {
+ $this->$key = ObjectCache::getLocalServerInstance( 'hash' );
+ } else {
+ $this->$key = $services->getService( $name );
+ }
+ }
+
+ return FileBackendGroup::singleton();
+ }
+}
function testHasForeignRepoNegative() {
$this->setMwGlobals( 'wgForeignFileRepos', [] );
- $this->overrideMwServices();
FileBackendGroup::destroySingleton();
$this->assertFalse( RepoGroup::singleton()->hasForeignRepos() );
}
function testForEachForeignRepoNone() {
$this->setMwGlobals( 'wgForeignFileRepos', [] );
- $this->overrideMwServices();
FileBackendGroup::destroySingleton();
$fakeCallback = $this->createMock( RepoGroupTestHelper::class );
$fakeCallback->expects( $this->never() )->method( 'callback' );
'apiThumbCacheExpiry' => 86400,
'directory' => $wgUploadDirectory
] ] );
- $this->overrideMwServices();
FileBackendGroup::destroySingleton();
}
}
}
private function setWgInterwikiCache( $interwikiCache ) {
- $this->overrideMwServices();
MediaWikiServices::getInstance()->resetServiceForTesting( 'InterwikiLookup' );
$this->setMwGlobals( 'wgInterwikiCache', $interwikiCache );
}
/** @covers ObjectCache::newAnything */
public function testNewAnythingNoAccelNoDb() {
- $this->overrideMwServices(); // Ensures restore on tear down
- MediaWiki\MediaWikiServices::disableStorageBackend();
-
$this->setMwGlobals( [
'wgMainCacheType' => CACHE_ACCEL
] );
CACHE_ACCEL => [ 'class' => EmptyBagOStuff::class ]
] );
+ MediaWiki\MediaWikiServices::disableStorageBackend();
+
$this->assertInstanceOf(
EmptyBagOStuff::class,
ObjectCache::newAnything( [] ),
/** @covers ObjectCache::newAnything */
public function testNewAnythingNothingNoDb() {
- $this->overrideMwServices();
MediaWiki\MediaWikiServices::disableStorageBackend();
$this->assertInstanceOf(
$this->tablesUsed += $this->getMcrTablesToReset();
- $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_NEW );
- $this->setMwGlobals( 'wgContentHandlerUseDB', $this->getContentHandlerUseDB() );
- $this->setMwGlobals(
- 'wgMultiContentRevisionSchemaMigrationStage',
- $this->getMcrMigrationStage()
- );
- $this->overrideMwServices();
+ $this->setMwGlobals( [
+ 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
+ 'wgContentHandlerUseDB' => $this->getContentHandlerUseDB(),
+ 'wgMultiContentRevisionSchemaMigrationStage' => $this->getMcrMigrationStage(),
+ ] );
// First create our dummy page
$page = Title::newFromText( 'PageArchiveTest_thePage' );
$this->getMcrMigrationStage()
);
$this->pagesToDelete = [];
-
- $this->overrideMwServices();
}
protected function tearDown() {
* @coversNothing
*/
public function testServiceWiring() {
- $this->overrideMwServices();
-
$ranHook = 0;
$this->setMwGlobals( 'wgHooks', [
'ResourceLoaderRegisterModules' => [
$this->originalHandlers = TestingAccessWrapper::newFromClass( Hooks::class )->handlers;
TestingAccessWrapper::newFromClass( Hooks::class )->handlers = [];
-
- $this->overrideMwServices();
}
public function tearDown() {
public function testRcHidemyselfFilter() {
$this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_NEW );
- $this->overrideMwServices();
$user = $this->getTestUser()->getUser();
$user->getActorId( wfGetDB( DB_MASTER ) );
$this->setMwGlobals(
'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
);
- $this->overrideMwServices();
$user = $this->getTestUser()->getUser();
$user->getActorId( wfGetDB( DB_MASTER ) );
public function testRcHidebyothersFilter() {
$this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_NEW );
- $this->overrideMwServices();
$user = $this->getTestUser()->getUser();
$user->getActorId( wfGetDB( DB_MASTER ) );
$this->setMwGlobals(
'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
);
- $this->overrideMwServices();
$user = $this->getTestUser()->getUser();
$user->getActorId( wfGetDB( DB_MASTER ) );
public function testFilterUserExpLevelAllExperienceLevels() {
$this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_NEW );
- $this->overrideMwServices();
$this->assertConditions(
[
$this->setMwGlobals(
'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
);
- $this->overrideMwServices();
$this->assertConditions(
[
public function testFilterUserExpLevelRegistrered() {
$this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_NEW );
- $this->overrideMwServices();
$this->assertConditions(
[
$this->setMwGlobals(
'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
);
- $this->overrideMwServices();
$this->assertConditions(
[
public function testFilterUserExpLevelUnregistrered() {
$this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_NEW );
- $this->overrideMwServices();
$this->assertConditions(
[
$this->setMwGlobals(
'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
);
- $this->overrideMwServices();
$this->assertConditions(
[
public function testFilterUserExpLevelRegistreredOrLearner() {
$this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_NEW );
- $this->overrideMwServices();
$this->assertConditions(
[
$this->setMwGlobals(
'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
);
- $this->overrideMwServices();
$this->assertConditions(
[
public function testFilterUserExpLevelUnregistreredOrExperienced() {
$this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_NEW );
- $this->overrideMwServices();
$conds = $this->buildQuery( [ 'userExpLevel' => 'unregistered;experienced' ] );
$this->setMwGlobals(
'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD
);
- $this->overrideMwServices();
$conds = $this->buildQuery( [ 'userExpLevel' => 'unregistered;experienced' ] );
$count++;
}
] ] );
- $this->overrideMwServices();
$spf = MediaWikiServices::getInstance()->getSpecialPageFactory();
$spf->getNames();
$spf->getNames();
*/
public function testGetPage( $spec, $shouldReuseInstance ) {
$this->mergeMwGlobalArrayValue( 'wgSpecialPages', [ 'testdummy' => $spec ] );
- $this->overrideMwServices();
$page = SpecialPageFactory::getPage( 'testdummy' );
$this->assertInstanceOf( SpecialPage::class, $page );
*/
public function testGetNames() {
$this->mergeMwGlobalArrayValue( 'wgSpecialPages', [ 'testdummy' => SpecialAllPages::class ] );
- $this->overrideMwServices();
$names = SpecialPageFactory::getNames();
$this->assertInternalType( 'array', $names );
*/
public function testResolveAlias() {
$this->setContentLang( 'de' );
- $this->overrideMwServices();
list( $name, $param ) = SpecialPageFactory::resolveAlias( 'Spezialseiten/Foo' );
$this->assertEquals( 'Specialpages', $name );
*/
public function testGetLocalNameFor() {
$this->setContentLang( 'de' );
- $this->overrideMwServices();
$name = SpecialPageFactory::getLocalNameFor( 'Specialpages', 'Foo' );
$this->assertEquals( 'Spezialseiten/Foo', $name );
*/
public function testGetTitleForAlias() {
$this->setContentLang( 'de' );
- $this->overrideMwServices();
$title = SpecialPageFactory::getTitleForAlias( 'Specialpages/Foo' );
$this->assertEquals( 'Spezialseiten/Foo', $title->getText() );
$this->setMwGlobals( 'wgSpecialPages',
array_combine( array_keys( $aliasesList ), array_keys( $aliasesList ) )
);
- $this->overrideMwServices();
$this->setContentLang( $lang );
// Catch the warnings we expect to be raised
}
],
] );
- $this->overrideMwServices();
SpecialPageFactory::getLocalNameFor( 'Specialpages' );
$this->assertTrue( $called, 'Recursive call succeeded' );
}
'wgRestrictionLevels' => [ '', 'autoconfirmed', 'sysop' ],
'wgAutopromote' => []
] );
- $this->resetServices();
$obj = $this->newObj();
$user = is_null( $groups ) ? null : $this->getTestUser( $groups )->getUser();
$this->assertSame( $expected, $obj->getRestrictionLevels( $ns, $user ) );
$this->userTester->addGroup( 'unittesters' );
$this->expiryTime = wfTimestamp( TS_MW, time() + 100500 );
$this->userTester->addGroup( 'testwriters', $this->expiryTime );
-
- $this->resetServices();
}
/**
'wgRevokePermissions' => [],
'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
] );
- $this->overrideMwServices();
$this->setUpPermissionGlobals();
RequestContext::getMain()->setRequest( $request );
TestingAccessWrapper::newFromObject( $user )->mRequest = $request;
$request->getSession()->setUser( $user );
- $this->overrideMwServices();
}
/**
$rights = array_diff( $rights, [ 'writetest' ] );
} );
- $this->resetServices();
$rights = $user->getRights();
$this->assertContains( 'test', $rights );
$this->assertContains( 'runtest', $rights );
$this->assertFalse( $user->isPingLimitable() );
$this->setMwGlobals( 'wgRateLimitsExcludedIPs', [] );
- $noRateLimitUser = $this->getMockBuilder( User::class )->disableOriginalConstructor()
- ->setMethods( [ 'getIP', 'getId', 'getGroups' ] )->getMock();
- $noRateLimitUser->expects( $this->any() )->method( 'getIP' )->willReturn( '1.2.3.4' );
- $noRateLimitUser->expects( $this->any() )->method( 'getId' )->willReturn( 0 );
- $noRateLimitUser->expects( $this->any() )->method( 'getGroups' )->willReturn( [] );
- $this->overrideUserPermissions( $noRateLimitUser, 'noratelimit' );
- $this->assertFalse( $noRateLimitUser->isPingLimitable() );
+ $this->overrideUserPermissions( $user, 'noratelimit' );
+ $this->assertFalse( $user->isPingLimitable() );
}
public function provideExperienceLevel() {
$this->setMwGlobals( [
'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
] );
- $this->overrideMwServices();
$domain = MediaWikiServices::getInstance()->getDBLoadBalancer()->getLocalDomainID();
$this->hideDeprecated( 'User::selectFields' );
new WatchedItem( $user, $targets[0], '20151212010101' ),
new WatchedItem( $user, $targets[1], null ),
];
- $mockDb = $this->getMockDb();
- $mockDb->expects( $this->never() )->method( $this->anything() );
+ $mockDb = $this->createNoOpMock( IDatabase::class );
$mockCache = $this->getMockCache();
$mockCache->expects( $this->at( 1 ) )
}
public function testGetNotificationTimestampsBatch_anonymousUser() {
+ if ( defined( 'HHVM_VERSION' ) ) {
+ $this->markTestSkipped( 'HHVM Reflection buggy' );
+ }
+
$targets = [
new TitleValue( 0, 'SomeDbKey' ),
new TitleValue( 1, 'AnotherDbKey' ),
];
- $mockDb = $this->getMockDb();
- $mockDb->expects( $this->never() )->method( $this->anything() );
+ $mockDb = $this->createNoOpMock( IDatabase::class );
- $mockCache = $this->getMockCache();
- $mockCache->expects( $this->never() )->method( $this->anything() );
+ $mockCache = $this->createNoOpMock( HashBagOStuff::class );
$store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
$mockQueueGroup = $this->getMockJobQueueGroup();
- $mockRevisionRecord = $this->createMock( RevisionRecord::class );
- $mockRevisionRecord->expects( $this->never() )->method( $this->anything() );
+ $mockRevisionRecord = $this->createNoOpMock( RevisionRecord::class );
$mockRevisionLookup = $this->getMockRevisionLookup( [
'getTimestampFromId' => function () {
$oldid = 22;
$title = new TitleValue( 0, 'SomeDbKey' );
- $mockRevision = $this->createMock( RevisionRecord::class );
- $mockRevision->expects( $this->never() )->method( $this->anything() );
-
- $mockNextRevision = $this->createMock( RevisionRecord::class );
- $mockNextRevision->expects( $this->never() )->method( $this->anything() );
+ $mockRevision = $this->createNoOpMock( RevisionRecord::class );
+ $mockNextRevision = $this->createNoOpMock( RevisionRecord::class );
$mockDb = $this->getMockDb();
$mockDb->expects( $this->once() )
$mockQueueGroup = $this->getMockJobQueueGroup();
- $mockRevision = $this->createMock( RevisionRecord::class );
- $mockRevision->expects( $this->never() )->method( $this->anything() );
-
- $mockNextRevision = $this->createMock( RevisionRecord::class );
- $mockNextRevision->expects( $this->never() )->method( $this->anything() );
+ $mockRevision = $this->createNoOpMock( RevisionRecord::class );
+ $mockNextRevision = $this->createNoOpMock( RevisionRecord::class );
$mockRevisionLookup = $this->getMockRevisionLookup(
[
$mockQueueGroup = $this->getMockJobQueueGroup();
- $mockRevision = $this->createMock( RevisionRecord::class );
- $mockRevision->expects( $this->never() )->method( $this->anything() );
-
- $mockNextRevision = $this->createMock( RevisionRecord::class );
- $mockNextRevision->expects( $this->never() )->method( $this->anything() );
+ $mockRevision = $this->createNoOpMock( RevisionRecord::class );
+ $mockNextRevision = $this->createNoOpMock( RevisionRecord::class );
$mockRevisionLookup = $this->getMockRevisionLookup(
[
$mockQueueGroup = $this->getMockJobQueueGroup();
- $mockRevision = $this->createMock( RevisionRecord::class );
- $mockRevision->expects( $this->never() )->method( $this->anything() );
-
- $mockNextRevision = $this->createMock( RevisionRecord::class );
- $mockNextRevision->expects( $this->never() )->method( $this->anything() );
+ $mockRevision = $this->createNoOpMock( RevisionRecord::class );
+ $mockNextRevision = $this->createNoOpMock( RevisionRecord::class );
$mockRevisionLookup = $this->getMockRevisionLookup(
[
--- /dev/null
+<?php
+
+/**
+ * @coversDefaultClass Language
+ */
+class LanguageFallbackStaticMethodsTest extends MediaWikiIntegrationTestCase {
+ use LanguageFallbackTestTrait;
+
+ private function getCallee( array $options = [] ) {
+ if ( isset( $options['siteLangCode'] ) ) {
+ $this->setMwGlobals( 'wgLanguageCode', $options['siteLangCode'] );
+ $this->resetServices();
+ }
+ return Language::class;
+ }
+
+ private function getMessagesKey() {
+ return Language::MESSAGES_FALLBACKS;
+ }
+
+ private function getStrictKey() {
+ return Language::STRICT_FALLBACKS;
+ }
+}
<?php
+use MediaWiki\MediaWikiServices;
+
/**
* Try to make sure that extensions register all rights in $wgAvailableRights
* or via the 'UserGetAllRights' hook.
private function getAllVisibleRights() {
global $wgGroupPermissions, $wgRevokePermissions;
- $rights = User::getAllRights();
+ $rights = MediaWikiServices::getInstance()->getPermissionManager()->getAllPermissions();
foreach ( $wgGroupPermissions as $permissions ) {
$rights = array_merge( $rights, array_keys( $permissions ) );
public function testAvailableRights() {
$missingRights = array_diff(
$this->getAllVisibleRights(),
- User::getAllRights()
+ MediaWikiServices::getInstance()->getPermissionManager()->getAllPermissions()
);
$this->assertEquals(
*/
private function checkMessagesExist( $prefix ) {
// Getting all user rights, for core: User::$mCoreRights, for extensions: $wgAvailableRights
- $allRights = User::getAllRights();
+ $allRights = MediaWikiServices::getInstance()->getPermissionManager()->getAllPermissions();
$allMessageKeys = Language::getMessageKeysFor( 'en' );
$messagesToCheck = [];
// the actual test: change config, reset services.
$this->setMwGlobals( 'wgLanguageCode', 'qqx' );
- $this->resetServices();
// the overridden service instance should still be there
$this->assertSame( $myReadOnlyMode, $services->getService( 'ReadOnlyMode' ) );
--- /dev/null
+<?php
+
+use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
+use MediaWiki\FileBackend\LockManager\LockManagerGroupFactory;
+use MediaWiki\Logger\LoggerFactory;
+
+/**
+ * Code shared by the FileBackendGroup integration and unit tests. They need merely provide a
+ * suitable newObj() method and everything else works magically.
+ */
+trait FileBackendGroupTestTrait {
+ /**
+ * @param array $options Dictionary to use as a source for ServiceOptions before defaults, plus
+ * the following options are available to override other arguments:
+ * * 'configuredROMode'
+ * * 'lmgFactory'
+ * * 'mimeAnalyzer'
+ * * 'tmpFileFactory'
+ */
+ abstract protected function newObj( array $options = [] ) : FileBackendGroup;
+
+ /**
+ * @param string $domain Expected argument that LockManagerGroupFactory::getLockManagerGroup
+ * will receive
+ */
+ abstract protected function getLockManagerGroupFactory( $domain )
+ : LockManagerGroupFactory;
+
+ /**
+ * @return string As from wfWikiID()
+ */
+ abstract protected static function getWikiID();
+
+ /** @var BagOStuff */
+ private $srvCache;
+
+ /** @var WANObjectCache */
+ private $wanCache;
+
+ /** @var LockManagerGroupFactory */
+ private $lmgFactory;
+
+ /** @var TempFSFileFactory */
+ private $tmpFileFactory;
+
+ private static function getDefaultLocalFileRepo() {
+ return [
+ 'class' => LocalRepo::class,
+ 'name' => 'local',
+ 'directory' => 'upload-dir',
+ 'thumbDir' => 'thumb/',
+ 'transcodedDir' => 'transcoded/',
+ 'fileMode' => 0664,
+ 'scriptDirUrl' => 'script-path/',
+ 'url' => 'upload-path/',
+ 'hashLevels' => 2,
+ 'thumbScriptUrl' => false,
+ 'transformVia404' => false,
+ 'deletedDir' => 'deleted/',
+ 'deletedHashLevels' => 3,
+ 'backend' => 'local-backend',
+ ];
+ }
+
+ private static function getDefaultOptions() {
+ return [
+ 'DirectoryMode' => 0775,
+ 'FileBackends' => [],
+ 'ForeignFileRepos' => [],
+ 'LocalFileRepo' => self::getDefaultLocalFileRepo(),
+ 'wikiId' => self::getWikiID(),
+ ];
+ }
+
+ /**
+ * @covers ::__construct
+ */
+ public function testConstructor_overrideImplicitBackend() {
+ $obj = $this->newObj( [ 'FileBackends' =>
+ [ [ 'name' => 'local-backend', 'class' => '', 'lockManager' => 'fsLockManager' ] ]
+ ] );
+ $this->assertSame( '', $obj->config( 'local-backend' )['class'] );
+ }
+
+ /**
+ * @covers ::__construct
+ */
+ public function testConstructor_backendObject() {
+ // 'backend' being an object makes that repo from configuration ignored
+ // XXX This is not documented in DefaultSettings.php, does it do anything useful?
+ $obj = $this->newObj( [ 'ForeignFileRepos' => [ [ 'backend' => new stdclass ] ] ] );
+ $this->assertSame( FSFileBackend::class, $obj->config( 'local-backend' )['class'] );
+ }
+
+ /**
+ * @dataProvider provideRegister_domainId
+ * @param string $key Key to check in return value of config()
+ * @param string|callable $expected Expected value of config()[$key], or callable returning it
+ * @param array $extraBackendsOptions To add to the FileBackends entry passed to newObj()
+ * @param array $otherExtraOptions To add to the array passed to newObj() (e.g., services)
+ * @covers ::register
+ */
+ public function testRegister(
+ $key, $expected, array $extraBackendsOptions = [], array $otherExtraOptions = []
+ ) {
+ if ( $expected instanceof Closure ) {
+ // Lame hack to get around providers being called too early
+ $expected = $expected();
+ }
+ if ( $key === 'domainId' ) {
+ // This will change the expected LMG name too
+ $otherExtraOptions['lmgFactory'] = $this->getLockManagerGroupFactory( $expected );
+ }
+ $obj = $this->newObj( $otherExtraOptions + [
+ 'FileBackends' => [
+ $extraBackendsOptions + [
+ 'name' => 'myname', 'class' => '', 'lockManager' => 'fsLockManager'
+ ]
+ ],
+ ] );
+ $this->assertSame( $expected, $obj->config( 'myname' )[$key] );
+ }
+
+ public static function provideRegister_domainId() {
+ return [
+ 'domainId with neither wikiId nor domainId set' => [
+ 'domainId',
+ function () {
+ return self::getWikiID();
+ },
+ ],
+ 'domainId with wikiId set but no domainId' =>
+ [ 'domainId', 'id0', [ 'wikiId' => 'id0' ] ],
+ 'domainId with wikiId and domainId set' =>
+ [ 'domainId', 'dom1', [ 'wikiId' => 'id0', 'domainId' => 'dom1' ] ],
+ 'readOnly without readOnly set' => [ 'readOnly', false ],
+ 'readOnly with readOnly set to string' =>
+ [ 'readOnly', 'cuz', [ 'readOnly' => 'cuz' ] ],
+ 'readOnly without readOnly set but with string in passed object' => [
+ 'readOnly',
+ 'cuz',
+ [],
+ [ 'configuredROMode' => new ConfiguredReadOnlyMode( 'cuz' ) ],
+ ],
+ 'readOnly with readOnly set to false but string in passed object' => [
+ 'readOnly',
+ false,
+ [ 'readOnly' => false ],
+ [ 'configuredROMode' => new ConfiguredReadOnlyMode( 'cuz' ) ],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideRegister_exception
+ * @param array $fileBackends Value of FileBackends to pass to constructor
+ * @param string $class Expected exception class
+ * @param string $msg Expected exception message
+ * @covers ::__construct
+ * @covers ::register
+ */
+ public function testRegister_exception( $fileBackends, $class, $msg ) {
+ $this->setExpectedException( $class, $msg );
+ $this->newObj( [ 'FileBackends' => $fileBackends ] );
+ }
+
+ public static function provideRegister_exception() {
+ return [
+ 'Nameless' => [
+ [ [] ], InvalidArgumentException::class, "Cannot register a backend with no name."
+ ],
+ 'Duplicate' => [
+ [ [ 'name' => 'dupe', 'class' => '' ], [ 'name' => 'dupe' ] ],
+ LogicException::class,
+ "Backend with name 'dupe' already registered.",
+ ],
+ 'Classless' => [
+ [ [ 'name' => 'classless' ] ],
+ InvalidArgumentException::class,
+ "Backend with name 'classless' has no class.",
+ ],
+ ];
+ }
+
+ /**
+ * @covers ::__construct
+ * @covers ::config
+ * @covers ::get
+ */
+ public function testGet() {
+ $backend = $this->newObj()->get( 'local-backend' );
+ $this->assertTrue( $backend instanceof FSFileBackend );
+ }
+
+ /**
+ * @covers ::get
+ */
+ public function testGetUnrecognized() {
+ $this->setExpectedException( InvalidArgumentException::class,
+ "No backend defined with the name 'unrecognized'." );
+ $this->newObj()->get( 'unrecognized' );
+ }
+
+ /**
+ * @covers ::__construct
+ * @covers ::config
+ */
+ public function testConfig() {
+ $obj = $this->newObj();
+ $config = $obj->config( 'local-backend' );
+
+ // XXX How to actually test that a profiler is loaded?
+ $this->assertNull( $config['profiler']( 'x' ) );
+ // Equality comparison doesn't work for closures, so just set to null
+ $config['profiler'] = null;
+
+ $this->assertEquals( [
+ 'mimeCallback' => [ $obj, 'guessMimeInternal' ],
+ 'obResetFunc' => 'wfResetOutputBuffers',
+ 'streamMimeFunc' => [ StreamFile::class, 'contentTypeFromPath' ],
+ 'tmpFileFactory' => $this->tmpFileFactory,
+ 'statusWrapper' => [ Status::class, 'wrap' ],
+ 'wanCache' => $this->wanCache,
+ 'srvCache' => $this->srvCache,
+ 'logger' => LoggerFactory::getInstance( 'FileOperation' ),
+ // This was set to null above in $config, it's not really null
+ 'profiler' => null,
+ 'name' => 'local-backend',
+ 'containerPaths' => [
+ 'local-public' => 'upload-dir',
+ 'local-thumb' => 'thumb/',
+ 'local-transcoded' => 'transcoded/',
+ 'local-deleted' => 'deleted/',
+ 'local-temp' => 'upload-dir/temp',
+ ],
+ 'fileMode' => 0664,
+ 'directoryMode' => 0775,
+ 'domainId' => self::getWikiID(),
+ 'readOnly' => false,
+ 'class' => FSFileBackend::class,
+ 'lockManager' =>
+ $this->lmgFactory->getLockManagerGroup( self::getWikiID() )->get( 'fsLockManager' ),
+ 'fileJournal' =>
+ FileJournal::factory( [ 'class' => NullFileJournal::class ], 'local-backend' ),
+ ], $config );
+
+ // For config values that are objects, check object identity.
+ $this->assertSame( [ $obj, 'guessMimeInternal' ], $config['mimeCallback'] );
+ $this->assertSame( $this->tmpFileFactory, $config['tmpFileFactory'] );
+ $this->assertSame( $this->wanCache, $config['wanCache'] );
+ $this->assertSame( $this->srvCache, $config['srvCache'] );
+ }
+
+ /**
+ * @dataProvider provideConfig_default
+ * @param string $expected Expected default value
+ * @param string $inputName Name to set to null in LocalFileRepo setting
+ * @param string|array $key Key to check in array returned by config(), or array [ 'key1',
+ * 'key2' ] for nested key
+ * @covers ::__construct
+ * @covers ::config
+ */
+ public function testConfig_defaultNull( $expected, $inputName, $key ) {
+ $config = self::getDefaultLocalFileRepo();
+ $config[$inputName] = null;
+
+ $result = $this->newObj( [ 'LocalFileRepo' => $config ] )->config( 'local-backend' );
+
+ $actual = is_string( $key ) ? $result[$key] : $result[$key[0]][$key[1]];
+
+ $this->assertSame( $expected, $actual );
+ }
+
+ /**
+ * @dataProvider provideConfig_default
+ * @param string $expected Expected default value
+ * @param string $inputName Name to unset in LocalFileRepo setting
+ * @param string|array $key Key to check in array returned by config(), or array [ 'key1',
+ * 'key2' ] for nested key
+ * @covers ::__construct
+ * @covers ::config
+ */
+ public function testConfig_defaultUnset( $expected, $inputName, $key ) {
+ $config = self::getDefaultLocalFileRepo();
+ unset( $config[$inputName] );
+
+ $result = $this->newObj( [ 'LocalFileRepo' => $config ] )->config( 'local-backend' );
+
+ $actual = is_string( $key ) ? $result[$key] : $result[$key[0]][$key[1]];
+
+ $this->assertSame( $expected, $actual );
+ }
+
+ public static function provideConfig_default() {
+ return [
+ 'deletedDir' => [ false, 'deletedDir', [ 'containerPaths', 'local-deleted' ] ],
+ 'thumbDir' => [ 'upload-dir/thumb', 'thumbDir', [ 'containerPaths', 'local-thumb' ] ],
+ 'transcodedDir' => [
+ 'upload-dir/transcoded', 'transcodedDir', [ 'containerPaths', 'local-transcoded' ]
+ ],
+ 'fileMode' => [ 0644, 'fileMode', 'fileMode' ],
+ ];
+ }
+
+ /**
+ * @covers ::config
+ */
+ public function testConfig_fileJournal() {
+ $mockJournal = $this->createMock( FileJournal::class );
+ $mockJournal->expects( $this->never() )->method( $this->anything() );
+
+ $obj = $this->newObj( [ 'FileBackends' => [ [
+ 'name' => 'name',
+ 'class' => '',
+ 'lockManager' => 'fsLockManager',
+ 'fileJournal' => [ 'factory' =>
+ function () use ( $mockJournal ) {
+ return $mockJournal;
+ }
+ ],
+ ] ] ] );
+
+ $this->assertSame( $mockJournal, $obj->config( 'name' )['fileJournal'] );
+ }
+
+ /**
+ * @covers ::config
+ */
+ public function testConfigUnrecognized() {
+ $this->setExpectedException( InvalidArgumentException::class,
+ "No backend defined with the name 'unrecognized'." );
+ $this->newObj()->config( 'unrecognized' );
+ }
+
+ /**
+ * @dataProvider provideBackendFromPath
+ * @covers ::backendFromPath
+ * @param string|null $expected Name of backend that will be returned from 'get', or null
+ * @param string $storagePath
+ */
+ public function testBackendFromPath( $expected = null, $storagePath ) {
+ $obj = $this->newObj( [ 'FileBackends' => [
+ [ 'name' => '', 'class' => stdclass::class, 'lockManager' => 'fsLockManager' ],
+ [ 'name' => 'a', 'class' => stdclass::class, 'lockManager' => 'fsLockManager' ],
+ [ 'name' => 'b', 'class' => stdclass::class, 'lockManager' => 'fsLockManager' ],
+ ] ] );
+ $this->assertSame(
+ $expected === null ? null : $obj->get( $expected ),
+ $obj->backendFromPath( $storagePath )
+ );
+ }
+
+ public static function provideBackendFromPath() {
+ return [
+ 'Empty string' => [ null, '' ],
+ 'mwstore://' => [ null, 'mwstore://' ],
+ 'mwstore://a' => [ null, 'mwstore://a' ],
+ 'mwstore:///' => [ null, 'mwstore:///' ],
+ 'mwstore://a/' => [ null, 'mwstore://a/' ],
+ 'mwstore://a//' => [ null, 'mwstore://a//' ],
+ 'mwstore://a/b' => [ 'a', 'mwstore://a/b' ],
+ 'mwstore://a/b/' => [ 'a', 'mwstore://a/b/' ],
+ 'mwstore://a/b////' => [ 'a', 'mwstore://a/b////' ],
+ 'mwstore://a/b/c' => [ 'a', 'mwstore://a/b/c' ],
+ 'mwstore://a/b/c/d' => [ 'a', 'mwstore://a/b/c/d' ],
+ 'mwstore://b/b' => [ 'b', 'mwstore://b/b' ],
+ 'mwstore://c/b' => [ null, 'mwstore://c/b' ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideGuessMimeInternal
+ * @covers ::guessMimeInternal
+ * @param string $storagePath
+ * @param string|null $content
+ * @param string|null $fsPath
+ * @param string|null $expectedExtensionType Expected return of
+ * MimeAnalyzer::guessTypesForExtension
+ * @param string|null $expectedGuessedMimeType Expected return value of
+ * MimeAnalyzer::guessMimeType (null if expected not to be called)
+ */
+ public function testGuessMimeInternal(
+ $storagePath,
+ $content,
+ $fsPath,
+ $expectedExtensionType,
+ $expectedGuessedMimeType
+ ) {
+ $mimeAnalyzer = $this->createMock( MimeAnalyzer::class );
+ $mimeAnalyzer->expects( $this->once() )->method( 'guessTypesForExtension' )
+ ->willReturn( $expectedExtensionType );
+ $tmpFileFactory = $this->createMock( TempFSFileFactory::class );
+
+ if ( !$expectedExtensionType && $fsPath ) {
+ $tmpFileFactory->expects( $this->never() )->method( 'newTempFSFile' );
+ $mimeAnalyzer->expects( $this->once() )->method( 'guessMimeType' )
+ ->with( $fsPath, false )->willReturn( $expectedGuessedMimeType );
+ } elseif ( !$expectedExtensionType && strlen( $content ) ) {
+ // XXX What should we do about the file creation here? Really we should mock
+ // file_put_contents() somehow. It's not very nice to ignore the value of
+ // $wgTmpDirectory.
+ $tmpFile = ( new TempFSFileFactory() )->newTempFSFile( 'mime_', '' );
+
+ $tmpFileFactory->expects( $this->once() )->method( 'newTempFSFile' )
+ ->with( 'mime_', '' )->willReturn( $tmpFile );
+ $mimeAnalyzer->expects( $this->once() )->method( 'guessMimeType' )
+ ->with( $tmpFile->getPath(), false )->willReturn( $expectedGuessedMimeType );
+ } else {
+ $tmpFileFactory->expects( $this->never() )->method( 'newTempFSFile' );
+ $mimeAnalyzer->expects( $this->never() )->method( 'guessMimeType' );
+ }
+
+ $mimeAnalyzer->expects( $this->never() )
+ ->method( $this->anythingBut( 'guessTypesForExtension', 'guessMimeType' ) );
+ $tmpFileFactory->expects( $this->never() )
+ ->method( $this->anythingBut( 'newTempFSFile' ) );
+
+ $obj = $this->newObj( [
+ 'mimeAnalyzer' => $mimeAnalyzer,
+ 'tmpFileFactory' => $tmpFileFactory,
+ ] );
+
+ $this->assertSame( $expectedExtensionType ?? $expectedGuessedMimeType ?? 'unknown/unknown',
+ $obj->guessMimeInternal( $storagePath, $content, $fsPath ) );
+ }
+
+ public static function provideGuessMimeInternal() {
+ return [
+ 'With extension' =>
+ [ 'foo.txt', null, null, 'text/plain', null ],
+ 'No extension' =>
+ [ 'foo', null, null, null, null ],
+ 'Empty content, with extension' =>
+ [ 'foo.txt', '', null, 'text/plain', null ],
+ 'Empty content, no extension' =>
+ [ 'foo', '', null, null, null ],
+ 'Non-empty content, with extension' =>
+ [ 'foo.txt', '<b>foo</b>', null, 'text/plain', null ],
+ 'Non-empty content, no extension' =>
+ [ 'foo', '<b>foo</b>', null, null, 'text/html' ],
+ 'Empty path, with extension' =>
+ [ 'foo.txt', null, '', 'text/plain', null ],
+ 'Empty path, no extension' =>
+ [ 'foo', null, '', null, null ],
+ 'Non-empty path, with extension' =>
+ [ 'foo.txt', null, '/bogus/path', 'text/plain', null ],
+ 'Non-empty path, no extension' =>
+ [ 'foo', null, '/bogus/path', null, 'text/html' ],
+ 'Empty path and content, with extension' =>
+ [ 'foo.txt', '', '', 'text/plain', null ],
+ 'Empty path and content, no extension' =>
+ [ 'foo', '', '', null, null ],
+ 'Non-empty path and content, with extension' =>
+ [ 'foo.txt', '<b>foo</b>', '/bogus/path', 'text/plain', null ],
+ 'Non-empty path and content, no extension' =>
+ [ 'foo', '<b>foo</b>', '/bogus/path', null, 'image/jpeg' ],
+ ];
+ }
+}
*/
class LockManagerGroupFactoryTest extends MediaWikiUnitTestCase {
public function testGetLockManagerGroup() {
- $mockLbFactory = $this->createMock( LBFactory::class );
- $mockLbFactory->expects( $this->never() )->method( $this->anything() );
+ $mockLbFactory = $this->createNoOpMock( LBFactory::class );
$factory = new LockManagerGroupFactory( 'defaultDomain', [], $mockLbFactory );
$lbmUnspecified = $factory->getLockManagerGroup();
--- /dev/null
+<?php
+
+/**
+ * Code to test the getFallbackFor, getFallbacksFor, and getFallbacksIncludingSiteLanguage methods
+ * that have historically been static methods of the Language class. It can be used to test any
+ * class or object that implements those three methods.
+ */
+trait LanguageFallbackTestTrait {
+ /**
+ * @param array $options Valid keys:
+ * * expectedGets: How many times we expect to hit the localisation cache. (This can be
+ * ignored in integration tests -- it's enough to test in unit tests.)
+ * * siteLangCode
+ * @return string|object Name of class or object with the three methods getFallbackFor,
+ * getFallbacksFor, and getFallbacksIncludingSiteLanguage.
+ */
+ abstract protected function getCallee( array $options = [] );
+
+ /**
+ * @return int Value that was historically in Language::MESSAGES_FALLBACKS
+ */
+ abstract protected function getMessagesKey();
+
+ /**
+ * @return int Value that was historically in Language::STRICT_FALLBACKS
+ */
+ abstract protected function getStrictKey();
+
+ /**
+ * Convenience/readability wrapper to call a method on a class or object.
+ *
+ * @param string|object As in return value of getCallee()
+ * @param string $method Name of method to call
+ * @param mixed ...$params To pass to method
+ * @return mixed Return value of method
+ */
+ private function callMethod( $callee, $method, ...$params ) {
+ return [ $callee, $method ]( ...$params );
+ }
+
+ /**
+ * @param string $code
+ * @param array $expected
+ * @param array $options
+ * @dataProvider provideGetFallbacksFor
+ * @covers ::getFallbackFor
+ * @covers Language::getFallbackFor
+ */
+ public function testGetFallbackFor( $code, array $expected, array $options = [] ) {
+ $callee = $this->getCallee( $options );
+ // One behavior difference between the old static methods and the new instance methods:
+ // returning null instead of false.
+ $defaultExpected = is_object( $callee ) ? null : false;
+ $this->assertSame( $expected[0] ?? $defaultExpected,
+ $this->callMethod( $callee, 'getFallbackFor', $code ) );
+ }
+
+ /**
+ * @param string $code
+ * @param array $expected
+ * @param array $options
+ * @dataProvider provideGetFallbacksFor
+ * @covers ::getFallbacksFor
+ * @covers Language::getFallbacksFor
+ */
+ public function testGetFallbacksFor( $code, array $expected, array $options = [] ) {
+ $this->assertSame( $expected,
+ $this->callMethod( $this->getCallee( $options ), 'getFallbacksFor', $code ) );
+ }
+
+ /**
+ * @param string $code
+ * @param array $expected
+ * @param array $options
+ * @dataProvider provideGetFallbacksFor
+ * @covers ::getFallbacksFor
+ * @covers Language::getFallbacksFor
+ */
+ public function testGetFallbacksFor_messages( $code, array $expected, array $options = [] ) {
+ $this->assertSame( $expected,
+ $this->callMethod( $this->getCallee( $options ), 'getFallbacksFor',
+ $code, $this->getMessagesKey() ) );
+ }
+
+ public static function provideGetFallbacksFor() {
+ return [
+ 'en' => [ 'en', [], [ 'expectedGets' => 0 ] ],
+ 'fr' => [ 'fr', [ 'en' ] ],
+ 'sco' => [ 'sco', [ 'en' ] ],
+ 'yi' => [ 'yi', [ 'he', 'en' ] ],
+ 'ruq' => [ 'ruq', [ 'ruq-latn', 'ro', 'en' ] ],
+ 'sh' => [ 'sh', [ 'bs', 'sr-el', 'hr', 'en' ] ],
+ ];
+ }
+
+ /**
+ * @param string $code
+ * @param array $expected
+ * @param array $options
+ * @dataProvider provideGetFallbacksFor_strict
+ * @covers ::getFallbacksFor
+ * @covers Language::getFallbacksFor
+ */
+ public function testGetFallbacksFor_strict( $code, array $expected, array $options = [] ) {
+ $this->assertSame( $expected,
+ $this->callMethod( $this->getCallee( $options ), 'getFallbacksFor',
+ $code, $this->getStrictKey() ) );
+ }
+
+ public static function provideGetFallbacksFor_strict() {
+ return [
+ 'en' => [ 'en', [], [ 'expectedGets' => 0 ] ],
+ 'fr' => [ 'fr', [] ],
+ 'sco' => [ 'sco', [ 'en' ] ],
+ 'yi' => [ 'yi', [ 'he' ] ],
+ 'ruq' => [ 'ruq', [ 'ruq-latn', 'ro' ] ],
+ 'sh' => [ 'sh', [ 'bs', 'sr-el', 'hr' ] ],
+ ];
+ }
+
+ /**
+ * @covers ::getFallbacksFor
+ * @covers Language::getFallbacksFor
+ */
+ public function testGetFallbacksFor_exception() {
+ $this->setExpectedException( MWException::class, 'Invalid fallback mode "7"' );
+
+ $callee = $this->getCallee( [ 'expectedGets' => 0 ] );
+
+ // These should not throw, because of short-circuiting. If they do, it will fail the test,
+ // because we pass 5 and 6 instead of 7.
+ $this->callMethod( $callee, 'getFallbacksFor', 'en', 5 );
+ $this->callMethod( $callee, 'getFallbacksFor', '!!!', 6 );
+
+ // This is the one that should throw.
+ $this->callMethod( $callee, 'getFallbacksFor', 'fr', 7 );
+ }
+
+ /**
+ * @param string $code
+ * @param string $siteLangCode
+ * @param array $expected
+ * @param int $expectedGets
+ * @dataProvider provideGetFallbacksIncludingSiteLanguage
+ * @covers ::getFallbacksIncludingSiteLanguage
+ * @covers Language::getFallbacksIncludingSiteLanguage
+ */
+ public function testGetFallbacksIncludingSiteLanguage(
+ $code, $siteLangCode, array $expected, $expectedGets = 1
+ ) {
+ $callee = $this->getCallee(
+ [ 'siteLangCode' => $siteLangCode, 'expectedGets' => $expectedGets ] );
+ $this->assertSame( $expected,
+ $this->callMethod( $callee, 'getFallbacksIncludingSiteLanguage', $code ) );
+
+ // Call again to make sure we don't call LocalisationCache again
+ $this->callMethod( $callee, 'getFallbacksIncludingSiteLanguage', $code );
+ }
+
+ public static function provideGetFallbacksIncludingSiteLanguage() {
+ return [
+ 'en on en' => [ 'en', 'en', [ [], [ 'en' ] ], 0 ],
+ 'fr on en' => [ 'fr', 'en', [ [ 'en' ], [] ] ],
+ 'en on fr' => [ 'en', 'fr', [ [], [ 'fr', 'en' ] ] ],
+ 'fr on fr' => [ 'fr', 'fr', [ [ 'en' ], [ 'fr' ] ] ],
+
+ 'sco on en' => [ 'sco', 'en', [ [ 'en' ], [] ] ],
+ 'en on sco' => [ 'en', 'sco', [ [], [ 'sco', 'en' ] ] ],
+ 'sco on sco' => [ 'sco', 'sco', [ [ 'en' ], [ 'sco' ] ] ],
+
+ 'fr on sco' => [ 'fr', 'sco', [ [ 'en' ], [ 'sco' ] ], 2 ],
+ 'sco on fr' => [ 'sco', 'fr', [ [ 'en' ], [ 'fr' ] ], 2 ],
+
+ 'fr on yi' => [ 'fr', 'yi', [ [ 'en' ], [ 'yi', 'he' ] ], 2 ],
+ 'yi on fr' => [ 'yi', 'fr', [ [ 'he', 'en' ], [ 'fr' ] ], 2 ],
+ 'yi on yi' => [ 'yi', 'yi', [ [ 'he', 'en' ], [ 'yi' ] ] ],
+
+ 'sh on ruq' => [ 'sh', 'ruq',
+ [ [ 'bs', 'sr-el', 'hr', 'en' ], [ 'ruq', 'ruq-latn', 'ro' ] ], 2 ],
+ 'ruq on sh' => [ 'ruq', 'sh',
+ [ [ 'ruq-latn', 'ro', 'en' ], [ 'sh', 'bs', 'sr-el', 'hr' ] ], 2 ],
+ ];
+ }
+}
'Parse an ftp URI correctly with user and password'
);
+ uri = new mw.Uri( 'http://example.com/?foo[1]=b&foo[0]=a&foo[]=c' );
+
+ assert.deepEqual(
+ uri.query,
+ {
+ 'foo[1]': 'b',
+ 'foo[0]': 'a',
+ 'foo[]': 'c'
+ },
+ 'Array query parameters parsed as normal with arrayParams:false'
+ );
+
assert.throws(
function () {
return new mw.Uri( 'glaswegian penguins' );
window.Set = this.nativeSet;
mw.redefineFallbacksForTest();
}
+ if ( this.resetStoreKey ) {
+ localStorage.removeItem( mw.loader.store.key );
+ }
// Remove any remaining temporary statics
// exposed for cross-file mocks.
delete mw.loader.testCallback;
} );
} );
+ QUnit.test( 'mw.loader.store.init - Invalid JSON', function ( assert ) {
+ // Reset
+ this.sandbox.stub( mw.loader.store, 'enabled', null );
+ this.sandbox.stub( mw.loader.store, 'items', {} );
+ this.resetStoreKey = true;
+ localStorage.setItem( mw.loader.store.key, 'invalid' );
+
+ mw.loader.store.init();
+ assert.strictEqual( mw.loader.store.enabled, true, 'Enabled' );
+ assert.strictEqual(
+ $.isEmptyObject( mw.loader.store.items ),
+ true,
+ 'Items starts fresh'
+ );
+ } );
+
+ QUnit.test( 'mw.loader.store.init - Wrong JSON', function ( assert ) {
+ // Reset
+ this.sandbox.stub( mw.loader.store, 'enabled', null );
+ this.sandbox.stub( mw.loader.store, 'items', {} );
+ this.resetStoreKey = true;
+ localStorage.setItem( mw.loader.store.key, JSON.stringify( { wrong: true } ) );
+
+ mw.loader.store.init();
+ assert.strictEqual( mw.loader.store.enabled, true, 'Enabled' );
+ assert.strictEqual(
+ $.isEmptyObject( mw.loader.store.items ),
+ true,
+ 'Items starts fresh'
+ );
+ } );
+
+ QUnit.test( 'mw.loader.store.init - Expired JSON', function ( assert ) {
+ // Reset
+ this.sandbox.stub( mw.loader.store, 'enabled', null );
+ this.sandbox.stub( mw.loader.store, 'items', {} );
+ this.resetStoreKey = true;
+ localStorage.setItem( mw.loader.store.key, JSON.stringify( {
+ items: { use: 'not me' },
+ vary: mw.loader.store.vary,
+ asOf: 130161 // 2011-04-01 12:00
+ } ) );
+
+ mw.loader.store.init();
+ assert.strictEqual( mw.loader.store.enabled, true, 'Enabled' );
+ assert.strictEqual(
+ $.isEmptyObject( mw.loader.store.items ),
+ true,
+ 'Items starts fresh'
+ );
+ } );
+
+ QUnit.test( 'mw.loader.store.init - Good JSON', function ( assert ) {
+ // Reset
+ this.sandbox.stub( mw.loader.store, 'enabled', null );
+ this.sandbox.stub( mw.loader.store, 'items', {} );
+ this.resetStoreKey = true;
+ localStorage.setItem( mw.loader.store.key, JSON.stringify( {
+ items: { use: 'me' },
+ vary: mw.loader.store.vary,
+ asOf: Math.ceil( Date.now() / 1e7 ) - 5 // ~ 13 hours ago
+ } ) );
+
+ mw.loader.store.init();
+ assert.strictEqual( mw.loader.store.enabled, true, 'Enabled' );
+ assert.deepEqual(
+ mw.loader.store.items,
+ { use: 'me' },
+ 'Stored items are loaded'
+ );
+ } );
+
QUnit.test( 'require()', function ( assert ) {
mw.loader.register( [
[ 'test.require1', '0' ],
/* Grade X */
+ // Open WebOS < 1.5 (Palm Pre, Palm Pixi)
+ 'Mozilla/5.0 (webOS/1.0; U; en-US) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/1.0 Safari/525.27.1 Pre/1.0',
+ 'Mozilla/5.0 (webOS/1.4.0; U; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Version/1.0 Safari/532.2 Pixi/1.1 ',
+ // SymbianOS
+ 'NokiaN95_8GB-3;Mozilla/5.0 SymbianOS/9.2;U;Series60/3.1 NokiaN95_8GB-3/11.2.011 Profile/MIDP-2.0 Configuration/CLDC-1.1 AppleWebKit/413 (KHTML, like Gecko)',
+ 'Nokia7610/2.0 (5.0509.0) SymbianOS/7.0s Series60/2.1 Profile/MIDP-2.0 Configuration/CLDC-1.0 ',
+ 'Mozilla/5.0 (SymbianOS/9.1; U; [en]; SymbianOS/91 Series60/3.0) AppleWebKit/413 (KHTML, like Gecko) Safari/413',
+ 'Mozilla/5.0 (SymbianOS/9.3; Series60/3.2 NokiaE52-2/091.003; Profile/MIDP-2.1 Configuration/CLDC-1.1 ) AppleWebKit/533.4 (KHTML, like Gecko) NokiaBrowser/7.3.1.34 Mobile Safari/533.4',
// Gecko
'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.7) Gecko/20060928 (Debian|Debian-1.8.0.7-1) Epiphany/2.14',
'Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.1.6) Gecko/20070817 IceWeasel/2.0.0.6-g2',
'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)',
// IE Mobile 10
'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; HTC; Windows Phone 8X by HTC)',
- // Open WebOS < 1.5 (Palm Pre, Palm Pixi)
- 'Mozilla/5.0 (webOS/1.0; U; en-US) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/1.0 Safari/525.27.1 Pre/1.0',
- 'Mozilla/5.0 (webOS/1.4.0; U; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Version/1.0 Safari/532.2 Pixi/1.1 ',
- // SymbianOS
- 'NokiaN95_8GB-3;Mozilla/5.0 SymbianOS/9.2;U;Series60/3.1 NokiaN95_8GB-3/11.2.011 Profile/MIDP-2.0 Configuration/CLDC-1.1 AppleWebKit/413 (KHTML, like Gecko)',
- 'Nokia7610/2.0 (5.0509.0) SymbianOS/7.0s Series60/2.1 Profile/MIDP-2.0 Configuration/CLDC-1.0 ',
- 'Mozilla/5.0 (SymbianOS/9.1; U; [en]; SymbianOS/91 Series60/3.0) AppleWebKit/413 (KHTML, like Gecko) Safari/413',
- 'Mozilla/5.0 (SymbianOS/9.3; Series60/3.2 NokiaE52-2/091.003; Profile/MIDP-2.1 Configuration/CLDC-1.1 ) AppleWebKit/533.4 (KHTML, like Gecko) NokiaBrowser/7.3.1.34 Mobile Safari/533.4',
// NetFront
'Mozilla/4.0 (compatible; Linux 2.6.10) NetFront/3.3 Kindle/1.0 (screen 600x800)',
'Mozilla/4.0 (compatible; Linux 2.6.22) NetFront/3.4 Kindle/2.0 (screen 824x1200; rotate)',