* The following response properties from action=login, deprecated in 1.27, are
now removed: lgtoken, cookieprefix, sessionid. Clients should handle cookies
to properly manage session state.
+* Submitting the lgtoken and lgpassword parameters in the query string to
+ action=login is now deprecated and outputs a warning. They should be submitted
+ in the POST body instead.
+* Submitting sensitive authentication request parameters to action=clientlogin,
+ action=createaccount, action=linkaccount, and action=changeauthenticationdata
+ in the query string is now deprecated and outputs a warning. They should be
+ submitted in the POST body instead.
=== Action API internal changes in 1.28 ===
* Added a new hook, 'ApiMakeParserOptions', to allow extensions to better
'DoubleRedirectsPage' => __DIR__ . '/includes/specials/SpecialDoubleRedirects.php',
'DoubleReplacer' => __DIR__ . '/includes/libs/replacers/DoubleReplacer.php',
'DummyLinker' => __DIR__ . '/includes/DummyLinker.php',
+ 'DummySearchIndexFieldDefinition' => __DIR__ . '/includes/search/DummySearchIndexFieldDefinition.php',
'DummyTermColorer' => __DIR__ . '/maintenance/term/MWTerm.php',
'Dump7ZipOutput' => __DIR__ . '/includes/export/Dump7ZipOutput.php',
'DumpBZip2Output' => __DIR__ . '/includes/export/DumpBZip2Output.php',
'MediaWiki\\Logger\\NullSpi' => __DIR__ . '/includes/debug/logger/NullSpi.php',
'MediaWiki\\Logger\\Spi' => __DIR__ . '/includes/debug/logger/Spi.php',
'MediaWiki\\MediaWikiServices' => __DIR__ . '/includes/MediaWikiServices.php',
+ 'MediaWiki\\Search\\ParserOutputSearchDataExtractor' => __DIR__ . '/includes/search/ParserOutputSearchDataExtractor.php',
'MediaWiki\\Services\\CannotReplaceActiveServiceException' => __DIR__ . '/includes/Services/CannotReplaceActiveServiceException.php',
'MediaWiki\\Services\\ContainerDisabledException' => __DIR__ . '/includes/Services/ContainerDisabledException.php',
'MediaWiki\\Services\\DestructibleService' => __DIR__ . '/includes/Services/DestructibleService.php',
/** @var string */
private $rlUserModuleState;
+ /** @var array */
+ private $rlExemptStyleModules;
+
/** @var array */
protected $mJsConfigVars = [];
public function getRlClient() {
if ( !$this->rlClient ) {
$context = $this->getRlClientContext();
- $userModule = $this->getResourceLoader()->getModule( 'user' );
- // Manually handled by getBottomScripts()
- $userState = $userModule->isKnownEmpty( $context ) && !$this->isUserModulePreview()
- ? 'ready'
- : 'loading';
- $this->rlUserModuleState = $userState;
-
+ $rl = $this->getResourceLoader();
$this->addModules( [
'user.options',
'user.tokens',
] );
+ $this->addModuleStyles( [
+ 'site.styles',
+ 'noscript',
+ 'user.styles',
+ 'user.cssprefs',
+ ] );
+
+ // Prepare exempt modules for buildExemptModules()
+ $exemptGroups = [ 'site' => [], 'noscript' => [], 'private' => [], 'user' => [] ];
+ $exemptStates = [];
+ $moduleStyles = array_filter( $this->getModuleStyles( /*filter*/ true ),
+ function ( $name ) use ( $rl, $context, &$exemptGroups, &$exemptStates ) {
+ $module = $rl->getModule( $name );
+ if ( $module ) {
+ $group = $module->getGroup();
+ if ( $name === 'user.styles' && $this->isUserCssPreview() ) {
+ $exemptStates[$name] = 'ready';
+ // Special case in buildExemptModules()
+ return false;
+ }
+ if ( $name === 'site.styles' ) {
+ // HACK: Technically, 'site.styles' isn't in a separate request group.
+ // But, in order to ensure its styles are in the right position,
+ // pretend it's in a group called 'site'.
+ $group = 'site';
+ }
+ if ( isset( $exemptGroups[$group] ) ) {
+ $exemptStates[$name] = 'ready';
+ if ( !$module->isKnownEmpty( $context ) ) {
+ // E.g. Don't output empty <styles>
+ $exemptGroups[$group][] = $name;
+ }
+ return false;
+ }
+ }
+ return true;
+ }
+ );
+ $this->rlExemptStyleModules = $exemptGroups;
+
+ // Manually handled by getBottomScripts()
+ $userModule = $rl->getModule( 'user' );
+ $userState = $userModule->isKnownEmpty( $context ) && !$this->isUserJsPreview()
+ ? 'ready'
+ : 'loading';
+ $this->rlUserModuleState = $exemptStates['user'] = $userState;
+
$rlClient = new ResourceLoaderClientHtml( $context, $this->getTarget() );
$rlClient->setConfig( $this->getJSVars() );
$rlClient->setModules( $this->getModules( /*filter*/ true ) );
- $rlClient->setModuleStyles( $this->getModuleStyles( /*filter*/ true ) );
+ $rlClient->setModuleStyles( $moduleStyles );
$rlClient->setModuleScripts( $this->getModuleScripts( /*filter*/ true ) );
- $rlClient->setExemptStates( [
- 'user' => $userState,
- // Manually handled by buildExemptModules() and getBottomScripts()
- 'site.styles' => 'ready',
- 'noscript' => 'ready',
- 'user.cssprefs' => 'ready',
- 'user.styles' => 'ready',
- ] );
+ $rlClient->setExemptStates( $exemptStates );
$this->rlClient = $rlClient;
}
return $this->rlClient;
return WrappedString::join( "\n", $chunks );
}
- /** @return bool */
- private function isUserModulePreview() {
+ private function isUserJsPreview() {
return $this->getConfig()->get( 'AllowUserJs' )
&& $this->getUser()->isLoggedIn()
&& $this->getTitle()
&& $this->userCanPreview();
}
+ private function isUserCssPreview() {
+ return $this->getConfig()->get( 'AllowUserCss' )
+ && $this->getUser()->isLoggedIn()
+ && $this->getTitle()
+ && $this->getTitle()->isCssSubpage()
+ && $this->userCanPreview();
+ }
+
/**
* JS stuff to put at the bottom of the `<body>`. These are modules with position 'bottom',
* legacy scripts ($this->mScripts), and user JS.
// ensures execution is scheduled after the "site" module.
// - Don't load if module state is already resolved as "ready".
if ( $this->rlUserModuleState === 'loading' ) {
- if ( $this->isUserModulePreview() ) {
+ if ( $this->isUserJsPreview() ) {
$chunks[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED,
[ 'excludepage' => $this->getTitle()->getPrefixedDBkey() ]
);
$resourceLoader = $this->getResourceLoader();
$chunks = [];
- // Things that should be appended after the other link and style chunks
+ // Things that go after the ResourceLoaderDynamicStyles marker
$append = [];
- $moduleStyles = [
- 'site.styles',
- 'noscript'
- ];
- // Exempt 'user' styles module.
- // - May need excludepages for live preview.
- // - Position after ResourceLoaderDynamicStyles marker
- if ( $this->getConfig()->get( 'AllowUserCss' ) && $this->getTitle()->isCssSubpage()
- && $this->userCanPreview()
- ) {
+ // Exempt 'user' styles module (may need 'excludepages' for live preview)
+ if ( $this->isUserCssPreview() ) {
$append[] = $this->makeResourceLoaderLink(
'user.styles',
ResourceLoaderModule::TYPE_STYLES,
$previewedCSS = CSSJanus::transform( $previewedCSS, true, false );
}
$append[] = Html::inlineStyle( $previewedCSS );
- } else {
- $module = $this->getResourceLoader()->getModule( 'user.styles' );
- if ( !$module->isKnownEmpty( $this->getRlClientContext() ) ) {
- // Load styles normally
- $moduleStyles[] = 'user.styles';
- }
- }
-
- // Exempt 'user.cssprefs' module
- // - Position after ResourceLoaderDynamicStyles marker
- $moduleStyles[] = 'user.cssprefs';
-
- $groups = [
- 'other' => [],
- 'site' => [],
- 'noscript' => [],
- 'private' => [],
- 'user' => [],
- ];
- foreach ( $moduleStyles as $name ) {
- $module = $resourceLoader->getModule( $name );
- if ( !$module || $module->isKnownEmpty( $this->getRlClientContext() ) ) {
- // E.g. Don't output empty <styles> for user.cssprefs
- continue;
- }
- if ( $name === 'site.styles' ) {
- // HACK: Technically, the 'site.styles' module isn't in a separate request group.
- // But, in order to ensure its styles are in the right position after the marker,
- // pretend it's in a group called 'site'.
- $groups['site'][] = $name;
- continue;
- }
- $group = $module->getGroup();
- // Use "other" in case. All exempt modules are in one of the known groups though.
- $groups[isset( $groups[$group] ) ? $group : 'other'][] = $name;
}
- // We want site, private and user styles to override dynamically added
- // styles from modules, but we want dynamically added styles to override
- // statically added styles from other modules. So the order has to be
- // other, dynamic, site, private, user. Add statically added styles for
- // other modules
+ // We want site, private and user styles to override dynamically added styles from
+ // general modules, but we want dynamically added styles to override statically added
+ // style modules. So the order has to be:
+ // - page style modules (formatted by ResourceLoaderClientHtml::getHeadHtml())
+ // - dynamically loaded styles (added by mw.loader before ResourceLoaderDynamicStyles)
+ // - ResourceLoaderDynamicStyles marker
+ // - site/private/user styles
// Add legacy styles added through addStyle()/addInlineStyle() here
$chunks[] = implode( '', $this->buildCssLinksArray() ) . $this->mInlineStyles;
- // Client-side mw.loader will inject dynamic styles before this marker.
$chunks[] = Html::element(
'meta',
[ 'name' => 'ResourceLoaderDynamicStyles', 'content' => '' ]
);
- foreach ( [ 'other', 'site', 'noscript', 'private', 'user' ] as $group ) {
- $chunks[] = $this->makeResourceLoaderLink( $groups[$group],
+ foreach ( $this->rlExemptStyleModules as $group => $moduleNames ) {
+ $chunks[] = $this->makeResourceLoaderLink( $moduleNames,
ResourceLoaderModule::TYPE_STYLES
);
}
'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' );
}
/**
// Collect the fields for all the requests
$fields = [];
+ $sensitive = [];
foreach ( $reqs as $req ) {
- $fields += (array)$req->getFieldInfo();
+ $info = (array)$req->getFieldInfo();
+ $fields += $info;
+ $sensitive += array_filter( $info, function ( $opts ) {
+ return !empty( $opts['sensitive'] );
+ } );
}
// Extract the request data for the fields and mark those request
$data = array_intersect_key( $this->module->getRequest()->getValues(), $fields );
$this->module->getMain()->markParamsUsed( array_keys( $data ) );
+ if ( $sensitive ) {
+ try {
+ $this->module->requirePostedParameters( array_keys( $sensitive ), 'noprefix' );
+ } catch ( UsageException $ex ) {
+ // Make this a warning for now, upgrade to an error in 1.29.
+ $this->module->setWarning( $ex->getMessage() );
+ $this->module->logFeatureUsage( $this->module->getModuleName() . '-params-in-query-string' );
+ }
+ }
+
return AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
}
$this->formatMessage( $ret, 'label', $field['label'] );
$this->formatMessage( $ret, 'help', $field['help'] );
$ret['optional'] = !empty( $field['optional'] );
+ $ret['sensitive'] = !empty( $field['sensitive'] );
$retFields[$name] = $ret;
}
}
}
+ /**
+ * Die if any of the specified parameters were found in the query part of
+ * the URL rather than the post body.
+ * @since 1.28
+ * @param string[] $params Parameters to check
+ * @param string $prefix Set to 'noprefix' to skip calling $this->encodeParamName()
+ */
+ public function requirePostedParameters( $params, $prefix = 'prefix' ) {
+ // Skip if $wgDebugAPI is set or we're in internal mode
+ if ( $this->getConfig()->get( 'DebugAPI' ) || $this->getMain()->isInternalMode() ) {
+ return;
+ }
+
+ $queryValues = $this->getRequest()->getQueryValues();
+ $badParams = [];
+ foreach ( $params as $param ) {
+ if ( $prefix !== 'noprefix' ) {
+ $param = $this->encodeParamName( $param );
+ }
+ if ( array_key_exists( $param, $queryValues ) ) {
+ $badParams[] = $param;
+ }
+ }
+
+ if ( $badParams ) {
+ $this->dieUsage(
+ 'The following parameters were found in the query string, but must be in the POST body: '
+ . join( ', ', $badParams ),
+ 'mustpostparams'
+ );
+ }
+ }
+
/**
* Callback function used in requireOnlyOneParameter to check whether required parameters are set
*
* analysis.
* @param string $feature Feature being used.
*/
- protected function logFeatureUsage( $feature ) {
+ public function logFeatureUsage( $feature ) {
$request = $this->getRequest();
$s = '"' . addslashes( $feature ) . '"' .
' "' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) . '"' .
return;
}
+ try {
+ $this->requirePostedParameters( [ 'password', 'token' ] );
+ } catch ( UsageException $ex ) {
+ // Make this a warning for now, upgrade to an error in 1.29.
+ $this->setWarning( $ex->getMessage() );
+ $this->logFeatureUsage( 'login-params-in-query-string' );
+ }
+
$params = $this->extractRequestParams();
$result = [];
$this->dieUsageMsg( [ 'missingparam', 'token' ] );
}
- if ( !$this->getConfig()->get( 'DebugAPI' ) &&
- array_key_exists(
- $module->encodeParamName( 'token' ),
- $this->getRequest()->getQueryValues()
- )
- ) {
- $this->dieUsage(
- "The '{$module->encodeParamName( 'token' )}' parameter was " .
- 'found in the query string, but must be in the POST body',
- 'mustposttoken'
- );
- }
+ $module->requirePostedParameters( [ 'token' ] );
if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) {
$this->dieUsageMsg( 'sessionfailure' );
'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__ );
$this->dieStatusWithCode( $status, 'stashfailed' );
}
+ // We can only get warnings like 'duplicate' after concatenating the chunks
+ $warnings = $this->getApiWarnings();
+ if ( $warnings ) {
+ $result['warnings'] = $warnings;
+ }
+
// The fully concatenated file has a new filekey. So remove
// the old filekey and fetch the new one.
UploadBase::setSessionStatus( $this->getUser(), $filekey, false );
$this->mUpload->stash->removeFile( $filekey );
- $filekey = $this->mUpload->getLocalFile()->getFileKey();
+ $filekey = $this->mUpload->getStashFile()->getFileKey();
$result['result'] = 'Success';
}
if ( isset( $progress['status']->value['verification'] ) ) {
$this->checkVerification( $progress['status']->value['verification'] );
}
+ if ( isset( $progress['status']->value['warnings'] ) ) {
+ $warnings = $this->transformWarnings( $progress['status']->value['warnings'] );
+ if ( $warnings ) {
+ $progress['warnings'] = $warnings;
+ }
+ }
unset( $progress['status'] ); // remove Status object
$this->getResult()->addValue( null, $this->getModuleName(), $progress );
$this->mParams['text'] = $this->mParams['comment'];
}
- /** @var $file File */
+ /** @var $file LocalFile */
$file = $this->mUpload->getLocalFile();
// For preferences mode, we want to watch if 'watchdefault' is set,
"Lbayle",
"Verdy p",
"Yasten",
- "Trial"
+ "Trial",
+ "Pols12"
]
},
"apihelp-main-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:API:Main_page|Documentation]]\n* [[mw:API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Liste de diffusion]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Annonces de l’API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bogues et demandes]\n</div>\n<strong>État :</strong> Toutes les fonctionnalités affichées sur cette page devraient fonctionner, mais l’API est encore en cours de développement et peut changer à tout moment. Inscrivez-vous à [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la liste de diffusion mediawiki-api-announce] pour être informé des mises à jour.\n\n<strong>Requêtes erronées :</strong> Si des requêtes erronées sont envoyées à l’API, un en-tête HTTP sera renvoyé avec la clé « MediaWiki-API-Error ». La valeur de cet en-tête et le code d’erreur renvoyé prendront la même valeur. Pour plus d’information, voyez [[mw:API:Errors_and_warnings|API: Errors and warnings]].\n\n<strong>Test :</strong> Pour faciliter le test des requêtes de l’API, voyez [[Special:ApiSandbox]].",
"apihelp-stashedit-param-section": "Numéro de section. <kbd>0</kbd> pour la section du haut, <kbd>new</kbd> pour une nouvelle section.",
"apihelp-stashedit-param-sectiontitle": "Le titre pour une nouvelle section.",
"apihelp-stashedit-param-text": "Contenu de la page.",
- "apihelp-stashedit-param-stashedtexthash": "A substituer par le hachage du contenu de la page venant d’une réserve antérieure.",
+ "apihelp-stashedit-param-stashedtexthash": "Empreinte du contenu de la page venant d’une réserve préalable à utiliser à la place.",
"apihelp-stashedit-param-contentmodel": "Modèle de contenu du nouveau contenu.",
"apihelp-stashedit-param-contentformat": "Format de sérialisation de contenu utilisé pour le texte saisi.",
"apihelp-stashedit-param-baserevid": "ID de révision de la révision de base.",
"apihelp-query+search-paramvalue-prop-snippet": "Adds a parsed snippet of the page.",
"apihelp-query+search-paramvalue-prop-titlesnippet": "Adds a parsed snippet of the page title.",
"apihelp-query+search-paramvalue-prop-redirectsnippet": "添加被解析的重定向标题的片段。",
- "apihelp-query+search-paramvalue-prop-redirecttitle": "Adds the title of the matching redirect.",
+ "apihelp-query+search-paramvalue-prop-redirecttitle": "添加匹配的重定向的标题。",
"apihelp-query+search-paramvalue-prop-sectionsnippet": "Adds a parsed snippet of the matching section title.",
"apihelp-query+search-paramvalue-prop-sectiontitle": "Adds the title of the matching section.",
"apihelp-query+search-paramvalue-prop-categorysnippet": "Adds a parsed snippet of the matching category.",
"apihelp-query+search-param-enablerewrites": "启用内部查询重写。一些搜索后端可以重写查询到它认为会给出更好结果的地方,例如纠正拼写错误。",
"apihelp-query+search-example-simple": "搜索<kbd>meaning</kbd>。",
"apihelp-query+search-example-text": "搜索文本<kbd>meaning</kbd>。",
- "apihelp-query+search-example-generator": "è\8e·å¾\97有关搜索<kbd>meaning</kbd>返回页面的页面信息。",
+ "apihelp-query+search-example-generator": "è\8e·å\8f\96有关搜索<kbd>meaning</kbd>返回页面的页面信息。",
"apihelp-query+siteinfo-description": "返回有关网站的一般信息。",
"apihelp-query+siteinfo-param-prop": "要获取的信息:",
"apihelp-query+siteinfo-paramvalue-prop-general": "全部系统信息。",
$logEntry->setParameters( [
'4::userid' => $user->getId(),
] );
- $logid = $logEntry->insert();
+ $logEntry->insert();
}
- // Commit database changes, so even if something else later blows up
- // the newly-created user doesn't get lost.
- wfGetLBFactory()->commitMasterChanges( __METHOD__ );
-
$trxProfiler->setSilenced( false );
if ( $login ) {
* - label: (Message) Text suitable for a label in an HTML form
* - help: (Message) Text suitable as a description of what the field is
* - optional: (bool) If set and truthy, the field may be left empty
+ * - sensitive: (bool) If set and truthy, the field is considered sensitive. Code using the
+ * request should avoid exposing the value of the field.
*
* @return array As above
*/
$options['optional'] = !empty( $options['optional'] );
}
+ $options['sensitive'] = !empty( $options['sensitive'] );
+
if ( !array_key_exists( $name, $merged ) ) {
$merged[$name] = $options;
} elseif ( $merged[$name]['type'] !== $options['type'] ) {
}
$merged[$name]['optional'] = $merged[$name]['optional'] && $options['optional'];
+ $merged[$name]['sensitive'] = $merged[$name]['sensitive'] || $options['sensitive'];
// No way to merge 'value', 'image', 'help', or 'label', so just use
// the value from the first request.
&& $user->getEmail()
&& !$this->manager->getAuthenticationSessionData( 'no-email' )
) {
- $status = $user->sendConfirmationMail();
- $user->saveSettings();
- if ( $status->isGood() ) {
- // TODO show 'confirmemail_oncreate' success message
- } else {
- // TODO show 'confirmemail_sendfailed' error message
- $this->logger->warning( 'Could not send confirmation email: ' .
- $status->getWikiText( false, false, 'en' ) );
- }
+ // TODO show 'confirmemail_oncreate'/'confirmemail_sendfailed' message
+ wfGetDB( DB_MASTER )->onTransactionIdle( function () use ( $user ) {
+ $user = $user->getInstanceForUpdate();
+ $status = $user->sendConfirmationMail();
+ $user->saveSettings();
+ if ( !$status->isGood() ) {
+ $this->logger->warning( 'Could not send confirmation email: ' .
+ $status->getWikiText( false, false, 'en' ) );
+ }
+ } );
}
return AuthenticationResponse::newPass();
'type' => 'password',
'label' => wfMessage( $passwordLabel ),
'help' => wfMessage( 'authmanager-password-help' ),
+ 'sensitive' => true,
],
];
'type' => 'password',
'label' => wfMessage( $retypeLabel ),
'help' => wfMessage( 'authmanager-retype-help' ),
+ 'sensitive' => true,
];
}
);
if ( $sendMail ) {
- $this->sendPasswordResetEmail( $req );
+ // Send email after DB commit
+ $dbw->onTransactionIdle( function () use ( $req ) {
+ /** @var TemporaryPasswordAuthenticationRequest $req */
+ $this->sendPasswordResetEmail( $req );
+ } );
}
}
$this->providerChangeAuthenticationData( $req );
if ( $mailpassword ) {
- $this->sendNewAccountEmail( $user, $creator, $req->password );
+ // Send email after DB commit
+ wfGetDB( DB_MASTER )->onTransactionIdle( function () use ( $user, $creator, $req ) {
+ $this->sendNewAccountEmail( $user, $creator, $req->password );
+ } );
}
return $mailpassword ? 'byemail' : null;
<?php
+
+use MediaWiki\Search\ParserOutputSearchDataExtractor;
+
/**
* Base class for content handling.
*
*
* @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 ) {
/**
* Get fields definition for search index
+ *
+ * @todo Expose title, redirect, namespace, text, source_text, text_bytes
+ * field mappings here. (see T142670 and T143409)
+ *
* @param SearchEngine $engine
* @return SearchIndexField[] List of fields this content handler can provide.
* @since 1.28
*/
public function getFieldsForSearchIndex( SearchEngine $engine ) {
- /* Default fields:
- /*
- * namespace
- * namespace_text
- * redirect
- * source_text
- * suggest
- * timestamp
- * title
- * text
- * text_bytes
- */
- return [];
+ $fields['category'] = $engine->makeSearchFieldMapping(
+ 'category',
+ SearchIndexField::INDEX_TYPE_TEXT
+ );
+
+ $fields['category']->setFlag( SearchIndexField::FLAG_CASEFOLD );
+
+ $fields['external_link'] = $engine->makeSearchFieldMapping(
+ 'external_link',
+ SearchIndexField::INDEX_TYPE_KEYWORD
+ );
+
+ $fields['outgoing_link'] = $engine->makeSearchFieldMapping(
+ 'outgoing_link',
+ SearchIndexField::INDEX_TYPE_KEYWORD
+ );
+
+ $fields['template'] = $engine->makeSearchFieldMapping(
+ 'template',
+ SearchIndexField::INDEX_TYPE_KEYWORD
+ );
+
+ $fields['template']->setFlag( SearchIndexField::FLAG_CASEFOLD );
+
+ return $fields;
}
/**
*/
public function getDataForSearchIndex( WikiPage $page, ParserOutput $output,
SearchEngine $engine ) {
- $fields = [];
+ $fieldData = [];
$content = $page->getContent();
+
if ( $content ) {
+ $searchDataExtractor = new ParserOutputSearchDataExtractor();
+
+ $fieldData['category'] = $searchDataExtractor->getCategories( $output );
+ $fieldData['external_link'] = $searchDataExtractor->getExternalLinks( $output );
+ $fieldData['outgoing_link'] = $searchDataExtractor->getOutgoingLinks( $output );
+ $fieldData['template'] = $searchDataExtractor->getTemplates( $output );
+
$text = $content->getTextForSearchIndex();
- $fields['text'] = $text;
- $fields['source_text'] = $text;
- $fields['text_bytes'] = $content->getSize();
+
+ $fieldData['text'] = $text;
+ $fieldData['source_text'] = $text;
+ $fieldData['text_bytes'] = $content->getSize();
}
- Hooks::run( 'SearchDataForIndex', [ &$fields, $this, $page, $output, $engine ] );
- return $fields;
+
+ Hooks::run( 'SearchDataForIndex', [ &$fieldData, $this, $page, $output, $engine ] );
+ return $fieldData;
}
/**
$this->parserOutput = $parserOutput;
}
- /**
- * Get categories in the text.
- * @return string[]
- */
- public function categories() {
- $categories = [];
- foreach ( array_keys( $this->parserOutput->getCategories() ) as $key ) {
- $categories[] = Category::newFromName( $key )->getTitle()->getText();
- }
- return $categories;
- }
-
- /**
- * Get outgoing links.
- * @return string[]
- */
- public function outgoingLinks() {
- $outgoingLinks = [];
- foreach ( $this->parserOutput->getLinks() as $linkedNamespace => $namespaceLinks ) {
- foreach ( array_keys( $namespaceLinks ) as $linkedDbKey ) {
- $outgoingLinks[] =
- Title::makeTitle( $linkedNamespace, $linkedDbKey )->getPrefixedDBkey();
- }
- }
- return $outgoingLinks;
- }
-
- /**
- * Get templates in the text.
- * @return string[]
- */
- public function templates() {
- $templates = [];
- foreach ( $this->parserOutput->getTemplates() as $tNS => $templatesInNS ) {
- foreach ( array_keys( $templatesInNS ) as $tDbKey ) {
- $templateTitle = Title::makeTitleSafe( $tNS, $tDbKey );
- if ( $templateTitle && $templateTitle->exists() ) {
- $templates[] = $templateTitle->getPrefixedText();
- }
- }
- }
- return $templates;
- }
-
/**
* Get headings on the page.
* @return string[]
public function getFieldsForSearchIndex( SearchEngine $engine ) {
$fields = parent::getFieldsForSearchIndex( $engine );
- $fields['category'] =
- $engine->makeSearchFieldMapping( 'category', SearchIndexField::INDEX_TYPE_TEXT );
- $fields['category']->setFlag( SearchIndexField::FLAG_CASEFOLD );
-
- $fields['external_link'] =
- $engine->makeSearchFieldMapping( 'external_link', SearchIndexField::INDEX_TYPE_KEYWORD );
-
$fields['heading'] =
$engine->makeSearchFieldMapping( 'heading', SearchIndexField::INDEX_TYPE_TEXT );
$fields['heading']->setFlag( SearchIndexField::FLAG_SCORING );
$fields['opening_text']->setFlag( SearchIndexField::FLAG_SCORING |
SearchIndexField::FLAG_NO_HIGHLIGHT );
- $fields['outgoing_link'] =
- $engine->makeSearchFieldMapping( 'outgoing_link', SearchIndexField::INDEX_TYPE_KEYWORD );
-
- $fields['template'] =
- $engine->makeSearchFieldMapping( 'template', SearchIndexField::INDEX_TYPE_KEYWORD );
- $fields['template']->setFlag( SearchIndexField::FLAG_CASEFOLD );
-
// FIXME: this really belongs in separate file handler but files
// do not have separate handler. Sadness.
$fields['file_text'] =
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;
$fields = parent::getDataForSearchIndex( $page, $parserOutput, $engine );
$structure = new WikiTextStructure( $parserOutput );
- $fields['external_link'] = array_keys( $parserOutput->getExternalLinks() );
- $fields['category'] = $structure->categories();
$fields['heading'] = $structure->headings();
- $fields['outgoing_link'] = $structure->outgoingLinks();
- $fields['template'] = $structure->templates();
// text fields
$fields['opening_text'] = $structure->getOpeningText();
$fields['text'] = $structure->getMainText(); // overwrites one from ContentHandler
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.
*
* @param array $values
* @return string
*/
- protected function getInputHTMLForKey( $key, $values ) {
+ protected function getInputHTMLForKey( $key, array $values ) {
$displayFormat = isset( $this->mParams['format'] )
? $this->mParams['format']
: $this->mParent->getDisplayFormat();
return false;
}
+ // We can only get warnings like 'duplicate' after concatenating the chunks
+ $status = Status::newGood();
+ $status->value = [ 'warnings' => $upload->checkWarnings() ];
+
// We have a new filekey for the fully concatenated file
- $newFileKey = $upload->getLocalFile()->getFileKey();
+ $newFileKey = $upload->getStashFile()->getFileKey();
// Remove the old stash file row and first chunk file
$upload->stash->removeFileNoAuth( $this->params['filekey'] );
'stage' => 'assembling',
'filekey' => $newFileKey,
'imageinfo' => $imageInfo,
- 'status' => Status::newGood()
+ 'status' => $status
]
);
} catch ( Exception $e ) {
return false;
}
+ public function add( $key, $value, $exp = 0 ) {
+ return true;
+ }
+
public function set( $key, $value, $exp = 0, $flags = 0 ) {
return true;
}
* This class is intended for caching data from primary stores.
* If the get() method does not return a value, then the caller
* should query the new value and backfill the cache using set().
+ * The preferred way to do this logic is through getWithSetCallback().
* When querying the store on cache miss, the closest DB replica
* should be used. Try to avoid heavyweight DB master or quorum reads.
* When the source data changes, a purge method should be called.
*
* The simplest purge method is delete().
*
- * Instances of this class must be configured to point to a valid
- * PubSub endpoint, and there must be listeners on the cache servers
- * that subscribe to the endpoint and update the caches.
+ * There are two supported ways to handle broadcasted operations:
+ * - a) Configure the 'purge' EventRelayer to point to a valid PubSub endpoint
+ * that has subscribed listeners on the cache servers applying the cache updates.
+ * - b) Ignore the 'purge' EventRelayer configuration (default is NullEventRelayer)
+ * and set up mcrouter as the underlying cache backend, using one of the memcached
+ * BagOStuff classes as 'cache'. Use OperationSelectorRoute in the mcrouter settings
+ * to configure 'set' and 'delete' operations to go to all DCs via AllAsyncRoute and
+ * configure other operations to go to the local DC via PoolRoute (for reference,
+ * see https://github.com/facebook/mcrouter/wiki/List-of-Route-Handles).
*
- * Broadcasted operations like delete() and touchCheckKey() are done
- * synchronously in the local datacenter, but are relayed asynchronously.
- * This means that callers in other datacenters will see older values
- * for however many milliseconds the datacenters are apart. As with
- * any cache, this should not be relied on for cases where reads are
- * used to determine writes to source (e.g. non-cache) data stores.
+ * Broadcasted operations like delete() and touchCheckKey() are done asynchronously
+ * in all datacenters this way, though the local one should likely be near immediate.
+ *
+ * This means that callers in all datacenters may see older values for however many
+ * milliseconds that the purge took to reach that datacenter. As with any cache, this
+ * should not be relied on for cases where reads are used to determine writes to source
+ * (e.g. non-cache) data stores, except when reading immutable data.
*
* All values are wrapped in metadata arrays. Keys use a "WANCache:" prefix
* to avoid collisions with keys that are not wrapped as metadata arrays. The
* - a) "WANCache:v" : used for regular value keys
* - b) "WANCache:i" : used for temporarily storing values of tombstoned keys
* - c) "WANCache:t" : used for storing timestamp "check" keys
+ * - d) "WANCache:m" : used for temporary mutex keys to avoid cache stampedes
*
* @ingroup Cache
* @since 1.26
const VALUE_KEY_PREFIX = 'WANCache:v:';
const INTERIM_KEY_PREFIX = 'WANCache:i:';
const TIME_KEY_PREFIX = 'WANCache:t:';
+ const MUTEX_KEY_PREFIX = 'WANCache:m:';
const PURGE_VAL_PREFIX = 'PURGED:';
*
* When using potentially long-running ACID transactions, a good pattern is
* to use a pre-commit hook to issue the delete. This means that immediately
- * after commit, callers will see the tombstone in cache in the local datacenter
- * and in the others upon relay. It also avoids the following race condition:
+ * after commit, callers will see the tombstone in cache upon purge relay.
+ * It also avoids the following race condition:
* - a) T1 begins, changes a row, and calls delete()
* - b) The HOLDOFF_TTL passes, expiring the delete() tombstone
* - c) T2 starts, reads the row and calls set() due to a cache miss
$key = self::VALUE_KEY_PREFIX . $key;
if ( $ttl <= 0 ) {
- // Update the local datacenter immediately
- $ok = $this->cache->delete( $key );
// Publish the purge to all datacenters
- $ok = $this->relayDelete( $key ) && $ok;
+ $ok = $this->relayDelete( $key );
} else {
- // Update the local datacenter immediately
- $ok = $this->cache->set( $key,
- $this->makePurgeValue( microtime( true ), self::HOLDOFF_NONE ),
- $ttl
- );
// Publish the purge to all datacenters
- $ok = $this->relayPurge( $key, $ttl, self::HOLDOFF_NONE ) && $ok;
+ $ok = $this->relayPurge( $key, $ttl, self::HOLDOFF_NONE );
}
return $ok;
* keys, the relevant "check" keys must be supplied for this to work.
*
* The "check" key essentially represents a last-modified field.
- * When touched, keys using it via get(), getMulti(), or getWithSetCallback()
- * will be invalidated. It is treated as being HOLDOFF_TTL seconds in the future
+ * When touched, the field will be updated on all cache servers.
+ * Keys using it via get(), getMulti(), or getWithSetCallback() will
+ * be invalidated. It is treated as being HOLDOFF_TTL seconds in the future
* by those methods to avoid race conditions where dependent keys get updated
* with stale values (e.g. from a DB slave).
*
* When a few important keys get a large number of hits, a high cache
* time is usually desired as well as "lockTSE" logic. The resetCheckKey()
* method is less appropriate in such cases since the "time since expiry"
- * cannot be inferred.
+ * cannot be inferred, causing any get() after the reset to treat the key
+ * as being "hot", resulting in more stale value usage.
*
* Note that "check" keys won't collide with other regular keys.
*
* @return bool True if the item was purged or not found, false on failure
*/
final public function touchCheckKey( $key, $holdoff = self::HOLDOFF_TTL ) {
- $key = self::TIME_KEY_PREFIX . $key;
- // Update the local datacenter immediately
- $ok = $this->cache->set( $key,
- $this->makePurgeValue( microtime( true ), $holdoff ),
- self::CHECK_KEY_TTL
- );
// Publish the purge to all datacenters
- return $this->relayPurge( $key, self::CHECK_KEY_TTL, $holdoff ) && $ok;
+ return $this->relayPurge( self::TIME_KEY_PREFIX . $key, self::CHECK_KEY_TTL, $holdoff );
}
/**
*
* This is similar to touchCheckKey() in that keys using it via get(), getMulti(),
* or getWithSetCallback() will be invalidated. The differences are:
- * - a) The timestamp will be deleted from all caches and lazily
+ * - a) The "check" key will be deleted from all caches and lazily
* re-initialized when accessed (rather than set everywhere)
* - b) Thus, dependent keys will be known to be invalid, but not
* for how long (they are treated as "just" purged), which
* effects any lockTSE logic in getWithSetCallback()
+ * - c) Since "check" keys are initialized only on the server the key hashes
+ * to, any temporary ejection of that server will cause the value to be
+ * seen as purged as a new server will initialize the "check" key.
*
* The advantage is that this does not place high TTL keys on every cache
* server, making it better for code that will cache many different keys
* @return bool True if the item was purged or not found, false on failure
*/
final public function resetCheckKey( $key ) {
- $key = self::TIME_KEY_PREFIX . $key;
- // Update the local datacenter immediately
- $ok = $this->cache->delete( $key );
// Publish the purge to all datacenters
- return $this->relayDelete( $key ) && $ok;
+ return $this->relayDelete( self::TIME_KEY_PREFIX . $key );
}
/**
$lockAcquired = false;
if ( $useMutex ) {
// Acquire a datacenter-local non-blocking lock
- if ( $this->cache->lock( $key, 0, self::LOCK_TTL ) ) {
+ if ( $this->cache->add( self::MUTEX_KEY_PREFIX . $key, 1, self::LOCK_TTL ) ) {
// Lock acquired; this thread should update the key
$lockAcquired = true;
} elseif ( $value !== false && $this->isValid( $value, $versioned, $asOf, $minTime ) ) {
if ( ( $isTombstone && $lockTSE > 0 ) && $value !== false && $ttl >= 0 ) {
$tempTTL = max( 1, (int)$lockTSE ); // set() expects seconds
$wrapped = $this->wrap( $value, $tempTTL, $asOf );
- $this->cache->set( self::INTERIM_KEY_PREFIX . $key, $wrapped, $tempTTL );
- }
-
- if ( $lockAcquired ) {
- $this->cache->unlock( $key );
+ // Avoid using set() to avoid pointless mcrouter broadcasting
+ $this->cache->merge(
+ self::INTERIM_KEY_PREFIX . $key,
+ function () use ( $wrapped ) {
+ return $wrapped;
+ },
+ $tempTTL,
+ 1
+ );
}
if ( $value !== false && $ttl >= 0 ) {
$this->set( $key, $value, $ttl, $setOpts );
}
+ if ( $lockAcquired ) {
+ // Avoid using delete() to avoid pointless mcrouter broadcasting
+ $this->cache->changeTTL( self::MUTEX_KEY_PREFIX . $key, 1 );
+ }
+
return $value;
}
* @return bool Success
*/
protected function relayPurge( $key, $ttl, $holdoff ) {
- $event = $this->cache->modifySimpleRelayEvent( [
- 'cmd' => 'set',
- 'key' => $key,
- 'val' => 'PURGED:$UNIXTIME$:' . (int)$holdoff,
- 'ttl' => max( $ttl, 1 ),
- 'sbt' => true, // substitute $UNIXTIME$ with actual microtime
- ] );
-
- $ok = $this->purgeRelayer->notify( $this->purgeChannel, $event );
- if ( !$ok ) {
- $this->lastRelayError = self::ERR_RELAY;
+ if ( $this->purgeRelayer instanceof EventRelayerNull ) {
+ // This handles the mcrouter and the single-DC case
+ $ok = $this->cache->set( $key,
+ $this->makePurgeValue( microtime( true ), self::HOLDOFF_NONE ),
+ $ttl
+ );
+ } else {
+ $event = $this->cache->modifySimpleRelayEvent( [
+ 'cmd' => 'set',
+ 'key' => $key,
+ 'val' => 'PURGED:$UNIXTIME$:' . (int)$holdoff,
+ 'ttl' => max( $ttl, 1 ),
+ 'sbt' => true, // substitute $UNIXTIME$ with actual microtime
+ ] );
+
+ $ok = $this->purgeRelayer->notify( $this->purgeChannel, $event );
+ if ( !$ok ) {
+ $this->lastRelayError = self::ERR_RELAY;
+ }
}
return $ok;
* @return bool Success
*/
protected function relayDelete( $key ) {
- $event = $this->cache->modifySimpleRelayEvent( [
- 'cmd' => 'delete',
- 'key' => $key,
- ] );
-
- $ok = $this->purgeRelayer->notify( $this->purgeChannel, $event );
- if ( !$ok ) {
- $this->lastRelayError = self::ERR_RELAY;
+ if ( $this->purgeRelayer instanceof EventRelayerNull ) {
+ // This handles the mcrouter and the single-DC case
+ $ok = $this->cache->delete( $key );
+ } else {
+ $event = $this->cache->modifySimpleRelayEvent( [
+ 'cmd' => 'delete',
+ 'key' => $key,
+ ] );
+
+ $ok = $this->purgeRelayer->notify( $this->purgeChannel, $event );
+ if ( !$ok ) {
+ $this->lastRelayError = self::ERR_RELAY;
+ }
}
return $ok;
public function getDefinitionSummary( ResourceLoaderContext $context ) {
$this->loadFromDefinition();
$summary = parent::getDefinitionSummary( $context );
+
+ $options = [];
foreach ( [
'localBasePath',
'images',
'selectorWithoutVariant',
'selectorWithVariant',
] as $member ) {
- $summary[$member] = $this->{$member};
+ $options[$member] = $this->{$member};
};
+
+ $summary[] = [
+ 'options' => $options,
+ 'fileHashes' => $this->getFileHashes( $context ),
+ ];
return $summary;
}
/**
- * Get the last modified timestamp of this module.
- *
- * @param ResourceLoaderContext $context Context in which to calculate
- * the modified time
- * @return int UNIX timestamp
+ * Helper method for getDefinitionSummary.
*/
- public function getModifiedTime( ResourceLoaderContext $context ) {
+ protected function getFileHashes( ResourceLoaderContext $context ) {
$this->loadFromDefinition();
$files = [];
foreach ( $this->getImages( $context ) as $name => $image ) {
$files[] = $image->getPath( $context );
}
-
$files = array_values( array_unique( $files ) );
- $filesMtime = max( array_map( [ __CLASS__, 'safeFilemtime' ], $files ) );
-
- return $filesMtime;
+ return array_map( [ __CLASS__, 'safeFileHash' ], $files );
}
/**
$this->loadFromDefinition();
return $this->position;
}
+
+ /**
+ * @return string
+ */
+ public function getType() {
+ return self::LOAD_STYLES;
+ }
}
--- /dev/null
+<?php
+
+/**
+ * Dummy implementation of SearchIndexFieldDefinition for testing purposes.
+ *
+ * @since 1.28
+ */
+class DummySearchIndexFieldDefinition extends SearchIndexFieldDefinition {
+
+ /**
+ * @param SearchEngine $engine
+ *
+ * @return array
+ */
+ public function getMapping( SearchEngine $engine ) {
+ $mapping = [
+ 'name' => $this->name,
+ 'type' => $this->type,
+ 'flags' => $this->flags,
+ 'subfields' => []
+ ];
+
+ foreach ( $this->subfields as $subfield ) {
+ $mapping['subfields'][] = $subfield->getMapping();
+ }
+
+ return $mapping;
+ }
+
+}
--- /dev/null
+<?php
+
+namespace MediaWiki\Search;
+
+use Category;
+use ParserOutput;
+use Title;
+
+/**
+ * Extracts data from ParserOutput for indexing in the search engine.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @since 1.28
+ */
+class ParserOutputSearchDataExtractor {
+
+ /**
+ * Get a list of categories, as an array with title text strings.
+ *
+ * @return string[]
+ */
+ public function getCategories( ParserOutput $parserOutput ) {
+ $categories = [];
+
+ foreach ( $parserOutput->getCategoryLinks() as $key ) {
+ $categories[] = Category::newFromName( $key )->getTitle()->getText();
+ }
+
+ return $categories;
+ }
+
+ /**
+ * Get a list of external links from ParserOutput, as an array of strings.
+ *
+ * @return string[]
+ */
+ public function getExternalLinks( ParserOutput $parserOutput ) {
+ return array_keys( $parserOutput->getExternalLinks() );
+ }
+
+ /**
+ * Get a list of outgoing wiki links (including interwiki links), as
+ * an array of prefixed title strings.
+ *
+ * @return string[]
+ */
+ public function getOutgoingLinks( ParserOutput $parserOutput ) {
+ $outgoingLinks = [];
+
+ foreach ( $parserOutput->getLinks() as $linkedNamespace => $namespaceLinks ) {
+ foreach ( array_keys( $namespaceLinks ) as $linkedDbKey ) {
+ $outgoingLinks[] =
+ Title::makeTitle( $linkedNamespace, $linkedDbKey )->getPrefixedDBkey();
+ }
+ }
+
+ return $outgoingLinks;
+ }
+
+ /**
+ * Get a list of templates used in the ParserOutput content, as prefixed title strings
+ *
+ * @return string[]
+ */
+ public function getTemplates( ParserOutput $parserOutput ) {
+ $templates = [];
+
+ foreach ( $parserOutput->getTemplates() as $tNS => $templatesInNS ) {
+ foreach ( array_keys( $templatesInNS ) as $tDbKey ) {
+ $templateTitle = Title::makeTitle( $tNS, $tDbKey );
+ $templates[] = $templateTitle->getPrefixedText();
+ }
+ }
+
+ return $templates;
+ }
+
+}
/**
* Basic infrastructure of the field definition.
- * Specific engines will need to override it at least for getMapping,
- * but can reuse other parts.
+ *
+ * Specific engines should extend this class and at at least,
+ * override the getMapping method, but can reuse other parts.
+ *
* @since 1.28
*/
abstract class SearchIndexFieldDefinition implements SearchIndexField {
$this->subfields = $subfields;
return $this;
}
+
+ /**
+ * @param SearchEngine $engine
+ *
+ * @return array
+ */
+ abstract public function getMapping( SearchEngine $engine );
+
}
* @param string $par
*/
public function execute( $par ) {
+ $request = $this->getRequest();
+
+ // Fetch the search term
+ $search = str_replace( "\n", " ", $request->getText( 'search' ) );
+
+ // Historically search terms have been accepted not only in the search query
+ // parameter, but also as part of the primary url. This can have PII implications
+ // in releasing page view data. As such issue a 301 redirect to the correct
+ // URL.
+ if ( strlen( $par ) && !strlen( $search ) ) {
+ $query = $request->getValues();
+ unset( $query['title'] );
+ // Strip underscores from title parameter; most of the time we'll want
+ // text form here. But don't strip underscores from actual text params!
+ $query['search'] = str_replace( '_', ' ', $par );
+ $this->getOutput()->redirect( $this->getPageTitle()->getFullURL( $query ), 301 );
+ return;
+ }
+
$this->setHeaders();
$this->outputHeader();
$out = $this->getOutput();
] );
$this->addHelpLink( 'Help:Searching' );
- // Strip underscores from title parameter; most of the time we'll want
- // text form here. But don't strip underscores from actual text params!
- $titleParam = str_replace( '_', ' ', $par );
-
- $request = $this->getRequest();
-
- // Fetch the search term
- $search = str_replace( "\n", " ", $request->getText( 'search', $titleParam ) );
-
$this->load();
if ( !is_null( $request->getVal( 'nsRemember' ) ) ) {
$this->saveNamespaces();
protected $mDesiredDestName, $mDestName, $mRemoveTempFile, $mSourceType;
protected $mTitle = false, $mTitleError = 0;
protected $mFilteredName, $mFinalExtension;
- protected $mLocalFile, $mFileSize, $mFileProps;
+ protected $mLocalFile, $mStashFile, $mFileSize, $mFileProps;
protected $mBlackListedExtensions;
protected $mJavaDetected, $mSVGNSError;
/**
* Return the local file and initializes if necessary.
*
- * @return LocalFile|UploadStashFile|null
+ * @return LocalFile|null
*/
public function getLocalFile() {
if ( is_null( $this->mLocalFile ) ) {
return $this->mLocalFile;
}
+ /**
+ * @return UploadStashFile|null
+ */
+ public function getStashFile() {
+ return $this->mStashFile;
+ }
+
/**
* Like stashFile(), but respects extensions' wishes to prevent the stashing. verifyUpload() must
* be called before calling this method (unless $isPartial is true).
protected function doStashFile( User $user = null ) {
$stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $user );
$file = $stash->stashFile( $this->mTempPath, $this->getSourceType() );
- $this->mLocalFile = $file;
+ $this->mStashFile = $file;
return $file;
}
* @return array Image info
*/
public function getImageInfo( $result ) {
- $file = $this->getLocalFile();
- /** @todo This cries out for refactoring.
- * We really want to say $file->getAllInfo(); here.
- * Perhaps "info" methods should be moved into files, and the API should
- * just wrap them in queries.
- */
- if ( $file instanceof UploadStashFile ) {
+ $localFile = $this->getLocalFile();
+ $stashFile = $this->getStashFile();
+ // Calling a different API module depending on whether the file was stashed is less than optimal.
+ // In fact, calling API modules here at all is less than optimal. Maybe it should be refactored.
+ if ( $stashFile ) {
$imParam = ApiQueryStashImageInfo::getPropertyNames();
- $info = ApiQueryStashImageInfo::getInfo( $file, array_flip( $imParam ), $result );
+ $info = ApiQueryStashImageInfo::getInfo( $stashFile, array_flip( $imParam ), $result );
} else {
$imParam = ApiQueryImageInfo::getPropertyNames();
- $info = ApiQueryImageInfo::getInfo( $file, array_flip( $imParam ), $result );
+ $info = ApiQueryImageInfo::getInfo( $localFile, array_flip( $imParam ), $result );
}
return $info;
$this->verifyChunk();
// Create a local stash target
- $this->mLocalFile = parent::doStashFile( $user );
+ $this->mStashFile = parent::doStashFile( $user );
// Update the initial file offset (based on file size)
- $this->mOffset = $this->mLocalFile->getSize();
- $this->mFileKey = $this->mLocalFile->getFileKey();
+ $this->mOffset = $this->mStashFile->getSize();
+ $this->mFileKey = $this->mStashFile->getFileKey();
// Output a copy of this first to chunk 0 location:
- $this->outputChunk( $this->mLocalFile->getPath() );
+ $this->outputChunk( $this->mStashFile->getPath() );
// Update db table to reflect initial "chunk" state
$this->updateChunkStatus();
- return $this->mLocalFile;
+ return $this->mStashFile;
}
/**
return $status;
}
- // Update the mTempPath and mLocalFile
+ // Update the mTempPath and mStashFile
// (for FileUpload or normal Stash to take over)
$tStart = microtime( true );
// This is a re-implementation of UploadBase::tryStashFile(), we can't call it because we
return $status;
}
try {
- $this->mLocalFile = parent::doStashFile( $this->user );
+ $this->mStashFile = 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())
+ $this->mStashFile->setLocalReference( $tmpFile ); // reuse (e.g. for getImageInfo())
wfDebugLog( 'fileconcatenate', "Stashed combined file ($i chunks) in $tAmount seconds." );
return $status;
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;
/**
"right-changetags": "নির্দিষ্ট সংস্করণ এবং দীর্ঘ সম্পাদনাগুলোতে [[Special:Tags|ট্যাগ]] সংযোজন ও অপসারণ করুন",
"right-deletechangetags": "ডাটাবেজ থেকে [[Special:Tags|ট্যাগ]] অপসারণ করা",
"grant-group-email": "ইমেইল পাঠান",
+ "grant-group-private-information": "আপনার সম্পর্কিত ব্যক্তিগত তথ্যে প্রবেশাধিকার পায়",
"grant-group-other": "বিবিধ কার্যকলাপ",
"grant-createaccount": "অ্যাকাউন্ট তৈরি করুন",
"grant-createeditmovepage": "পাতা তৈরি, সম্পাদনা এবং স্থানান্তর করুন",
"grant-editmyoptions": "আপনার ব্যবহারকারী পছন্দসমূহ সম্পাদনা করুন",
"grant-editmywatchlist": "আপনার নজরতালিকা সম্পাদনা করুন",
"grant-editprotected": "সংরক্ষিত পাতা সম্পাদনা করুন",
+ "grant-privateinfo": "ব্যক্তিগত তথ্যে প্রবেশাধিকার",
"grant-sendemail": "অন্য ব্যবহারকারীকে ইমেইল পাঠান",
"grant-uploadfile": "নতুন ফাইল আপলোড করুন",
"grant-basic": "মৌলিক অধিকার",
"nolinkstoimage": "Aucune page n'utilise ce fichier.",
"morelinkstoimage": "Voir [[Special:WhatLinksHere/$1|plus de liens]] vers ce fichier.",
"linkstoimage-redirect": "$1 (redirection de fichier) $2",
- "duplicatesoffile": "{{PLURAL:$1|Le fichier suivant est un duplicata|Les fichiers suivants sont des duplicatas}} de celui-ci ([[Special:FileDuplicateSearch/$2|plus de détails]]) :",
+ "duplicatesoffile": "{{PLURAL:$1|Le fichier suivant est un doublon|Les $1 fichiers suivants sont des doublons}} de celui-ci ([[Special:FileDuplicateSearch/$2|plus de détails]]) :",
"sharedupload": "Ce fichier provient de : $1. Il peut être utilisé par d'autres projets.",
"sharedupload-desc-there": "Ce fichier provient de : $1. Il peut être utilisé par d'autres projets.\nVeuillez consulter [$2 sa page de description] pour plus d'informations.",
"sharedupload-desc-here": "Ce fichier provient de $1. Il peut être utilisé par d'autres projets.\nSa description sur sa [$2 page de description] est affichée ci-dessous.",
"filedelete-intro-old": "Vous êtes en train d'effacer la version de '''[[Media:$1|$1]]''' du [$4 $2 à $3].",
"filedelete-comment": "Motif :",
"filedelete-submit": "Supprimer",
- "filedelete-success": "'''$1''' a été supprimé.",
- "filedelete-success-old": "La version de '''[[Media:$1|$1]]''' du $2 à $3 a été supprimée.",
- "filedelete-nofile": "'''$1''' n'existe pas.",
- "filedelete-nofile-old": "Il n'existe aucune version archivée de '''$1''' avec les attributs indiqués.",
+ "filedelete-success": "<strong>$1</strong> a été supprimé.",
+ "filedelete-success-old": "La version de <strong>[[Media:$1|$1]]</strong> du $2 à $3 a été supprimée.",
+ "filedelete-nofile": "<strong>$1</strong> n'existe pas.",
+ "filedelete-nofile-old": "Il n'existe aucune version archivée de <strong>$1</strong> avec les attributs indiqués.",
"filedelete-otherreason": "Motif autre / supplémentaire :",
"filedelete-reason-otherlist": "Autre motif",
"filedelete-reason-dropdown": "* Motifs fréquents de suppression de fichiers\n** Violation du droit d'auteur\n** Fichier dupliqué",
- "filedelete-edit-reasonlist": "Modifier les motifs fréquents de suppression",
- "filedelete-maintenance": "La suppression et restauration de fichiers est temporairement désactivée durant la maintenance.",
+ "filedelete-edit-reasonlist": "Modifier les motifs de suppression",
+ "filedelete-maintenance": "La suppression et la restauration de fichiers sont temporairement désactivées durant la maintenance.",
"filedelete-maintenance-title": "Impossible de supprimer le fichier",
"mimesearch": "Recherche par type de contenu MIME",
"mimesearch-summary": "Cette page vous permet de filtrer les fichiers par leur type de contenu MIME.\nEntrée : type_de_contenu/sous-type ou type_de_contenu/*, par ex. <code>image/jpeg</code>.",
"randompage-nopages": "Il n'y a aucune page dans {{PLURAL:$2|l'espace de noms|les espaces de noms}} : $1.",
"randomincategory": "Page au hasard dans la catégorie",
"randomincategory-invalidcategory": "« $1 » n’est pas un nom de catégorie valide.",
- "randomincategory-nopages": "Il n’y a pas de page dans [[:Category:$1]].",
+ "randomincategory-nopages": "Il n’y a pas de pages dans la catégorie [[:Category:$1|$1]].",
"randomincategory-category": "Catégorie :",
"randomincategory-legend": "Page aléatoire dans la catégorie",
"randomincategory-submit": "Lancer",
"mainpage-nstab": "𐌰𐌽𐌰𐍃𐍄𐍉𐌳𐌴𐌹𐌽𐌹𐌻𐌰𐌿𐍆𐍃",
"error": "𐌰𐌹𐍂𐌶𐌴𐌹",
"databaseerror-error": "𐌰𐌹𐍂𐌶𐌴𐌹: $1",
- "missing-article": "ð\90\8d\83ð\90\8c° ð\90\8c³ð\90\8c°ð\90\8d\84ð\90\8c°ð\90\8c±ð\90\8c¿ð\90\8d\83 ð\90\8c½ð\90\8c¹ ð\90\8c²ð\90\8c°ð\90\8c½ð\90\8c°ð\90\8c¼ ð\90\8c¸ð\90\8c°ð\90\8c½ð\90\8c° ð\90\8c±ð\90\8d\89ð\90\8cºð\90\8c°ð\90\8d\85ð\90\8c°ð\90\8c¿ð\90\8d\82ð\90\8c³ð\90\8c°ð\90\8c½ ð\90\8c´ð\90\8c¹ ð\90\8c¹ð\90\8d\84ð\90\8c° ð\90\8d\83ð\90\8cºð\90\8c°ð\90\8c» ð\90\8c±ð\90\8c¹ð\90\8c²ð\90\8c¹ð\90\8d\84ð\90\8c°ð\90\8c½: \"$1\" $2\n\n(The data base did not find the text of a page that it should have found, named \"$1\" $2.\n\nThis is usually caused by following an outdated diff or history link to a page that has been deleted.\n\nIf this is not the case, you may have found a bug in the software.\nPlease report this to an [[Special:ListUsers/sysop|administrator]], making note of the URL.)",
+ "missing-article": "ð\90\8c³ð\90\8c°ð\90\8d\84ð\90\8c°ð\90\8c±ð\90\8c´ð\90\8d\83 ð\90\8c½ð\90\8c¹ ð\90\8c±ð\90\8c¹ð\90\8c²ð\90\8c°ð\90\8d\84 ð\90\8c±ð\90\8d\89ð\90\8cºð\90\8d\89ð\90\8d\83 ð\90\8c»ð\90\8c°ð\90\8c¿ð\90\8c±ð\90\8c¹ð\90\8d\83 ð\90\8c¸ð\90\8c¹ð\90\8c¶ð\90\8c´ð\90\8c¹ ð\90\8d\83ð\90\8cºð\90\8c¿ð\90\8c»ð\90\8c³ð\90\8c´ð\90\8c³ð\90\8c¹ ð\90\8c±ð\90\8c¹ð\90\8c²ð\90\8c¹ð\90\8d\84ð\90\8c°ð\90\8c½, ð\90\8c·ð\90\8c°ð\90\8c¹ð\90\8d\84ð\90\8c°ð\90\8c½ð\90\8d\83 \"$1\" $2. \n\nð\90\8c¸ð\90\8c°ð\90\8d\84ð\90\8c° ð\90\8c¿ð\90\8d\86ð\90\8d\84ð\90\8c° ð\90\8d\85ð\90\8c°ð\90\8c¹ð\90\8d\82ð\90\8c¸ð\90\8c¹ð\90\8c¸ ð\90\8c¾ð\90\8c°ð\90\8c±ð\90\8c°ð\90\8c¹ ð\90\8c»ð\90\8c°ð\90\8c¹ð\90\8d\83ð\90\8d\84ð\90\8c¾ð\90\8c°ð\90\8c³ð\90\8c° ð\90\8d\86ð\90\8c°ð\90\8c¹ð\90\8d\82ð\90\8c½ð\90\8c¾ð\90\8c° ð\90\8c³ð\90\8c¹ð\90\8d\86ð\90\8d\86 ð\90\8c¸ð\90\8c°ð\90\8c¿ ð\90\8d\83ð\90\8d\80ð\90\8c¹ð\90\8c»ð\90\8c»ð\90\8c°ð\90\8c²ð\90\8c°ð\90\8d\85ð\90\8c¹ð\90\8d\83ð\90\8d\83 ð\90\8d\83ð\90\8c´ð\90\8c¹ ð\90\8d\86ð\90\8d\82ð\90\8c°ð\90\8cµð\90\8c¹ð\90\8d\83ð\90\8d\84ð\90\8c¹ð\90\8c³ð\90\8c° ð\90\8c¹ð\90\8d\83ð\90\8d\84. ð\90\8c½ð\90\8c¹ð\90\8c±ð\90\8c°ð\90\8c¹ ð\90\8c¹ð\90\8d\83ð\90\8d\84, ð\90\8c¼ð\90\8c°ð\90\8c·ð\90\8d\84ð\90\8d\83 ð\90\8c¹ð\90\8d\83ð\90\8d\84 ð\90\8c´ð\90\8c¹ ð\90\8c±ð\90\8c¹ð\90\8c²ð\90\8c´ð\90\8d\84ð\90\8c´ð\90\8c¹ð\90\8d\83 ð\90\8c°ð\90\8c¹ð\90\8d\82ð\90\8c¶ð\90\8c´ð\90\8c¹ð\90\8c½ ð\90\8c¹ð\90\8c½ ð\90\8d\83ð\90\8c°ð\90\8c¿ð\90\8d\86ð\90\8d\84ð\90\8d\85ð\90\8c°ð\90\8c¹ð\90\8d\82ð\90\8c°. \n\nð\90\8c±ð\90\8c¹ð\90\8c³ð\90\8c¾ð\90\8c°ð\90\8c¼ ð\90\8c¸ð\90\8c¿ð\90\8cº, ð\90\8c¼ð\90\8c´ð\90\8d\82ð\90\8c´ð\90\8c¹ ð\90\8c¸ð\90\8c°ð\90\8d\84ð\90\8c° ð\90\8c³ð\90\8c¿ [[Special:ListUsers/sysop\n|ð\90\8d\82ð\90\8c´ð\90\8c¹ð\90\8cº]] ð\90\8c²ð\90\8c¹ð\90\8d\86ð\90\8c¿ð\90\8c· ð\90\8c²ð\90\8c°ð\90\8d\85ð\90\8c¹ð\90\8d\83ð\90\8d\83.",
"badtitle": "𐌿𐌽𐍂𐌰𐌹𐌷𐍄𐌰𐍄𐌰 𐌿𐍆𐌰𐍂𐌼𐌴𐌻𐌹",
"badtitletext": "𐍆𐍂𐌰𐌹𐌷𐌰𐌽𐍃 𐌻𐌰𐌿𐍆𐍃 𐍅𐌰𐍃 𐌿𐌽𐌲𐌰𐌼𐌰𐌲𐌰𐌽𐌳𐍃, 𐌻𐌰𐌿𐍃, 𐌰𐌹𐌸𐌸𐌰𐌿 𐌿𐌽𐍂𐌰𐌹𐌷𐍄𐌰𐌱𐌰 𐌲𐌰𐍅𐌹𐌳𐌰𐌽𐍃 𐌼𐌹𐌸𐍂𐌰𐌶𐌳𐌰 𐌸𐌰𐌿 𐌼𐌹𐌸-𐍅𐌹𐌺𐌹 𐌿𐍆𐌰𐍂𐌼𐌴𐌻𐌹. 𐌼𐌰𐌲𐌹 𐌷𐌰𐌱𐌰𐌽 𐌰𐌹𐌽𐌰 𐌸𐌰𐌿 𐌼𐌰𐌽𐌰𐌲𐌹𐌶𐍉𐍃 𐌱𐍉𐌺𐍉𐍃 𐌱𐍂𐌿𐌺𐌹𐌳𐍉𐍃 𐌹𐌽 𐌿𐍆𐌰𐍂𐌼𐌴𐌻𐌾𐌰𐌼.",
"viewsource": "𐍃𐌰𐌹𐍈 𐌱𐍂𐌿𐌽𐌽𐌰𐌽",
"action-applychangetags": "appreca tag pe' tramente ca se fanno 'e cagnamiente vuoste",
"action-changetags": "azzecca o lèva tag a caso dint'a verziune nnividuale e riggistre 'e log",
"action-deletechangetags": "scancellare 'e tag d' 'o database",
+ "action-purge": "agghiuorna sta paggena",
"nchanges": "$1 {{PLURAL:$1|cagnamiento|cagnamiente}}",
"enhancedrc-since-last-visit": "$1 {{PLURAL:$1|'a ll'urdema visita}}",
"enhancedrc-history": "cronologgia",
"uploadstash-errclear": "'A pulezzia d' 'e file scassaje.",
"uploadstash-refresh": "Agghiuorna l'elenco d' 'e file",
"uploadstash-thumbnail": "vide miniatura",
+ "uploadstash-exception": "Nun s'è pututo sarvà 'a càrreca dint' 'a stash ($1): \"$2\".",
"invalid-chunk-offset": "Distanza d' 'a parte nun valida",
"img-auth-accessdenied": "Acciesso negato",
"img-auth-nopathinfo": "PATH_INFO mancante.\n'O server nun è mpustato pe' passà sta nfurmazione.\nPuò darse ca, essenno basato ncopp'a CGI, nun putesse suppurtà img_auth.\nVide https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization",
"watchnologin": "Acciesso nun affettuato",
"addwatch": "Miette dint' 'a l'elenco 'e paggene cuntrullate",
"addedwatchtext": "'A paggena \"[[:$1]]\" e 'a paggena 'e chiacchiera è stata azzeccata dint'a l'elenco 'e [[Special:Watchlist|paggene cuntrullate]].",
+ "addedwatchtext-talk": "\"[[:$1]]\" e 'a paggena 'e chiacchiera suòccia è stata azzeccata dint'a l'elenco 'e [[Special:Watchlist|paggene cuntrullate]] vuosto.",
"addedwatchtext-short": "Chista paggena \"$1\" è stata azzeccata a l'elenco 'e paggene cuntrullate.",
"removewatch": "Leva 'a l'elenco 'e paggene cuntrullate",
- "removedwatchtext": "\"[[:$1]]\" 'e 'a paggena 'e chiacchiera soja so' state scancellata 'a l'elenco [[Special:Watchlist|'e paggene cuntrullate]] vuosto.",
+ "removedwatchtext": "\"[[:$1]]\" e 'a paggena 'e chiacchiera soja so' state scancellate 'a l'elenco [[Special:Watchlist|'e paggene cuntrullate]] vuosto.",
+ "removedwatchtext-talk": "\"[[:$1]]\" e 'a paggena 'e chiacchiera soja so' state luvate 'a l'elenco [[Special:Watchlist|'e paggene cuntrullate]] vuosto.",
"removedwatchtext-short": "Chista paggena \"$1\" è stata luvata a l'elenco 'e paggene cuntrullate.",
"watch": "Secuta",
"watchthispage": "Tiene d'uocchio sta paggena",
'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;
'type' => 'string',
'label' => $msg,
'help' => $msg,
+ 'sensitive' => true,
],
'string3' => [
'type' => 'string',
$expect = $req1->getFieldInfo();
foreach ( $expect as $name => &$options ) {
$options['optional'] = !empty( $options['optional'] );
+ $options['sensitive'] = !empty( $options['sensitive'] );
}
unset( $options );
$this->assertEquals( $expect, $fields );
$fields = AuthenticationRequest::mergeFieldInfo( [ $req1, $req2 ] );
$expect += $req2->getFieldInfo();
+ $expect['string1']['sensitive'] = true;
$expect['string2']['optional'] = false;
$expect['string3']['optional'] = false;
+ $expect['string3']['sensitive'] = false;
$expect['select']['options']['bar'] = $msg;
$this->assertEquals( $expect, $fields );
$fields = AuthenticationRequest::mergeFieldInfo( [ $req1, $req2 ] );
$expect += $req2->getFieldInfo();
$expect['string1']['optional'] = false;
+ $expect['string1']['sensitive'] = true;
$expect['string3']['optional'] = false;
$expect['select']['optional'] = false;
$expect['select']['options']['bar'] = $msg;
$fields = AuthenticationRequest::mergeFieldInfo( [ $req1, $req2 ] );
$expect = $req1->getFieldInfo() + $req2->getFieldInfo();
+ foreach ( $expect as $name => &$options ) {
+ $options['sensitive'] = !empty( $options['sensitive'] );
+ }
$expect['string1']['optional'] = false;
+ $expect['string1']['sensitive'] = true;
$expect['string2']['optional'] = true;
$expect['string3']['optional'] = true;
$expect['select']['optional'] = false;
if ( isset( $data['image'] ) ) {
$this->assertType( 'string', $data['image'], "Field $field, image" );
}
+ if ( isset( $data['sensitive'] ) ) {
+ $this->assertType( 'bool', $data['sensitive'], "Field $field, sensitive" );
+ }
+ if ( $data['type'] === 'password' ) {
+ $this->assertTrue( !empty( $data['sensitive'] ),
+ "Field $field, password field must be sensitive" );
+ }
switch ( $data['type'] ) {
case 'string':
$creator = $this->getMock( 'User' );
$userWithoutEmail = $this->getMock( 'User' );
$userWithoutEmail->expects( $this->any() )->method( 'getEmail' )->willReturn( '' );
+ $userWithoutEmail->expects( $this->any() )->method( 'getInstanceForUpdate' )->willReturnSelf();
$userWithoutEmail->expects( $this->never() )->method( 'sendConfirmationMail' );
$userWithEmailError = $this->getMock( 'User' );
$userWithEmailError->expects( $this->any() )->method( 'getEmail' )->willReturn( 'foo@bar.baz' );
+ $userWithEmailError->expects( $this->any() )->method( 'getInstanceForUpdate' )->willReturnSelf();
$userWithEmailError->expects( $this->any() )->method( 'sendConfirmationMail' )
->willReturn( \Status::newFatal( 'fail' ) );
$userExpectsConfirmation = $this->getMock( 'User' );
$userExpectsConfirmation->expects( $this->any() )->method( 'getEmail' )
->willReturn( 'foo@bar.baz' );
+ $userExpectsConfirmation->expects( $this->any() )->method( 'getInstanceForUpdate' )
+ ->willReturnSelf();
$userExpectsConfirmation->expects( $this->once() )->method( 'sendConfirmationMail' )
->willReturn( \Status::newGood() );
$userNotExpectsConfirmation = $this->getMock( 'User' );
$userNotExpectsConfirmation->expects( $this->any() )->method( 'getEmail' )
->willReturn( 'foo@bar.baz' );
+ $userNotExpectsConfirmation->expects( $this->any() )->method( 'getInstanceForUpdate' )
+ ->willReturnSelf();
$userNotExpectsConfirmation->expects( $this->never() )->method( 'sendConfirmationMail' );
$provider = new EmailNotificationSecondaryAuthenticationProvider( [
$content = new WikitextContent( 'test text' );
$ok = ContentHandler::runLegacyHooks(
'testRunLegacyHooks',
- [ 'foo', &$content, 'bar' ],
- false
+ [ 'foo', &$content, 'bar' ]
);
$this->assertTrue( $ok, "runLegacyHooks should have returned true" );
$this->assertInstanceOf( $handlerClass, $handler );
}
+ public function testGetFieldsForSearchIndex() {
+ $searchEngine = $this->newSearchEngine();
+
+ $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
+
+ $fields = $handler->getFieldsForSearchIndex( $searchEngine );
+
+ $this->assertArrayHasKey( 'category', $fields );
+ $this->assertArrayHasKey( 'external_link', $fields );
+ $this->assertArrayHasKey( 'outgoing_link', $fields );
+ $this->assertArrayHasKey( 'template', $fields );
+ }
+
+ private function newSearchEngine() {
+ $searchEngine = $this->getMockBuilder( 'SearchEngine' )
+ ->getMock();
+
+ $searchEngine->expects( $this->any() )
+ ->method( 'makeSearchFieldMapping' )
+ ->will( $this->returnCallback( function( $name, $type ) {
+ return new DummySearchIndexFieldDefinition( $name, $type );
+ } ) );
+
+ return $searchEngine;
+ }
+
/**
* @covers ContentHandler::getDataForSearchIndex
*/
$this->setTemporaryHook( 'SearchDataForIndex',
function ( &$fields, ContentHandler $handler, WikiPage $page, ParserOutput $output,
- SearchEngine $engine ) {
+ SearchEngine $engine ) {
$fields['testDataField'] = 'test content';
} );
return new WikiTextStructure( $this->getParserOutput( $text ) );
}
- public function testCategories() {
- $text = <<<END
-We also have a {{Template}} and an {{Another template}} in addition.
-This text also has [[Category:Some Category| ]] and then [[Category:Yet another category]].
-And [[Category:Some Category| this category]] is repeated.
-END;
- $struct = $this->getStructure( $text );
- $cats = $struct->categories();
- $this->assertCount( 2, $cats );
- $this->assertContains( "Some Category", $cats );
- $this->assertContains( "Yet another category", $cats );
- }
-
- public function testOutgoingLinks() {
- $text = <<<END
-Here I add link to [[Some Page]]. And [[Some Page|This same page]] gets linked twice.
-We also have [[File:Image.jpg|image]].
-We also have a {{Template}} and an {{Another template}} in addition.
-Some templates are {{lowercase}}.
-And [[Some_Page]] is linked again.
-It also has [[Category:Some Category| ]] and then [[Category:Yet another category]].
-Also link to a [[Talk:TestTitle|talk page]] is here.
-END;
- $struct = $this->getStructure( $text );
- $links = $struct->outgoingLinks();
- $this->assertContains( "Some_Page", $links );
- $this->assertContains( "Template:Template", $links );
- $this->assertContains( "Template:Another_template", $links );
- $this->assertContains( "Template:Lowercase", $links );
- $this->assertContains( "Talk:TestTitle", $links );
- $this->assertCount( 5, $links );
- }
-
- public function testTemplates() {
- $text = <<<END
-We have a {{Template}} and an {{Another template}} in addition.
-Some templates are {{lowercase}}. And this {{Template}} is repeated.
-Here is {{another_template|with=argument}}.
-This is a template that {{Xdoes not exist}}.
-END;
- $this->setTemporaryHook( 'TitleExists', function ( Title $title, &$exists ) {
- $txt = $title->getBaseText();
- if ( $txt[0] != 'X' ) {
- $exists = true;
- }
- return true;
- } );
- $struct = $this->getStructure( $text );
- $templates = $struct->templates();
- $this->assertCount( 3, $templates );
- $this->assertContains( "Template:Template", $templates );
- $this->assertContains( "Template:Another template", $templates );
- $this->assertContains( "Template:Lowercase", $templates );
- }
-
public function testHeadings() {
$text = <<<END
Some text here
$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() {
$value = wfRandomString();
$calls = 0;
- $func = function() use ( &$calls, $value ) {
+ $func = function() use ( &$calls, $value, $cache, $key ) {
++$calls;
+ // Immediately kill any mutex rather than waiting a second
+ $cache->delete( $cache::MUTEX_KEY_PREFIX . $key );
return $value;
};
$this->assertEquals( 1, $calls, 'Value was populated' );
// Acquire a lock to verify that getWithSetCallback uses lockTSE properly
- $this->internalCache->lock( $key, 0 );
+ $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
$checkKeys = [ wfRandomString() ]; // new check keys => force misses
$ret = $cache->getWithSetCallback( $key, 30, $func,
$value = wfRandomString();
$calls = 0;
- $func = function( $oldValue, &$ttl, &$setOpts ) use ( &$calls, $value ) {
+ $func = function( $oldValue, &$ttl, &$setOpts ) use ( &$calls, $value, $cache, $key ) {
++$calls;
$setOpts['since'] = microtime( true ) - 10;
+ // Immediately kill any mutex rather than waiting a second
+ $cache->delete( $cache::MUTEX_KEY_PREFIX . $key );
return $value;
};
$this->assertEquals( 1, $calls, 'Value was generated' );
// Acquire a lock to verify that getWithSetCallback uses lockTSE properly
- $this->internalCache->lock( $key, 0 );
+ $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
$ret = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] );
$this->assertEquals( $value, $ret );
$this->assertEquals( 1, $calls, 'Callback was not used' );
$busyValue = wfRandomString();
$calls = 0;
- $func = function() use ( &$calls, $value ) {
+ $func = function() use ( &$calls, $value, $cache, $key ) {
++$calls;
+ // Immediately kill any mutex rather than waiting a second
+ $cache->delete( $cache::MUTEX_KEY_PREFIX . $key );
return $value;
};
$this->assertEquals( 1, $calls, 'Value was populated' );
// Acquire a lock to verify that getWithSetCallback uses busyValue properly
- $this->internalCache->lock( $key, 0 );
+ $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
$checkKeys = [ wfRandomString() ]; // new check keys => force misses
$ret = $cache->getWithSetCallback( $key, 30, $func,
$this->assertEquals( $busyValue, $ret, 'Callback was not used; used busy value' );
$this->assertEquals( 2, $calls, 'Callback was not used; used busy value' );
- $this->internalCache->unlock( $key );
+ $this->internalCache->delete( $cache::MUTEX_KEY_PREFIX . $key );
$ret = $cache->getWithSetCallback( $key, 30, $func,
[ 'lockTSE' => 30, 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
$this->assertEquals( $value, $ret, 'Callback was used; saved interim' );
$this->assertEquals( 3, $calls, 'Callback was used; saved interim' );
- $this->internalCache->lock( $key, 0 );
+ $this->internalCache->add( $cache::MUTEX_KEY_PREFIX . $key, 1, 0 );
$ret = $cache->getWithSetCallback( $key, 30, $func,
[ 'busyValue' => $busyValue, 'checkKeys' => $checkKeys ] );
$this->assertEquals( $value, $ret, 'Callback was not used; used interim' );
$this->cache->set( $key, $value, 30, $opts );
$this->assertEquals( false, $this->cache->get( $key ), "Pending value not written." );
}
+
+ public function testMcRouterSupport() {
+ $localBag = $this->getMock( 'EmptyBagOStuff', [ 'set', 'delete' ] );
+ $localBag->expects( $this->never() )->method( 'set' );
+ $localBag->expects( $this->never() )->method( 'delete' );
+ $wanCache = new WANObjectCache( [
+ 'cache' => $localBag,
+ 'pool' => 'testcache-hash',
+ 'relayer' => new EventRelayerNull( [] )
+ ] );
+ $valFunc = function () {
+ return 1;
+ };
+
+ // None of these should use broadcasting commands (e.g. SET, DELETE)
+ $wanCache->get( 'x' );
+ $wanCache->get( 'x', $ctl, [ 'check1' ] );
+ $wanCache->getMulti( [ 'x', 'y' ] );
+ $wanCache->getMulti( [ 'x', 'y' ], $ctls, [ 'check2' ] );
+ $wanCache->getWithSetCallback( 'p', 30, $valFunc );
+ $wanCache->getCheckKeyTime( 'zzz' );
+ }
}
--- /dev/null
+<?php
+
+use MediaWiki\Search\ParserOutputSearchDataExtractor;
+
+/**
+ * @group Search
+ * @covers MediaWiki\Search\ParserOutputSearchDataExtractor
+ */
+class ParserOutputSearchDataExtractorTest extends MediaWikiLangTestCase {
+
+ public function testGetCategories() {
+ $categories = [
+ 'Foo_bar' => 'Bar',
+ 'New_page' => ''
+ ];
+
+ $parserOutput = new ParserOutput( '', [], $categories );
+
+ $searchDataExtractor = new ParserOutputSearchDataExtractor();
+
+ $this->assertEquals(
+ [ 'Foo bar', 'New page' ],
+ $searchDataExtractor->getCategories( $parserOutput )
+ );
+ }
+
+ public function testGetExternalLinks() {
+ $parserOutput = new ParserOutput();
+
+ $parserOutput->addExternalLink( 'https://foo' );
+ $parserOutput->addExternalLink( 'https://bar' );
+
+ $searchDataExtractor = new ParserOutputSearchDataExtractor();
+
+ $this->assertEquals(
+ [ 'https://foo', 'https://bar' ],
+ $searchDataExtractor->getExternalLinks( $parserOutput )
+ );
+ }
+
+ public function testGetOutgoingLinks() {
+ $parserOutput = new ParserOutput();
+
+ $parserOutput->addLink( Title::makeTitle( NS_MAIN, 'Foo_bar' ), 1 );
+ $parserOutput->addLink( Title::makeTitle( NS_HELP, 'Contents' ), 2 );
+
+ $searchDataExtractor = new ParserOutputSearchDataExtractor();
+
+ // this indexes links with db key
+ $this->assertEquals(
+ [ 'Foo_bar', 'Help:Contents' ],
+ $searchDataExtractor->getOutgoingLinks( $parserOutput )
+ );
+ }
+
+ public function testGetTemplates() {
+ $title = Title::makeTitle( NS_TEMPLATE, 'Cite_news' );
+
+ $parserOutput = new ParserOutput();
+ $parserOutput->addTemplate( $title, 10, 100 );
+
+ $searchDataExtractor = new ParserOutputSearchDataExtractor();
+
+ $this->assertEquals(
+ [ 'Template:Cite news' ],
+ $searchDataExtractor->getTemplates( $parserOutput )
+ );
+ }
+
+}
--- /dev/null
+<?php
+
+class SpecialSearchText extends \PHPUnit_Framework_TestCase {
+ public function testSubPageRedirect() {
+ $ctx = new RequestContext;
+
+ SpecialPageFactory::executePath(
+ Title::newFromText( 'Special:Search/foo_bar' ),
+ $ctx
+ );
+ $url = $ctx->getOutput()->getRedirect();
+ // some older versions of hhvm have a bug that doesn't parse relative
+ // urls with a port, so help it out a little bit.
+ // https://github.com/facebook/hhvm/issues/7136
+ $url = wfExpandUrl( $url, PROTO_CURRENT );
+
+ $parts = parse_url( $url );
+ $this->assertEquals( '/w/index.php', $parts['path'] );
+ parse_str( $parts['query'], $query );
+ $this->assertEquals( 'Special:Search', $query['title'] );
+ $this->assertEquals( 'foo bar', $query['search'] );
+ }
+}
}
}
+ /**
+ * 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 ) );