Use ...->stashFile()->getFileKey() instead.
* "Public domain" was removed as a wiki license option from the installer, in
favour of CC-0.
+* AuthenticationRequest::$required is now changed from REQUIRED to PRIMARY_REQUIRED
+ on requests needed by primary providers even if all primaries need them.
+ Primary providers are discouraged from returning multiple REQUIRED requests.
== Compatibility ==
case self::AS_CANNOT_USE_CUSTOM_MODEL:
case self::AS_PARSE_ERROR:
- $wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>' );
+ $wgOut->addWikiText( '<div class="error">' . "\n" . $status->getWikiText() . '</div>' );
return true;
case self::AS_SUCCESS_NEW_ARTICLE:
// is if an extension hook aborted from inside ArticleSave.
// Render the status object into $this->hookError
// FIXME this sucks, we should just use the Status object throughout
- $this->hookError = '<div class="error">' . $status->getWikiText() .
+ $this->hookError = '<div class="error">' ."\n" . $status->getWikiText() .
'</div>';
return true;
}
* 3) Adds a "delimiter" element to the array, either '://', ':' or '//' (see (2)).
*
* @param string $url A URL to parse
- * @return string[] Bits of the URL in an associative array, per PHP docs
+ * @return string[]|bool Bits of the URL in an associative array, per PHP docs, false on failure
*/
function wfParseUrl( $url ) {
global $wgUrlProtocols; // Allow all protocols defined in DefaultSettings/LocalSettings.php
$pageid = $this->oldTitle->getArticleID( Title::GAID_FOR_UPDATE );
$protected = $this->oldTitle->isProtected();
- // Do the actual move
+ // Do the actual move; if this fails, it will throw an MWException(!)
$nullRevision = $this->moveToInternal( $user, $this->newTitle, $reason, $createRedirect );
// Refresh the sortkey for this row. Be careful to avoid resetting
*
* @fixme This was basically directly moved from Title, it should be split into smaller functions
* @param User $user the User doing the move
- * @param Title $nt The page to move to, which should be a redirect or nonexistent
+ * @param Title $nt The page to move to, which should be a redirect or non-existent
* @param string $reason The reason for the move
* @param bool $createRedirect Whether to leave a redirect at the old title. Does not check
* if the user has the suppressredirect right
$logType = 'move';
}
+ if ( $moveOverRedirect ) {
+ $overwriteMessage = wfMessage(
+ 'delete_and_move_reason',
+ $this->oldTitle->getPrefixedText()
+ )->text();
+ $newpage = WikiPage::factory( $nt );
+ $errs = [];
+ $status = $newpage->doDeleteArticleReal(
+ $overwriteMessage,
+ /* $suppress */ false,
+ $nt->getArticleId(),
+ /* $commit */ false,
+ $errs,
+ $user
+ );
+
+ if ( !$status->isGood() ) {
+ throw new MWException( 'Failed to delete page-move revision: ' . $status );
+ }
+
+ $nt->resetArticleID( false );
+ }
+
if ( $createRedirect ) {
if ( $this->oldTitle->getNamespace() == NS_CATEGORY
&& !wfMessage( 'category-move-redirect-override' )->inContentLanguage()->isDisabled()
$newpage = WikiPage::factory( $nt );
- if ( $moveOverRedirect ) {
- $newid = $nt->getArticleID();
- $newcontent = $newpage->getContent();
-
- # Delete the old redirect. We don't save it to history since
- # by definition if we've got here it's rather uninteresting.
- # We have to remove it so that the next step doesn't trigger
- # a conflict on the unique namespace+title index...
- $dbw->delete( 'page', [ 'page_id' => $newid ], __METHOD__ );
-
- $newpage->doDeleteUpdates( $newid, $newcontent );
- }
-
# Save a null revision in the page's history notifying of the move
$nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true, $user );
if ( !is_object( $nullRevision ) ) {
);
}
- if ( !$moveOverRedirect ) {
- WikiPage::onArticleCreate( $nt );
- }
+ WikiPage::onArticleCreate( $nt );
# Recreate the redirect, this time in the other direction.
if ( $redirectContent ) {
'oojs-ui.styles.textures',
'mediawiki.widgets.styles',
] );
- // Used by 'skipFunction' of the four 'oojs-ui.styles.*' modules. Please don't treat this as a
- // public API or you'll be severely disappointed when T87871 is fixed and it disappears.
- $this->addMeta( 'X-OOUI-PHP', '1' );
}
/**
// Build map of extension directories to extension info
if ( self::$extensionInfo === null ) {
+ $extDir = $this->getConfig()->get( 'ExtensionDirectory' );
self::$extensionInfo = [
realpath( __DIR__ ) ?: __DIR__ => [
'path' => $IP,
'license-name' => 'GPL-2.0+',
],
realpath( "$IP/extensions" ) ?: "$IP/extensions" => null,
+ realpath( $extDir ) ?: $extDir => null,
];
$keep = [
'path' => null,
$authRes = 'Failed';
$message = $res->message;
\MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
- ->info( __METHOD__ . ': Authentication failed: ' . $message->plain() );
+ ->info( __METHOD__ . ': Authentication failed: '
+ . $message->inLanguage( 'en' )->plain() );
break;
default:
+ \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
+ ->info( __METHOD__ . ': Authentication failed due to unsupported response type: '
+ . $res->status, $this->getAuthenticationResponseLogData( $res ) );
$authRes = 'Aborted';
break;
}
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/API:Login';
}
+
+ /**
+ * Turns an AuthenticationResponse into a hash suitable for passing to Logger
+ * @param AuthenticationResponse $response
+ * @return array
+ */
+ protected function getAuthenticationResponseLogData( AuthenticationResponse $response ) {
+ $ret = [
+ 'status' => $response->status,
+ ];
+ if ( $response->message ) {
+ $ret['message'] = $response->message->inLanguage( 'en' )->plain();
+ };
+ $reqs = [
+ 'neededRequests' => $response->neededRequests,
+ 'createRequest' => $response->createRequest,
+ 'linkRequest' => $response->linkRequest,
+ ];
+ foreach ( $reqs as $k => $v ) {
+ if ( $v ) {
+ $v = is_array( $v ) ? $v : [ $v ];
+ $reqClasses = array_unique( array_map( 'get_class', $v ) );
+ sort( $reqClasses );
+ $ret[$k] = implode( ', ', $reqClasses );
+ }
+ }
+ return $ret;
+ }
}
* @defgroup API API
*/
+use MediaWiki\Logger\LoggerFactory;
+
/**
* This is the main API class, used for both external and internal processing.
* When executed, it will create the requested formatter object,
$config->get( 'CrossSiteAJAXdomainExceptions' )
)
) ) {
- MediaWiki\Logger\LoggerFactory::getInstance( 'cors' )->warning(
+ LoggerFactory::getInstance( 'cors' )->warning(
'Non-whitelisted CORS request with session cookies', [
'origin' => $originHeader,
'cookies' => $sessionCookies,
protected function setRequestExpectations( ApiBase $module ) {
$limits = $this->getConfig()->get( 'TrxProfilerLimits' );
$trxProfiler = Profiler::instance()->getTransactionProfiler();
+ $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
if ( $this->getRequest()->hasSafeMethod() ) {
$trxProfiler->setExpectations( $limits['GET'], __METHOD__ );
} elseif ( $this->getRequest()->wasPosted() && !$module->isWriteMode() ) {
'code' => 'lh',
'prefix' => 'pl',
'linktable' => 'pagelinks',
+ 'indexes' => [ 'pl_namespace', 'pl_backlinks_namespace' ],
'from_namespace' => true,
'showredirects' => true,
],
'code' => 'ti',
'prefix' => 'tl',
'linktable' => 'templatelinks',
+ 'indexes' => [ 'tl_namespace', 'tl_backlinks_namespace' ],
'from_namespace' => true,
'showredirects' => true,
],
'code' => 'fu',
'prefix' => 'il',
'linktable' => 'imagelinks',
+ 'indexes' => [ 'il_to', 'il_backlinks_namespace' ],
'from_namespace' => true,
'to_namespace' => NS_FILE,
'exampletitle' => 'File:Example.jpg',
// Override any ORDER BY from above with what we calculated earlier.
$this->addOption( 'ORDER BY', array_keys( $sortby ) );
+ // MySQL's optimizer chokes if we have too many values in "$bl_title IN
+ // (...)" and chooses the wrong index, so specify the correct index to
+ // use for the query. See T139056 for details.
+ if ( !empty( $settings['indexes'] ) ) {
+ list( $idxNoFromNS, $idxWithFromNS ) = $settings['indexes'];
+ if ( $params['namespace'] !== null && !empty( $settings['from_namespace'] ) ) {
+ $this->addOption( 'USE INDEX', [ $settings['linktable'] => $idxWithFromNS ] );
+ } else {
+ $this->addOption( 'USE INDEX', [ $settings['linktable'] => $idxNoFromNS ] );
+ }
+ }
+
$this->addOption( 'LIMIT', $params['limit'] + 1 );
$res = $this->select( __METHOD__ );
'offset' => $this->mUpload->getOffset(),
];
- $this->dieUsage( $status->getWikiText( false, false, 'en' ), 'stashfailed', 0, $extradata );
+ $this->dieStatusWithCode( $status, 'stashfailed', $extradata );
}
}
$filekey,
[ 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status ]
);
- $this->dieUsage( $status->getWikiText( false, false, 'en' ), 'stashfailed' );
+ $this->dieStatusWithCode( $status, 'stashfailed' );
}
// The fully concatenated file has a new filekey. So remove
$this->dieUsage( $parsed['info'], $parsed['code'], 0, $data );
}
+ /**
+ * Like dieStatus(), but always uses $overrideCode for the error code, unless the code comes from
+ * IApiMessage.
+ *
+ * @param Status $status
+ * @param string $overrideCode Error code to use if there isn't one from IApiMessage
+ * @param array|null $moreExtraData
+ * @throws UsageException
+ */
+ public function dieStatusWithCode( $status, $overrideCode, $moreExtraData = null ) {
+ $extraData = null;
+ list( $code, $msg ) = $this->getErrorFromStatus( $status, $extraData );
+ $errors = $status->getErrorsByType( 'error' ) ?: $status->getErrorsByType( 'warning' );
+ if ( !( $errors[0]['message'] instanceof IApiMessage ) ) {
+ $code = $overrideCode;
+ }
+ if ( $moreExtraData ) {
+ $extraData += $moreExtraData;
+ }
+ $this->dieUsage( $msg, $code, 0, $extraData );
+ }
+
/**
* Select an upload module and set it to mUpload. Dies on failure. If the
* request was a status request and not a true upload, returns false;
if ( !$progress ) {
$this->dieUsage( 'No result in status data', 'missingresult' );
} elseif ( !$progress['status']->isGood() ) {
- $this->dieUsage( $progress['status']->getWikiText( false, false, 'en' ), 'stashfailed' );
+ $this->dieStatusWithCode( $progress['status'], 'stashfailed' );
}
if ( isset( $progress['status']->value['verification'] ) ) {
$this->checkVerification( $progress['status']->value['verification'] );
if ( $this->mParams['chunk'] ) {
// Chunk upload
- $this->mUpload = new UploadFromChunks();
+ $this->mUpload = new UploadFromChunks( $this->getUser() );
if ( isset( $this->mParams['filekey'] ) ) {
if ( $this->mParams['offset'] === 0 ) {
$this->dieUsage( 'Cannot supply a filekey when offset is 0', 'badparams' );
// Query them and merge results
$reqs = [];
- $allPrimaryRequired = null;
foreach ( $providers as $provider ) {
$isPrimary = $provider instanceof PrimaryAuthenticationProvider;
- $thisRequired = [];
foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
$id = $req->getUniqueId();
- // If it's from a Primary, mark it as "primary-required" but
- // track it for later.
+ // If a required request if from a Primary, mark it as "primary-required" instead
if ( $isPrimary ) {
if ( $req->required ) {
- $thisRequired[$id] = true;
$req->required = AuthenticationRequest::PRIMARY_REQUIRED;
}
}
- if ( !isset( $reqs[$id] ) || $req->required === AuthenticationRequest::REQUIRED ) {
+ if (
+ !isset( $reqs[$id] )
+ || $req->required === AuthenticationRequest::REQUIRED
+ || $reqs[$id] === AuthenticationRequest::OPTIONAL
+ ) {
$reqs[$id] = $req;
}
}
-
- // Track which requests are required by all primaries
- if ( $isPrimary ) {
- $allPrimaryRequired = $allPrimaryRequired === null
- ? $thisRequired
- : array_intersect_key( $allPrimaryRequired, $thisRequired );
- }
- }
- // Any requests that were required by all primaries are required.
- foreach ( (array)$allPrimaryRequired as $id => $dummy ) {
- $reqs[$id]->required = AuthenticationRequest::REQUIRED;
}
// AuthManager has its own req for some actions
const REQUIRED = 1;
/** Indicates that the request is required by a primary authentication
- * provdier, but other primary authentication providers do not require it. */
+ * provdier. Since the user can choose which primary to authenticate with,
+ * the request might or might not end up being actually required. */
const PRIMARY_REQUIRED = 2;
/** @var string|null The AuthManager::ACTION_* constant this request was
/** Provider cannot create or link to accounts */
const TYPE_NONE = 'none';
+ /**
+ * {@inheritdoc}
+ *
+ * Of the requests returned by this method, exactly one should have
+ * {@link AuthenticationRequest::$required} set to REQUIRED.
+ */
+ public function getAuthenticationRequests( $action, array $options );
+
/**
* Start an authentication flow
*
*
* @param string $event Event name
* @param array $args Parameters passed to hook functions
- * @param bool $warn Whether to log a warning.
- * Default to self::$enableDeprecationWarnings.
- * May be set to false for testing.
+ * @param string|null $deprecatedVersion Emit a deprecation notice
+ * when the hook is run for the provided version
*
* @return bool True if no handler aborted the hook
- *
- * @see ContentHandler::$enableDeprecationWarnings
*/
public static function runLegacyHooks( $event, $args = [],
- $warn = null
+ $deprecatedVersion = null
) {
- if ( $warn === null ) {
- $warn = self::$enableDeprecationWarnings;
- }
-
if ( !Hooks::isRegistered( $event ) ) {
return true; // nothing to do here
}
- if ( $warn ) {
- // Log information about which handlers are registered for the legacy hook,
- // so we can find and fix them.
-
- $handlers = Hooks::getHandlers( $event );
- $handlerInfo = [];
-
- MediaWiki\suppressWarnings();
-
- foreach ( $handlers as $handler ) {
- if ( is_array( $handler ) ) {
- if ( is_object( $handler[0] ) ) {
- $info = get_class( $handler[0] );
- } else {
- $info = $handler[0];
- }
-
- if ( isset( $handler[1] ) ) {
- $info .= '::' . $handler[1];
- }
- } elseif ( is_object( $handler ) ) {
- $info = get_class( $handler[0] );
- $info .= '::on' . $event;
- } else {
- $info = $handler;
- }
-
- $handlerInfo[] = $info;
- }
-
- MediaWiki\restoreWarnings();
-
- wfWarn( "Using obsolete hook $event via ContentHandler::runLegacyHooks()! Handlers: " .
- implode( ', ', $handlerInfo ), 2 );
- }
-
// convert Content objects to text
$contentObjects = [];
$contentTexts = [];
}
// call the hook functions
- $ok = Hooks::run( $event, $args );
+ $ok = Hooks::run( $event, $args, $deprecatedVersion );
// see if the hook changed the text
foreach ( $contentTexts as $k => $orig ) {
protected function getFileText( Title $title ) {
$file = wfLocalFile( $title );
if ( $file && $file->exists() ) {
- return $file->getHandler()->getEntireText( $file );
+ $handler = $file->getHandler();
+ if ( !$handler ) {
+ return null;
+ }
+ return $handler->getEntireText( $file );
}
return null;
return $this->__call( __FUNCTION__, func_get_args() );
}
- public function begin( $fname = __METHOD__ ) {
+ public function begin( $fname = __METHOD__, $mode = IDatabase::TRANSACTION_EXPLICIT ) {
return $this->__call( __FUNCTION__, func_get_args() );
}
if ( !$this->mTrxLevel && $this->getFlag( DBO_TRX )
&& $this->isTransactableQuery( $sql )
) {
- $this->begin( __METHOD__ . " ($fname)" );
+ $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
$this->mTrxAutomatic = true;
}
$useTrx = !$this->mTrxLevel;
if ( $useTrx ) {
- $this->begin( $fname );
+ $this->begin( $fname, self::TRANSACTION_INTERNAL );
}
try {
# Update any existing conflicting row(s)
throw $e;
}
if ( $useTrx ) {
- $this->commit( $fname );
+ $this->commit( $fname, self::TRANSACTION_INTERNAL );
}
return $ok;
$this->mTrxPreCommitCallbacks[] = [ $callback, wfGetCaller() ];
} else {
// If no transaction is active, then make one for this callback
- $this->begin( __METHOD__ );
+ $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
try {
call_user_func( $callback );
$this->commit( __METHOD__ );
final public function startAtomic( $fname = __METHOD__ ) {
if ( !$this->mTrxLevel ) {
- $this->begin( $fname );
+ $this->begin( $fname, self::TRANSACTION_INTERNAL );
$this->mTrxAutomatic = true;
// If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
// in all changes being in one transaction to keep requests transactional.
final public function doAtomicSection( $fname, callable $callback ) {
$this->startAtomic( $fname );
try {
- call_user_func_array( $callback, [ $this, $fname ] );
+ $res = call_user_func_array( $callback, [ $this, $fname ] );
} catch ( Exception $e ) {
$this->rollback( $fname );
throw $e;
}
$this->endAtomic( $fname );
+
+ return $res;
}
- final public function begin( $fname = __METHOD__ ) {
- if ( $this->mTrxLevel ) { // implicit commit
+ final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
+ // Protect against mismatched atomic section, transaction nesting, and snapshot loss
+ if ( $this->mTrxLevel ) {
if ( $this->mTrxAtomicLevels ) {
- // If the current transaction was an automatic atomic one, then we definitely have
- // a problem. Same if there is any unclosed atomic level.
$levels = implode( ', ', $this->mTrxAtomicLevels );
- throw new DBUnexpectedError(
- $this,
- "Got explicit BEGIN from $fname while atomic section(s) $levels are open."
- );
+ $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
+ throw new DBUnexpectedError( $this, $msg );
} elseif ( !$this->mTrxAutomatic ) {
- // We want to warn about inadvertently nested begin/commit pairs, but not about
- // auto-committing implicit transactions that were started by query() via DBO_TRX
- throw new DBUnexpectedError(
- $this,
- "$fname: Transaction already in progress (from {$this->mTrxFname}), " .
- " performing implicit commit!"
- );
- } elseif ( $this->mTrxDoneWrites ) {
- // The transaction was automatic and has done write operations
- throw new DBUnexpectedError(
- $this,
- "$fname: Automatic transaction with writes in progress" .
- " (from {$this->mTrxFname}), performing implicit commit!\n"
- );
- }
-
- $this->runOnTransactionPreCommitCallbacks();
- $writeTime = $this->pendingWriteQueryDuration();
- $this->doCommit( $fname );
- if ( $this->mTrxDoneWrites ) {
- $this->mDoneWrites = microtime( true );
- $this->getTransactionProfiler()->transactionWritingOut(
- $this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime );
+ $msg = "$fname: Explicit transaction already active (from {$this->mTrxFname}).";
+ throw new DBUnexpectedError( $this, $msg );
+ } else {
+ // @TODO: make this an exception at some point
+ $msg = "$fname: Implicit transaction already active (from {$this->mTrxFname}).";
+ wfLogDBError( $msg );
+ return; // join the main transaction set
}
-
- $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
+ } elseif ( $this->getFlag( DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
+ // @TODO: make this an exception at some point
+ wfLogDBError( "$fname: Implicit transaction expected (DBO_TRX set)." );
+ return; // let any writes be in the main transaction
}
// Avoid fatals if close() was called
$levels = implode( ', ', $this->mTrxAtomicLevels );
throw new DBUnexpectedError(
$this,
- "Got COMMIT while atomic sections $levels are still open"
+ "$fname: Got COMMIT while atomic sections $levels are still open."
);
}
} elseif ( !$this->mTrxAutomatic ) {
throw new DBUnexpectedError(
$this,
- "$fname: Flushing an explicit transaction, getting out of sync!"
+ "$fname: Flushing an explicit transaction, getting out of sync."
);
}
} else {
if ( !$this->mTrxLevel ) {
- wfWarn( "$fname: No transaction to commit, something got out of sync!" );
+ wfWarn( "$fname: No transaction to commit, something got out of sync." );
return; // nothing to do
} elseif ( $this->mTrxAutomatic ) {
- throw new DBUnexpectedError(
- $this,
- "$fname: Explicit commit of implicit transaction."
- );
+ // @TODO: make this an exception at some point
+ wfLogDBError( "$fname: Explicit commit of implicit transaction." );
+ return; // wait for the main transaction set commit round
}
}
}
final public function rollback( $fname = __METHOD__, $flush = '' ) {
- if ( $flush !== self::FLUSHING_INTERNAL && $flush !== self::FLUSHING_ALL_PEERS ) {
+ if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
if ( !$this->mTrxLevel ) {
- wfWarn( "$fname: No transaction to rollback, something got out of sync!" );
return; // nothing to do
}
} else {
if ( !$this->mTrxLevel ) {
+ wfWarn( "$fname: No transaction to rollback, something got out of sync." );
return; // nothing to do
+ } elseif ( $this->getFlag( DBO_TRX ) ) {
+ throw new DBUnexpectedError(
+ $this,
+ "$fname: Expected mass rollback of all peer databases (DBO_TRX set)."
+ );
}
}
}
public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
+ if ( $this->writesOrCallbacksPending() ) {
+ // This only flushes transactions to clear snapshots, not to write data
+ throw new DBUnexpectedError(
+ $this,
+ "$fname: Cannot COMMIT to clear snapshot because writes are pending."
+ );
+ }
+
if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
return null;
}
$unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
- $this->commit( __METHOD__, self::FLUSHING_INTERNAL );
- $this->unlock( $lockKey, $fname );
+ if ( $this->trxLevel() ) {
+ // There is a good chance an exception was thrown, causing any early return
+ // from the caller. Let any error handler get a chance to issue rollback().
+ // If there isn't one, let the error bubble up and trigger server-side rollback.
+ $this->onTransactionResolution( function () use ( $lockKey, $fname ) {
+ $this->unlock( $lockKey, $fname );
+ } );
+ } else {
+ $this->unlock( $lockKey, $fname );
+ }
} );
$this->commit( __METHOD__, self::FLUSHING_INTERNAL );
$this->didbegin = false;
/* If we are not in a transaction, we need to be for savepoint trickery */
if ( !$dbw->trxLevel() ) {
- $dbw->begin( "FOR SAVEPOINT" );
+ $dbw->begin( "FOR SAVEPOINT", DatabasePostgres::TRANSACTION_INTERNAL );
$this->didbegin = true;
}
}
* @param string $desiredSchema
*/
function determineCoreSchema( $desiredSchema ) {
- $this->begin( __METHOD__ );
+ $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
if ( $this->schemaExists( $desiredSchema ) ) {
if ( in_array( $desiredSchema, $this->getSchemas() ) ) {
$this->mCoreSchema = $desiredSchema;
/** @var int Callback triggered by rollback */
const TRIGGER_ROLLBACK = 3;
+ /** @var string Transaction is requested by regular caller outside of the DB layer */
+ const TRANSACTION_EXPLICIT = '';
+ /** @var string Transaction is requested interally via DBO_TRX/startAtomic() */
+ const TRANSACTION_INTERNAL = 'implicit';
+
/** @var string Transaction operation comes from service managing all DBs */
const FLUSHING_ALL_PEERS = 'flush';
/** @var string Transaction operation comes from the database class internally */
*
* @param string $fname Caller name (usually __METHOD__)
* @param callable $callback Callback that issues DB updates
+ * @return mixed $res Result of the callback (since 1.28)
* @throws DBError
* @throws RuntimeException
* @throws UnexpectedValueException
* Begin a transaction. If a transaction is already in progress,
* that transaction will be committed before the new transaction is started.
*
+ * Only call this from code with outer transcation scope.
+ * See https://www.mediawiki.org/wiki/Database_transactions for details.
+ * Nesting of transactions is not supported.
+ *
* Note that when the DBO_TRX flag is set (which is usually the case for web
* requests, but not for maintenance scripts), any previous database query
* will have started a transaction automatically.
* automatically because of the DBO_TRX flag.
*
* @param string $fname
+ * @param string $mode A situationally valid IDatabase::TRANSACTION_* constant [optional]
* @throws DBError
*/
- public function begin( $fname = __METHOD__ );
+ public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT );
/**
* Commits a transaction previously started using begin().
* If no transaction is in progress, a warning is issued.
*
+ * Only call this from code with outer transcation scope.
+ * See https://www.mediawiki.org/wiki/Database_transactions for details.
* Nesting of transactions is not supported.
*
* @param string $fname
* Rollback a transaction previously started using begin().
* If no transaction is in progress, a warning is issued.
*
- * No-op on non-transactional databases.
+ * Only call this from code with outer transcation scope.
+ * See https://www.mediawiki.org/wiki/Database_transactions for details.
+ * Nesting of transactions is not supported. If a serious unexpected error occurs,
+ * throwing an Exception is preferrable, using a pre-installed error handler to trigger
+ * rollback (in any case, failure to issue COMMIT will cause rollback server-side).
*
* @param string $fname
* @param string $flush Flush flag, set to a situationally valid IDatabase::FLUSHING_*
/**
* Acquire a named lock, flush any transaction, and return an RAII style unlocker object
*
+ * Only call this from outer transcation scope and when only one DB will be affected.
+ * See https://www.mediawiki.org/wiki/Database_transactions for details.
+ *
* This is suitiable for transactions that need to be serialized using cooperative locks,
* where each transaction can see each others' changes. Any transaction is flushed to clear
* out stale REPEATABLE-READ snapshot data. Once the returned object falls out of PHP scope,
- * any transaction will be committed and the lock will be released.
+ * the lock will be released unless a transaction is active. If one is active, then the lock
+ * will be released when it either commits or rolls back.
*
* If the lock acquisition failed, then no transaction flush happens, and null is returned.
*
return $ok;
}
+ public function changeTTL( $key, $expiry = 0 ) {
+ list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+ try {
+ $db = $this->getDB( $serverIndex );
+ $db->update(
+ $tableName,
+ [ 'exptime' => $db->timestamp( $this->convertExpiry( $expiry ) ) ],
+ [ 'keyname' => $key, 'exptime > ' . $db->addQuotes( $db->timestamp( time() ) ) ],
+ __METHOD__
+ );
+ if ( $db->affectedRows() == 0 ) {
+ return false;
+ }
+ } catch ( DBError $e ) {
+ $this->handleWriteError( $e, $serverIndex );
+ return false;
+ }
+
+ return true;
+ }
+
/**
* @param IDatabase $db
* @param string $exptime
$this->loadFromDefinition();
return $this->position;
}
+
+ /**
+ * @return string
+ */
+ public function getType() {
+ return self::LOAD_STYLES;
+ }
}
*/
public function tryStashFile( User $user, $isPartial = false ) {
if ( !$isPartial ) {
- $props = $this->mFileProps;
- $error = null;
- Hooks::run( 'UploadStashFile', [ $this, $user, $props, &$error ] );
+ $error = $this->runUploadStashFileHook( $user );
if ( $error ) {
- if ( !is_array( $error ) ) {
- $error = [ $error ];
- }
return call_user_func_array( 'Status::newFatal', $error );
}
}
}
}
+ /**
+ * @param User $user
+ * @return array|null Error message and parameters, null if there's no error
+ */
+ protected function runUploadStashFileHook( User $user ) {
+ $props = $this->mFileProps;
+ $error = null;
+ Hooks::run( 'UploadStashFile', [ $this, $user, $props, &$error ] );
+ if ( $error ) {
+ if ( !is_array( $error ) ) {
+ $error = [ $error ];
+ }
+ }
+ return $error;
+ }
+
/**
* If the user does not supply all necessary information in the first upload
* form submission (either by accident or by design) then we may want to
/**
* Setup local pointers to stash, repo and user (similar to UploadFromStash)
*
- * @param User|null $user Default: null
+ * @param User $user
* @param UploadStash|bool $stash Default: false
* @param FileRepo|bool $repo Default: false
*/
- public function __construct( $user = null, $stash = false, $repo = false ) {
- // user object. sometimes this won't exist, as when running from cron.
+ public function __construct( User $user, $stash = false, $repo = false ) {
$this->user = $user;
if ( $repo ) {
// Update the mTempPath and mLocalFile
// (for FileUpload or normal Stash to take over)
$tStart = microtime( true );
- $this->mLocalFile = parent::doStashFile( $this->user );
+ // This is a re-implementation of UploadBase::tryStashFile(), we can't call it because we
+ // override doStashFile() with completely different functionality in this class...
+ $error = $this->runUploadStashFileHook( $this->user );
+ if ( $error ) {
+ call_user_func_array( [ $status, 'fatal' ], $error );
+ return $status;
+ }
+ try {
+ $this->mLocalFile = parent::doStashFile( $this->user );
+ } catch ( UploadStashException $e ) {
+ $status->fatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() );
+ return $status;
+ }
+
$tAmount = microtime( true ) - $tStart;
$this->mLocalFile->setLocalReference( $tmpFile ); // reuse (e.g. for getImageInfo())
wfDebugLog( 'fileconcatenate', "Stashed combined file ($i chunks) in $tAmount seconds." );
return $this->mFileProps['sha1'];
}
- /*
- * protected function verifyFile() inherited
- */
-
- /**
- * Stash the file.
- *
- * @param User $user
- * @return UploadStashFile
- */
- protected function doStashFile( User $user = null ) {
- // replace mLocalFile with an instance of UploadStashFile, which adds some methods
- // that are useful for stashed files.
- $this->mLocalFile = parent::doStashFile( $user );
-
- return $this->mLocalFile;
- }
-
/**
* Remove a temporarily kept file stashed by saveTempUploadedFile().
* @return bool Success
* @defgroup Language Language
*/
-if ( !defined( 'MEDIAWIKI' ) ) {
- echo "This file is part of MediaWiki, it is not a valid entry point.\n";
- exit( 1 );
-}
-
use CLDRPluralRuleParser\Evaluator;
/**
"alllogstext": "{{SITENAME}} üçün bütün mövcud qeydlərin birgə göstərişi.\nQeyd növü, istifadəçi adı və ya təsir edilmiş səhifəni seçməklə daha spesifik ola bilərsiniz.",
"logempty": "Jurnalda uyğun qeyd tapılmadı.",
"log-title-wildcard": "Bu mətnlə başlayan başlıqları axtar",
+ "checkbox-select": "Seçin: $1",
+ "checkbox-all": "Hamısı",
+ "checkbox-none": "Heç biri",
+ "checkbox-invert": "Çevir",
"allpages": "Bütün səhifələr",
"nextpage": "Sonrakı səhifə ($1)",
"prevpage": "Əvvəlki səhifə ($1)",
"editlink": "دەستکاری",
"viewsourcelink": "بینینی سەرچاوە",
"editsectionhint": "دەستکاریکردنی بەش: $1",
- "toc": "Ù\86اÙ\88Û\95Ú\95Û\86Ú©",
+ "toc": "Ù¾Û\8eرست",
"showtoc": "نیشانیبدە",
"hidetoc": "بیشارەوە",
"collapsible-collapse": "کۆی بکەوە",
"grant-group-high-volume": "Velkoobjemové činnosti",
"grant-group-customization": "Nastavení a přizpůsobení",
"grant-group-administration": "Provádění správcovských činností",
- "grant-group-private-information": "Zpřístupnit soukromá data o vás",
+ "grant-group-private-information": "Přístup k soukromým údajům o vás",
"grant-group-other": "Různé činnosti",
"grant-blockusers": "Blokovat a odblokovávat uživatele",
"grant-createaccount": "Zakládat účty",
"grant-highvolume": "Hromadné editace",
"grant-oversight": "Skrývat uživatele a utajovat revize",
"grant-patrol": "Patrolovat změny stránek",
+ "grant-privateinfo": "Přístup k soukromým údajům",
"grant-protect": "Zamykat a odemykat stránky",
"grant-rollback": "Vracet editace zpět",
"grant-sendemail": "Posílat e-maily ostatním uživatelům",
"uploadstash-errclear": "Soubory se nepodařilo vymazat.",
"uploadstash-refresh": "Aktualizovat seznam souborů",
"uploadstash-thumbnail": "zobrazit náhled",
+ "uploadstash-exception": "Načtený soubor se nepodařilo uložit do skrýše ($1): „$2“.",
"invalid-chunk-offset": "Neplatný posun bloku",
"img-auth-accessdenied": "Přístup odepřen",
"img-auth-nopathinfo": "Chybí PATH_INFO.\nVáš server není nastaven tak, aby tuto informaci poskytoval.\nMožná funguje pomocí CGI a img_auth na něm nemůže fungovat.\nVizte https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
"trackingcategories-name": "Název hlášení",
"trackingcategories-desc": "Kritéria pro vložení do kategorie",
"restricted-displaytitle-ignored": "Stránky s ignorovanými zobrazovanými názvy",
- "restricted-displaytitle-ignored-desc": "Stránky obsahující příkaz <code><nowiki>{{DISPLAYTITLE}}</nowiki></code>, který se ignoruje, neboť není ekvivalentní skutečnému názvu stránky.",
+ "restricted-displaytitle-ignored-desc": "Stránka obsahuje příkaz <code><nowiki>{{DISPLAYTITLE}}</nowiki></code>, který se ignoruje, neboť není ekvivalentní skutečnému názvu stránky.",
"noindex-category-desc": "Stránka není indexována roboty, protože obsahuje kouzelné slovo <code><nowiki>__NOINDEX__</nowiki></code> a je ve jmenném prostoru, ve kterém je tento příznak dovolen.",
"index-category-desc": "Stránka obsahuje kouzelné slovo <code><nowiki>__INDEX__</nowiki></code> (a je ve jmenném prostoru, ve kterém je tento příznak dovolen), takže je indexována roboty, přestože by normálně nebyla.",
"post-expand-template-inclusion-category-desc": "Stránka je po rozbalení všech šablon větší než <code>$wgMaxArticleSize</code>, takže některé šablony rozbaleny nebyly.",
"anontalk": "Werênayış",
"navigation": "Pusula",
"and": " u",
- "qbfind": "Bıvêne",
+ "qbfind": "Bıvin",
"qbbrowse": "Çım ra viyarne",
"qbedit": "Bıvurne",
"qbpageoptions": "Ena pele",
"view-foreign": "$1 de bıvêne",
"edit": "Bıvurne",
"edit-local": "Şınasnayışê lokali bıvurne",
- "create": "Vıraze",
+ "create": "Bıvıraz",
"create-local": "Şınasnayışê lokali cı ke",
"editthispage": "Ena pele bıvurne",
"create-this-page": "Na pele bınuse",
"talkpagelinktext": "werênayış",
"specialpage": "Pela xısusiye",
"personaltools": "Hacetê şexsiy",
- "articlepage": "Pela zerreki bıvêne",
+ "articlepage": "Pera zerreki bıvin",
"talk": "Werênayış",
"views": "Asayışi",
"toolbox": "Haceti",
"imagepage": "Pera dosya bıasne",
"mediawikipage": "Pera mesaci bıasne",
"templatepage": "Pera şabloni bıasne",
- "viewhelppage": "Pela peşti bıvêne",
+ "viewhelppage": "Pera peşti bıvin",
"categorypage": "Pela kategoriya bıasne",
"viewtalkpage": "Werênayışi bıvêne",
"otherlanguages": "Zıwananê binan de",
"redirectedfrom": "($1 ra kırışı yê)",
"redirectpagesub": "Pela berdışi",
"redirectto": "Beno hetê:",
- "lastmodifiedat": "Ena pele tewr peyên roca $1, saeta $2 de biye rocane.",
+ "lastmodifiedat": "Per roca $1, sehat $2 de biye neye.",
"viewcount": "Ena pele {{PLURAL:$1|rae|$1 rey}} vêniya.",
"protectedpage": "Pela pawıtiye",
"jumpto": "Şo be:",
"confirmable-yes": "Eya",
"confirmable-no": "Nê",
"thisisdeleted": "Bıvêne ya zi $1 peyser biya?",
- "viewdeleted": "$1 bıvêne?",
+ "viewdeleted": "$1 bıvin?",
"restorelink": "{{PLURAL:$1|jew vurnayış besteriya|$1 vurnayışi besteriyaye}}",
"feedlinks": "Warikerdış:",
"feed-invalid": "Qeydey cıresnayışê beğşi nêvêreno.",
"perfcached": "Datay cı ver hazır biye. No semedê ra nıkayin niyo! tewr zaf {{PLURAL:$1|netice|$1 netice}} debêno de",
"perfcachedts": "Cêr de malumatê nımıteyi esti, demdê newe kerdışo peyın: $1. Tewr zaf {{PLURAL:$4|netice|$4 neticey cı}} debyayo de",
"querypage-no-updates": "Rocanebiyayışê na pele nıka cadayiyê.\nDayiyi tiya nıka newe nêbenê.",
- "viewsource": "Çımey bıvêne",
+ "viewsource": "Çemi bıvin",
"viewsource-title": "Cı geyrayışê $1'i bıvin",
"actionthrottled": "Kerden peysnaya",
"actionthrottledtext": "Riyê tedbirê anti-spami ra, wextê do kılmek de şıma nê fealiyeti nêşkenê zaf zêde bıkerê, şıma ki no hedi viyarna ra.\nÇend deqey ra tepeya reyna bıcerrebnên.",
"nologinlink": "Yew hesab ake",
"createaccount": "Hesab vıraze",
"gotaccount": "Hesabê şıma esto? '''$1'''.",
- "gotaccountlink": "Cı kewe",
+ "gotaccountlink": "Cıkewtış",
"userlogin-resetlink": "Melumatê cıkewtışi xo vira kerdê?",
"userlogin-resetpassword-link": "Parola xo kerda xo vira?",
"userlogin-helplink2": "Heqa qeydbiyayışi de peşti bıgêrên",
"botpasswords-label-appid": "Nameyê boti:",
"botpasswords-label-create": "Vıraze",
"botpasswords-label-update": "Rocane ke",
- "botpasswords-label-cancel": "Bıtexelne",
+ "botpasswords-label-cancel": "İbtal ke",
"botpasswords-label-delete": "Bestere",
"botpasswords-label-resetpassword": "Parola raçarne",
"botpasswords-label-grants-column": "Dayen",
"resetpass_forbidden": "parolayi nêvuryayi",
"resetpass-no-info": "şıma gani hesab akere u hona bıeşke bırese cı",
"resetpass-submit-loggedin": "Parola bıvurne",
- "resetpass-submit-cancel": "Bıtexelne",
+ "resetpass-submit-cancel": "İbtal ke",
"resetpass-wrong-oldpass": "parolayo parola maqbul niyo.\nşıma ya parolaye xo vurnayo ya zi parolayo muwaqqat waşto.",
"resetpass-recycled": "Parolaya şımaya newiye wa paroloya şımaya verêne ra ferqıne bo.",
"resetpass-temp-emailed": "E postaya rışyayê yubkoda şıma ronıştış akerdo. Ronıştışi xo temammkerdışi rê yu parolaya newi lazım a",
"hr_tip": "Xeta verardiye (teserrufın bıgureyne/bıxebetne)",
"summary": "Xulasa:",
"subject": "Mewzu:",
- "minoredit": "No yew vurnayışo werdiyo",
- "watchthis": "Ena pele seyr ke",
- "savearticle": "Pele qeyd ke",
+ "minoredit": "Vurriyayışo werdiyo",
+ "watchthis": "Seyr kı",
+ "savearticle": "Qeyd kı",
"savechanges": "Vurnayışan qeyd ke",
"publishpage": "Perer bıhesırne",
"publishchanges": "Vurnayışa vıla ke",
"accmailtext": "[[User talk:$1|$1]] parolayo ke raşt ameyo şırawiyo na adres $2.\n\nQey na hesabê newe parola, cıkewtış dıma şıma eşkeni na qısım de ''[[Special:ChangePassword|parola bıvurn]]'' bıvurni.",
"newarticle": "(Newe)",
"newarticletext": "To yew gıre tıkna be ra yew pela ke hewna çıniya.\nSeba afernayışê pele ra, qutiya metnê cêrêni bıgurene (seba melumati qaytê [$1 pela peşti] ke).\nEke be ğeletine ameya tiya, wa gocega <strong>peyser</strong>i programê xo de bıtıkne.",
- "anontalkpagetext": "----''No pel, pel o karbero hesab a nêkerdeyan o, ya zi karbero hesab akerdeyan o labele pê hesabê xo nêkewto de. No sebeb ra ma IP adres şuxulneni û ney IP adresan herkes eşkeno bıvino. Eke şıma qayil niye ina bo xo ri [[Special:CreateAccount|yew hesab bıvıraze]] veyaxut [[Special:UserLogin|hesab akere]].''",
+ "anontalkpagetext": "----''Na per, perêk kı karbero hesab a nêkerdeyan o, ya zi karbero hesab akerdeyan o labele pê hesabê xo nêkewto de. No sebeb ra ma IP adres xebetneno û ney IP adresan herkes nêşeno bıvino. Eke şıma qayil niye ina bo xorê [[Special:CreateAccount|yew hesab bıvıraze]] veya xut [[Special:UserLogin|hesab akere]].''",
"noarticletext": "Ena pele de hewna theba çıniyo.\nTı şenê zerreyê pelanê binan de [[Special:Search/{{PAGENAME}}|qandê sernameyê ena pele cı geyre]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} qeydan miyan de cı geyre],\nya zi [{{fullurl:{{FULLPAGENAME}}|action=edit}} ena pele vıraze]</span>.{{MediaWiki mesaca pera newi}}",
"noarticletext-nopermission": "Ena pele de hewna theba çıniyo.\nTı şenay zerreyê pelanê binan de [[Special:Search/{{PAGENAME}}|seba sernameyê na pele cı geyre]], ya zi <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} qeydan miyan de cı geyre]</span>, ema destur çıniyo ke na pele vırazê.",
"missing-revision": "Rewizyonê name dê pela da #$1 \"{{FULLPAGENAME}}\" dı çıniyo.\n\nNo normal de tarix dê pelanê besterneyan dı ena xırabin asena.\nDetayê besternayışi [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} tiya dı] aseno.",
"next-page": "Pela peyên",
"prevn-title": "$1o verên {{PLURAL:$1|netice|neticeyan}}",
"nextn-title": "$1o ke yeno {{PLURAL:$1|netice|neticey}}",
- "shown-title": "bimocne $1î {{PLURAL:$1|netice|neticeyan}} ser her pel",
+ "shown-title": "Herg per sero $1 {{PLURAL:$1|netici|netica}} bıasne",
"viewprevnext": "($1 {{int:pipe-separator}} $2) ($3) bıvênên",
"searchmenu-exists": "''Ena 'Wikipediya de ser \"[[:$1]]\" yew pel esto'''",
"searchmenu-new": "<strong>Na wiki de pela \"[[:$1]]\" vıraze!</strong> {{PLURAL:$2|0=|Sewbina pela ke şıma geyrayê cı aye bıvênê.|Yew zi neticanê cıgeyrayışê xo bıvênê.}}",
"rightslogtext": "Ena listeyê loganê ke heqqa karbaranî mucneno.",
"action-read": "ena pela wanayış",
"action-edit": "ena pela bıvurnê",
- "action-createpage": "na pele vıraze",
+ "action-createpage": "na perer bıvıraz",
"action-createtalk": "pelanê werênayışi bıvıraze",
"action-createaccount": "hesabê nê karberi bıvıraze",
"action-autocreateaccount": "nê hesabê karberiyê teberi otomatik vıraze",
"enhancedrc-history": "tarix",
"recentchanges": "Vurriyayışê peyêni",
"recentchanges-legend": "Tercihê vurnayışanê peyênan",
- "recentchanges-summary": "Ena pele de wiki sero vurriyayışanê peyênan teqib ke.",
+ "recentchanges-summary": "Wiki sero vurriyayışê peyêni asenê.",
"recentchanges-noresult": "Goreyê kriteranê kıfşkerdeyan ra qet yew vurnayış nêvêniya.",
"recentchanges-feed-description": "Ena feed dı vurnayişanê tewr peniyan teqip bık.",
"recentchanges-label-newpage": "Enê vurnayışi ra yew pela newiye vıraziye",
- "recentchanges-label-minor": "No yew vurnayışo werdiyo",
+ "recentchanges-label-minor": "Vurriyayışo werdiyo",
"recentchanges-label-bot": "Eno vurnayış terefê yew boti ra vıraziyo",
"recentchanges-label-unpatrolled": "Eno vurnayış hewna dewriya nêbiyo",
"recentchanges-label-plusminus": "Ebadê pele de bazê bayti de vayeyê cı",
"rcshowhidecategorization": "kategorizasyonê pele $1",
"rcshowhidecategorization-show": "Bıasne",
"rcshowhidecategorization-hide": "Bınımne",
- "rclinks": "Peyniya $2 rocan de $1 vurriyayışan bımocne <br />$3",
+ "rclinks": "Peyniya $2 rocan de $1 vurriyayışê <br />$3 asenê",
"diff": "ferq",
"hist": "verên",
"hide": "Bınımne",
"removedwatchtext": "Ena pela \"[[:$1]]\" biya wedariya [[Special:Watchlist|listeyê seyr-kerdışi şıma]].",
"removedwatchtext-short": "Pera $1`i listeya seyran de şıma ra wedari yê",
"watch": "Seyr ke",
- "watchthispage": "Ena pele seyr ke",
+ "watchthispage": "Seyr kı",
"unwatch": "Teqib meke",
"unwatchthispage": "temaşa kerdışê peli vındarn.",
"notanarticle": "mebhesê peli niyo",
"pagesize": "(bitî)",
"restriction-edit": "Bıvurne",
"restriction-move": "Bıkırış",
- "restriction-create": "Vıraze",
+ "restriction-create": "Bıvıraz",
"restriction-upload": "Bar ke",
"restriction-level-sysop": "tam pawiyayo",
"restriction-level-autoconfirmed": "nêm pawiyayo",
"undeleterevision-missing": "revizyonê nemeqbul u vindbiyayeyi.\nRevizyoni ya hewn a biyê ya arşiw ra veciyayê ya zi cıresayişê şımayi şaş o.",
"undelete-nodiff": "revizyonê verıni nidiya",
"undeletebtn": "Timar bike",
- "undeletelink": "bıvêne/peyser biya",
+ "undeletelink": "bıewni/peyser biya",
"undeleteviewlink": "bıvin",
"undeleteinvert": "Weçinayışi dimlaşt ke",
"undeletecomment": "Sebeb:",
"unblocked-id": "Blokê $1î wedariyayo",
"blocklist": "Karberê kılitbiyayey",
"ipblocklist": "Karberê kılitbiyayey",
- "ipblocklist-legend": "Yew karberê kılitbiyayey bıvêne",
+ "ipblocklist-legend": "Karberê kılit biyayey bıvin",
"blocklist-userblocks": "Kılitkerdışê hesaban bınımne",
"blocklist-tempblocks": "Kılitkerdışan mıweqet bınımne",
"blocklist-addressblocks": "Tenya kılitkerdışanê IPy bınımne",
"articleexists": "Ena nameyê pela database ma dı esta ya zi tı raşt nınuşt. .\nYewna name bınus.",
"cantmove-titleprotected": "şıma nêşkeni yew peli bıhewelnê tiya çunke pawıyeno",
"movetalk": "Pela werênayışiê elaqedare bere",
- "move-subpages": "Pelanê bınênan bıkırışe (heta pela $1)",
- "move-talk-subpages": "Pelanê werênayışiyê bınênan bıkırışe (heta pela $1)",
+ "move-subpages": "Peranê bınênan bıkırış (hetana $1)",
+ "move-talk-subpages": "Bın peranê peranê vatena bıkırış (hetana $1)",
"movepage-page-exists": "maddeya $1i ca ra esta u newe ra otomatikmen nênusyena.",
"movepage-page-moved": "pelê $1i kırışiya pelê $2i.",
"movepage-page-unmoved": "pelê $1i nêkırışiyeno sernameyê $2i.",
"fileduplicatesearch-result-1": "Dosyayê ''$1î'' de hem-kopya çini yo.",
"fileduplicatesearch-result-n": "Dosyayê ''$1î'' de {{PLURAL:$2|1 hem-kopya|$2 hem-kopyayî'}} esto.",
"fileduplicatesearch-noresults": "Ebe namey \"$1\" ra dosya nêdiyayê.",
- "specialpages": "Pelê xısusiyi",
+ "specialpages": "Page bağsey",
"specialpages-note-top": "Kıtabek",
"specialpages-note": "* Pelê xasê normali.\n* <span class=\"mw-specialpagerestricted\">Pelê xasê nımıtey.</span>",
"specialpages-group-maintenance": "Raporê pawıtışi",
"feedback-bugcheck": "Harika! Sadece [xırabina ke $1 ] çınyayışê cı kontrol keno.",
"feedback-bugnew": "Mı qontrol ke. Xetaya newi xeber ke",
"feedback-bugornote": "Jew mersela teferruato teknik esta şıma reca malumatê şıma hazıro se [ $1 jew xırab rapor] bıvinê.Zewbi zi, formê cerê xo rê şenê karfiyê. Vatışê xo pela da \"[ $3 $2 ]\", namey karber dê xoya piya u wasteriya karfiye.",
- "feedback-cancel": "Bıtexelne",
+ "feedback-cancel": "İbtal kı",
"feedback-close": "Biya star",
"feedback-error1": "Xeta: API ra neticey ne vıcyay",
"feedback-error2": "Xeta: Timar kerdış nebı",
"undeletehistorynoadmin": "Esta páxina foi borrada.\nO motivo do borrado consta no resumo que aparece a continuación, xunto cos detalles dos usuarios que editaron esta páxina antes da súa eliminación.\nO texto destas revisións eliminadas só está á disposición dos administradores.",
"undelete-revision": "Revisión eliminada de \"$1\" (o $4 ás $5) feita por $3:",
"undeleterevision-missing": "Revisión non válida ou inexistente. Pode que a ligazón conteña un erro ou que a revisión se restaurase ou eliminase do arquivo.",
+ "undeleterevision-duplicate-revid": "{{PLURAL:$1|Unha revisión non pode ser restaurada|$1 revisións non poden ser restauradas}} porque {{PLURAL:$1|o seu|os seus}} <code>rev_id</code> xa {{PLURAL:$1|está|están}} en uso.",
"undelete-nodiff": "Non se atopou ningunha revisión anterior.",
"undeletebtn": "Restaurar",
"undeletelink": "ver/restaurar",
"undeletedrevisions": "{{PLURAL:$1|Restaurouse $1 revisión|Restauráronse $1 revisións}}",
"undeletedrevisions-files": "Restauráronse $1 {{PLURAL:$1|revisión|revisións}} e $2 {{PLURAL:$2|ficheiro|ficheiros}}",
"undeletedfiles": "{{PLURAL:$1|Restaurouse $1 ficheiro|Restauráronse $1 ficheiros}}",
- "cannotundelete": "Houbo un erro durante a restauración:\n$1",
+ "cannotundelete": "Algunhas ou todas as restauracións fallaronː\n$1",
"undeletedpage": "'''A páxina \"$1\" foi restaurada'''\n\nComprobe o [[Special:Log/delete|rexistro de borrados]] para ver as entradas recentes no rexistro de páxinas eliminadas e restauradas.",
"undelete-header": "Consulte [[Special:Log/delete|no rexistro de borrados]] as páxinas borradas recentemente.",
"undelete-search-title": "Procurar páxinas borradas",
"sp-contributions-newbies-sub": "Contribucións dos usuarios novos",
"sp-contributions-newbies-title": "Contribucións dos usuarios novos",
"sp-contributions-blocklog": "rexistro de bloqueos",
- "sp-contributions-suppresslog": "contribucións borradas do usuario",
- "sp-contributions-deleted": "contribucións borradas do usuario",
+ "sp-contributions-suppresslog": "contribucións {{GENDER:$1|do usuario|da usuaria}} suprimidas",
+ "sp-contributions-deleted": "contribucións {{GENDER:$1|do usuario|da usuaria}} borradas",
"sp-contributions-uploads": "cargas",
"sp-contributions-logs": "rexistros",
"sp-contributions-talk": "conversa",
"datedefault": "ברירת המחדל",
"prefs-labs": "אפשרויות מעבדה",
"prefs-user-pages": "דפי משתמש",
- "prefs-personal": "פרטי המשתמש",
+ "prefs-personal": "פרטי ה{{GENDER:|משתמש|משתמשת}}",
"prefs-rc": "שינויים אחרונים",
"prefs-watchlist": "רשימת המעקב",
"prefs-editwatchlist": "עריכת רשימת המעקב",
"undeletehistorynoadmin": "Questa pagina è stata cancellata.\nIl motivo della cancellazione è mostrato qui sotto, assieme ai dettagli dell'utente che ha modificato questa pagina prima della cancellazione.\nIl testo contenuto nelle versioni cancellate è disponibile solo agli amministratori.",
"undelete-revision": "Versione cancellata della pagina $1, inserita il $4 alle $5 da $3:",
"undeleterevision-missing": "Versione errata o mancante. Il collegamento è errato oppure la versione è stata già ripristinata o eliminata dall'archivio.",
+ "undeleterevision-duplicate-revid": "{{PLURAL:$1|Una versione non può essere ripristinata|$1 versioni non possono essere ripristinate}}, poiché {{PLURAL:$1|il suo|i loro}} <code>rev_id</code> {{PLURAL:$1|è già utilizzato|sono già utilizzati}}.",
"undelete-nodiff": "Non è stata trovata nessuna versione precedente.",
"undeletebtn": "Ripristina",
"undeletelink": "visualizza/ripristina",
"sp-contributions-username": "IP-мекенжайы немесе қатысушы аты:",
"sp-contributions-toponly": "Өңдемелердің тек соңғы нұсқаларын көрсету",
"sp-contributions-newonly": "Бет бастау өңдемелерін ғана көрсету",
+ "sp-contributions-hideminor": "Шағын өңдемелерді жасыру",
"sp-contributions-submit": "Іздеу",
"whatlinkshere": "Мұнда сілтейтін беттер",
"whatlinkshere-title": "$1 дегенге сілтейтін беттер",
"acct_creation_throttle_hit": "당신의 IP 주소를 이용한 방문자가 이전에 이미 {{PLURAL:$1|계정 $1개}}를 만들어, 계정 만들기 한도를 초과하였습니다.\n따라서 지금은 이 IP 주소로는 더 이상 계정을 만들 수 없습니다.",
"emailauthenticated": "이메일 주소가 $2 $3에 인증되었습니다.",
"emailnotauthenticated": "이메일 주소를 인증하지 않았습니다.\n이메일 확인 절차를 거치지 않으면 다음 이메일 기능을 사용할 수 없습니다.",
- "noemailprefs": "이 기능을 사용하기 위해서는 사용자 환경 설정에서 이메일 주소를 설정해야 합니다.",
+ "noemailprefs": "이 기능을 사용하려면 사용자 환경 설정에서 이메일 주소를 지정하세요.",
"emailconfirmlink": "이메일 주소 확인",
"invalidemailaddress": "이메일 주소의 형식이 잘못되어 인식할 수 없습니다.\n정상적인 형식의 이메일을 입력하거나 칸을 비워 주세요.",
"cannotchangeemail": "이 위키에서는 계정의 이메일 주소를 바꿀 수 없습니다.",
"rev-showdeleted": "보이기",
"revisiondelete": "판 삭제/되살리기",
"revdelete-nooldid-title": "대상 판이 잘못되었습니다.",
- "revdelete-nooldid-text": "이 기능을 수행할 특정 판을 제시하지 않았거나 해당 판이 없습니다. 또는 현재 판을 숨기려 하고 있을 수도 있습니다.",
+ "revdelete-nooldid-text": "이 기능을 수행할 대상 판을 지정하지 않았거나 해당 판이 존재하지 않습니다. 아니면 현재 판을 숨기려 하고 있을 수도 있습니다.",
"revdelete-no-file": "해당 파일이 존재하지 않습니다.",
"revdelete-show-file-confirm": "정말 \"<nowiki>$1</nowiki>\" 파일의 삭제된 $2 $3 버전을 보시겠습니까?",
"revdelete-show-file-submit": "예",
"tags-edit-success": "바뀜이 적용되었습니다.",
"tags-edit-failure": "수정 사항이 적용될 수 없습니다: $1",
"tags-edit-nooldid-title": "대상 판이 잘못되었습니다",
- "tags-edit-nooldid-text": "이 기능을 수행할 특정 판을 제시하지 않았거나 해당 판이 없습니다.",
+ "tags-edit-nooldid-text": "이 기능을 수행할 대상 판을 지정하지 않았거나 해당 판이 존재하지 않습니다.",
"tags-edit-none-selected": "추가하거나 제거할 최소 하나 이상의 태그를 선택하세요.",
"comparepages": "문서 비교",
"compare-page1": "첫 번째 문서",
"passwordreset-emailelement": "सदस्यनाव: \n$1\n\nअस्थायी परवलीचा शब्द: \n$2",
"passwordreset-emailsentemail": "जर हा विपत्रपत्ता आपल्या खात्याशी संलग्न असेल तर, परवलीच्या शब्दाच्या पुनर्स्थापनेबाबत एक विपत्र पाठवण्यात येईल.",
"passwordreset-emailsentusername": "जर या सदस्यनावाशी संलग्न विपत्रपत्ता असेल तर, परवलीचा शब्द पुनर्स्थापनाबाबत विपत्र पाठविल्या जाईल.",
- "passwordreset-emailsent-capture": "'परवलीचा शब्द' पुनर्स्थापनेबाबत एक विपत्र पाठवण्यात आले आहे जे खाली दर्शविण्यात आले आहे.",
- "passwordreset-emailerror-capture": "'परवलीचा शब्द' पुनर्स्थापनेबाबत एक विपत्र निर्माण करण्यात आले, जे खाली दर्शविण्यात आले आहे.परंतु,{{GENDER:$2|सदस्य}}ला पाठविणे असफल झाले: $1",
"changeemail": "विपत्रपत्ता बदला किंवा हटवा",
"changeemail-header": "आपला विपत्रपत्ता बदलण्यास हे आवेदन पूर्ण करा.जर आपणास आपल्या खात्याशी संलग्न कोणताही विपत्रपत्ता हटवायचा असेल तर,आवेदन सादर करण्यापूर्वी, नविन विपत्रपत्त्यासाठी असलेली जागा कोरी ठेवा.",
- "changeemail-passwordrequired": "हे बदल नक्की करण्यासाठी आपणास आपला परवलीचा शब्द टाकावा लागेल.",
"changeemail-no-info": "हे पान थेट बघण्यासठी तुम्हाला सनोंद-प्रवेशित असावे लागेल.",
"changeemail-oldemail": "सध्याचा ईमेल पत्ता :",
"changeemail-newemail": "नवा ईमेल पत्ता:",
"undo-nochange": "असे दिसते कि हे संपादन पूर्ववत केल्या गेले आहे.",
"undo-summary": "[[Special:Contributions/$2|$2]] ([[User talk:$2|चर्चा]])यांची आवृत्ती $1 परतवली.",
"undo-summary-username-hidden": "अज्ञात सदस्याची $1 आवृत्ती परतवा",
- "cantcreateaccounttitle": "खाते उघडू शकत नाही",
"cantcreateaccount-text": "('''$1''')या आंतरजाल अंकपत्त्याकडूनच्या खाते निर्मितीस [[User:$3|$3]]ने अटकाव केला आहे.\n\n$3ने ''$2'' कारण दिले आहे.",
"cantcreateaccount-range-text": "<strong>$1</strong>आवाक्यातील आंतरजाल अंकपत्ते,ज्यात आपल्या (<strong>$4</strong>) या अंकपत्त्याचा समावेश आहे, [[User:$3|$3]] ने त्यांच्या खाते निर्मितीस प्रतिबंध केला आहे.\n\n$3 ने <em>$2</em>कारण दिले आहे.",
"viewpagelogs": "या पानाच्या नोंदी पहा",
"undeletedrevisions": "{{PLURAL:$1|1 आवर्तन|$1 आवर्तने}} पुनर्स्थापित",
"undeletedrevisions-files": "{{PLURAL:$1|1 आवर्तन|$1 आवर्तने}}आणि {{PLURAL:$2|1 संचिका|$2 संचिका}} पुनर्स्थापित",
"undeletedfiles": "{{PLURAL:$1|1 संचिका|$1 संचिका}} पुनर्स्थापित",
- "cannotundelete": "उलटवणे फसले:$1",
+ "cannotundelete": "à¤\95ाहà¥\80 à¤\95िà¤\82वा सरà¥\8dवà¤\9a à¤\89लà¤\9fवणà¥\87 फसलà¥\87:$1",
"undeletedpage": "<strong>$1ला पुनर्स्थापित केले</strong>\n\nअलिकडिल वगळलेल्या आणि पुनर्स्थापितांच्या नोंदीकरिता [[Special:Log/delete|वगळल्याच्या नोंदी]] पहा .",
"undelete-header": "अलीकडील वगळलेल्या पानांकरिता [[Special:Log/delete|वगळलेल्या नोंदी]] पहा.",
"undelete-search-title": "वगळलेली पाने शोधा",
"sp-contributions-newbies-sub": "नवशिक्यांसाठी",
"sp-contributions-newbies-title": "नवीन खात्यांसाठी सदस्य योगदान",
"sp-contributions-blocklog": "रोध नोंदी",
- "sp-contributions-suppresslog": "सदस्य योगदानाचे दमन केले",
- "sp-contributions-deleted": "वगळलेली सदस्य संपादने",
+ "sp-contributions-suppresslog": "{{GENDER:$1|सदस्य}} योगदानाचे दमन केले",
+ "sp-contributions-deleted": "वगळलेली {{GENDER:$1|सदस्य}} संपादने",
"sp-contributions-uploads": "अपभारणे",
"sp-contributions-logs": "नोंदी",
"sp-contributions-talk": "चर्चा",
"grant-group-high-volume": "Realizar actividades em grande quantidade",
"grant-group-customization": "Personalização e preferências",
"grant-group-administration": "Executar acções administrativas",
+ "grant-group-private-information": "Aceder aos seus dados privados",
"grant-group-other": "Actividade diversa",
"grant-blockusers": "Bloquear e desbloquear utilizadores",
"grant-createaccount": "Criar contas",
"grant-group-high-volume": "Izvajanje visokoobsežnih dejavnosti",
"grant-group-customization": "Prilagoditve in nastavitve",
"grant-group-administration": "Izvajanje administrativnih dejanj",
+ "grant-group-private-information": "Dostop do zasebnih podatkov o vas",
"grant-group-other": "Druga dejavnost",
"grant-blockusers": "Blokiranje in odblokiranje uporabnikov",
"grant-createaccount": "Ustvarjanje računov",
"grant-highvolume": "Visokoobsežno urejanje",
"grant-oversight": "Skrivanje uporabnikov in zatiranje redakcij",
"grant-patrol": "Nadzor sprememb strani",
+ "grant-privateinfo": "Dostop do zasebnih podatkov",
"grant-protect": "Zaščita in odstranitev zaščite strani",
"grant-rollback": "Razveljavitev sprememb strani",
"grant-sendemail": "Pošiljanje e-pošte drugim uporabnikom",
"watchthis": "Спостерігати за цією сторінкою",
"savearticle": "Зберегти сторінку",
"savechanges": "Зберегти зміни",
- "publishpage": "Ð\9eпÑ\83блÑ\96кÑ\83вати сторінку",
- "publishchanges": "Ð\9eпÑ\83блÑ\96кÑ\83вати зміни",
+ "publishpage": "Ð\97беÑ\80егти сторінку",
+ "publishchanges": "Ð\97беÑ\80егти зміни",
"preview": "Попередній перегляд",
"showpreview": "Попередній перегляд",
"showdiff": "Показати зміни",
"shortpages": "چھوٹے صفحات",
"longpages": "طویل ترین صفحات",
"deadendpages": "مردہ صفحات",
- "protectedpages": "محفوظ شدہ صفحات",
+ "protectedpages": "محفوظ کردہ صفحات",
"protectedpages-noredirect": "رجوع مکررات چھپائیں",
"protectedpages-timestamp": "وقت کی مہر",
"protectedpages-page": "صفحہ",
"minoredit": "Bu kichik tahrir",
"watchthis": "Sahifani kuzatish",
"savearticle": "Saqla",
+ "publishpage": "Sahifani chop et",
+ "publishchanges": "Oʻzgarishlarni chop et",
"preview": "Ko‘rib chiqish",
"showpreview": "Koʻrib chiqish",
"showdiff": "Kiritilgan o‘zgarishlar",
"undo-success": "Tahrirni bekor qilish imkoniyati bor. Iltimos, solishtirish oynasini koʻrib chiqib, aynan shu oʻzgarishlarni bekor qilmoqchiligingizga ishonch hosil qiling va undan keyin «Saqla» tugmasini bosing.",
"undo-failure": "Keyingi tahrirlar bilan chalkashib ketgani sababli, ushbu tahrirni alohida oʻzini bekor qilishni iloji yoʻq.",
"undo-summary": "[[Special:Contributions/$2|$2]] ([[User talk:$2|mun.]]) tomonidan qilingan $1-sonli tahrir qaytarildi",
- "cantcreateaccounttitle": "Ro‘yxatdan o‘tib bo‘lmadi",
"cantcreateaccount-text": "[[User:$3|$3]] ushbu IP manzil (<strong>$1</strong>) orqali ro‘yxatdan o‘tishni bloklab qo‘ygan.\n\n$3 <em>$2</em>ni sabab qilib ko‘rsatdi",
"cantcreateaccount-range-text": "[[User:$3|$3]] <strong>$1</strong> sohaga tegishli IP manzillar, shu jumladan sizning IP manzilingiz (<strong>$4</strong>), orqali ro‘yxatdan o‘tishni bloklab qo‘ygan.\n\n$3 <em>$2</em>ni sabab qilib ko‘rsatdi",
"viewpagelogs": "Ushbu sahifaga doir qaydlarni koʻrsat",
"revdelete-uname-unhid": "公开用户名",
"revdelete-restricted": "应用对管理员的限制",
"revdelete-unrestricted": "删除对管理员的限制",
- "logentry-block-block": "$1{{GENDER:$2|封禁了}}{{GENDER:$4|$3}},期限为$5 $6",
+ "logentry-block-block": "$1{{GENDER:$2|封禁了}}{{GENDER:$4|$3}},期限至$5 $6",
"logentry-block-unblock": "$1{{GENDER:$2|解封了}}{{GENDER:$4|$3}}",
"logentry-block-reblock": "$1将{{GENDER:$4|$3}}的封禁设置{{GENDER:$2|更改为}}持续时间$5 $6",
"logentry-suppress-block": "$1{{GENDER:$2|封禁了}}{{GENDER:$4|$3}},持续时间$5 $6",
"log-action-filter-upload-upload": "新上传",
"log-action-filter-upload-overwrite": "重新上传",
"authmanager-authn-not-in-progress": "身份验证尚未进行,或会话数据丢失。请从头重新开始。",
- "authmanager-authn-no-primary": "提供的证书不能被验证。",
+ "authmanager-authn-no-primary": "提供的凭据不能通过验证。",
"authmanager-authn-no-local-user": "提供的证书没有与该wiki上的任何用户相关联。",
"authmanager-authn-no-local-user-link": "提供的证书有效,但没有与该wiki上的任何用户相关联。请通过不同方式登录,或创建一个新用户,然后您将拥有一个把您之前的证书链接到对应账户的选项。",
"authmanager-authn-autocreate-failed": "所有账户的自动创建失败:$1",
require_once __DIR__ . '/cleanupTable.inc';
/**
- * Maintenance script to clean up broken page links when somebody turns on $wgCapitalLinks.
+ * Maintenance script to clean up broken page links when somebody turns
+ * on or off $wgCapitalLinks.
*
* @ingroup Maintenance
*/
class CapsCleanup extends TableCleanup {
private $user;
+ private $namespace;
public function __construct() {
parent::__construct();
}
public function execute() {
- global $wgCapitalLinks;
-
- if ( $wgCapitalLinks ) {
- $this->error( "\$wgCapitalLinks is on -- no need for caps links cleanup.", true );
- }
-
$this->user = User::newSystemUser( 'Conversion script', [ 'steal' => true ] );
$this->namespace = intval( $this->getOption( 'namespace', 0 ) );
+
+ if ( MWNamespace::isCapitalized( $this->namespace ) ) {
+ $this->output( "Will be moving pages to first letter capitalized titles" );
+ $callback = 'processRowToUppercase';
+ } else {
+ $this->output( "Will be moving pages to first letter lowercase titles" );
+ $callback = 'processRowToLowercase';
+ }
+
$this->dryrun = $this->hasOption( 'dry-run' );
$this->runTable( [
'table' => 'page',
'conds' => [ 'page_namespace' => $this->namespace ],
'index' => 'page_id',
- 'callback' => 'processRow' ] );
+ 'callback' => $callback ] );
}
- protected function processRow( $row ) {
+ protected function processRowToUppercase( $row ) {
+ global $wgContLang;
+
+ $current = Title::makeTitle( $row->page_namespace, $row->page_title );
+ $display = $current->getPrefixedText();
+ $lower = $row->page_title;
+ $upper = $wgContLang->ucfirst( $row->page_title );
+ if ( $upper == $lower ) {
+ $this->output( "\"$display\" already uppercase.\n" );
+
+ return $this->progress( 0 );
+ }
+
+ $target = Title::makeTitle( $row->page_namespace, $upper );
+ if ( $target->exists() ) {
+ // Prefix "CapsCleanup" to bypass the conflict
+ $target = Title::newFromText( __CLASS__ . '/' . $display );
+ }
+ $ok = $this->movePage(
+ $current,
+ $target,
+ 'Converting page title to first-letter uppercase',
+ false
+ );
+ if ( $ok ) {
+ $this->progress( 1 );
+ if ( $row->page_namespace == $this->namespace ) {
+ $talk = $target->getTalkPage();
+ $row->page_namespace = $talk->getNamespace();
+ if ( $talk->exists() ) {
+ return $this->processRowToUppercase( $row );
+ }
+ }
+ }
+
+ return $this->progress( 0 );
+ }
+
+ protected function processRowToLowercase( $row ) {
global $wgContLang;
$current = Title::makeTitle( $row->page_namespace, $row->page_title );
}
$target = Title::makeTitle( $row->page_namespace, $lower );
- $targetDisplay = $target->getPrefixedText();
if ( $target->exists() ) {
+ $targetDisplay = $target->getPrefixedText();
$this->output( "\"$display\" skipped; \"$targetDisplay\" already exists\n" );
return $this->progress( 0 );
}
- if ( $this->dryrun ) {
- $this->output( "\"$display\" -> \"$targetDisplay\": DRY RUN, NOT MOVED\n" );
- $ok = true;
- } else {
- $mp = new MovePage( $current, $target );
- $status = $mp->move( $this->user, 'Converting page titles to lowercase', true );
- $ok = $status->isOK() ? 'OK' : $status->getWikiText( false, false, 'en' );
- $this->output( "\"$display\" -> \"$targetDisplay\": $ok\n" );
- }
+ $ok = $this->movePage( $current, $target, 'Converting page titles to lowercase', true );
if ( $ok === true ) {
$this->progress( 1 );
if ( $row->page_namespace == $this->namespace ) {
$talk = $target->getTalkPage();
$row->page_namespace = $talk->getNamespace();
if ( $talk->exists() ) {
- return $this->processRow( $row );
+ return $this->processRowToLowercase( $row );
}
}
}
return $this->progress( 0 );
}
+
+ /**
+ * @param Title $current
+ * @param Title $target
+ * @param string $reason
+ * @param bool $createRedirect
+ * @return bool Success
+ */
+ private function movePage( Title $current, Title $target, $reason, $createRedirect ) {
+ $display = $current->getPrefixedText();
+ $targetDisplay = $target->getPrefixedText();
+
+ if ( $this->dryrun ) {
+ $this->output( "\"$display\" -> \"$targetDisplay\": DRY RUN, NOT MOVED\n" );
+ $ok = 'OK';
+ } else {
+ $mp = new MovePage( $current, $target );
+ $status = $mp->move( $this->user, $reason, $createRedirect );
+ $ok = $status->isOK() ? 'OK' : $status->getWikiText( false, false, 'en' );
+ $this->output( "\"$display\" -> \"$targetDisplay\": $ok\n" );
+ }
+
+ return $ok === 'OK';
+ }
}
$maintClass = "CapsCleanup";
'es5-shim',
'oojs',
'oojs-ui-core.styles',
+ 'oojs-ui.styles.icons',
+ 'oojs-ui.styles.indicators',
+ 'oojs-ui.styles.textures',
'mediawiki.language',
],
'targets' => [ 'desktop', 'mobile' ],
'styles' => 'resources/src/oojs-ui-local.css', // HACK, see inside the file
'skinStyles' => $getSkinSpecific( 'core' ),
'targets' => [ 'desktop', 'mobile' ],
- // ResourceLoaderImageModule doesn't support 'skipFunction', so instead we set this up so that
- // this module is skipped together with its dependencies. Nothing else depends on these modules.
- 'dependencies' => [
- 'oojs-ui.styles.icons',
- 'oojs-ui.styles.indicators',
- 'oojs-ui.styles.textures',
- ],
- 'skipFunction' => 'resources/src/oojs-ui-styles-skip.js',
];
// Additional widgets and layouts module.
These license icons are used in LocalSettings.php files that are generated by
-the installer. Although public domain has been removed from the installer as
-an option, the image needs to remain here to support installations which refer
-to it in LocalSettings.php.
+the installer. Although "Public domain" has been removed from the installer as
+an option, the public-domain.png image needs to remain here to support older
+installations that refer to it in LocalSettings.php.
+++ /dev/null
-/*!
- * Skip function for OOjs UI PHP style modules.
- *
- * The `<meta name="X-OOUI-PHP" />` is added to pages by OutputPage::enableOOUI().
- *
- * Looking for elements in the DOM might be expensive, but it's probably better than double-loading
- * 200 KB of CSS with embedded images because of bug T87871.
- */
-return !!jQuery( 'meta[name="X-OOUI-PHP"]' ).length;
$actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
$expected = [
$rememberReq,
- $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
+ $makeReq( "primary-shared", AuthenticationRequest::PRIMARY_REQUIRED ),
$makeReq( "required", AuthenticationRequest::PRIMARY_REQUIRED ),
$makeReq( "required2", AuthenticationRequest::PRIMARY_REQUIRED ),
$makeReq( "optional", AuthenticationRequest::OPTIONAL ),
$actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
$expected = [
$rememberReq,
- $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
- $makeReq( "required", AuthenticationRequest::REQUIRED ),
+ $makeReq( "primary-shared", AuthenticationRequest::PRIMARY_REQUIRED ),
+ $makeReq( "required", AuthenticationRequest::PRIMARY_REQUIRED ),
$makeReq( "optional", AuthenticationRequest::OPTIONAL ),
- $makeReq( "foo", AuthenticationRequest::REQUIRED ),
+ $makeReq( "foo", AuthenticationRequest::PRIMARY_REQUIRED ),
$makeReq( "bar", AuthenticationRequest::REQUIRED ),
$makeReq( "baz", AuthenticationRequest::REQUIRED ),
];
$content = new WikitextContent( 'test text' );
$ok = ContentHandler::runLegacyHooks(
'testRunLegacyHooks',
- [ 'foo', &$content, 'bar' ],
- false
+ [ 'foo', &$content, 'bar' ]
);
$this->assertTrue( $ok, "runLegacyHooks should have returned true" );
$called = true;
$db->setFlag( DBO_TRX );
} );
- $db->rollback( __METHOD__ );
+ $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
$this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
$this->assertTrue( $called, 'Callback reached' );
}
+
+ public function testGetScopedLock() {
+ $db = $this->db;
+
+ $db->setFlag( DBO_TRX );
+ try {
+ $this->badLockingMethodImplicit( $db );
+ } catch ( RunTimeException $e ) {
+ $this->assertTrue( $db->trxLevel() > 0, "Transaction not committed." );
+ }
+ $db->clearFlag( DBO_TRX );
+ $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
+ $this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ) );
+
+ try {
+ $this->badLockingMethodExplicit( $db );
+ } catch ( RunTimeException $e ) {
+ $this->assertTrue( $db->trxLevel() > 0, "Transaction not committed." );
+ }
+ $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
+ $this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ) );
+ }
+
+ private function badLockingMethodImplicit( IDatabase $db ) {
+ $lock = $db->getScopedLockAndFlush( 'meow', __METHOD__, 1 );
+ $db->query( "SELECT 1" ); // trigger DBO_TRX
+ throw new RunTimeException( "Uh oh!" );
+ }
+
+ private function badLockingMethodExplicit( IDatabase $db ) {
+ $lock = $db->getScopedLockAndFlush( 'meow', __METHOD__, 1 );
+ $db->begin( __METHOD__ );
+ throw new RunTimeException( "Uh oh!" );
+ }
}
/**
* @covers ObjectFactory::getObjectFromSpec
+ * @covers ObjectFactory::expandClosures
*/
public function testClosureExpansionEnabled() {
$obj = ObjectFactory::getObjectFromSpec( [
$this->assertSame( 'unwrapped', $obj->setterArgs[0] );
}
+ /**
+ * @covers ObjectFactory::getObjectFromSpec
+ */
+ public function testGetObjectFromFactory() {
+ $args = [ 'a', 'b' ];
+ $obj = ObjectFactory::getObjectFromSpec( [
+ 'factory' => function ( $a, $b ) {
+ return new ObjectFactoryTestFixture( $a, $b );
+ },
+ 'args' => $args,
+ ] );
+ $this->assertSame( $args, $obj->args );
+ }
+
+ /**
+ * @covers ObjectFactory::getObjectFromSpec
+ * @expectedException InvalidArgumentException
+ */
+ public function testGetObjectFromInvalid() {
+ $args = [ 'a', 'b' ];
+ $obj = ObjectFactory::getObjectFromSpec( [
+ // Missing 'class' or 'factory'
+ 'args' => $args,
+ ] );
+ }
+
+ /**
+ * @covers ObjectFactory::getObjectFromSpec
+ * @dataProvider provideConstructClassInstance
+ */
+ public function testGetObjectFromClass( $args ) {
+ $obj = ObjectFactory::getObjectFromSpec( [
+ 'class' => 'ObjectFactoryTestFixture',
+ 'args' => $args,
+ ] );
+ $this->assertSame( $args, $obj->args );
+ }
+
/**
* @covers ObjectFactory::constructClassInstance
* @dataProvider provideConstructClassInstance
$this->assertSame( $args, $obj->args );
}
- public function provideConstructClassInstance() {
+ public static function provideConstructClassInstance() {
// These args go to 11. I thought about making 10 one louder, but 11!
return [
'0 args' => [ [] ],
}
/**
+ * @covers ObjectFactory::constructClassInstance
* @expectedException InvalidArgumentException
*/
public function testNamedArgs() {
}
}
+ /**
+ * Verify that all specified messages actually exist.
+ */
+ public function testMissingMessages() {
+ $data = self::getAllModules();
+ $validDeps = array_keys( $data['modules'] );
+ $lang = Language::factory( 'en' );
+
+ /** @var ResourceLoaderModule $module */
+ foreach ( $data['modules'] as $moduleName => $module ) {
+ foreach ( $module->getMessages() as $msgKey ) {
+ $this->assertTrue(
+ wfMessage( $msgKey )->useDatabase( false )->inLanguage( $lang )->exists(),
+ "Message '$msgKey' required by '$moduleName' must exist"
+ );
+ }
+ }
+ }
+
/**
* Verify that all dependencies of all modules are always satisfiable with the 'targets' defined
* for the involved modules.
'tests/qunit/suites/resources/mediawiki/mediawiki.template.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.template.mustache.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.test.js',
+ 'tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.html.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.toc.test.js',
--- /dev/null
+( function ( mw, $ ) {
+ QUnit.module( 'mediawiki (mw.loader)' );
+
+ mw.loader.addSource(
+ 'testloader',
+ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/load.mock.php' )
+ );
+
+ /**
+ * The sync style load test (for @import). This is, in a way, also an open bug for
+ * ResourceLoader ("execute js after styles are loaded"), but browsers don't offer a
+ * way to get a callback from when a stylesheet is loaded (that is, including any
+ * `@import` rules inside). To work around this, we'll have a little time loop to check
+ * if the styles apply.
+ *
+ * Note: This test originally used new Image() and onerror to get a callback
+ * when the url is loaded, but that is fragile since it doesn't monitor the
+ * same request as the css @import, and Safari 4 has issues with
+ * onerror/onload not being fired at all in weird cases like this.
+ */
+ function assertStyleAsync( assert, $element, prop, val, fn ) {
+ var styleTestStart,
+ el = $element.get( 0 ),
+ styleTestTimeout = ( QUnit.config.testTimeout || 5000 ) - 200;
+
+ function isCssImportApplied() {
+ // Trigger reflow, repaint, redraw, whatever (cross-browser)
+ var x = $element.css( 'height' );
+ x = el.innerHTML;
+ el.className = el.className;
+ x = document.documentElement.clientHeight;
+
+ return $element.css( prop ) === val;
+ }
+
+ function styleTestLoop() {
+ var styleTestSince = new Date().getTime() - styleTestStart;
+ // If it is passing or if we timed out, run the real test and stop the loop
+ if ( isCssImportApplied() || styleTestSince > styleTestTimeout ) {
+ assert.equal( $element.css( prop ), val,
+ 'style "' + prop + ': ' + val + '" from url is applied (after ' + styleTestSince + 'ms)'
+ );
+
+ if ( fn ) {
+ fn();
+ }
+
+ return;
+ }
+ // Otherwise, keep polling
+ setTimeout( styleTestLoop );
+ }
+
+ // Start the loop
+ styleTestStart = new Date().getTime();
+ styleTestLoop();
+ }
+
+ function urlStyleTest( selector, prop, val ) {
+ return QUnit.fixurl(
+ mw.config.get( 'wgScriptPath' ) +
+ '/tests/qunit/data/styleTest.css.php?' +
+ $.param( {
+ selector: selector,
+ prop: prop,
+ val: val
+ } )
+ );
+ }
+
+ QUnit.test( 'Basic', 2, function ( assert ) {
+ var isAwesomeDone;
+
+ mw.loader.testCallback = function () {
+ assert.strictEqual( isAwesomeDone, undefined, 'Implementing module is.awesome: isAwesomeDone should still be undefined' );
+ isAwesomeDone = true;
+ };
+
+ mw.loader.implement( 'test.callback', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/callMwLoaderTestCallback.js' ) ] );
+
+ return mw.loader.using( 'test.callback', function () {
+ assert.strictEqual( isAwesomeDone, true, 'test.callback module should\'ve caused isAwesomeDone to be true' );
+ delete mw.loader.testCallback;
+
+ }, function () {
+ assert.ok( false, 'Error callback fired while loader.using "test.callback" module' );
+ } );
+ } );
+
+ QUnit.test( 'Object method as module name', 2, function ( assert ) {
+ var isAwesomeDone;
+
+ mw.loader.testCallback = function () {
+ assert.strictEqual( isAwesomeDone, undefined, 'Implementing module hasOwnProperty: isAwesomeDone should still be undefined' );
+ isAwesomeDone = true;
+ };
+
+ mw.loader.implement( 'hasOwnProperty', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/callMwLoaderTestCallback.js' ) ], {}, {} );
+
+ return mw.loader.using( 'hasOwnProperty', function () {
+ assert.strictEqual( isAwesomeDone, true, 'hasOwnProperty module should\'ve caused isAwesomeDone to be true' );
+ delete mw.loader.testCallback;
+
+ }, function () {
+ assert.ok( false, 'Error callback fired while loader.using "hasOwnProperty" module' );
+ } );
+ } );
+
+ QUnit.test( '.using( .. ) Promise', 2, function ( assert ) {
+ var isAwesomeDone;
+
+ mw.loader.testCallback = function () {
+ assert.strictEqual( isAwesomeDone, undefined, 'Implementing module is.awesome: isAwesomeDone should still be undefined' );
+ isAwesomeDone = true;
+ };
+
+ mw.loader.implement( 'test.promise', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/callMwLoaderTestCallback.js' ) ] );
+
+ return mw.loader.using( 'test.promise' )
+ .done( function () {
+ assert.strictEqual( isAwesomeDone, true, 'test.promise module should\'ve caused isAwesomeDone to be true' );
+ delete mw.loader.testCallback;
+
+ } )
+ .fail( function () {
+ assert.ok( false, 'Error callback fired while loader.using "test.promise" module' );
+ } );
+ } );
+
+ QUnit.test( '.implement( styles={ "css": [text, ..] } )', 2, function ( assert ) {
+ var $element = $( '<div class="mw-test-implement-a"></div>' ).appendTo( '#qunit-fixture' );
+
+ assert.notEqual(
+ $element.css( 'float' ),
+ 'right',
+ 'style is clear'
+ );
+
+ mw.loader.implement(
+ 'test.implement.a',
+ function () {
+ assert.equal(
+ $element.css( 'float' ),
+ 'right',
+ 'style is applied'
+ );
+ },
+ {
+ all: '.mw-test-implement-a { float: right; }'
+ }
+ );
+
+ return mw.loader.using( 'test.implement.a' );
+ } );
+
+ QUnit.test( '.implement( styles={ "url": { <media>: [url, ..] } } )', 7, function ( assert ) {
+ var $element1 = $( '<div class="mw-test-implement-b1"></div>' ).appendTo( '#qunit-fixture' ),
+ $element2 = $( '<div class="mw-test-implement-b2"></div>' ).appendTo( '#qunit-fixture' ),
+ $element3 = $( '<div class="mw-test-implement-b3"></div>' ).appendTo( '#qunit-fixture' ),
+ done = assert.async();
+
+ assert.notEqual(
+ $element1.css( 'text-align' ),
+ 'center',
+ 'style is clear'
+ );
+ assert.notEqual(
+ $element2.css( 'float' ),
+ 'left',
+ 'style is clear'
+ );
+ assert.notEqual(
+ $element3.css( 'text-align' ),
+ 'right',
+ 'style is clear'
+ );
+
+ mw.loader.implement(
+ 'test.implement.b',
+ function () {
+ // Note: done() must only be called when the entire test is
+ // complete. So, make sure that we don't start until *both*
+ // assertStyleAsync calls have completed.
+ var pending = 2;
+ assertStyleAsync( assert, $element2, 'float', 'left', function () {
+ assert.notEqual( $element1.css( 'text-align' ), 'center', 'print style is not applied' );
+
+ pending--;
+ if ( pending === 0 ) {
+ done();
+ }
+ } );
+ assertStyleAsync( assert, $element3, 'float', 'right', function () {
+ assert.notEqual( $element1.css( 'text-align' ), 'center', 'print style is not applied' );
+
+ pending--;
+ if ( pending === 0 ) {
+ done();
+ }
+ } );
+ },
+ {
+ url: {
+ print: [ urlStyleTest( '.mw-test-implement-b1', 'text-align', 'center' ) ],
+ screen: [
+ // bug 40834: Make sure it actually works with more than 1 stylesheet reference
+ urlStyleTest( '.mw-test-implement-b2', 'float', 'left' ),
+ urlStyleTest( '.mw-test-implement-b3', 'float', 'right' )
+ ]
+ }
+ }
+ );
+
+ mw.loader.load( 'test.implement.b' );
+ } );
+
+ // Backwards compatibility
+ QUnit.test( '.implement( styles={ <media>: text } ) (back-compat)', 2, function ( assert ) {
+ var $element = $( '<div class="mw-test-implement-c"></div>' ).appendTo( '#qunit-fixture' );
+
+ assert.notEqual(
+ $element.css( 'float' ),
+ 'right',
+ 'style is clear'
+ );
+
+ mw.loader.implement(
+ 'test.implement.c',
+ function () {
+ assert.equal(
+ $element.css( 'float' ),
+ 'right',
+ 'style is applied'
+ );
+ },
+ {
+ all: '.mw-test-implement-c { float: right; }'
+ }
+ );
+
+ return mw.loader.using( 'test.implement.c' );
+ } );
+
+ // Backwards compatibility
+ QUnit.test( '.implement( styles={ <media>: [url, ..] } ) (back-compat)', 4, function ( assert ) {
+ var $element = $( '<div class="mw-test-implement-d"></div>' ).appendTo( '#qunit-fixture' ),
+ $element2 = $( '<div class="mw-test-implement-d2"></div>' ).appendTo( '#qunit-fixture' ),
+ done = assert.async();
+
+ assert.notEqual(
+ $element.css( 'float' ),
+ 'right',
+ 'style is clear'
+ );
+ assert.notEqual(
+ $element2.css( 'text-align' ),
+ 'center',
+ 'style is clear'
+ );
+
+ mw.loader.implement(
+ 'test.implement.d',
+ function () {
+ assertStyleAsync( assert, $element, 'float', 'right', function () {
+ assert.notEqual( $element2.css( 'text-align' ), 'center', 'print style is not applied (bug 40500)' );
+ done();
+ } );
+ },
+ {
+ all: [ urlStyleTest( '.mw-test-implement-d', 'float', 'right' ) ],
+ print: [ urlStyleTest( '.mw-test-implement-d2', 'text-align', 'center' ) ]
+ }
+ );
+
+ mw.loader.load( 'test.implement.d' );
+ } );
+
+ // @import (bug 31676)
+ QUnit.test( '.implement( styles has @import )', 7, function ( assert ) {
+ var isJsExecuted, $element,
+ done = assert.async();
+
+ mw.loader.implement(
+ 'test.implement.import',
+ function () {
+ assert.strictEqual( isJsExecuted, undefined, 'script not executed multiple times' );
+ isJsExecuted = true;
+
+ assert.equal( mw.loader.getState( 'test.implement.import' ), 'executing', 'module state during implement() script execution' );
+
+ $element = $( '<div class="mw-test-implement-import">Foo bar</div>' ).appendTo( '#qunit-fixture' );
+
+ assert.equal( mw.msg( 'test-foobar' ), 'Hello Foobar, $1!', 'messages load before script execution' );
+
+ assertStyleAsync( assert, $element, 'float', 'right', function () {
+ assert.equal( $element.css( 'text-align' ), 'center',
+ 'CSS styles after the @import rule are working'
+ );
+
+ done();
+ } );
+ },
+ {
+ css: [
+ '@import url(\''
+ + urlStyleTest( '.mw-test-implement-import', 'float', 'right' )
+ + '\');\n'
+ + '.mw-test-implement-import { text-align: center; }'
+ ]
+ },
+ {
+ 'test-foobar': 'Hello Foobar, $1!'
+ }
+ );
+
+ mw.loader.using( 'test.implement.import' ).always( function () {
+ assert.strictEqual( isJsExecuted, true, 'script executed' );
+ assert.equal( mw.loader.getState( 'test.implement.import' ), 'ready', 'module state after script execution' );
+ } );
+ } );
+
+ QUnit.test( '.implement( dependency with styles )', 4, function ( assert ) {
+ var $element = $( '<div class="mw-test-implement-e"></div>' ).appendTo( '#qunit-fixture' ),
+ $element2 = $( '<div class="mw-test-implement-e2"></div>' ).appendTo( '#qunit-fixture' );
+
+ assert.notEqual(
+ $element.css( 'float' ),
+ 'right',
+ 'style is clear'
+ );
+ assert.notEqual(
+ $element2.css( 'float' ),
+ 'left',
+ 'style is clear'
+ );
+
+ mw.loader.register( [
+ [ 'test.implement.e', '0', [ 'test.implement.e2' ] ],
+ [ 'test.implement.e2', '0' ]
+ ] );
+
+ mw.loader.implement(
+ 'test.implement.e',
+ function () {
+ assert.equal(
+ $element.css( 'float' ),
+ 'right',
+ 'Depending module\'s style is applied'
+ );
+ },
+ {
+ all: '.mw-test-implement-e { float: right; }'
+ }
+ );
+
+ mw.loader.implement(
+ 'test.implement.e2',
+ function () {
+ assert.equal(
+ $element2.css( 'float' ),
+ 'left',
+ 'Dependency\'s style is applied'
+ );
+ },
+ {
+ all: '.mw-test-implement-e2 { float: left; }'
+ }
+ );
+
+ return mw.loader.using( 'test.implement.e' );
+ } );
+
+ QUnit.test( '.implement( only scripts )', 1, function ( assert ) {
+ mw.loader.implement( 'test.onlyscripts', function () {} );
+ assert.strictEqual( mw.loader.getState( 'test.onlyscripts' ), 'ready' );
+ } );
+
+ QUnit.test( '.implement( only messages )', 2, function ( assert ) {
+ assert.assertFalse( mw.messages.exists( 'bug_29107' ), 'Verify that the test message doesn\'t exist yet' );
+
+ // jscs: disable requireCamelCaseOrUpperCaseIdentifiers
+ mw.loader.implement( 'test.implement.msgs', [], {}, { bug_29107: 'loaded' } );
+ // jscs: enable requireCamelCaseOrUpperCaseIdentifiers
+
+ return mw.loader.using( 'test.implement.msgs', function () {
+ assert.ok( mw.messages.exists( 'bug_29107' ), 'Bug 29107: messages-only module should implement ok' );
+ }, function () {
+ assert.ok( false, 'Error callback fired while implementing "test.implement.msgs" module' );
+ } );
+ } );
+
+ QUnit.test( '.implement( empty )', 1, function ( assert ) {
+ mw.loader.implement( 'test.empty' );
+ assert.strictEqual( mw.loader.getState( 'test.empty' ), 'ready' );
+ } );
+
+ QUnit.test( 'Broken indirect dependency', 4, function ( assert ) {
+ // don't emit an error event
+ this.sandbox.stub( mw, 'track' );
+
+ mw.loader.register( [
+ [ 'test.module1', '0' ],
+ [ 'test.module2', '0', [ 'test.module1' ] ],
+ [ 'test.module3', '0', [ 'test.module2' ] ]
+ ] );
+ mw.loader.implement( 'test.module1', function () {
+ throw new Error( 'expected' );
+ }, {}, {} );
+ assert.strictEqual( mw.loader.getState( 'test.module1' ), 'error', 'Expected "error" state for test.module1' );
+ assert.strictEqual( mw.loader.getState( 'test.module2' ), 'error', 'Expected "error" state for test.module2' );
+ assert.strictEqual( mw.loader.getState( 'test.module3' ), 'error', 'Expected "error" state for test.module3' );
+
+ assert.strictEqual( mw.track.callCount, 1 );
+ } );
+
+ QUnit.test( 'Circular dependency', 1, function ( assert ) {
+ mw.loader.register( [
+ [ 'test.circle1', '0', [ 'test.circle2' ] ],
+ [ 'test.circle2', '0', [ 'test.circle3' ] ],
+ [ 'test.circle3', '0', [ 'test.circle1' ] ]
+ ] );
+ assert.throws( function () {
+ mw.loader.using( 'test.circle3' );
+ }, /Circular/, 'Detect circular dependency' );
+ } );
+
+ QUnit.test( 'Out-of-order implementation', 9, function ( assert ) {
+ mw.loader.register( [
+ [ 'test.module4', '0' ],
+ [ 'test.module5', '0', [ 'test.module4' ] ],
+ [ 'test.module6', '0', [ 'test.module5' ] ]
+ ] );
+ mw.loader.implement( 'test.module4', function () {} );
+ assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
+ assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' );
+ assert.strictEqual( mw.loader.getState( 'test.module6' ), 'registered', 'Expected "registered" state for test.module6' );
+ mw.loader.implement( 'test.module6', function () {} );
+ assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
+ assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' );
+ assert.strictEqual( mw.loader.getState( 'test.module6' ), 'loaded', 'Expected "loaded" state for test.module6' );
+ mw.loader.implement( 'test.module5', function () {} );
+ assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
+ assert.strictEqual( mw.loader.getState( 'test.module5' ), 'ready', 'Expected "ready" state for test.module5' );
+ assert.strictEqual( mw.loader.getState( 'test.module6' ), 'ready', 'Expected "ready" state for test.module6' );
+ } );
+
+ QUnit.test( 'Missing dependency', 13, function ( assert ) {
+ mw.loader.register( [
+ [ 'test.module7', '0' ],
+ [ 'test.module8', '0', [ 'test.module7' ] ],
+ [ 'test.module9', '0', [ 'test.module8' ] ]
+ ] );
+ mw.loader.implement( 'test.module8', function () {} );
+ assert.strictEqual( mw.loader.getState( 'test.module7' ), 'registered', 'Expected "registered" state for test.module7' );
+ assert.strictEqual( mw.loader.getState( 'test.module8' ), 'loaded', 'Expected "loaded" state for test.module8' );
+ assert.strictEqual( mw.loader.getState( 'test.module9' ), 'registered', 'Expected "registered" state for test.module9' );
+ mw.loader.state( 'test.module7', 'missing' );
+ assert.strictEqual( mw.loader.getState( 'test.module7' ), 'missing', 'Expected "missing" state for test.module7' );
+ assert.strictEqual( mw.loader.getState( 'test.module8' ), 'error', 'Expected "error" state for test.module8' );
+ assert.strictEqual( mw.loader.getState( 'test.module9' ), 'error', 'Expected "error" state for test.module9' );
+ mw.loader.implement( 'test.module9', function () {} );
+ assert.strictEqual( mw.loader.getState( 'test.module7' ), 'missing', 'Expected "missing" state for test.module7' );
+ assert.strictEqual( mw.loader.getState( 'test.module8' ), 'error', 'Expected "error" state for test.module8' );
+ assert.strictEqual( mw.loader.getState( 'test.module9' ), 'error', 'Expected "error" state for test.module9' );
+ mw.loader.using(
+ [ 'test.module7' ],
+ function () {
+ assert.ok( false, 'Success fired despite missing dependency' );
+ assert.ok( true, 'QUnit expected() count dummy' );
+ },
+ function ( e, dependencies ) {
+ assert.strictEqual( $.isArray( dependencies ), true, 'Expected array of dependencies' );
+ assert.deepEqual( dependencies, [ 'test.module7' ], 'Error callback called with module test.module7' );
+ }
+ );
+ mw.loader.using(
+ [ 'test.module9' ],
+ function () {
+ assert.ok( false, 'Success fired despite missing dependency' );
+ assert.ok( true, 'QUnit expected() count dummy' );
+ },
+ function ( e, dependencies ) {
+ assert.strictEqual( $.isArray( dependencies ), true, 'Expected array of dependencies' );
+ dependencies.sort();
+ assert.deepEqual(
+ dependencies,
+ [ 'test.module7', 'test.module8', 'test.module9' ],
+ 'Error callback called with all three modules as dependencies'
+ );
+ }
+ );
+ } );
+
+ QUnit.test( 'Dependency handling', 5, function ( assert ) {
+ var done = assert.async();
+ mw.loader.register( [
+ // [module, version, dependencies, group, source]
+ [ 'testMissing', '1', [], null, 'testloader' ],
+ [ 'testUsesMissing', '1', [ 'testMissing' ], null, 'testloader' ],
+ [ 'testUsesNestedMissing', '1', [ 'testUsesMissing' ], null, 'testloader' ]
+ ] );
+
+ function verifyModuleStates() {
+ assert.equal( mw.loader.getState( 'testMissing' ), 'missing', 'Module not known to server must have state "missing"' );
+ assert.equal( mw.loader.getState( 'testUsesMissing' ), 'error', 'Module with missing dependency must have state "error"' );
+ assert.equal( mw.loader.getState( 'testUsesNestedMissing' ), 'error', 'Module with indirect missing dependency must have state "error"' );
+ }
+
+ mw.loader.using( [ 'testUsesNestedMissing' ],
+ function () {
+ assert.ok( false, 'Error handler should be invoked.' );
+ assert.ok( true ); // Dummy to reach QUnit expect()
+
+ verifyModuleStates();
+
+ done();
+ },
+ function ( e, badmodules ) {
+ assert.ok( true, 'Error handler should be invoked.' );
+ // As soon as server spits out state('testMissing', 'missing');
+ // it will bubble up and trigger the error callback.
+ // Therefor the badmodules array is not testUsesMissing or testUsesNestedMissing.
+ assert.deepEqual( badmodules, [ 'testMissing' ], 'Bad modules as expected.' );
+
+ verifyModuleStates();
+
+ done();
+ }
+ );
+ } );
+
+ QUnit.test( 'Skip-function handling', 5, function ( assert ) {
+ mw.loader.register( [
+ // [module, version, dependencies, group, source, skip]
+ [ 'testSkipped', '1', [], null, 'testloader', 'return true;' ],
+ [ 'testNotSkipped', '1', [], null, 'testloader', 'return false;' ],
+ [ 'testUsesSkippable', '1', [ 'testSkipped', 'testNotSkipped' ], null, 'testloader' ]
+ ] );
+
+ function verifyModuleStates() {
+ assert.equal( mw.loader.getState( 'testSkipped' ), 'ready', 'Module is ready when skipped' );
+ assert.equal( mw.loader.getState( 'testNotSkipped' ), 'ready', 'Module is ready when not skipped but loaded' );
+ assert.equal( mw.loader.getState( 'testUsesSkippable' ), 'ready', 'Module is ready when skippable dependencies are ready' );
+ }
+
+ return mw.loader.using( [ 'testUsesSkippable' ],
+ function () {
+ assert.ok( true, 'Success handler should be invoked.' );
+ assert.ok( true ); // Dummy to match error handler and reach QUnit expect()
+
+ verifyModuleStates();
+ },
+ function ( e, badmodules ) {
+ assert.ok( false, 'Error handler should not be invoked.' );
+ assert.deepEqual( badmodules, [], 'Bad modules as expected.' );
+
+ verifyModuleStates();
+ }
+ );
+ } );
+
+ QUnit.asyncTest( '.load( "//protocol-relative" ) - T32825', 2, function ( assert ) {
+ // This bug was actually already fixed in 1.18 and later when discovered in 1.17.
+ // Test is for regressions!
+
+ // Forge a URL to the test callback script
+ var target = QUnit.fixurl(
+ mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/qunitOkCall.js'
+ );
+
+ // Confirm that mw.loader.load() works with protocol-relative URLs
+ target = target.replace( /https?:/, '' );
+
+ assert.equal( target.slice( 0, 2 ), '//',
+ 'URL must be relative to test relative URLs!'
+ );
+
+ // Async!
+ // The target calls QUnit.start
+ mw.loader.load( target );
+ } );
+
+ QUnit.asyncTest( '.load( "/absolute-path" )', 2, function ( assert ) {
+ // Forge a URL to the test callback script
+ var target = QUnit.fixurl(
+ mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/qunitOkCall.js'
+ );
+
+ // Confirm that mw.loader.load() works with absolute-paths (relative to current hostname)
+ assert.equal( target.slice( 0, 1 ), '/', 'URL is relative to document root' );
+
+ // Async!
+ // The target calls QUnit.start
+ mw.loader.load( target );
+ } );
+
+ QUnit.test( 'Executing race - T112232', 2, function ( assert ) {
+ var done = false;
+
+ // The red herring schedules its CSS buffer first. In T112232, a bug in the
+ // state machine would cause the job for testRaceLoadMe to run with an earlier job.
+ mw.loader.implement(
+ 'testRaceRedHerring',
+ function () {},
+ { css: [ '.mw-testRaceRedHerring {}' ] }
+ );
+ mw.loader.implement(
+ 'testRaceLoadMe',
+ function () {
+ done = true;
+ },
+ { css: [ '.mw-testRaceLoadMe { float: left; }' ] }
+ );
+
+ mw.loader.load( [ 'testRaceRedHerring', 'testRaceLoadMe' ] );
+ return mw.loader.using( 'testRaceLoadMe', function () {
+ assert.strictEqual( done, true, 'script ran' );
+ assert.strictEqual( mw.loader.getState( 'testRaceLoadMe' ), 'ready', 'state' );
+ } );
+ } );
+
+ QUnit.test( 'require()', 6, function ( assert ) {
+ mw.loader.register( [
+ [ 'test.require1', '0' ],
+ [ 'test.require2', '0' ],
+ [ 'test.require3', '0' ],
+ [ 'test.require4', '0', [ 'test.require3' ] ]
+ ] );
+ mw.loader.implement( 'test.require1', function () {} );
+ mw.loader.implement( 'test.require2', function ( $, jQuery, require, module ) {
+ module.exports = 1;
+ } );
+ mw.loader.implement( 'test.require3', function ( $, jQuery, require, module ) {
+ module.exports = function () {
+ return 'hello world';
+ };
+ } );
+ mw.loader.implement( 'test.require4', function ( $, jQuery, require, module ) {
+ var other = require( 'test.require3' );
+ module.exports = {
+ pizza: function () {
+ return other();
+ }
+ };
+ } );
+ return mw.loader.using( [ 'test.require1', 'test.require2', 'test.require3', 'test.require4' ] )
+ .then( function ( require ) {
+ var module1, module2, module3, module4;
+
+ module1 = require( 'test.require1' );
+ module2 = require( 'test.require2' );
+ module3 = require( 'test.require3' );
+ module4 = require( 'test.require4' );
+
+ assert.strictEqual( typeof module1, 'object', 'export of module with no export' );
+ assert.strictEqual( module2, 1, 'export a number' );
+ assert.strictEqual( module3(), 'hello world', 'export a function' );
+ assert.strictEqual( typeof module4.pizza, 'function', 'export an object' );
+ assert.strictEqual( module4.pizza(), 'hello world', 'module can require other modules' );
+
+ assert.throws( function () {
+ require( '_badmodule' );
+ }, /is not loaded/, 'Requesting non-existent modules throws error.' );
+ } );
+ } );
+
+ QUnit.test( 'require() in debug mode', 1, function ( assert ) {
+ var path = mw.config.get( 'wgScriptPath' );
+ mw.loader.register( [
+ [ 'test.require.define', '0' ],
+ [ 'test.require.callback', '0', [ 'test.require.define' ] ]
+ ] );
+ mw.loader.implement( 'test.require.callback', [ QUnit.fixurl( path + '/tests/qunit/data/requireCallMwLoaderTestCallback.js' ) ] );
+ mw.loader.implement( 'test.require.define', [ QUnit.fixurl( path + '/tests/qunit/data/defineCallMwLoaderTestCallback.js' ) ] );
+
+ return mw.loader.using( 'test.require.callback' ).then( function ( require ) {
+ var exported = require( 'test.require.callback' );
+ assert.strictEqual( exported, 'Require worked.Define worked.',
+ 'module.exports worked in debug mode' );
+ }, function () {
+ assert.ok( false, 'Error callback fired while loader.using "test.require.callback" module' );
+ } );
+ } );
+
+}( mediaWiki, jQuery ) );
/*jshint -W024 */
-( function ( mw, $ ) {
+( function ( mw ) {
var specialCharactersPageName,
// Can't mock SITENAME since jqueryMsg caches it at load
siteName = mw.config.get( 'wgSiteName' );
}
} ) );
- mw.loader.addSource(
- 'testloader',
- QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/load.mock.php' )
- );
-
QUnit.test( 'Initial check', 8, function ( assert ) {
assert.ok( window.jQuery, 'jQuery defined' );
assert.ok( window.$, '$ defined' );
assert.equal( mw.msg( 'int-msg' ), 'Some Other Message', 'int is resolved' );
} );
- /**
- * The sync style load test (for @import). This is, in a way, also an open bug for
- * ResourceLoader ("execute js after styles are loaded"), but browsers don't offer a
- * way to get a callback from when a stylesheet is loaded (that is, including any
- * `@import` rules inside). To work around this, we'll have a little time loop to check
- * if the styles apply.
- *
- * Note: This test originally used new Image() and onerror to get a callback
- * when the url is loaded, but that is fragile since it doesn't monitor the
- * same request as the css @import, and Safari 4 has issues with
- * onerror/onload not being fired at all in weird cases like this.
- */
- function assertStyleAsync( assert, $element, prop, val, fn ) {
- var styleTestStart,
- el = $element.get( 0 ),
- styleTestTimeout = ( QUnit.config.testTimeout || 5000 ) - 200;
-
- function isCssImportApplied() {
- // Trigger reflow, repaint, redraw, whatever (cross-browser)
- var x = $element.css( 'height' );
- x = el.innerHTML;
- el.className = el.className;
- x = document.documentElement.clientHeight;
-
- return $element.css( prop ) === val;
- }
-
- function styleTestLoop() {
- var styleTestSince = new Date().getTime() - styleTestStart;
- // If it is passing or if we timed out, run the real test and stop the loop
- if ( isCssImportApplied() || styleTestSince > styleTestTimeout ) {
- assert.equal( $element.css( prop ), val,
- 'style "' + prop + ': ' + val + '" from url is applied (after ' + styleTestSince + 'ms)'
- );
-
- if ( fn ) {
- fn();
- }
-
- return;
- }
- // Otherwise, keep polling
- setTimeout( styleTestLoop );
- }
-
- // Start the loop
- styleTestStart = new Date().getTime();
- styleTestLoop();
- }
-
- function urlStyleTest( selector, prop, val ) {
- return QUnit.fixurl(
- mw.config.get( 'wgScriptPath' ) +
- '/tests/qunit/data/styleTest.css.php?' +
- $.param( {
- selector: selector,
- prop: prop,
- val: val
- } )
- );
- }
-
- QUnit.test( 'mw.loader', 2, function ( assert ) {
- var isAwesomeDone;
-
- mw.loader.testCallback = function () {
- assert.strictEqual( isAwesomeDone, undefined, 'Implementing module is.awesome: isAwesomeDone should still be undefined' );
- isAwesomeDone = true;
- };
-
- mw.loader.implement( 'test.callback', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/callMwLoaderTestCallback.js' ) ] );
-
- return mw.loader.using( 'test.callback', function () {
- assert.strictEqual( isAwesomeDone, true, 'test.callback module should\'ve caused isAwesomeDone to be true' );
- delete mw.loader.testCallback;
-
- }, function () {
- assert.ok( false, 'Error callback fired while loader.using "test.callback" module' );
- } );
- } );
-
- QUnit.test( 'mw.loader with Object method as module name', 2, function ( assert ) {
- var isAwesomeDone;
-
- mw.loader.testCallback = function () {
- assert.strictEqual( isAwesomeDone, undefined, 'Implementing module hasOwnProperty: isAwesomeDone should still be undefined' );
- isAwesomeDone = true;
- };
-
- mw.loader.implement( 'hasOwnProperty', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/callMwLoaderTestCallback.js' ) ], {}, {} );
-
- return mw.loader.using( 'hasOwnProperty', function () {
- assert.strictEqual( isAwesomeDone, true, 'hasOwnProperty module should\'ve caused isAwesomeDone to be true' );
- delete mw.loader.testCallback;
-
- }, function () {
- assert.ok( false, 'Error callback fired while loader.using "hasOwnProperty" module' );
- } );
- } );
-
- QUnit.test( 'mw.loader.using( .. ) Promise', 2, function ( assert ) {
- var isAwesomeDone;
-
- mw.loader.testCallback = function () {
- assert.strictEqual( isAwesomeDone, undefined, 'Implementing module is.awesome: isAwesomeDone should still be undefined' );
- isAwesomeDone = true;
- };
-
- mw.loader.implement( 'test.promise', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/callMwLoaderTestCallback.js' ) ] );
-
- return mw.loader.using( 'test.promise' )
- .done( function () {
- assert.strictEqual( isAwesomeDone, true, 'test.promise module should\'ve caused isAwesomeDone to be true' );
- delete mw.loader.testCallback;
-
- } )
- .fail( function () {
- assert.ok( false, 'Error callback fired while loader.using "test.promise" module' );
- } );
- } );
-
- QUnit.test( 'mw.loader.implement( styles={ "css": [text, ..] } )', 2, function ( assert ) {
- var $element = $( '<div class="mw-test-implement-a"></div>' ).appendTo( '#qunit-fixture' );
-
- assert.notEqual(
- $element.css( 'float' ),
- 'right',
- 'style is clear'
- );
-
- mw.loader.implement(
- 'test.implement.a',
- function () {
- assert.equal(
- $element.css( 'float' ),
- 'right',
- 'style is applied'
- );
- },
- {
- all: '.mw-test-implement-a { float: right; }'
- }
- );
-
- return mw.loader.using( 'test.implement.a' );
- } );
-
- QUnit.test( 'mw.loader.implement( styles={ "url": { <media>: [url, ..] } } )', 7, function ( assert ) {
- var $element1 = $( '<div class="mw-test-implement-b1"></div>' ).appendTo( '#qunit-fixture' ),
- $element2 = $( '<div class="mw-test-implement-b2"></div>' ).appendTo( '#qunit-fixture' ),
- $element3 = $( '<div class="mw-test-implement-b3"></div>' ).appendTo( '#qunit-fixture' ),
- done = assert.async();
-
- assert.notEqual(
- $element1.css( 'text-align' ),
- 'center',
- 'style is clear'
- );
- assert.notEqual(
- $element2.css( 'float' ),
- 'left',
- 'style is clear'
- );
- assert.notEqual(
- $element3.css( 'text-align' ),
- 'right',
- 'style is clear'
- );
-
- mw.loader.implement(
- 'test.implement.b',
- function () {
- // Note: done() must only be called when the entire test is
- // complete. So, make sure that we don't start until *both*
- // assertStyleAsync calls have completed.
- var pending = 2;
- assertStyleAsync( assert, $element2, 'float', 'left', function () {
- assert.notEqual( $element1.css( 'text-align' ), 'center', 'print style is not applied' );
-
- pending--;
- if ( pending === 0 ) {
- done();
- }
- } );
- assertStyleAsync( assert, $element3, 'float', 'right', function () {
- assert.notEqual( $element1.css( 'text-align' ), 'center', 'print style is not applied' );
-
- pending--;
- if ( pending === 0 ) {
- done();
- }
- } );
- },
- {
- url: {
- print: [ urlStyleTest( '.mw-test-implement-b1', 'text-align', 'center' ) ],
- screen: [
- // bug 40834: Make sure it actually works with more than 1 stylesheet reference
- urlStyleTest( '.mw-test-implement-b2', 'float', 'left' ),
- urlStyleTest( '.mw-test-implement-b3', 'float', 'right' )
- ]
- }
- }
- );
-
- mw.loader.load( 'test.implement.b' );
- } );
-
- // Backwards compatibility
- QUnit.test( 'mw.loader.implement( styles={ <media>: text } ) (back-compat)', 2, function ( assert ) {
- var $element = $( '<div class="mw-test-implement-c"></div>' ).appendTo( '#qunit-fixture' );
-
- assert.notEqual(
- $element.css( 'float' ),
- 'right',
- 'style is clear'
- );
-
- mw.loader.implement(
- 'test.implement.c',
- function () {
- assert.equal(
- $element.css( 'float' ),
- 'right',
- 'style is applied'
- );
- },
- {
- all: '.mw-test-implement-c { float: right; }'
- }
- );
-
- return mw.loader.using( 'test.implement.c' );
- } );
-
- // Backwards compatibility
- QUnit.test( 'mw.loader.implement( styles={ <media>: [url, ..] } ) (back-compat)', 4, function ( assert ) {
- var $element = $( '<div class="mw-test-implement-d"></div>' ).appendTo( '#qunit-fixture' ),
- $element2 = $( '<div class="mw-test-implement-d2"></div>' ).appendTo( '#qunit-fixture' ),
- done = assert.async();
-
- assert.notEqual(
- $element.css( 'float' ),
- 'right',
- 'style is clear'
- );
- assert.notEqual(
- $element2.css( 'text-align' ),
- 'center',
- 'style is clear'
- );
-
- mw.loader.implement(
- 'test.implement.d',
- function () {
- assertStyleAsync( assert, $element, 'float', 'right', function () {
- assert.notEqual( $element2.css( 'text-align' ), 'center', 'print style is not applied (bug 40500)' );
- done();
- } );
- },
- {
- all: [ urlStyleTest( '.mw-test-implement-d', 'float', 'right' ) ],
- print: [ urlStyleTest( '.mw-test-implement-d2', 'text-align', 'center' ) ]
- }
- );
-
- mw.loader.load( 'test.implement.d' );
- } );
-
- // @import (bug 31676)
- QUnit.test( 'mw.loader.implement( styles has @import )', 7, function ( assert ) {
- var isJsExecuted, $element,
- done = assert.async();
-
- mw.loader.implement(
- 'test.implement.import',
- function () {
- assert.strictEqual( isJsExecuted, undefined, 'script not executed multiple times' );
- isJsExecuted = true;
-
- assert.equal( mw.loader.getState( 'test.implement.import' ), 'executing', 'module state during implement() script execution' );
-
- $element = $( '<div class="mw-test-implement-import">Foo bar</div>' ).appendTo( '#qunit-fixture' );
-
- assert.equal( mw.msg( 'test-foobar' ), 'Hello Foobar, $1!', 'messages load before script execution' );
-
- assertStyleAsync( assert, $element, 'float', 'right', function () {
- assert.equal( $element.css( 'text-align' ), 'center',
- 'CSS styles after the @import rule are working'
- );
-
- done();
- } );
- },
- {
- css: [
- '@import url(\''
- + urlStyleTest( '.mw-test-implement-import', 'float', 'right' )
- + '\');\n'
- + '.mw-test-implement-import { text-align: center; }'
- ]
- },
- {
- 'test-foobar': 'Hello Foobar, $1!'
- }
- );
-
- mw.loader.using( 'test.implement.import' ).always( function () {
- assert.strictEqual( isJsExecuted, true, 'script executed' );
- assert.equal( mw.loader.getState( 'test.implement.import' ), 'ready', 'module state after script execution' );
- } );
- } );
-
- QUnit.test( 'mw.loader.implement( dependency with styles )', 4, function ( assert ) {
- var $element = $( '<div class="mw-test-implement-e"></div>' ).appendTo( '#qunit-fixture' ),
- $element2 = $( '<div class="mw-test-implement-e2"></div>' ).appendTo( '#qunit-fixture' );
-
- assert.notEqual(
- $element.css( 'float' ),
- 'right',
- 'style is clear'
- );
- assert.notEqual(
- $element2.css( 'float' ),
- 'left',
- 'style is clear'
- );
-
- mw.loader.register( [
- [ 'test.implement.e', '0', [ 'test.implement.e2' ] ],
- [ 'test.implement.e2', '0' ]
- ] );
-
- mw.loader.implement(
- 'test.implement.e',
- function () {
- assert.equal(
- $element.css( 'float' ),
- 'right',
- 'Depending module\'s style is applied'
- );
- },
- {
- all: '.mw-test-implement-e { float: right; }'
- }
- );
-
- mw.loader.implement(
- 'test.implement.e2',
- function () {
- assert.equal(
- $element2.css( 'float' ),
- 'left',
- 'Dependency\'s style is applied'
- );
- },
- {
- all: '.mw-test-implement-e2 { float: left; }'
- }
- );
-
- return mw.loader.using( 'test.implement.e' );
- } );
-
- QUnit.test( 'mw.loader.implement( only scripts )', 1, function ( assert ) {
- mw.loader.implement( 'test.onlyscripts', function () {} );
- assert.strictEqual( mw.loader.getState( 'test.onlyscripts' ), 'ready' );
- } );
-
- QUnit.test( 'mw.loader.implement( only messages )', 2, function ( assert ) {
- assert.assertFalse( mw.messages.exists( 'bug_29107' ), 'Verify that the test message doesn\'t exist yet' );
-
- // jscs: disable requireCamelCaseOrUpperCaseIdentifiers
- mw.loader.implement( 'test.implement.msgs', [], {}, { bug_29107: 'loaded' } );
- // jscs: enable requireCamelCaseOrUpperCaseIdentifiers
-
- return mw.loader.using( 'test.implement.msgs', function () {
- assert.ok( mw.messages.exists( 'bug_29107' ), 'Bug 29107: messages-only module should implement ok' );
- }, function () {
- assert.ok( false, 'Error callback fired while implementing "test.implement.msgs" module' );
- } );
- } );
-
- QUnit.test( 'mw.loader.implement( empty )', 1, function ( assert ) {
- mw.loader.implement( 'test.empty' );
- assert.strictEqual( mw.loader.getState( 'test.empty' ), 'ready' );
- } );
-
- QUnit.test( 'mw.loader with broken indirect dependency', 4, function ( assert ) {
- // don't emit an error event
- this.sandbox.stub( mw, 'track' );
-
- mw.loader.register( [
- [ 'test.module1', '0' ],
- [ 'test.module2', '0', [ 'test.module1' ] ],
- [ 'test.module3', '0', [ 'test.module2' ] ]
- ] );
- mw.loader.implement( 'test.module1', function () {
- throw new Error( 'expected' );
- }, {}, {} );
- assert.strictEqual( mw.loader.getState( 'test.module1' ), 'error', 'Expected "error" state for test.module1' );
- assert.strictEqual( mw.loader.getState( 'test.module2' ), 'error', 'Expected "error" state for test.module2' );
- assert.strictEqual( mw.loader.getState( 'test.module3' ), 'error', 'Expected "error" state for test.module3' );
-
- assert.strictEqual( mw.track.callCount, 1 );
- } );
-
- QUnit.test( 'mw.loader with circular dependency', 1, function ( assert ) {
- mw.loader.register( [
- [ 'test.circle1', '0', [ 'test.circle2' ] ],
- [ 'test.circle2', '0', [ 'test.circle3' ] ],
- [ 'test.circle3', '0', [ 'test.circle1' ] ]
- ] );
- assert.throws( function () {
- mw.loader.using( 'test.circle3' );
- }, /Circular/, 'Detect circular dependency' );
- } );
-
- QUnit.test( 'mw.loader out-of-order implementation', 9, function ( assert ) {
- mw.loader.register( [
- [ 'test.module4', '0' ],
- [ 'test.module5', '0', [ 'test.module4' ] ],
- [ 'test.module6', '0', [ 'test.module5' ] ]
- ] );
- mw.loader.implement( 'test.module4', function () {} );
- assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
- assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' );
- assert.strictEqual( mw.loader.getState( 'test.module6' ), 'registered', 'Expected "registered" state for test.module6' );
- mw.loader.implement( 'test.module6', function () {} );
- assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
- assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' );
- assert.strictEqual( mw.loader.getState( 'test.module6' ), 'loaded', 'Expected "loaded" state for test.module6' );
- mw.loader.implement( 'test.module5', function () {} );
- assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
- assert.strictEqual( mw.loader.getState( 'test.module5' ), 'ready', 'Expected "ready" state for test.module5' );
- assert.strictEqual( mw.loader.getState( 'test.module6' ), 'ready', 'Expected "ready" state for test.module6' );
- } );
-
- QUnit.test( 'mw.loader missing dependency', 13, function ( assert ) {
- mw.loader.register( [
- [ 'test.module7', '0' ],
- [ 'test.module8', '0', [ 'test.module7' ] ],
- [ 'test.module9', '0', [ 'test.module8' ] ]
- ] );
- mw.loader.implement( 'test.module8', function () {} );
- assert.strictEqual( mw.loader.getState( 'test.module7' ), 'registered', 'Expected "registered" state for test.module7' );
- assert.strictEqual( mw.loader.getState( 'test.module8' ), 'loaded', 'Expected "loaded" state for test.module8' );
- assert.strictEqual( mw.loader.getState( 'test.module9' ), 'registered', 'Expected "registered" state for test.module9' );
- mw.loader.state( 'test.module7', 'missing' );
- assert.strictEqual( mw.loader.getState( 'test.module7' ), 'missing', 'Expected "missing" state for test.module7' );
- assert.strictEqual( mw.loader.getState( 'test.module8' ), 'error', 'Expected "error" state for test.module8' );
- assert.strictEqual( mw.loader.getState( 'test.module9' ), 'error', 'Expected "error" state for test.module9' );
- mw.loader.implement( 'test.module9', function () {} );
- assert.strictEqual( mw.loader.getState( 'test.module7' ), 'missing', 'Expected "missing" state for test.module7' );
- assert.strictEqual( mw.loader.getState( 'test.module8' ), 'error', 'Expected "error" state for test.module8' );
- assert.strictEqual( mw.loader.getState( 'test.module9' ), 'error', 'Expected "error" state for test.module9' );
- mw.loader.using(
- [ 'test.module7' ],
- function () {
- assert.ok( false, 'Success fired despite missing dependency' );
- assert.ok( true, 'QUnit expected() count dummy' );
- },
- function ( e, dependencies ) {
- assert.strictEqual( $.isArray( dependencies ), true, 'Expected array of dependencies' );
- assert.deepEqual( dependencies, [ 'test.module7' ], 'Error callback called with module test.module7' );
- }
- );
- mw.loader.using(
- [ 'test.module9' ],
- function () {
- assert.ok( false, 'Success fired despite missing dependency' );
- assert.ok( true, 'QUnit expected() count dummy' );
- },
- function ( e, dependencies ) {
- assert.strictEqual( $.isArray( dependencies ), true, 'Expected array of dependencies' );
- dependencies.sort();
- assert.deepEqual(
- dependencies,
- [ 'test.module7', 'test.module8', 'test.module9' ],
- 'Error callback called with all three modules as dependencies'
- );
- }
- );
- } );
-
- QUnit.test( 'mw.loader dependency handling', 5, function ( assert ) {
- var done = assert.async();
- mw.loader.register( [
- // [module, version, dependencies, group, source]
- [ 'testMissing', '1', [], null, 'testloader' ],
- [ 'testUsesMissing', '1', [ 'testMissing' ], null, 'testloader' ],
- [ 'testUsesNestedMissing', '1', [ 'testUsesMissing' ], null, 'testloader' ]
- ] );
-
- function verifyModuleStates() {
- assert.equal( mw.loader.getState( 'testMissing' ), 'missing', 'Module not known to server must have state "missing"' );
- assert.equal( mw.loader.getState( 'testUsesMissing' ), 'error', 'Module with missing dependency must have state "error"' );
- assert.equal( mw.loader.getState( 'testUsesNestedMissing' ), 'error', 'Module with indirect missing dependency must have state "error"' );
- }
-
- mw.loader.using( [ 'testUsesNestedMissing' ],
- function () {
- assert.ok( false, 'Error handler should be invoked.' );
- assert.ok( true ); // Dummy to reach QUnit expect()
-
- verifyModuleStates();
-
- done();
- },
- function ( e, badmodules ) {
- assert.ok( true, 'Error handler should be invoked.' );
- // As soon as server spits out state('testMissing', 'missing');
- // it will bubble up and trigger the error callback.
- // Therefor the badmodules array is not testUsesMissing or testUsesNestedMissing.
- assert.deepEqual( badmodules, [ 'testMissing' ], 'Bad modules as expected.' );
-
- verifyModuleStates();
-
- done();
- }
- );
- } );
-
- QUnit.test( 'mw.loader skin-function handling', 5, function ( assert ) {
- mw.loader.register( [
- // [module, version, dependencies, group, source, skip]
- [ 'testSkipped', '1', [], null, 'testloader', 'return true;' ],
- [ 'testNotSkipped', '1', [], null, 'testloader', 'return false;' ],
- [ 'testUsesSkippable', '1', [ 'testSkipped', 'testNotSkipped' ], null, 'testloader' ]
- ] );
-
- function verifyModuleStates() {
- assert.equal( mw.loader.getState( 'testSkipped' ), 'ready', 'Module is ready when skipped' );
- assert.equal( mw.loader.getState( 'testNotSkipped' ), 'ready', 'Module is ready when not skipped but loaded' );
- assert.equal( mw.loader.getState( 'testUsesSkippable' ), 'ready', 'Module is ready when skippable dependencies are ready' );
- }
-
- return mw.loader.using( [ 'testUsesSkippable' ],
- function () {
- assert.ok( true, 'Success handler should be invoked.' );
- assert.ok( true ); // Dummy to match error handler and reach QUnit expect()
-
- verifyModuleStates();
- },
- function ( e, badmodules ) {
- assert.ok( false, 'Error handler should not be invoked.' );
- assert.deepEqual( badmodules, [], 'Bad modules as expected.' );
-
- verifyModuleStates();
- }
- );
- } );
-
- QUnit.asyncTest( 'mw.loader( "//protocol-relative" ) (bug 30825)', 2, function ( assert ) {
- // This bug was actually already fixed in 1.18 and later when discovered in 1.17.
- // Test is for regressions!
-
- // Forge a URL to the test callback script
- var target = QUnit.fixurl(
- mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/qunitOkCall.js'
- );
-
- // Confirm that mw.loader.load() works with protocol-relative URLs
- target = target.replace( /https?:/, '' );
-
- assert.equal( target.slice( 0, 2 ), '//',
- 'URL must be relative to test relative URLs!'
- );
-
- // Async!
- // The target calls QUnit.start
- mw.loader.load( target );
- } );
-
- QUnit.asyncTest( 'mw.loader( "/absolute-path" )', 2, function ( assert ) {
- // Forge a URL to the test callback script
- var target = QUnit.fixurl(
- mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/qunitOkCall.js'
- );
-
- // Confirm that mw.loader.load() works with absolute-paths (relative to current hostname)
- assert.equal( target.slice( 0, 1 ), '/', 'URL is relative to document root' );
-
- // Async!
- // The target calls QUnit.start
- mw.loader.load( target );
- } );
-
- QUnit.test( 'mw.loader() executing race (T112232)', 2, function ( assert ) {
- var done = false;
-
- // The red herring schedules its CSS buffer first. In T112232, a bug in the
- // state machine would cause the job for testRaceLoadMe to run with an earlier job.
- mw.loader.implement(
- 'testRaceRedHerring',
- function () {},
- { css: [ '.mw-testRaceRedHerring {}' ] }
- );
- mw.loader.implement(
- 'testRaceLoadMe',
- function () {
- done = true;
- },
- { css: [ '.mw-testRaceLoadMe { float: left; }' ] }
- );
-
- mw.loader.load( [ 'testRaceRedHerring', 'testRaceLoadMe' ] );
- return mw.loader.using( 'testRaceLoadMe', function () {
- assert.strictEqual( done, true, 'script ran' );
- assert.strictEqual( mw.loader.getState( 'testRaceLoadMe' ), 'ready', 'state' );
- } );
- } );
-
QUnit.test( 'mw.hook', 13, function ( assert ) {
var hook, add, fire, chars, callback;
);
} );
- QUnit.test( 'mw.loader require()', 6, function ( assert ) {
- mw.loader.register( [
- [ 'test.require1', '0' ],
- [ 'test.require2', '0' ],
- [ 'test.require3', '0' ],
- [ 'test.require4', '0', [ 'test.require3' ] ]
- ] );
- mw.loader.implement( 'test.require1', function () {} );
- mw.loader.implement( 'test.require2', function ( $, jQuery, require, module ) {
- module.exports = 1;
- } );
- mw.loader.implement( 'test.require3', function ( $, jQuery, require, module ) {
- module.exports = function () {
- return 'hello world';
- };
- } );
- mw.loader.implement( 'test.require4', function ( $, jQuery, require, module ) {
- var other = require( 'test.require3' );
- module.exports = {
- pizza: function () {
- return other();
- }
- };
- } );
- return mw.loader.using( [ 'test.require1', 'test.require2', 'test.require3', 'test.require4' ] )
- .then( function ( require ) {
- var module1, module2, module3, module4;
-
- module1 = require( 'test.require1' );
- module2 = require( 'test.require2' );
- module3 = require( 'test.require3' );
- module4 = require( 'test.require4' );
-
- assert.strictEqual( typeof module1, 'object', 'export of module with no export' );
- assert.strictEqual( module2, 1, 'export a number' );
- assert.strictEqual( module3(), 'hello world', 'export a function' );
- assert.strictEqual( typeof module4.pizza, 'function', 'export an object' );
- assert.strictEqual( module4.pizza(), 'hello world', 'module can require other modules' );
-
- assert.throws( function () {
- require( '_badmodule' );
- }, /is not loaded/, 'Requesting non-existent modules throws error.' );
- } );
- } );
-
- QUnit.test( 'mw.loader require() in debug mode', 1, function ( assert ) {
- var path = mw.config.get( 'wgScriptPath' );
- mw.loader.register( [
- [ 'test.require.define', '0' ],
- [ 'test.require.callback', '0', [ 'test.require.define' ] ]
- ] );
- mw.loader.implement( 'test.require.callback', [ QUnit.fixurl( path + '/tests/qunit/data/requireCallMwLoaderTestCallback.js' ) ] );
- mw.loader.implement( 'test.require.define', [ QUnit.fixurl( path + '/tests/qunit/data/defineCallMwLoaderTestCallback.js' ) ] );
-
- return mw.loader.using( 'test.require.callback' ).then( function ( require ) {
- var exported = require( 'test.require.callback' );
- assert.strictEqual( exported, 'Require worked.Define worked.',
- 'module.exports worked in debug mode' );
- }, function () {
- assert.ok( false, 'Error callback fired while loader.using "test.require.callback" module' );
- } );
- } );
-
-}( mediaWiki, jQuery ) );
+}( mediaWiki ) );