* (T182366) UploadBase::checkXMLEncodingMissmatch() now works on PHP 7.1+
* (T118683) Fix exception from &$user deref on HHVM in the TitleMoveComplete hook.
* (T196672) The mtime of extension.json files is now able to be zero
+* (T180403) Validate $length in padleft/padright parser functions.
+* (T143790) Make $wgEmailConfirmToEdit only affect edit actions.
=== Changes since MediaWiki 1.31.0-rc.0 ===
* (T33223) Drop archive.ar_text and ar_flags.
reauthenticating.
* FormSpecialPage::execute() will now call checkLoginSecurityLevel() if
getLoginSecurityLevel() returns non-false.
+* The 'ImageBeforeProduceHTML' hook is now passed three new parameters, $parser,
+ &$query and &$widthOption, allowing extensions even finer control over the resulting
+ HTML code.
=== External library changes in 1.32 ===
* …
minor change to implement the toggle feature with CSS instead. To restore
prior functionality, either explicitly load "jquery.mw-jump" in your skin
or refer to T195256 for details on how to make the same change.
+* Hook 'EditPageBeforeEditChecks' was removed;
+ use 'EditPageGetCheckboxesDefinition' instead.
+* Linker::getLinkColour() and DummyLinker::getLinkColour(), deprecated since
+ 1.28, were removed. LinkRenderer::getLinkClasses() should be used instead.
=== Deprecations in 1.32 ===
* Use of a StartProfiler.php file is deprecated in favour of placing
* The ApiQueryContributions class has been renamed to ApiQueryUserContribs.
* The XMPInfo, XMPReader, and XMPValidate classes have been deprecated in favor
of the namespaced classes provided by the wikimedia/xmp-reader library.
+* SearchResultSet::{next,rewind} are deprecated. Calling code should
+ use foreach on the SearchResultSet, or the extractResults method. Extending
+ code should override extractResults.
+* Instantiating SearchResultSet directly is deprecated. SearchEngine
+ implementations must subclass SearchResultSet for their purposes.
+* SearchResult::setExtensionData argument has been changed from accepting an
+ array to accepting a Closure that returns the array when called.
* Class CryptRand, everything in MWCryptRand except generateHex() and function
MediaWikiServices::getCryptRand() are deprecated, use random_bytes() to
generate cryptographically secure random byte sequences.
+* Parser::getConverterLanguage() is deprecated. Use ::getTargetLanguage()
+ instead.
=== Other changes in 1.32 ===
* …
'PageProps' => __DIR__ . '/includes/PageProps.php',
'PageQueryPage' => __DIR__ . '/includes/specialpage/PageQueryPage.php',
'Pager' => __DIR__ . '/includes/pager/Pager.php',
+ 'PaginatingSearchEngine' => __DIR__ . '/includes/search/PaginatingSearchEngine.php',
'ParameterizedPassword' => __DIR__ . '/includes/password/ParameterizedPassword.php',
'Parser' => __DIR__ . '/includes/parser/Parser.php',
'ParserCache' => __DIR__ . '/includes/parser/ParserCache.php',
"nikic/php-parser": "3.1.3",
"nmred/kafka-php": "0.1.5",
"phpunit/phpunit": "4.8.36 || ^6.5",
- "psy/psysh": "0.8.11",
+ "psy/psysh": "0.9.6",
"wikimedia/avro": "1.8.0",
"wikimedia/testing-access-wrapper": "~1.0",
"wmde/hamcrest-html-matchers": "^0.1.0"
stuff in our mainline code. Using hooks, the function becomes:
function showAnArticle( $article ) {
- if ( Hooks::run( 'ArticleShow', array( &$article ) ) ) {
+ if ( Hooks::run( 'ArticleShow', [ &$article ] ) ) {
# code to actually show the article goes here
- Hooks::run( 'ArticleShowComplete', array( &$article ) );
+ Hooks::run( 'ArticleShowComplete', [ &$article ] );
}
}
event. All the following are valid ways to define hooks:
$wgHooks['EventName'][] = 'someFunction'; # function, no data
- $wgHooks['EventName'][] = array( 'someFunction', $someData );
- $wgHooks['EventName'][] = array( 'someFunction' ); # weird, but OK
+ $wgHooks['EventName'][] = [ 'someFunction', $someData ];
+ $wgHooks['EventName'][] = [ 'someFunction' ]; # weird, but OK
$wgHooks['EventName'][] = $object; # object only
- $wgHooks['EventName'][] = array( $object, 'someMethod' );
- $wgHooks['EventName'][] = array( $object, 'someMethod', $someData );
- $wgHooks['EventName'][] = array( $object ); # weird but OK
+ $wgHooks['EventName'][] = [ $object, 'someMethod' ];
+ $wgHooks['EventName'][] = [ $object, 'someMethod', $someData ];
+ $wgHooks['EventName'][] = [ $object ]; # weird but OK
When an event occurs, the function (or object method) will be called with the
optional data provided as well as event-specific parameters. The above examples
The extra data is useful if we want to use the same function or object for
different purposes. For example:
- $wgHooks['PageContentSaveComplete'][] = array( 'ircNotify', 'TimStarling' );
- $wgHooks['PageContentSaveComplete'][] = array( 'ircNotify', 'brion' );
+ $wgHooks['PageContentSaveComplete'][] = [ 'ircNotify', 'TimStarling' ];
+ $wgHooks['PageContentSaveComplete'][] = [ 'ircNotify', 'brion' ];
This code would result in ircNotify being run twice when an article is saved:
once for 'TimStarling', and once for 'brion'.
functionality. For example, if you wanted to authenticate users to a custom
system (LDAP, another PHP program, whatever), you could do:
- $wgHooks['UserLogin'][] = array( 'ldapLogin', $ldapServer );
+ $wgHooks['UserLogin'][] = [ 'ldapLogin', $ldapServer ];
function ldapLogin( $username, $password ) {
# log user into LDAP
Note that hook parameters are passed in an array; this is a necessary
inconvenience to make it possible to pass reference values (that can be changed)
into the hook code. Also note that earlier versions of wfRunHooks took a
-variable number of arguments; the array() calling protocol came about after
+variable number of arguments; the [] calling protocol came about after
MediaWiki 1.4rc1.
==Events and parameters==
This is a list of known events and parameters; please add to it if you're going
to add events to the MediaWiki code.
-'AbortAutoAccount': DEPRECATED! Create a PreAuthenticationProvider instead.
-Return false to cancel automated local account creation, where normally
+'AbortAutoAccount': DEPRECATED since 1.27! Create a PreAuthenticationProvider
+instead. Return false to cancel automated local account creation, where normally
authentication against an external auth plugin would be creating a local
account.
$user: the User object about to be created (read-only, incomplete)
$title: The Title of the page that was edited.
$rc: The current RecentChange object.
-'AbortLogin': DEPRECATED! Create a PreAuthenticationProvider instead.
+'AbortLogin': DEPRECATED since 1.27! Create a PreAuthenticationProvider instead.
Return false to cancel account login.
$user: the User object being authenticated against
$password: the password being submitted, not yet checked for validity
&$msg: the message identifier for abort reason (new in 1.18, not available
before 1.18)
-'AbortNewAccount': DEPRECATED! Create a PreAuthenticationProvider instead.
-Return false to cancel explicit account creation.
+'AbortNewAccount': DEPRECATED since 1.27! Create a PreAuthenticationProvider
+instead. Return false to cancel explicit account creation.
$user: the User object about to be created (read-only, incomplete)
&$msg: out parameter: HTML to display on abort
&$status: out parameter: Status object to return, replaces the older $msg param
&$fields: HTMLForm descriptor array
$article: Article object
-'AddNewAccount': DEPRECATED! Use LocalUserCreated.
+'AddNewAccount': DEPRECATED since 1.27! Use LocalUserCreated.
After a user account is created.
$user: the User object that was created. (Parameter added in 1.7)
$byEmail: true when account was created "by email" (added in 1.12)
-'AfterBuildFeedLinks': Executed in OutputPage.php after all feed links (atom, rss,...)
-are created. Can be used to omit specific feeds from being outputted. You must not use
-this hook to add feeds, use OutputPage::addFeedLink() instead.
+'AfterBuildFeedLinks': Executed in OutputPage.php after all feed links (atom,
+rss,...) are created. Can be used to omit specific feeds from being outputted.
+You must not use this hook to add feeds, use OutputPage::addFeedLink() instead.
&$feedLinks: Array of created feed links
'AfterFinalPageOutput': Nearly at the end of OutputPage::output() but
&$msgs: Message[] Messages to include in the help. Multiple messages will be
joined with spaces.
-'APIEditBeforeSave': DEPRECATED! Use EditFilterMergedContent instead.
+'APIEditBeforeSave': DEPRECATED since 1.28! Use EditFilterMergedContent instead.
Before saving a page with api.php?action=edit, after
processing request parameters. Return false to let the request fail, returning
an error message or an <edit result="Failure"> tag if $resultArr was filled.
&$params: Array of parameters
$flags: int zero or OR-ed flags like ApiBase::GET_VALUES_FOR_HELP
-'APIGetDescription': DEPRECATED! Use APIGetDescriptionMessages instead.
-Use this hook to modify a module's description.
+'APIGetDescription': DEPRECATED since 1.25! Use APIGetDescriptionMessages
+instead. Use this hook to modify a module's description.
&$module: ApiBase Module object
&$desc: String description, or array of description strings
$module: ApiBase Module object
&$msg: Array of Message objects
-'APIGetParamDescription': DEPRECATED! Use APIGetParamDescriptionMessages
-instead.
+'APIGetParamDescription': DEPRECATED since 1.25! Use
+APIGetParamDescriptionMessages instead.
Use this hook to modify a module's parameter descriptions.
&$module: ApiBase Module object
&$desc: Array of parameter descriptions
$module: ApiQueryBase module in question
$result: ResultWrapper|bool returned from the IDatabase::select()
&$hookData: array that was passed to the 'ApiQueryBaseBeforeQuery' hook and
- will be passed to the 'ApiQueryBaseProcessRow' hook, intended for inter-hook
- communication.
+ will be passed to the 'ApiQueryBaseProcessRow' hook, intended for inter-hook
+ communication.
'ApiQueryBaseBeforeQuery': Called for (some) API query modules before a
database query is made. WARNING: It would be very easy to misuse this hook and
&$query_options: array of options for the database request
&$join_conds: join conditions for the tables
&$hookData: array that will be passed to the 'ApiQueryBaseAfterQuery' and
- 'ApiQueryBaseProcessRow' hooks, intended for inter-hook communication.
+ 'ApiQueryBaseProcessRow' hooks, intended for inter-hook communication.
'ApiQueryBaseProcessRow': Called for (some) API query modules as each row of
the database result is processed. Return false to stop processing the result
$row: stdClass Database result row
&$data: array to be included in the ApiResult.
&$hookData: array that was be passed to the 'ApiQueryBaseBeforeQuery' and
- 'ApiQueryBaseAfterQuery' hooks, intended for inter-hook communication.
+ 'ApiQueryBaseAfterQuery' hooks, intended for inter-hook communication.
'APIQueryGeneratorAfterExecute': After calling the executeGenerator() method of
an action=query submodule. Use this to extend core API modules.
&$module: Module object
&$resultPageSet: ApiPageSet object
-'APIQueryInfoTokens': DEPRECATED! Use ApiQueryTokensRegisterTypes instead.
-Use this hook to add custom tokens to prop=info. Every token has an action,
-which will be used in the intoken parameter and in the output
+'APIQueryInfoTokens': DEPRECATED since 1.24! Use ApiQueryTokensRegisterTypes
+instead. Use this hook to add custom tokens to prop=info. Every token has an
+action, which will be used in the intoken parameter and in the output
(actiontoken="..."), and a callback function which should return the token, or
false if the user isn't allowed to obtain it. The prototype of the callback
function is func($pageid, $title), where $pageid is the page ID of the page the
token is requested for and $title is the associated Title object. In the hook,
just add your callback to the $tokenFunctions array and return true (returning
false makes no sense).
-&$tokenFunctions: array(action => callback)
+&$tokenFunctions: [ action => callback ]
-'APIQueryRecentChangesTokens': DEPRECATED! Use ApiQueryTokensRegisterTypes
-instead.
+'APIQueryRecentChangesTokens': DEPRECATED since 1.24! Use
+ApiQueryTokensRegisterTypes instead.
Use this hook to add custom tokens to list=recentchanges. Every token has an
action, which will be used in the rctoken parameter and in the output
(actiontoken="..."), and a callback function which should return the token, or
associated Title object and $rc the associated RecentChange object. In the
hook, just add your callback to the $tokenFunctions array and return true
(returning false makes no sense).
-&$tokenFunctions: array(action => callback)
+&$tokenFunctions: [ action => callback ]
-'APIQueryRevisionsTokens': DEPRECATED! Use ApiQueryTokensRegisterTypes instead.
+'APIQueryRevisionsTokens': DEPRECATED since 1.24! Use
+ApiQueryTokensRegisterTypes instead.
Use this hook to add custom tokens to prop=revisions. Every token has an
action, which will be used in the rvtoken parameter and in the output
(actiontoken="..."), and a callback function which should return the token, or
associated Title object and $rev the associated Revision object. In the hook,
just add your callback to the $tokenFunctions array and return true (returning
false makes no sense).
-&$tokenFunctions: array(action => callback)
+&$tokenFunctions: [ action => callback ]
'APIQuerySiteInfoGeneralInfo': Use this hook to add extra information to the
sites general information.
'ApiQueryTokensRegisterTypes': Use this hook to add additional token types to
action=query&meta=tokens. Note that most modules will probably be able to use
the 'csrf' token instead of creating their own token types.
-&$salts: array( type => salt to pass to User::getEditToken() or array of salt
- and key to pass to Session::getToken() )
+&$salts: [ type => salt to pass to User::getEditToken(), or array of salt
+ and key to pass to Session::getToken() ]
-'APIQueryUsersTokens': DEPRECATED! Use ApiQueryTokensRegisterTypes instead.
+'APIQueryUsersTokens': DEPRECATED since 1.24! Use ApiQueryTokensRegisterTypes
+instead.
Use this hook to add custom token to list=users. Every token has an action,
which will be used in the ustoken parameter and in the output
(actiontoken="..."), and a callback function which should return the token, or
function is func($user) where $user is the User object. In the hook, just add
your callback to the $tokenFunctions array and return true (returning false
makes no sense).
-&$tokenFunctions: array(action => callback)
+&$tokenFunctions: [ action => callback ]
'ApiQueryWatchlistExtractOutputData': Extract row data for ApiQueryWatchlist.
$module: ApiQueryWatchlist instance
key-value-pair identified by the apiLink key is required.
&$apis: array of services
-'ApiTokensGetTokenTypes': DEPRECATED! Use ApiQueryTokensRegisterTypes instead.
+'ApiTokensGetTokenTypes': DEPRECATED since 1.24! Use ApiQueryTokensRegisterTypes instead.
Use this hook to extend action=tokens with new token types.
&$tokenTypes: supported token types in format 'type' => callback function
used to retrieve this type of tokens.
from a set of AuthenticationRequest classes into a form descriptor; hooks
can tweak the array to change how login etc. forms should look.
$requests: array of AuthenticationRequests the fields are created from
-$fieldInfo: field information array (union of all AuthenticationRequest::getFieldInfo() responses).
+$fieldInfo: field information array (union of all
+ AuthenticationRequest::getFieldInfo() responses).
&$formDescriptor: HTMLForm descriptor. The special key 'weight' can be set
to change the order of the fields.
$action: one of the AuthManager::ACTION_* constants.
'AuthManagerLoginAuthenticateAudit': A login attempt either succeeded or failed
for a reason other than misconfiguration or session loss. No return data is
accepted; this hook is for auditing only.
-$response: The MediaWiki\Auth\AuthenticationResponse in either a PASS or FAIL state.
+$response: The MediaWiki\Auth\AuthenticationResponse in either a PASS or FAIL
+ state.
$user: The User object being authenticated against, or null if authentication
failed before getting that far.
$username: A guess at the user name being authenticated, or null if we can't
even determine that.
-'AuthPluginAutoCreate': DEPRECATED! Use the 'LocalUserCreated' hook instead.
-Called when creating a local account for an user logged in from an external
-authentication method.
+'AuthPluginAutoCreate': DEPRECATED since 1.27! Use the 'LocalUserCreated' hook
+instead. Called when creating a local account for an user logged in from an
+external authentication method.
$user: User object created locally
-'AuthPluginSetup': DEPRECATED! Extensions should be updated to use AuthManager.
-Update or replace authentication plugin object ($wgAuth). Gives a chance for an
-extension to set it programmatically to a variable class.
+'AuthPluginSetup': DEPRECATED since 1.27! Extensions should be updated to use
+AuthManager. Update or replace authentication plugin object ($wgAuth). Gives a
+chance for an extension to set it programmatically to a variable class.
&$auth: the $wgAuth object, probably a stub
'AutopromoteCondition': Check autopromote condition for user.
$req: AuthenticationRequest object describing the change (and target user)
$status: StatusValue with the result of the action
-'ChangePasswordForm': DEPRECATED! Use AuthChangeFormFields or security levels.
-For extensions that need to add a field to the ChangePassword form via the
-Preferences form.
+'ChangePasswordForm': DEPRECATED since 1.27! Use AuthChangeFormFields or
+security levels. For extensions that need to add a field to the ChangePassword
+form via the Preferences form.
&$extraFields: An array of arrays that hold fields like would be passed to the
pretty function.
$unpatrolled: Whether or not we are showing unpatrolled changes.
$watched: Whether or not the change is watched by the user.
-'ChangesListSpecialPageFilters': DEPRECATED! Use 'ChangesListSpecialPageStructuredFilters'
-instead.
+'ChangesListSpecialPageFilters': DEPRECATED since 1.29! Use
+'ChangesListSpecialPageStructuredFilters' instead.
Called after building form options on pages
inheriting from ChangesListSpecialPage (in core: RecentChanges,
RecentChangesLinked and Watchlist).
$rev_id: revision table id
$log_id: logging table id
$params: tag params
-$rc: RecentChange being tagged when the tagging accompanies the action or null
-$user: User who performed the tagging when the tagging is subsequent to the action or null
+$rc: RecentChange being tagged when the tagging accompanies the action, or null
+$user: User who performed the tagging when the tagging is subsequent to the
+ action, or null
'ChangeTagsAllowedAdd': Called when checking if a user can add tags to a change.
&$allowedTags: List of all the tags the user is allowed to add. Any tags the
converted Content object. Note that $result->getContentModel() must return
$toModel.
-'ContentSecurityPolicyDefaultSource': Modify the allowed CSP load sources. This affects all
-directives except for the script directive. If you want to add a script
-source, see ContentSecurityPolicyScriptSource hook.
+'ContentSecurityPolicyDefaultSource': Modify the allowed CSP load sources. This
+affects all directives except for the script directive. If you want to add a
+script source, see ContentSecurityPolicyScriptSource hook.
&$defaultSrc: Array of Content-Security-Policy allowed sources
$policyConfig: Current configuration for the Content-Security-Policy header
-$mode: ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE
- depending on type of header
+$mode: ContentSecurityPolicy::REPORT_ONLY_MODE or
+ ContentSecurityPolicy::FULL_MODE depending on type of header
-'ContentSecurityPolicyDirectives': Modify the content security policy directives.
-Use this only if ContentSecurityPolicyDefaultSource and
+'ContentSecurityPolicyDirectives': Modify the content security policy
+directives. Use this only if ContentSecurityPolicyDefaultSource and
ContentSecurityPolicyScriptSource do not meet your needs.
&$directives: Array of CSP directives
$policyConfig: Current configuration for the CSP header
whatever you add.
&$scriptSrc: Array of CSP directives
$policyConfig: Current configuration for the CSP header
-$mode: ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE
- depending on type of header
+$mode: ContentSecurityPolicy::REPORT_ONLY_MODE or
+ ContentSecurityPolicy::FULL_MODE depending on type of header
'CustomEditor': When invoking the page editor
Return true to allow the normal editor to be used, or false if implementing
Currently only data attributes reserved to MediaWiki are allowed
(see Sanitizer::isReservedDataAttribute).
-'DeleteUnknownPreferences': Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which
-to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences
-that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed
-with 'gadget-', and so anything with that prefix is excluded from the deletion.
-&where: An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted
- from the user_properties table.
+'DeleteUnknownPreferences': Called by the cleanupPreferences.php maintenance
+script to build a WHERE clause with which to delete preferences that are not
+known about. This hook is used by extensions that have dynamically-named
+preferences that should not be deleted in the usual cleanup process. For
+example, the Gadgets extension creates preferences prefixed with 'gadget-', and
+so anything with that prefix is excluded from the deletion.
+&where: An array that will be passed as the $cond parameter to
+ IDatabase::select() to determine what will be deleted from the user_properties
+ table.
$db: The IDatabase object, useful for accessing $db->buildLike() etc.
'DifferenceEngineAfterLoadNewText': called in DifferenceEngine::loadNewText()
This hook can be used to inject content into said class member variable.
$differenceEngine: DifferenceEngine object
-'DifferenceEngineMarkPatrolledLink': Allows extensions to change the "mark as patrolled" link
-which is shown both on the diff header as well as on the bottom of a page, usually
-wrapped in a span element which has class="patrollink".
+'DifferenceEngineMarkPatrolledLink': Allows extensions to change the "mark as
+patrolled" link which is shown both on the diff header as well as on the bottom
+of a page, usually wrapped in a span element which has class="patrollink".
$differenceEngine: DifferenceEngine object
&$markAsPatrolledLink: The "mark as patrolled" link HTML (string)
$rcid: Recent change ID (rc_id) for this change (int)
-'DifferenceEngineMarkPatrolledRCID': Allows extensions to possibly change the rcid parameter.
-For example the rcid might be set to zero due to the user being the same as the
-performer of the change but an extension might still want to show it under certain
-conditions.
+'DifferenceEngineMarkPatrolledRCID': Allows extensions to possibly change the
+rcid parameter. For example the rcid might be set to zero due to the user being
+the same as the performer of the change but an extension might still want to
+show it under certain conditions.
&$rcid: rc_id (int) of the change or 0
$differenceEngine: DifferenceEngine object
$change: RecentChange object
$user: User object representing the current user
-'DifferenceEngineNewHeader': Allows extensions to change the $newHeader variable, which
-contains information about the new revision, such as the revision's author, whether
-the revision was marked as a minor edit or not, etc.
+'DifferenceEngineNewHeader': Allows extensions to change the $newHeader
+variable, which contains information about the new revision, such as the
+revision's author, whether the revision was marked as a minor edit or not, etc.
$differenceEngine: DifferenceEngine object
&$newHeader: The string containing the various #mw-diff-otitle[1-5] divs, which
-include things like revision author info, revision comment, RevisionDelete link and more
+ include things like revision author info, revision comment, RevisionDelete
+ link and more
$formattedRevisionTools: Array containing revision tools, some of which may have
-been injected with the DiffRevisionTools hook
-$nextlink: String containing the link to the next revision (if any); also included in $newHeader
-$rollback: Rollback link (string) to roll this revision back to the previous one, if any
+ been injected with the DiffRevisionTools hook
+$nextlink: String containing the link to the next revision (if any); also
+ included in $newHeader
+$rollback: Rollback link (string) to roll this revision back to the previous
+ one, if any
$newminor: String indicating if the new revision was marked as a minor edit
$diffOnly: Boolean parameter passed to DifferenceEngine#showDiffPage, indicating
-whether we should show just the diff; passed in as a query string parameter to the
-various URLs constructed here (i.e. $nextlink)
+ whether we should show just the diff; passed in as a query string parameter to
+ the various URLs constructed here (i.e. $nextlink)
$rdel: RevisionDelete link for the new revision, if the current user is allowed
-to use the RevisionDelete feature
+ to use the RevisionDelete feature
$unhide: Boolean parameter indicating whether to show RevisionDeleted revisions
-'DifferenceEngineOldHeader': Allows extensions to change the $oldHeader variable, which
-contains information about the old revision, such as the revision's author, whether
-the revision was marked as a minor edit or not, etc.
+'DifferenceEngineOldHeader': Allows extensions to change the $oldHeader
+variable, which contains information about the old revision, such as the
+revision's author, whether the revision was marked as a minor edit or not, etc.
$differenceEngine: DifferenceEngine object
&$oldHeader: The string containing the various #mw-diff-otitle[1-5] divs, which
-include things like revision author info, revision comment, RevisionDelete link and more
-$prevlink: String containing the link to the previous revision (if any); also included in $oldHeader
+ include things like revision author info, revision comment, RevisionDelete
+ link and more
+$prevlink: String containing the link to the previous revision (if any); also
+ included in $oldHeader
$oldminor: String indicating if the old revision was marked as a minor edit
$diffOnly: Boolean parameter passed to DifferenceEngine#showDiffPage, indicating
-whether we should show just the diff; passed in as a query string parameter to the
-various URLs constructed here (i.e. $prevlink)
+ whether we should show just the diff; passed in as a query string parameter to
+ the various URLs constructed here (i.e. $prevlink)
$ldel: RevisionDelete link for the old revision, if the current user is allowed
-to use the RevisionDelete feature
+ to use the RevisionDelete feature
$unhide: Boolean parameter indicating whether to show RevisionDeleted revisions
-'DifferenceEngineOldHeaderNoOldRev': Change the $oldHeader variable in cases when
-there is no old revision
+'DifferenceEngineOldHeaderNoOldRev': Change the $oldHeader variable in cases
+when there is no old revision
&$oldHeader: empty string by default
-'DifferenceEngineRenderRevisionAddParserOutput': Allows extensions to change the parser output.
-Return false to not add parser output via OutputPage's addParserOutput method.
+'DifferenceEngineRenderRevisionAddParserOutput': Allows extensions to change the
+parser output. Return false to not add parser output via OutputPage's
+addParserOutput method.
$differenceEngine: DifferenceEngine object
$out: OutputPage object
$parserOutput: ParserOutput object
$wikiPage: WikiPage object
-'DifferenceEngineRenderRevisionShowFinalPatrolLink': An extension can hook into this hook
-point and return false to not show the final "mark as patrolled" link on the bottom
-of a page.
+'DifferenceEngineRenderRevisionShowFinalPatrolLink': An extension can hook into
+this hook point and return false to not show the final "mark as patrolled" link
+on the bottom of a page.
This hook has no arguments.
'DifferenceEngineShowDiff': Allows extensions to affect the diff text which
eventually gets sent to the OutputPage object.
$differenceEngine: DifferenceEngine object
-'DifferenceEngineShowEmptyOldContent': Allows extensions to change the diff table
-body (without header) in cases when there is no old revision or the old and new
-revisions are identical.
+'DifferenceEngineShowEmptyOldContent': Allows extensions to change the diff
+table body (without header) in cases when there is no old revision or the old
+and new revisions are identical.
$differenceEngine: DifferenceEngine object
-'DifferenceEngineShowDiffPage': Add additional output via the available OutputPage
-object into the diff view
+'DifferenceEngineShowDiffPage': Add additional output via the available
+OutputPage object into the diff view
$out: OutputPage object
'DifferenceEngineShowDiffPageMaybeShowMissingRevision': called in
&$article: article (object) being viewed
&$oldid: oldid (int) being viewed
-'DoEditSectionLink': DEPRECATED! Use SkinEditSectionLinks instead.
+'DoEditSectionLink': DEPRECATED since 1.25! Use SkinEditSectionLinks instead.
Override the HTML generated for section edit links
$skin: Skin object rendering the UI
$title: Title object for the title being linked to (may not be the same as
&$error: Error message to return
$summary: Edit summary for page
-
'EditFilterMergedContent': Post-section-merge edit filter.
This may be triggered by the EditPage or any other facility that modifies page
content. Use the $status object to indicate whether the edit should be allowed,
true to continue. Returning true if $status->isOK() returns false means "don't
save but continue user interaction", e.g. show the edit form.
$status->apiHookResult can be set to an array to be returned by api.php
-action=edit. This is used to deliver captchas.
+ action=edit. This is used to deliver captchas.
$context: object implementing the IContextSource interface.
$content: content of the edit box, as a Content object.
$status: Status object to represent errors, etc.
'EditPage::importFormData': allow extensions to read additional data
posted in the form
+Return value is ignored (should always return true)
$editpage: EditPage instance
$request: Webrequest
-return value is ignored (should always return true)
'EditPage::showEditForm:fields': allows injection of form field into edit form
Return value is ignored (should always return true)
&$buttons: Array of edit buttons "Save", "Preview", "Live", and "Diff"
&$tabindex: HTML tabindex of the last edit check/button
-'EditPageBeforeEditChecks': DEPRECATED! Use 'EditPageGetCheckboxesDefinition' instead,
-or 'EditPage::showStandardInputs:options' if you don't actually care about checkboxes
-and just want to add some HTML to the page.
-Allows modifying the edit checks below the textarea in the edit form.
-&$editpage: The current EditPage object
-&$checks: Array of the HTML for edit checks like "watch this page"/"minor edit"
-&$tabindex: HTML tabindex of the last edit check/button
-
'EditPageBeforeEditToolbar': Allows modifying the edit toolbar above the
textarea in the edit form.
+Hook subscribers can return false to avoid the default toolbar code being
+loaded.
&$toolbar: The toolbar HTML
-Hook subscribers can return false to avoid the default toolbar code being loaded.
'EditPageCopyrightWarning': Allow for site and per-namespace customization of
contribution/copyright notice.
'EditPageGetCheckboxesDefinition': Allows modifying the edit checkboxes
below the textarea in the edit form.
$editpage: The current EditPage object
-&$checkboxes: Array of checkbox definitions. See EditPage::getCheckboxesDefinition()
-for the format.
+&$checkboxes: Array of checkbox definitions. See
+ EditPage::getCheckboxesDefinition() for the format.
'EditPageGetDiffContent': Allow modifying the wikitext that will be used in
"Show changes". Note that it is preferable to implement diff handling for
&$from: MailAddress object of sending user
&$subject: subject of the mail
&$text: text of the mail
-&$error: Out-param for an error. Should be set to a Status object or boolean false.
+&$error: Out-param for an error. Should be set to a Status object or boolean
+ false.
'EmailUserCC': Before sending the copy of the email to the author.
&$to: MailAddress object of receiving user
a grouped recent change inner line in EnhancedChangesList.
Hook subscribers can return false to omit this line from recentchanges.
$changesList: EnhancedChangesList object
-&$data: An array with all the components that will be joined in order to create the line
+&$data: An array with all the components that will be joined in order to create
+ the line
$block: An array of RecentChange objects in that block
$rc: The RecentChange object for this line
&$classes: An array of classes to change
'EnhancedChangesListModifyBlockLineData': to alter data used to build
a non-grouped recent change line in EnhancedChangesList.
$changesList: EnhancedChangesList object
-&$data: An array with all the components that will be joined in order to create the line
+&$data: An array with all the components that will be joined in order to create
+ the line
$rc: The RecentChange object for this line
'ExemptFromAccountCreationThrottle': Exemption from the account creation
'GetExtendedMetadata': Get extended file metadata for the API
&$combinedMeta: Array of the form:
- 'MetadataPropName' => array(
+ 'MetadataPropName' => [
value' => prop value,
'source' => 'name of hook'
- ).
+ ].
$file: File object of file in question
$context: RequestContext (including language to use)
$single: Only extract the current language; if false, the prop value should
'getUserPermissionsErrors': Add a permissions error when permissions errors are
checked for. Use instead of userCan for most cases. Return false if the user
can't do it, and populate $result with the reason in the form of
-array( messagename, param1, param2, ... ) or a MessageSpecifier instance (you
+[ messagename, param1, param2, ... ] or a MessageSpecifier instance (you
might want to use ApiMessage to provide machine-readable details for the API).
For consistency, error messages
should be plain text with no special coloring, bolding, etc. to show that
'getUserPermissionsErrorsExpensive': Equal to getUserPermissionsErrors, but is
called only if expensive checks are enabled. Add a permissions error when
permissions errors are checked for. Return false if the user can't do it, and
-populate $result with the reason in the form of array( messagename, param1,
-param2, ... ) or a MessageSpecifier instance (you might want to use ApiMessage
-to provide machine-readable details for the API). For consistency, error
-messages should be plain text with no
-special coloring, bolding, etc. to show that they're errors; presenting them
-properly to the user as errors is done by the caller.
+populate $result with the reason in the form of [ messagename, param1, param2,
+... ] or a MessageSpecifier instance (you might want to use ApiMessage to
+provide machine-readable details for the API). For consistency, error messages
+should be plain text with no special coloring, bolding, etc. to show that
+they're errors; presenting them properly to the user as errors is done by the
+caller.
&$title: Title object being checked against
&$user: Current user object
$action: Action being checked
includes/Linker.php for Linker::makeImageLink
&$time: Timestamp of file in 'YYYYMMDDHHIISS' string form, or false for current
&$res: Final HTML output, used if you return false
+$parser: Parser instance
+&$query: Query params for desc URL
+&$widthOption: Used by the parser to remember the user preference thumbnailsize
'ImageOpenShowImageInlineBefore': Call potential extension just before showing
the image on an image page.
Return false to stop further processing of the tag
$reader: XMLReader object
-'ImportHandleUnknownUser': When a user doesn't exist locally, this hook is called
-to give extensions an opportunity to auto-create it. If the auto-creation is
-successful, return false.
+'ImportHandleUnknownUser': When a user doesn't exist locally, this hook is
+called to give extensions an opportunity to auto-create it. If the auto-creation
+is successful, return false.
$name: User name
'ImportHandleUploadXMLTag': When parsing a XML tag in a file upload.
$code: The language code or the language we're looking for a messages file for
&$file: The messages file path, you can override this to change the location.
-'LanguageGetMagic': DEPRECATED! Use $magicWords in a file listed in
+'LanguageGetMagic': DEPRECATED since 1.16! Use $magicWords in a file listed in
$wgExtensionMessagesFiles instead.
Use this to define synonyms of magic words depending of the language
&$magicExtensions: associative array of magic words synonyms
$out: The output page.
$cssClassName: CSS class name of the language selector.
-'LinkBegin': DEPRECATED! Use HtmlPageLinkRendererBegin instead.
-Used when generating internal and interwiki links in
-Linker::link(), before processing starts. Return false to skip default
-processing and return $ret. See documentation for Linker::link() for details on
-the expected meanings of parameters.
+'LinkBegin': DEPRECATED since 1.28! Use HtmlPageLinkRendererBegin instead.
+Used when generating internal and interwiki links in Linker::link(), before
+processing starts. Return false to skip default processing and return $ret. See
+documentation for Linker::link() for details on the expected meanings of
+parameters.
$skin: the Skin object
$target: the Title that the link is pointing to
&$html: the contents that the <a> tag should have (raw HTML); null means
&$options: array of options. Can include 'known', 'broken', 'noclasses'.
&$ret: the value to return if your hook returns false.
-'LinkEnd': DEPRECATED! Use HtmlPageLinkRendererEnd hook instead
+'LinkEnd': DEPRECATED since 1.28! Use HtmlPageLinkRendererEnd hook instead
Used when generating internal and interwiki links in Linker::link(),
just before the function returns a value. If you return true, an <a> element
with HTML attributes $attribs and contents $html will be returned. If you
&$messages: Already added messages (inclusive messages from
LoginForm::$validErrorMessages)
-'LoginUserMigrated': DEPRECATED! Create a PreAuthenticationProvider instead.
-Called during login to allow extensions the opportunity to inform a user that
-their username doesn't exist for a specific reason, instead of letting the
-login form give the generic error message that the account does not exist. For
-example, when the account has been renamed or deleted.
+'LoginUserMigrated': DEPRECATED since 1.27! Create a PreAuthenticationProvider
+instead. Called during login to allow extensions the opportunity to inform a
+user that their username doesn't exist for a specific reason, instead of letting
+the login form give the generic error message that the account does not exist.
+For example, when the account has been renamed or deleted.
$user: the User object being authenticated against.
&$msg: the message identifier for abort reason, or an array to pass a message
key and parameters.
'NewPagesLineEnding': Called before a NewPages line is finished.
$page: the SpecialNewPages object
&$ret: the HTML line
-$row: the database row for this page (the recentchanges record and a few extras - see
- NewPagesPager::getQueryInfo)
+$row: the database row for this page (the recentchanges record and a few extras
+ - see NewPagesPager::getQueryInfo)
&$classes: the classes to add to the surrounding <li>
&$attribs: associative array of other HTML attributes for the <li> element.
Currently only data attributes reserved to MediaWiki are allowed
&$parser: Parser object
&$varCache: variable cache (array)
-'ParserLimitReport': DEPRECATED! Use ParserLimitReportPrepare and
+'ParserLimitReport': DEPRECATED since 1.22! Use ParserLimitReportPrepare and
ParserLimitReportFormat instead.
Called at the end of Parser:parse() when the parser will
include comments about size of the text parsed.
be invalid. To avoid bugs, you'll need to handle that somehow (e.g. with the
RejectParserCacheValue hook) because MediaWiki won't do it for you.
&$defaults: Set the default value for your option here.
-&$inCacheKey: To fragment the parser cache on your option, set a truthy value here.
+&$inCacheKey: To fragment the parser cache on your option, set a truthy value
+ here.
&$lazyLoad: To lazy-initialize your option, set it null in $defaults and set a
callable here. The callable is passed the ParserOptions object and the option
name.
that tests continue to run properly.
&$tables: array of table names
-'ParserOutputStashForEdit': Called when an edit stash parse finishes, before the output is cached.
+'ParserOutputStashForEdit': Called when an edit stash parse finishes, before the
+output is cached.
$page: the WikiPage of the candidate edit
$content: the Content object of the candidate edit
$output: the ParserOutput result of the candidate edit
&$legend: the legend text. Defaults to wfMessage( "prefs-$key" )->text() but may
be overridden
-'PrefixSearchBackend': DEPRECATED! Override SearchEngine::completionSearchBackend instead.
+'PrefixSearchBackend': DEPRECATED since 1.27! Override
+SearchEngine::completionSearchBackend instead.
Override the title prefix search used for OpenSearch and
AJAX search suggestions. Put results into &$results outparam and return false.
$ns: array of int namespace keys to search in
ResourceLoaderStartUpModule::getConfigSettings(). Use this to export static
configuration variables to JavaScript. Things that depend on the current page
or request state must be added through MakeGlobalVariablesScript instead.
-&$vars: array( variable name => value )
+&$vars: [ variable name => value ]
'ResourceLoaderJqueryMsgModuleMagicWords': Called in
ResourceLoaderJqueryMsgModule to allow adding magic words for jQueryMsg.
&$testModules: array of JavaScript testing modules. The 'qunit' framework,
included in core, is fed using tests/qunit/QUnitTestResources.php.
To add a new qunit module named 'myext.tests':
- $testModules['qunit']['myext.tests'] = array(
+ $testModules['qunit']['myext.tests'] = [
'script' => 'extension/myext/tests.js',
'dependencies' => <any module dependency you might have>
- );
+ ];
For QUnit framework, the mediawiki.tests.qunit.testrunner dependency will be
added to any module.
&$ResourceLoader: object
'RevisionRecordInserted': Called after a revision is inserted into the database.
$revisionRecord: the RevisionRecord that has just been inserted.
-'RevisionInsertComplete': DEPRECATED! Use RevisionRecordInserted hook instead.
-Called after a revision is inserted into the database.
+'RevisionInsertComplete': DEPRECATED since 1.31! Use RevisionRecordInserted hook
+instead. Called after a revision is inserted into the database.
$revision: the Revision
$data: DEPRECATED! Always null!
$flags: DEPRECATED! Always null!
'SearchResultsAugment': Allows extension to add its code to the list of search
result augmentors.
-&$setAugmentors: List of whole-set augmentor objects, must implement ResultSetAugmentor
-&$rowAugmentors: List of per-row augmentor objects, must implement ResultAugmentor.
-Note that lists should be in the format name => object and the names in both lists should
-be distinct.
+&$setAugmentors: List of whole-set augmentor objects, must implement
+ ResultSetAugmentor.
+&$rowAugmentors: List of per-row augmentor objects, must implement
+ ResultAugmentor.
+Note that lists should be in the format name => object and the names in both
+ lists should be distinct.
'SecondaryDataUpdates': Allows modification of the list of DataUpdates to
perform when page content is modified. Currently called by
'ShowSearchHitTitle': Customise display of search hit title/link.
&$title: Title to link to
-&$titleSnippet: Label for the link representing the search result. Typically the article title.
+&$titleSnippet: Label for the link representing the search result. Typically the
+ article title.
$result: The SearchResult object
$terms: String of the search terms entered
$specialSearch: The SpecialSearch object
-&$query: Array of query string parameters for the link representing the search result.
+&$query: Array of query string parameters for the link representing the search
+ result.
&$attributes: Array of title link attributes, can be modified by extension.
'SidebarBeforeOutput': Allows to edit sidebar just before it is output by skins.
'SpecialListusersFormatRow': Called right before the end of
UsersPager::formatRow().
-&$item: HTML to be returned. Will be wrapped in <li></li> after the hook finishes
+&$item: HTML to be returned. Will be wrapped in an <li> after the hook finishes
$row: Database row object
'SpecialListusersHeader': Called after adding the submit button in
&$title: If the hook returns false, a Title object to use instead of the
result from the normal query
-'SpecialRecentChangesFilters': DEPRECATED! Use ChangesListSpecialPageStructuredFilters
-instead.
+'SpecialRecentChangesFilters': DEPRECATED since 1.23! Use
+ChangesListSpecialPageStructuredFilters instead.
Called after building form options at RecentChanges.
$special: the special page object
&$filters: associative array of filter definitions. The keys are the HTML
&$extraOpts: array of added items, to which can be added
$opts: FormOptions for this request
-'SpecialRecentChangesQuery': DEPRECATED! Use ChangesListSpecialPageStructuredFilters
-or ChangesListSpecialPageQuery instead.
+'SpecialRecentChangesQuery': DEPRECATED since 1.23! Use
+ChangesListSpecialPageStructuredFilters or ChangesListSpecialPageQuery instead.
Called when building SQL query for SpecialRecentChanges and
SpecialRecentChangesLinked.
&$conds: array of WHERE conditionals for query
'SpecialResetTokensTokens': Called when building token list for
SpecialResetTokens.
&$tokens: array of token information arrays in the format of
- array(
+ [
'preference' => '<preference-name>',
'label-message' => '<message-key>',
- )
+ ]
'SpecialSearchCreateLink': Called when making the message to create a page or
go to the existing page.
specified URL.
$term: The string the user searched for
$title: The title the 'go' feature has decided to forward the user to
-&$url: Initially null, hook subscribers can set this to specify the final url to redirect to
+&$url: Initially null, hook subscribers can set this to specify the final url to
+ redirect to
'SpecialSearchNogomatch': Called when the 'Go' feature is triggered (generally
from autocomplete search other than the main bar on Special:Search) and the
message key to use in the name column,
$context: IContextSource object
-'SpecialTrackingCategories::preprocess': Called after LinkBatch on Special:TrackingCategories
+'SpecialTrackingCategories::preprocess': Called after LinkBatch on
+Special:TrackingCategories
$specialPage: The SpecialTrackingCategories object
-$trackingCategories: Array of data from Special:TrackingCategories with msg and cats
+$trackingCategories: Array of data from Special:TrackingCategories with msg and
+ cats
-'SpecialTrackingCategories::generateCatLink': Called for each cat link on Special:TrackingCategories
+'SpecialTrackingCategories::generateCatLink': Called for each cat link on
+Special:TrackingCategories
$specialPage: The SpecialTrackingCategories object
$catTitle: The Title object of the linked category
&$html: The Result html
$wgVersion: Current $wgVersion for you to use
&$versionUrl: Raw url to link to (eg: release notes)
-'SpecialWatchlistFilters': DEPRECATED! Use ChangesListSpecialPageStructuredFilters
-instead.
+'SpecialWatchlistFilters': DEPRECATED since 1.23! Use
+ChangesListSpecialPageStructuredFilters instead.
Called after building form options at Watchlist.
$special: the special page object
&$filters: associative array of filter definitions. The keys are the HTML
inserted to rc_type so they can be returned as part of the watchlist.
&$nonRevisionTypes: array of values in the rc_type field of recentchanges table
-'SpecialWatchlistQuery': DEPRECATED! Use ChangesListSpecialPageStructuredFilters
-or ChangesListSpecialPageQuery instead.
+'SpecialWatchlistQuery': DEPRECATED since 1.23! Use
+ChangesListSpecialPageStructuredFilters or ChangesListSpecialPageQuery instead.
Called when building sql query for SpecialWatchlist.
&$conds: array of WHERE conditionals for query
&$tables: array of tables to be queried
$nt: new title
$user: user who does the move
-'TitleMoveStarting': Before moving an article (title), but just after the atomic DB section starts.
+'TitleMoveStarting': Before moving an article (title), but just after the atomic
+DB section starts.
$old: old title
$nt: new title
$user: user who does the move
$title: title object related to the revision
$rev: revision (object) that will be viewed
-'UnitTestsAfterDatabaseSetup': Called right after MediaWiki's test infrastructure
-has finished creating/duplicating core tables for unit tests.
+'UnitTestsAfterDatabaseSetup': Called right after MediaWiki's test
+infrastructure has finished creating/duplicating core tables for unit tests.
$database: Database in question
$prefix: Table prefix to be used in unit tests
test case files matching the suffix "Test.php".
&$paths: list of test cases and directories to search.
-'UnknownAction': DEPRECATED! To add an action in an extension,
+'UnknownAction': DEPRECATED since 1.19! To add an action in an extension,
create a subclass of Action, and add a new key to $wgActions.
An unknown "action" has occurred (useful for defining your own actions).
$action: action name
$upload: (object) An instance of UploadBase, with all info about the upload
$user: (object) An instance of User, the user uploading this file
$props: (array) File properties, as returned by FSFile::getPropsFromPath()
-&$error: output: If the file stashing should be prevented, set this to the reason
- in the form of array( messagename, param1, param2, ... ) or a MessageSpecifier
- instance (you might want to use ApiMessage to provide machine-readable details
- for the API).
+&$error: output: If the file stashing should be prevented, set this to the
+ reason in the form of [ messagename, param1, param2, ... ] or a
+ MessageSpecifier instance (you might want to use ApiMessage to provide machine
+ -readable details for the API).
-'UploadVerification': DEPRECATED! Use UploadVerifyFile instead.
+'UploadVerification': DEPRECATED since 1.28! Use UploadVerifyFile instead.
Additional chances to reject an uploaded file.
$saveName: (string) destination file name
$tempName: (string) filesystem path to the temporary file for checks
$upload: (object) an instance of UploadBase, with all info about the upload
$mime: (string) The uploaded file's MIME type, as detected by MediaWiki.
Handlers will typically only apply for specific MIME types.
-&$error: (object) output: true if the file is valid. Otherwise, set this to the reason
- in the form of array( messagename, param1, param2, ... ) or a MessageSpecifier
- instance (you might want to use ApiMessage to provide machine-readable details
- for the API).
+&$error: (object) output: true if the file is valid. Otherwise, set this to the
+ reason in the form of [ messagename, param1, param2, ... ] or a
+ MessageSpecifier instance (you might want to use ApiMessage to provide machine
+ -readable details for the API).
'UploadVerifyUpload': Upload verification, based on both file properties like
MIME type (same as UploadVerifyFile) and the information entered by the user
$comment: (string) Upload log comment (also used as edit summary)
$pageText: (string) File description page text (only used for new uploads)
&$error: output: If the file upload should be prevented, set this to the reason
- in the form of array( messagename, param1, param2, ... ) or a MessageSpecifier
+ in the form of [ messagename, param1, param2, ... ] or a MessageSpecifier
instance (you might want to use ApiMessage to provide machine-readable details
for the API).
&$user: User (object) that will clear the message
$oldid: ID of the talk page revision being viewed (0 means the most recent one)
-'UserCreateForm': DEPRECATED! Create an AuthenticationProvider instead.
-Manipulate the login form.
+'UserCreateForm': DEPRECATED since 1.27! Create an AuthenticationProvider
+instead. Manipulate the login form.
&$template: SimpleTemplate instance for the form
'UserEffectiveGroups': Called in User::getEffectiveGroups().
&$timestamp: timestamp, change this to override local email authentication
timestamp
-'UserGetImplicitGroups': DEPRECATED!
+'UserGetImplicitGroups': DEPRECATED since 1.25!
Called in User::getImplicitGroups().
&$groups: List of implicit (automatically-assigned) groups
$user: user object
&$s: database query object
-'UserLoadFromSession': DEPRECATED! Create a MediaWiki\Session\SessionProvider instead.
+'UserLoadFromSession': DEPRECATED since 1.27! Create a
+MediaWiki\Session\SessionProvider instead.
Called to authenticate users on external/environmental means; occurs before
session is loaded.
$user: user object being loaded
'UserLoggedIn': Called after a user is logged in
$user: User object for the logged-in user
-'UserLoginComplete': Show custom content after a user has logged in via the web interface.
-For functionality that needs to run after any login (API or web) use UserLoggedIn.
+'UserLoginComplete': Show custom content after a user has logged in via the Web
+interface. For functionality that needs to run after any login (API or web) use
+UserLoggedIn.
&$user: the user object that was created on login
&$inject_html: Any HTML to inject after the "logged in" message.
-$direct: (bool) The hook is called directly after a successful login. This will only happen once
- per login. A UserLoginComplete call with direct=false can happen when the user visits the login
- page while already logged in.
+$direct: (bool) The hook is called directly after a successful login. This will
+ only happen once per login. A UserLoginComplete call with direct=false can
+ happen when the user visits the login page while already logged in.
-'UserLoginForm': DEPRECATED! Create an AuthenticationProvider instead.
-Manipulate the login form.
+'UserLoginForm': DEPRECATED since 1.27! Create an AuthenticationProvider
+instead. Manipulate the login form.
&$template: QuickTemplate instance for the form
'UserLogout': Before a user logs out.
'UserMailerSplitTo': Called in UserMailer::send() to give extensions a chance
to split up an email with multiple the To: field into separate emails.
-&$to: array of MailAddress objects; unset the ones which should be mailed separately
+&$to: array of MailAddress objects; unset the ones which should be mailed
+separately
-'UserMailerTransformContent': Called in UserMailer::send() to change email contents.
-Extensions can block sending the email by returning false and setting $error.
+'UserMailerTransformContent': Called in UserMailer::send() to change email
+contents. Extensions can block sending the email by returning false and setting
+$error.
$to: array of MailAdresses of the targets
$from: MailAddress of the sender
-&$body: email body, either a string (for plaintext emails) or an array with 'text' and 'html' keys
+&$body: email body, either a string (for plaintext emails) or an array with
+ 'text' and 'html' keys
&$error: should be set to an error message string
-'UserMailerTransformMessage': Called in UserMailer::send() to change email after it has gone through
-the MIME transform. Extensions can block sending the email by returning false and setting $error.
+'UserMailerTransformMessage': Called in UserMailer::send() to change email after
+it has gone through the MIME transform. Extensions can block sending the email
+by returning false and setting $error.
$to: array of MailAdresses of the targets
$from: MailAddress of the sender
&$subject: email subject (not MIME encoded)
-&$headers: email headers (except To: and Subject:) as an array of header name => value pairs
+&$headers: email headers (except To: and Subject:) as an array of header
+name => value pairs
&$body: email body (in MIME format) as a string
&$error: should be set to an error message string
&$user: user retrieving new talks messages
&$talks: array of new talks page(s)
-'UserRights': DEPRECATED! Use UserGroupsChanged instead.
+'UserRights': DEPRECATED since 1.26! Use UserGroupsChanged instead.
After a user's group memberships are changed.
&$user: User object that was changed
$add: Array of strings corresponding to groups added
$remove: Array of strings corresponding to groups removed
-'UserSaveOptions': Called just before saving user preferences. Hook handlers can either add or
-manipulate options, or reset one back to it's default to block changing it. Hook handlers are also
-allowed to abort the process by returning false, e.g. to save to a global profile instead. Compare
-to the UserSaveSettings hook, which is called after the preferences have been saved.
+'UserSaveOptions': Called just before saving user preferences. Hook handlers can
+either add or manipulate options, or reset one back to it's default to block
+changing it. Hook handlers are also allowed to abort the process by returning
+false, e.g. to save to a global profile instead. Compare to the UserSaveSettings
+hook, which is called after the preferences have been saved.
$user: The User for which the options are going to be saved
&$options: The users options as an associative array, modifiable
-'UserSaveSettings': Called directly after user preferences (user_properties in the database) have
-been saved. Compare to the UserSaveOptions hook, which is called before.
+'UserSaveSettings': Called directly after user preferences (user_properties in
+the database) have been saved. Compare to the UserSaveOptions hook, which is
+called before.
$user: The User for which the options have been saved
-'UserSetCookies': DEPRECATED! If you're trying to replace core session cookie
-handling, you want to create a subclass of MediaWiki\Session\CookieSessionProvider
-instead. Otherwise, you can no longer count on user data being saved to cookies
-versus some other mechanism.
+'UserSetCookies': DEPRECATED since 1.27! If you're trying to replace core
+session cookie handling, you want to create a subclass of
+MediaWiki\Session\CookieSessionProvider instead. Otherwise, you can no longer
+count on user data being saved to cookies versus some other mechanism.
Called when setting user cookies.
$user: User object
&$session: session array, will be added to the session
$dbr: Read-only database handle
$userIds: Array of user IDs whose groups we should look up
&$cache: Array of user ID -> (array of internal group name (e.g. 'sysop') ->
-UserGroupMembership object)
+ UserGroupMembership object)
&$groups: Array of group name -> bool true mappings for members of a given user
group
&$opts: Options to use for the query
&$join: Join conditions
-'WikiPageDeletionUpdates': manipulate the list of DeferrableUpdates to be applied when
-a page is deleted. Called in WikiPage::getDeletionUpdates(). Note that updates
-specific to a content model should be provided by the respective Content's
-getDeletionUpdates() method.
+'WikiPageDeletionUpdates': manipulate the list of DeferrableUpdates to be
+applied when a page is deleted. Called in WikiPage::getDeletionUpdates(). Note
+that updates specific to a content model should be provided by the respective
+Content's getDeletionUpdates() method.
$page: the WikiPage
-$content: the Content to generate updates for, or null in case the page revision could not be
- loaded. The delete will succeed despite this.
-&$updates: the array of objects that implement DeferrableUpdate. Hook function may want to add to
- it.
+$content: the Content to generate updates for, or null in case the page revision
+ could not be loaded. The delete will succeed despite this.
+&$updates: the array of objects that implement DeferrableUpdate. Hook function
+ may want to add to it.
'WikiPageFactory': Override WikiPage class used for a title
$title: Title of the page
# this breaks strtotime().
$modsince = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] );
$modsinceTime = strtotime( $modsince );
- $ismodsince = wfTimestamp( TS_MW, $modsinceTime ? $modsinceTime : 1 );
+ $ismodsince = wfTimestamp( TS_MW, $modsinceTime ?: 1 );
wfDebug( "$fname: -- client send If-Modified-Since: $modsince", 'private' );
wfDebug( "$fname: -- we might send Last-Modified : $lastmod", 'private' );
* @return string
*/
public function getDomain() {
- if ( isset( $this->domain ) ) {
- return $this->domain;
- } else {
- return 'invaliddomain';
- }
+ return $this->domain ?? 'invaliddomain';
}
/**
$dbw->startAtomic( __METHOD__ );
+ // Lock the `category` row before locking `categorylinks` rows to try
+ // to avoid deadlocks with LinksDeletionUpdate (T195397)
+ $dbw->selectField(
+ 'category',
+ 1,
+ [ 'cat_title' => $this->mName ],
+ __METHOD__,
+ [ 'FOR UPDATE' ]
+ );
+
// Lock all the `categorylinks` records and gaps for this category;
// this is a separate query due to postgres/oracle limitations
$dbw->selectRowCount(
*/
class DummyLinker {
- /**
- * @deprecated since 1.28, use LinkRenderer::getLinkClasses() instead
- */
- public function getLinkColour( $t, $threshold ) {
- wfDeprecated( __METHOD__, '1.28' );
- return Linker::getLinkColour( $t, $threshold );
- }
-
public function link(
$target,
$html = null,
$this->autoSumm = md5( '' );
}
- $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
+ $autosumm = $this->autoSumm ?: md5( $this->summary );
$out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
$out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
);
}
- // Backwards-compatibility hack to run the EditPageBeforeEditChecks hook. It's important,
- // people have used it for the weirdest things completely unrelated to checkboxes...
- // And if we're gonna run it, might as well allow its legacy checkboxes to be shown.
- $legacyCheckboxes = [];
- if ( !$this->isNew ) {
- $legacyCheckboxes['minor'] = '';
- }
- $legacyCheckboxes['watch'] = '';
- // Copy new-style checkboxes into an old-style structure
- foreach ( $checkboxes as $name => $oouiLayout ) {
- $legacyCheckboxes[$name] = (string)$oouiLayout;
- }
- // Avoid PHP 7.1 warning of passing $this by reference
- $ep = $this;
- Hooks::run( 'EditPageBeforeEditChecks', [ &$ep, &$legacyCheckboxes, &$tabindex ], '1.29' );
- // Copy back any additional old-style checkboxes into the new-style structure
- foreach ( $legacyCheckboxes as $name => $html ) {
- if ( $html && !isset( $checkboxes[$name] ) ) {
- $checkboxes[$name] = new OOUI\Widget( [ 'content' => new OOUI\HtmlSnippet( $html ) ] );
- }
- }
-
return $checkboxes;
}
const TOOL_LINKS_NOBLOCK = 1;
const TOOL_LINKS_EMAIL = 2;
- /**
- * Return the CSS colour of a known link
- *
- * @deprecated since 1.28, use LinkRenderer::getLinkClasses() instead
- *
- * @since 1.16.3
- * @param LinkTarget $t
- * @param int $threshold User defined threshold
- * @return string CSS class
- */
- public static function getLinkColour( LinkTarget $t, $threshold ) {
- wfDeprecated( __METHOD__, '1.28' );
- $services = MediaWikiServices::getInstance();
- $linkRenderer = $services->getLinkRenderer();
- if ( $threshold !== $linkRenderer->getStubThreshold() ) {
- // Need to create a new instance with the right stub threshold...
- $linkRenderer = $services->getLinkRendererFactory()->create();
- $linkRenderer->setStubThreshold( $threshold );
- }
-
- return $linkRenderer->getLinkClasses( $t );
- }
-
/**
* This function returns an HTML link to the given target. It serves a few
* purposes:
$res = null;
$dummy = new DummyLinker;
if ( !Hooks::run( 'ImageBeforeProduceHTML', [ &$dummy, &$title,
- &$file, &$frameParams, &$handlerParams, &$time, &$res ] ) ) {
+ &$file, &$frameParams, &$handlerParams, &$time, &$res,
+ $parser, &$query, &$widthOption
+ ] ) ) {
return $res;
}
*/
public static function getCanonicalName( $index ) {
$nslist = self::getCanonicalNamespaces();
- if ( isset( $nslist[$index] ) ) {
- return $nslist[$index];
- } else {
- return false;
- }
+ return $nslist[$index] ?? false;
}
/**
if ( $ret === null || !$ret->isSpecialPage() ) {
// We can have urls with just ?diff=,?oldid= or even just ?diff=
$oldid = $request->getInt( 'oldid' );
- $oldid = $oldid ? $oldid : $request->getInt( 'diff' );
+ $oldid = $oldid ?: $request->getInt( 'diff' );
// Allow oldid to override a changed or missing title
if ( $oldid ) {
$rev = Revision::newFromId( $oldid );
// If $target is set, then a hook wanted to redirect.
if ( !$ignoreRedirect && ( $target || $page->isRedirect() ) ) {
// Is the target already set by an extension?
- $target = $target ? $target : $page->followRedirect();
+ $target = $target ?: $page->followRedirect();
if ( is_string( $target ) ) {
if ( !$this->config->get( 'DisableHardRedirects' ) ) {
// we'll need to redirect
MWExceptionHandler::rollbackMasterChangesAndLog( $e );
}
+ // Disable WebResponse setters for post-send processing (T191537).
+ WebResponse::disableForPostSend();
+
$blocksHttpClient = true;
// Defer everything else if possible...
$callback = function () use ( $mode, &$blocksHttpClient ) {
* @return mixed Property value or null if not found
*/
public function getProperty( $name ) {
- if ( isset( $this->mProperties[$name] ) ) {
- return $this->mProperties[$name];
- } else {
- return null;
- }
+ return $this->mProperties[$name] ?? null;
}
/**
// Pre-process information
$separatorTransTable = $lang->separatorTransformTable();
- $separatorTransTable = $separatorTransTable ? $separatorTransTable : [];
+ $separatorTransTable = $separatorTransTable ?: [];
$compactSeparatorTransTable = [
implode( "\t", array_keys( $separatorTransTable ) ),
implode( "\t", $separatorTransTable ),
];
$digitTransTable = $lang->digitTransformTable();
- $digitTransTable = $digitTransTable ? $digitTransTable : [];
+ $digitTransTable = $digitTransTable ?: [];
$compactDigitTransTable = [
implode( "\t", array_keys( $digitTransTable ) ),
implode( "\t", $digitTransTable ),
if ( !defined( 'MW_NO_SESSION' ) && !$wgCommandLineMode ) {
// If session.auto_start is there, we can't touch session name
if ( $wgPHPSessionHandling !== 'disable' && !wfIniGetBool( 'session.auto_start' ) ) {
- session_name( $wgSessionName ? $wgSessionName : $wgCookiePrefix . '_session' );
+ session_name( $wgSessionName ?: $wgCookiePrefix . '_session' );
}
// Create the SessionManager singleton and set up our session handler,
return $errors;
}
- if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) {
+ if ( $wgEmailConfirmToEdit
+ && !$user->isEmailConfirmed()
+ && $action === 'edit'
+ ) {
$errors[] = [ 'confirmedittext' ];
}
*/
protected static $setCookies = [];
+ /** @var bool Used to disable setters before running jobs post-request (T191537) */
+ protected static $disableForPostSend = false;
+
+ /**
+ * Disable setters for post-send processing
+ *
+ * After this call, self::setCookie(), self::header(), and
+ * self::statusHeader() will log a warning and return without
+ * setting cookies or headers.
+ *
+ * @since 1.32
+ */
+ public static function disableForPostSend() {
+ self::$disableForPostSend = true;
+ }
+
/**
* Output an HTTP header, wrapper for PHP's header()
* @param string $string Header to output
* @param null|int $http_response_code Forces the HTTP response code to the specified value.
*/
public function header( $string, $replace = true, $http_response_code = null ) {
+ if ( self::$disableForPostSend ) {
+ wfDebugLog( 'header', 'ignored post-send header {header}', 'all', [
+ 'header' => $string,
+ 'replace' => $replace,
+ 'http_response_code' => $http_response_code,
+ 'exception' => new RuntimeException( 'Ignored post-send header' ),
+ ] );
+ return;
+ }
+
\MediaWiki\HeaderCallback::warnIfHeadersSent();
if ( $http_response_code ) {
header( $string, $replace, $http_response_code );
* @param int $code Status code
*/
public function statusHeader( $code ) {
+ if ( self::$disableForPostSend ) {
+ wfDebugLog( 'header', 'ignored post-send status header {code}', 'all', [
+ 'code' => $code,
+ 'exception' => new RuntimeException( 'Ignored post-send status header' ),
+ ] );
+ return;
+ }
+
HttpStatus::header( $code );
}
$expire = time() + $wgCookieExpiration;
}
+ $cookie = $options['prefix'] . $name;
+ $data = [
+ 'name' => (string)$cookie,
+ 'value' => (string)$value,
+ 'expire' => (int)$expire,
+ 'path' => (string)$options['path'],
+ 'domain' => (string)$options['domain'],
+ 'secure' => (bool)$options['secure'],
+ 'httpOnly' => (bool)$options['httpOnly'],
+ ];
+
+ if ( self::$disableForPostSend ) {
+ wfDebugLog( 'cookie', 'ignored post-send cookie {cookie}', 'all', [
+ 'cookie' => $cookie,
+ 'data' => $data,
+ 'exception' => new RuntimeException( 'Ignored post-send cookie' ),
+ ] );
+ return;
+ }
+
$func = $options['raw'] ? 'setrawcookie' : 'setcookie';
if ( Hooks::run( 'WebResponseSetCookie', [ &$name, &$value, &$expire, &$options ] ) ) {
- $cookie = $options['prefix'] . $name;
- $data = [
- 'name' => (string)$cookie,
- 'value' => (string)$value,
- 'expire' => (int)$expire,
- 'path' => (string)$options['path'],
- 'domain' => (string)$options['domain'],
- 'secure' => (bool)$options['secure'],
- 'httpOnly' => (bool)$options['httpOnly'],
- ];
-
// Per RFC 6265, key is name + domain + path
$key = "{$data['name']}\n{$data['domain']}\n{$data['path']}";
public function getHeader( $key ) {
$key = strtoupper( $key );
- if ( isset( $this->headers[$key] ) ) {
- return $this->headers[$key];
- }
- return null;
+ return $this->headers[$key] ?? null;
}
/**
* @return array|null
*/
public function getCookieData( $name ) {
- if ( isset( $this->cookies[$name] ) ) {
- return $this->cookies[$name];
- }
- return null;
+ return $this->cookies[$name] ?? null;
}
/**
* @return string|null
*/
public function getAttribute( $name ) {
- if ( isset( $this->attributes[$name] ) ) {
- return $this->attributes[$name];
- } else {
- return null;
- }
+ return $this->attributes[$name] ?? null;
}
/**
$offset = $params['offset'];
$searchEngine = $this->buildSearchEngine( $params );
- $titles = $searchEngine->extractTitles( $searchEngine->completionSearchWithVariants( $search ) );
+ $suggestions = $searchEngine->completionSearchWithVariants( $search );
+ $titles = $searchEngine->extractTitles( $suggestions );
+
+ if ( $suggestions->hasMoreResults() ) {
+ $this->setContinueEnumParameter( 'offset', $offset + $limit );
+ }
if ( $resultPageSet ) {
$resultPageSet->setRedirectMergePolicy( function ( array $current, array $new ) {
}
return $current;
} );
- if ( count( $titles ) > $limit ) {
- $this->setContinueEnumParameter( 'offset', $offset + $limit );
- array_pop( $titles );
- }
$resultPageSet->populateFromTitles( $titles );
foreach ( $titles as $index => $title ) {
$resultPageSet->setGeneratorData( $title, [ 'index' => $index + $offset + 1 ] );
$result = $this->getResult();
$count = 0;
foreach ( $titles as $title ) {
- if ( ++$count > $limit ) {
- $this->setContinueEnumParameter( 'offset', $offset + $limit );
- break;
- }
$vals = [
'ns' => intval( $title->getNamespace() ),
'title' => $title->getPrefixedText(),
$vals['pageid'] = intval( $title->getArticleID() );
}
$fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $vals );
+ ++$count;
if ( !$fit ) {
- $this->setContinueEnumParameter( 'offset', $offset + $count - 1 );
+ $this->setContinueEnumParameter( 'offset', $offset + $count );
break;
}
}
$terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
$titles = [];
$count = 0;
- $result = $matches->next();
$limit = $params['limit'];
- while ( $result ) {
- if ( ++$count > $limit ) {
- // We've reached the one extra which shows that there are
- // additional items to be had. Stop here...
- $this->setContinueEnumParameter( 'offset', $params['offset'] + $params['limit'] );
- break;
- }
+ if ( $matches->hasMoreResults() ) {
+ $this->setContinueEnumParameter( 'offset', $params['offset'] + $params['limit'] );
+ }
+ foreach ( $matches as $result ) {
+ $count++;
// Silently skip broken and missing titles
if ( $result->isBrokenTitle() || $result->isMissingRevision() ) {
- $result = $matches->next();
continue;
}
} else {
$titles[] = $result->getTitle();
}
-
- $result = $matches->next();
}
// Here we assume interwiki results do not count with
// Include number of results if requested
$totalhits += $interwikiMatches->getTotalHits();
- $result = $interwikiMatches->next();
- while ( $result ) {
+ foreach ( $interwikiMatches as $result ) {
$title = $result->getTitle();
$vals = $this->getSearchResultData( $result, $prop, $terms );
// pagination info so just bail out
break;
}
-
- $result = $interwikiMatches->next();
}
}
if ( $totalhits !== null ) {
if ( $alternatives[0] === null ) {
$alternatives[0] = self::$BACKEND_NULL_PARAM;
}
- $this->allowedParams['backend'] = [
+ $params['backend'] = [
ApiBase::PARAM_DFLT => $searchConfig->getSearchType(),
ApiBase::PARAM_TYPE => $alternatives,
];
* will be set:
* - backend: which search backend to use
* - limit: mandatory
- * - offset: optional, if set limit will be incremented by
- * one ( to support the continue parameter )
+ * - offset: optional
* - namespace: mandatory
* - search engine profiles defined by SearchApi::getSearchProfileParams()
* @param string[]|null $params API request params (must be sanitized by
$searchEngine = MediaWikiServices::getInstance()->getSearchEngineFactory()->create( $type );
$limit = $params['limit'];
$searchEngine->setNamespaces( $params['namespace'] );
- $offset = null;
- if ( isset( $params['offset'] ) ) {
- // If the API supports offset then it probably
- // wants to fetch limit+1 so it can check if
- // more results are available to properly set
- // the continue param
- $offset = $params['offset'];
- $limit += 1;
- }
+ $offset = isset( $params['offset'] ) ? $params['offset'] : null;
$searchEngine->setLimitOffset( $limit, $offset );
// Initialize requested search profiles.
$res = $dbr->select( $table, $fields, $conds, $comment, [], $joins );
foreach ( $res as $row ) {
- $this->cache[$row->user_name] = $row->up_value ? $row->up_value : $default;
+ $this->cache[$row->user_name] = $row->up_value ?: $default;
}
}
$this->loadSubitem( $code, $key, $subkey );
}
- if ( isset( $this->data[$code][$key][$subkey] ) ) {
- return $this->data[$code][$key][$subkey];
- } else {
- return null;
- }
+ return $this->data[$code][$key][$subkey] ?? null;
}
/**
if ( $this->pluralRules === null ) {
$this->loadPluralFiles();
}
- if ( !isset( $this->pluralRules[$code] ) ) {
- return null;
- } else {
- return $this->pluralRules[$code];
- }
+ return $this->pluralRules[$code] ?? null;
}
/**
if ( $this->pluralRuleTypes === null ) {
$this->loadPluralFiles();
}
- if ( !isset( $this->pluralRuleTypes[$code] ) ) {
- return null;
- } else {
- return $this->pluralRuleTypes[$code];
- }
+ return $this->pluralRuleTypes[$code] ?? null;
}
/**
}
foreach ( $data['preloadedMessages'] as $subkey ) {
- if ( isset( $data['messages'][$subkey] ) ) {
- $subitem = $data['messages'][$subkey];
- } else {
- $subitem = null;
- }
+ $subitem = $data['messages'][$subkey] ?? null;
$preload['messages'][$subkey] = $subitem;
}
continue;
}
- if ( isset( $this->mExtra['actionCommentIRC'] ) ) {
- $actionComment = $this->mExtra['actionCommentIRC'];
- } else {
- $actionComment = null;
- }
+ $actionComment = $this->mExtra['actionCommentIRC'] ?? null;
$feed = RCFeed::factory( $params );
$feed->notify( $this, $actionComment );
// $prevTags can be out of date on replica DBs, especially when addTags is called consecutively,
// causing loss of tags added recently in tag_summary table.
$prevTags = $dbw->selectField( 'tag_summary', 'ts_tags', $tsConds, __METHOD__ );
- $prevTags = $prevTags ? $prevTags : '';
+ $prevTags = $prevTags ?: '';
$prevTags = array_filter( explode( ',', $prevTags ) );
// add tags
'3.4' => '4.1',
];
- if ( isset( $map[$versionPrefix] ) ) {
- return $map[$versionPrefix];
- } else {
- return false;
- }
+ return $map[$versionPrefix] ?? false;
}
}
if ( $status->successCount == 0 ) {
$status->setOK( false );
}
- if ( isset( $status->value[0] ) ) {
- $status->value = $status->value[0];
- } else {
- $status->value = false;
- }
+ $status->value = $status->value[0] ?? false;
return $status;
}
}
}
- $image = $image ? $image : false; // type sanity
+ $image = $image ?: false; // type sanity
# Cache file existence or non-existence
if ( $useCache && ( !$image || $image->isCacheable() ) ) {
$this->cache->set( $dbkey, $time, $image );
}
if ( $index === 'local' ) {
return $this->localRepo;
- } elseif ( isset( $this->foreignRepos[$index] ) ) {
- return $this->foreignRepos[$index];
- } else {
- return false;
}
+ return $this->foreignRepos[$index] ?? false;
}
/**
}
public function getDefault() {
- if ( isset( $this->mDefault ) ) {
- return $this->mDefault;
- } else {
- return [];
- }
+ return $this->mDefault ?? [];
}
public function filterDataForSubmit( $data ) {
}
else
{
- $n->setup = $n2 ? $n2 : null;
+ $n->setup = $n2 ?: null;
$this->t->mustMatch(OP_SEMICOLON);
$n->condition = $this->t->peek() == OP_SEMICOLON ? null : $this->Expression($x);
$this->t->mustMatch(OP_SEMICOLON);
{
if ($token = $t->currentToken())
{
- $this->type = $type ? $type : $token->type;
+ $this->type = $type ?: $token->type;
$this->value = $token->value;
$this->lineno = $token->lineno;
$this->start = $token->start;
public function init($source, $filename = '', $lineno = 1)
{
$this->source = $source;
- $this->filename = $filename ? $filename : '[inline]';
+ $this->filename = $filename ?: '[inline]';
$this->lineno = $lineno;
$this->cursor = 0;
$this->lockExpiry = $config['lockExpiry'];
} else {
$met = ini_get( 'max_execution_time' );
- $this->lockExpiry = $met ? $met : 60; // use some sane amount if 0
+ $this->lockExpiry = $met ?: 60; // use some sane amount if 0
}
$this->safeDelay = ( $this->lockExpiry <= 0 )
? 60 // pick a safe-ish number to match DB timeout default
< ( $counters['ctrl'] + $counters['high'] ) * 16
) {
$kindOfBinary = true;
- $type = $binaryType ? $binaryType : $textType;
+ $type = $binaryType ?: $textType;
if ( $type === false ) {
$type = 'application/octet-stream';
}
} else {
$kindOfBinary = false;
- $type = $textType ? $textType : $binaryType;
+ $type = $textType ?: $binaryType;
if ( $type === false ) {
$type = 'text/plain';
}
/**
* @param string $value Possible result of LBFactory::makeCookieValueFromCPIndex()
- * @param int $minTimestamp Lowest UNIX timestamp of non-expired values (if present)
+ * @param int $minTimestamp Lowest UNIX timestamp that a non-expired value can have
* @return array (index: int or null, clientId: string or null)
* @since 1.32
*/
public static function getCPInfoFromCookieValue( $value, $minTimestamp ) {
static $placeholder = [ 'index' => null, 'clientId' => null ];
- if ( !preg_match( '/^(\d+)(?:@(\d+))?(?:#([0-9a-f]{32}))?$/', $value, $m ) ) {
+ if ( !preg_match( '/^(\d+)@(\d+)#([0-9a-f]{32})$/', $value, $m ) ) {
return $placeholder; // invalid
}
$realHtml = $html = null;
}
if ( !Hooks::run( 'LinkBegin',
- [ $dummy, $title, &$html, &$extraAttribs, &$query, &$options, &$ret ] )
+ [ $dummy, $title, &$html, &$extraAttribs, &$query, &$options, &$ret ], '1.28' )
) {
return $ret;
}
$title = Title::newFromLinkTarget( $target );
$options = $this->getLegacyOptions( $isKnown );
if ( !Hooks::run( 'LinkEnd',
- [ $dummy, $title, $options, &$html, &$attribs, &$ret ] )
+ [ $dummy, $title, $options, &$html, &$attribs, &$ret ], '1.28' )
) {
return $ret;
}
* @since 1.25
*/
class TagLogFormatter extends LogFormatter {
+
+ protected function getMessageParameters() {
+ $params = parent::getMessageParameters();
+
+ $isRevLink = !empty( $params[3] );
+ if ( $isRevLink ) {
+ $id = $params[3];
+ $target = $this->entry->getTarget();
+ $query = [
+ 'oldid' => $id,
+ 'diff' => 'prev'
+ ];
+ } else {
+ $id = $params[4];
+ $target = SpecialPage::getTitleValueFor( 'Log' );
+ $query = [
+ 'logid' => $id,
+ ];
+ }
+
+ $formattedNumber = $this->context->getLanguage()->formatNum( $id, true );
+ if ( $this->plaintext ) {
+ $link = $formattedNumber;
+ } elseif ( !$isRevLink || $target->exists() ) {
+ $link = $this->getLinkRenderer()->makeKnownLink(
+ $target, $formattedNumber, [], $query );
+ } else {
+ $link = htmlspecialchars( $formattedNumber );
+ }
+
+ if ( $isRevLink ) {
+ $params[3] = Message::rawParam( $link );
+ } else {
+ $params[4] = Message::rawParam( $link );
+ }
+
+ return $params;
+ }
+
protected function getMessageKey() {
$key = parent::getMessageKey();
$params = $this->getMessageParameters();
return $key;
}
+
}
$parser->mOutput->setDisplayTitle( $text );
}
if ( $old !== false && $old !== $text && !$arg ) {
- $converter = $parser->getConverterLanguage()->getConverter();
+ $converter = $parser->getTargetLanguage()->getConverter();
return '<span class="error">' .
wfMessage( 'duplicate-displaytitle',
// Message should be parsed, but these params should only be escaped.
return '';
}
} else {
- $converter = $parser->getConverterLanguage()->getConverter();
+ $converter = $parser->getTargetLanguage()->getConverter();
$parser->getOutput()->addWarning(
wfMessage( 'restricted-displaytitle',
// Message should be parsed, but this param should only be escaped.
* Unicode-safe str_pad with the restriction that $length is forced to be <= 500
* @param Parser $parser
* @param string $string
- * @param int $length
+ * @param string $length
* @param string $padding
* @param int $direction
* @return string
}
# The remaining length to add counts down to 0 as padding is added
- $length = min( $length, 500 ) - mb_strlen( $string );
+ $length = min( (int)$length, 500 ) - mb_strlen( $string );
+ if ( $length <= 0 ) {
+ // Nothing to add
+ return $string;
+ }
+
# $finalPadding is just $padding repeated enough times so that
# mb_strlen( $string ) + mb_strlen( $finalPadding ) == $length
$finalPadding = '';
if ( $old === false || $old == $text || $arg ) {
return '';
} else {
- $converter = $parser->getConverterLanguage()->getConverter();
+ $converter = $parser->getTargetLanguage()->getConverter();
return '<span class="error">' .
wfMessage( 'duplicate-defaultsort',
// Message should be parsed, but these params should only be escaped.
|| isset( $this->mDoubleUnderscores['notitleconvert'] )
|| $this->mOutput->getDisplayTitle() !== false )
) {
- $convruletitle = $this->getConverterLanguage()->getConvRuleTitle();
+ $convruletitle = $this->getTargetLanguage()->getConvRuleTitle();
if ( $convruletitle ) {
$this->mOutput->setTitleText( $convruletitle );
} else {
- $titleText = $this->getConverterLanguage()->convertTitle( $title );
+ $titleText = $this->getTargetLanguage()->convertTitle( $title );
$this->mOutput->setTitleText( $titleText );
}
}
/**
* Get the language object for language conversion
+ * @deprecated since 1.32, just use getTargetLanguage()
* @return Language|null
*/
public function getConverterLanguage() {
# The position of the convert() call should not be changed. it
# assumes that the links are all replaced and the only thing left
# is the <nowiki> mark.
- $text = $this->getConverterLanguage()->convert( $text );
+ $text = $this->getTargetLanguage()->convert( $text );
}
}
if ( $text === false ) {
# Not an image, make a link
$text = Linker::makeExternalLink( $url,
- $this->getConverterLanguage()->markNoConversion( $url, true ),
+ $this->getTargetLanguage()->markNoConversion( $url, true ),
true, 'free',
$this->getExternalLinkAttribs( $url ), $this->mTitle );
# Register it in the output object...
list( $dtrail, $trail ) = Linker::splitTrail( $trail );
}
- $text = $this->getConverterLanguage()->markNoConversion( $text );
+ $text = $this->getTargetLanguage()->markNoConversion( $text );
$url = Sanitizer::cleanUrl( $url );
}
$sortkey = Sanitizer::decodeCharReferences( $sortkey );
$sortkey = str_replace( "\n", '', $sortkey );
- $sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
+ $sortkey = $this->getTargetLanguage()->convertCategoryKey( $sortkey );
$this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
continue;
$this->mOutput->setFlag( 'vary-revision' );
wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" );
}
- $value = $pageid ? $pageid : null;
+ $value = $pageid ?: null;
break;
case 'revisionid':
# Let the edit saving system know we should parse the page
if ( $title ) {
$titleText = $title->getPrefixedText();
# Check for language variants if the template is not found
- if ( $this->getConverterLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
- $this->getConverterLanguage()->findVariantLink( $part1, $title, true );
+ if ( $this->getTargetLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
+ $this->getTargetLanguage()->findVariantLink( $part1, $title, true );
}
# Do recursion depth check
$limit = $this->mOptions->getMaxTemplateDepth();
--- /dev/null
+<?php
+
+/**
+ * Marker class for search engines that can handle their own pagination, by
+ * reporting in their SearchResultSet when a next page is available. This
+ * only applies to search{Title,Text} and not to completion search.
+ *
+ * SearchEngine implementations not implementing this interface will have
+ * an over-fetch performed to determine next page availability.
+ */
+interface PaginatingSearchEngine {
+}
* @return SearchResultSet|Status|null
*/
public function searchText( $term ) {
- return $this->doSearchText( $term );
+ return $this->maybePaginate( function () use ( $term ) {
+ return $this->doSearchText( $term );
+ } );
}
/**
* @return SearchResultSet|null
*/
public function searchTitle( $term ) {
- return $this->doSearchTitle( $term );
+ return $this->maybePaginate( function () use ( $term ) {
+ return $this->doSearchTitle( $term );
+ } );
}
/**
return null;
}
+ /**
+ * Performs an overfetch and shrink operation to determine if
+ * the next page is available for search engines that do not
+ * explicitly implement their own pagination.
+ *
+ * @param Closure $fn Takes no arguments
+ * @return SearchResultSet|Status<SearchResultSet>|null Result of calling $fn
+ */
+ private function maybePaginate( Closure $fn ) {
+ if ( $this instanceof PaginatingSearchEngine ) {
+ return $fn();
+ }
+ $this->limit++;
+ try {
+ $resultSetOrStatus = $fn();
+ } finally {
+ $this->limit--;
+ }
+
+ $resultSet = null;
+ if ( $resultSetOrStatus instanceof SearchResultSet ) {
+ $resultSet = $resultSetOrStatus;
+ } elseif ( $resultSetOrStatus instanceof Status &&
+ $resultSetOrStatus->getValue() instanceof SearchResultSet
+ ) {
+ $resultSet = $resultSetOrStatus->getValue();
+ }
+ if ( $resultSet ) {
+ $resultSet->shrink( $this->limit );
+ }
+
+ return $resultSetOrStatus;
+ }
+
/**
* @since 1.18
* @param string $feature
return $search;
}
+ /**
+ * Perform an overfetch of completion search results. This allows
+ * determining if another page of results is available.
+ *
+ * @param string $search
+ * @return SearchSuggestionSet
+ */
+ protected function completionSearchBackendOverfetch( $search ) {
+ $this->limit++;
+ try {
+ return $this->completionSearchBackend( $search );
+ } finally {
+ $this->limit--;
+ }
+ }
+
/**
* Perform a completion search.
* Does not resolve namespaces and does not check variants.
return SearchSuggestionSet::emptySuggestionSet(); // Return empty result
}
$search = $this->normalizeNamespaces( $search );
- return $this->processCompletionResults( $search, $this->completionSearchBackend( $search ) );
+ $suggestions = $this->completionSearchBackendOverfetch( $search );
+ return $this->processCompletionResults( $search, $suggestions );
}
/**
}
$search = $this->normalizeNamespaces( $search );
- $results = $this->completionSearchBackend( $search );
- $fallbackLimit = $this->limit - $results->getSize();
+ $results = $this->completionSearchBackendOverfetch( $search );
+ $fallbackLimit = 1 + $this->limit - $results->getSize();
if ( $fallbackLimit > 0 ) {
global $wgContLang;
* @return SearchSuggestionSet
*/
protected function processCompletionResults( $search, SearchSuggestionSet $suggestions ) {
+ // We over-fetched to determine pagination. Shrink back down if we have extra results
+ // and mark if pagination is possible
+ $suggestions->shrink( $this->limit );
+
$search = trim( $search );
// preload the titles with LinkBatch
- $titles = $suggestions->map( function ( SearchSuggestion $sugg ) {
+ $lb = new LinkBatch( $suggestions->map( function ( SearchSuggestion $sugg ) {
return $sugg->getSuggestedTitle();
- } );
- $lb = new LinkBatch( $titles );
+ } ) );
$lb->setCaller( __METHOD__ );
$lb->execute();
+ $diff = $suggestions->filter( function ( SearchSuggestion $sugg ) {
+ return $sugg->getSuggestedTitle()->isKnown();
+ } );
+ if ( $diff > 0 ) {
+ MediaWikiServices::getInstance()->getStatsdDataFactory()
+ ->updateCount( 'search.completion.missing', $diff );
+ }
+
$results = $suggestions->map( function ( SearchSuggestion $sugg ) {
return $sugg->getSuggestedTitle()->getPrefixedText();
} );
$setAugmentors = [];
$rowAugmentors = [];
Hooks::run( "SearchResultsAugment", [ &$setAugmentors, &$rowAugmentors ] );
-
if ( !$setAugmentors && !$rowAugmentors ) {
// We're done here
return;
* A SearchResultSet wrapper for SearchNearMatcher
*/
class SearchNearMatchResultSet extends SearchResultSet {
- private $fetched = false;
-
/**
* @param Title|null $match Title if matched, else null
*/
public function __construct( $match ) {
- $this->result = $match;
- }
-
- public function numRows() {
- return $this->result ? 1 : 0;
- }
-
- public function next() {
- if ( $this->fetched || !$this->result ) {
- return false;
+ if ( $match === null ) {
+ $this->results = [];
+ } else {
+ $this->results = [ SearchResult::newFromTitle( $match, $this ) ];
}
- $this->fetched = true;
- return SearchResult::newFromTitle( $this->result, $this );
}
- public function rewind() {
- $this->fetched = false;
+ public function numRows() {
+ return $this->results ? 1 : 0;
}
}
protected $searchEngine;
/**
- * A set of extension data.
- * @var array[]
+ * A function returning a set of extension data.
+ * @var Closure|null
*/
protected $extensionData;
* @return array[]
*/
public function getExtensionData() {
- return $this->extensionData;
+ if ( $this->extensionData ) {
+ return call_user_func( $this->extensionData );
+ } else {
+ return [];
+ }
}
/**
* Set extension data for this result.
* The data is:
* augmentor name => data
- * @param array[] $extensionData
+ * @param Closure|array $extensionData Takes no arguments, returns
+ * either array of extension data or null.
*/
- public function setExtensionData( array $extensionData ) {
- $this->extensionData = $extensionData;
+ public function setExtensionData( $extensionData ) {
+ if ( $extensionData instanceof Closure ) {
+ $this->extensionData = $extensionData;
+ } elseif ( is_array( $extensionData ) ) {
+ wfDeprecated( __METHOD__ . ' with array argument', 1.32 );
+ $this->extensionData = function () use ( $extensionData ) {
+ return $extensionData;
+ };
+ } else {
+ $type = is_object( $extensionData )
+ ? get_class( $extensionData )
+ : gettype( $extensionData );
+ throw new \InvalidArgumentException(
+ __METHOD__ . " must be called with Closure|array, but received $type" );
+ }
}
-
}
/**
* @ingroup Search
*/
-class SearchResultSet {
-
+class SearchResultSet implements Countable, IteratorAggregate {
/**
* Types of interwiki results
*/
* as an array.
* @var SearchResult[]
*/
- private $results;
+ protected $results;
/**
* Set of result's extra data, indexed per result id
*/
protected $extraData = [];
- public function __construct( $containedSyntax = false ) {
+ /**
+ * @var boolean True when there are more pages of search results available.
+ */
+ private $hasMoreResults;
+
+ /**
+ * @var ArrayIterator|null Iterator supporting BC iteration methods
+ */
+ private $bcIterator;
+
+ /**
+ * @param bool $containedSyntax True when query is not requesting a simple
+ * term match
+ * @param bool $hasMoreResults True when there are more pages of search
+ * results available.
+ */
+ public function __construct( $containedSyntax = false, $hasMoreResults = false ) {
+ if ( static::class === __CLASS__ ) {
+ // This class will eventually be abstract. SearchEngine implementations
+ // already have to extend this class anyways to provide the actual
+ // search results.
+ wfDeprecated( __METHOD__, 1.32 );
+ }
$this->containedSyntax = $containedSyntax;
+ $this->hasMoreResults = $hasMoreResults;
}
/**
}
function numRows() {
- return 0;
+ return $this->count();
+ }
+
+ final public function count() {
+ return count( $this->extractResults() );
}
/**
/**
* Fetches next search result, or false.
- * STUB
- * FIXME: refactor as iterator, so we could use nicer interfaces.
- * @deprecated since 1.32; Use self::extractResults()
+ * @deprecated since 1.32; Use self::extractResults() or foreach
* @return SearchResult|false
*/
- function next() {
- return false;
+ public function next() {
+ wfDeprecated( __METHOD__, '1.32' );
+ $it = $this->bcIterator();
+ $searchResult = $it->current();
+ $it->next();
+ return $searchResult === null ? false : $searchResult;
}
/**
* Rewind result set back to beginning
- * @deprecated since 1.32; Use self::extractResults()
+ * @deprecated since 1.32; Use self::extractResults() or foreach
*/
- function rewind() {
+ public function rewind() {
+ wfDeprecated( __METHOD__, '1.32' );
+ $this->bcIterator()->rewind();
+ }
+
+ private function bcIterator() {
+ if ( $this->bcIterator === null ) {
+ $this->bcIterator = 'RECURSION';
+ $this->bcIterator = $this->getIterator();
+ } elseif ( $this->bcIterator === 'RECURSION' ) {
+ // Either next/rewind or extractResults must be implemented. This
+ // class was potentially instantiated directly. It should be
+ // abstract with abstract methods to enforce this but that's a
+ // breaking change...
+ wfDeprecated( static::class . ' without implementing extractResults', '1.32' );
+ $this->bcIterator = new ArrayIterator( [] );
+ }
+ return $this->bcIterator;
}
/**
return $this->containedSyntax;
}
+ /**
+ * @return bool True when there are more pages of search results available.
+ */
+ public function hasMoreResults() {
+ return $this->hasMoreResults;
+ }
+
+ /**
+ * @param int $limit Shrink result set to $limit and flag
+ * if more results are available.
+ */
+ public function shrink( $limit ) {
+ if ( $this->count() > $limit ) {
+ $this->hasMoreResults = true;
+ // shrinking result set for implementations that
+ // have not implemented extractResults and use
+ // the default cache location. Other implementations
+ // must override this as well.
+ if ( is_array( $this->results ) ) {
+ $this->results = array_slice( $this->results, 0, $limit );
+ } else {
+ throw new \UnexpectedValueException(
+ "When overriding result store extending classes must "
+ . " also override " . __METHOD__ );
+ }
+ }
+ }
+
/**
* Extract all the results in the result set as array.
* @return SearchResult[]
/**
* Returns extra data for specific result and store it in SearchResult object.
* @param SearchResult $result
- * @return array|null List of data as name => value or null if none present.
*/
public function augmentResult( SearchResult $result ) {
$id = $result->getTitle()->getArticleID();
- if ( !$id || !isset( $this->extraData[$id] ) ) {
- return null;
+ if ( $id === -1 ) {
+ return;
}
- $result->setExtensionData( $this->extraData[$id] );
- return $this->extraData[$id];
+ $result->setExtensionData( function () use ( $id ) {
+ if ( isset( $this->extraData[$id] ) ) {
+ return $this->extraData[$id];
+ } else {
+ return [];
+ }
+ } );
}
/**
public function getOffset() {
return null;
}
+
+ final public function getIterator() {
+ return new ArrayIterator( $this->extractResults() );
+ }
}
*/
private $pageMap = [];
+ /**
+ * @var bool Are more results available?
+ */
+ private $hasMoreResults;
+
/**
* Builds a new set of suggestions.
*
* unexpected behaviors.
*
* @param SearchSuggestion[] $suggestions (must be sorted by score)
+ * @param bool $hasMoreResults Are more results available?
*/
- public function __construct( array $suggestions ) {
+ public function __construct( array $suggestions, $hasMoreResults = false ) {
+ $this->hasMoreResults = $hasMoreResults;
foreach ( $suggestions as $suggestion ) {
$pageID = $suggestion->getSuggestedTitleID();
if ( $pageID && empty( $this->pageMap[$pageID] ) ) {
}
}
+ /**
+ * @return bool Are more results available?
+ */
+ public function hasMoreResults() {
+ return $this->hasMoreResults;
+ }
+
/**
* Get the list of suggestions.
* @return SearchSuggestion[]
return array_map( $callback, $this->suggestions );
}
+ /**
+ * Filter the suggestions array
+ * @param callback $callback Callable accepting single SearchSuggestion
+ * instance returning bool false to remove the item.
+ * @return int The number of suggestions removed
+ */
+ public function filter( $callback ) {
+ $before = count( $this->suggestions );
+ $this->suggestions = array_values( array_filter( $this->suggestions, $callback ) );
+ return $before - count( $this->suggestions );
+ }
+
/**
* Add a new suggestion at the end.
* If the score of the new suggestion is greater than the worst one,
public function shrink( $limit ) {
if ( count( $this->suggestions ) > $limit ) {
$this->suggestions = array_slice( $this->suggestions, 0, $limit );
+ $this->hasMoreResults = true;
}
}
* NOTE: Suggestion scores will be generated.
*
* @param Title[] $titles
+ * @param bool $hasMoreResults Are more results available?
* @return SearchSuggestionSet
*/
- public static function fromTitles( array $titles ) {
+ public static function fromTitles( array $titles, $hasMoreResults = false ) {
$score = count( $titles );
$suggestions = array_map( function ( $title ) use ( &$score ) {
return SearchSuggestion::fromTitle( $score--, $title );
}, $titles );
- return new SearchSuggestionSet( $suggestions );
+ return new SearchSuggestionSet( $suggestions, $hasMoreResults );
}
/**
* NOTE: Suggestion scores will be generated.
*
* @param string[] $titles
+ * @param bool $hasMoreResults Are more results available?
* @return SearchSuggestionSet
*/
- public static function fromStrings( array $titles ) {
+ public static function fromStrings( array $titles, $hasMoreResults = false ) {
$score = count( $titles );
$suggestions = array_map( function ( $title ) use ( &$score ) {
return SearchSuggestion::fromText( $score--, $title );
}, $titles );
- return new SearchSuggestionSet( $suggestions );
+ return new SearchSuggestionSet( $suggestions, $hasMoreResults );
}
/**
* @ingroup Search
*/
class SqlSearchResultSet extends SearchResultSet {
+ /** @var ResultWrapper Result object from database */
protected $resultSet;
+ /** @var string Requested search query */
protected $terms;
+ /** @var int|null Total number of hits for $terms */
protected $totalHits;
function __construct( ResultWrapper $resultSet, $terms, $total = null ) {
return $this->resultSet->numRows();
}
- function next() {
+ public function extractResults() {
if ( $this->resultSet === false ) {
- return false;
- }
-
- $row = $this->resultSet->fetchObject();
- if ( $row === false ) {
- return false;
+ return [];
}
- return SearchResult::newFromTitle(
- Title::makeTitle( $row->page_namespace, $row->page_title ), $this
- );
- }
-
- function rewind() {
- if ( $this->resultSet ) {
+ if ( $this->results === null ) {
+ $this->results = [];
$this->resultSet->rewind();
+ while ( ( $row = $this->resultSet->fetchObject() ) !== false ) {
+ $this->results[] = SearchResult::newFromTitle(
+ Title::makeTitle( $row->page_namespace, $row->page_title ), $this
+ );
+ }
}
+ return $this->results;
}
function free() {
* @return mixed The value of the data requested or the deafult
*/
public function get( $name, $default = null ) {
- if ( isset( $this->data[$name] ) ) {
- return $this->data[$name];
- } else {
- return $default;
- }
+ return $this->data[$name] ?? $default;
}
/**
$nu = User::newFromName( $target );
$error = self::validateTarget( $nu, $sender );
- return $error ? $error : $nu;
+ return $error ?: $nu;
}
/**
}
public function execute( $par ) {
- $this->mime = $par ? $par : $this->getRequest()->getText( 'mime' );
+ $this->mime = $par ?: $this->getRequest()->getText( 'mime' );
$this->mime = trim( $this->mime );
list( $this->major, $this->minor ) = File::splitMime( $this->mime );
}
// Hardcoded [def] if the language is set to null
- $logOld = $oldLanguage ? $oldLanguage : $defLang . '[def]';
- $logNew = $newLanguage ? $newLanguage : $defLang . '[def]';
+ $logOld = $oldLanguage ?: $defLang . '[def]';
+ $logNew = $newLanguage ?: $defLang . '[def]';
// Writing new page language to database
$dbw->update(
*/
public function getEntryPointInfo() {
global $wgArticlePath, $wgScriptPath;
- $scriptPath = $wgScriptPath ? $wgScriptPath : "/";
+ $scriptPath = $wgScriptPath ?: "/";
$entryPoints = [
'version-entrypoints-articlepath' => $wgArticlePath,
'version-entrypoints-scriptpath' => $scriptPath,
$this->talk = $this->msg( 'talkpagelinktext' )->escaped();
- $this->lang = ( $langObj ? $langObj : $wgContLang );
+ $this->lang = $langObj ?: $wgContLang;
$this->langcode = $this->lang->getCode();
$this->foreign = !$this->lang->equals( $wgContLang );
* @return null|UploadBase
*/
public static function createFromRequest( &$request, $type = null ) {
- $type = $type ? $type : $request->getVal( 'wpSourceType', 'File' );
+ $type = $type ?: $request->getVal( 'wpSourceType', 'File' );
if ( !$type ) {
return null;
$terms = $wgContLang->convertForSearchResult( $resultSet->termMatches() );
$hits = [];
- $result = $resultSet->next();
- while ( $result ) {
- $hits[] .= $this->resultWidget->render( $result, $terms, $offset++ );
- $result = $resultSet->next();
+ foreach ( $resultSet as $result ) {
+ $hits[] = $this->resultWidget->render( $result, $terms, $offset++ );
}
return "<ul class='mw-search-results'>" . implode( '', $hits ) . "</ul>";
$iwResults = [];
foreach ( $resultSets as $resultSet ) {
- $result = $resultSet->next();
- while ( $result ) {
+ foreach ( $resultSet as $result ) {
if ( !$result->isBrokenTitle() ) {
$iwResults[$result->getTitle()->getInterwiki()][] = $result;
}
- $result = $resultSet->next();
}
}
return;
}
$this->mMagicHookDone = true;
- Hooks::run( 'LanguageGetMagic', [ &$this->mMagicExtensions, $this->getCode() ] );
+ Hooks::run( 'LanguageGetMagic', [ &$this->mMagicExtensions, $this->getCode() ], '1.16' );
}
/**
$this->mExtendedSpecialPageAliases =
self::$dataCache->getItem( $this->mCode, 'specialPageAliases' );
Hooks::run( 'LanguageGetSpecialPageAliases',
- [ &$this->mExtendedSpecialPageAliases, $this->getCode() ] );
+ [ &$this->mExtendedSpecialPageAliases, $this->getCode() ], '1.16' );
}
return $this->mExtendedSpecialPageAliases;
"rcfilters-savedqueries-rename": "Rename",
"rcfilters-savedqueries-setdefault": "Set as default",
"rcfilters-savedqueries-unsetdefault": "Remove as default",
- "rcfilters-savedqueries-remove": "Remove",
+ "rcfilters-savedqueries-remove": "Delete",
"rcfilters-savedqueries-new-name-label": "Name",
"rcfilters-savedqueries-new-name-placeholder": "Describe the purpose of the filter",
"rcfilters-savedqueries-apply-label": "Create filter",
"rcfilters-savedqueries-rename": "Label for the menu option that edits a quick filter in [[Special:RecentChanges]]\n{{Identical|Rename}}",
"rcfilters-savedqueries-setdefault": "Label for the menu option that sets a quick filter as default in [[Special:RecentChanges]]",
"rcfilters-savedqueries-unsetdefault": "Label for the menu option that unsets a quick filter as default in [[Special:RecentChanges]]",
- "rcfilters-savedqueries-remove": "Label for the menu option that removes a quick filter as default in [[Special:RecentChanges]]\n{{Identical|Remove}}",
+ "rcfilters-savedqueries-remove": "Label for the menu option that removes a quick filter as default in [[Special:RecentChanges]]\n{{Identical|Delete}}",
"rcfilters-savedqueries-new-name-label": "Label for the input that holds the name of the new saved filters in [[Special:RecentChanges]]\n{{Identical|Name}}",
"rcfilters-savedqueries-new-name-placeholder": "Placeholder for the input that holds the name of the new saved filters in [[Special:RecentChanges]]",
"rcfilters-savedqueries-apply-label": "Label for the button to apply saving a new filter setting in [[Special:RecentChanges]]. This is for a small popup, please try to use a short string.",
function importRevision( &$revision, &$importer ) {
$id = $revision->getID();
$content = $revision->getContent( Revision::RAW );
- $id = $id ? $id : '';
+ $id = $id ?: '';
if ( $content === null ) {
echo "Revision $id is broken, we have no content available\n";
return htmlspecialchars(
'?' .
wfArrayToCgi( [
- 'filter' => $_filter ? $_filter : $filter,
- 'sort' => $_sort ? $_sort : $sort,
+ 'filter' => $_filter ?: $filter,
+ 'sort' => $_sort ?: $sort,
'expand' => implode( ',', array_keys( $_expand ) )
] )
);
@rcfilters-spinner-size: 12px;
@rcfilters-head-min-height: 210px;
@rcfilters-head-margin-bottom: 20px;
-@rcfilters-wl-head-min-height: 300px;
+@rcfilters-wl-head-min-height: 270px;
// Corrections for the standard special page
.client-js {
} ),
new OO.ui.MenuOptionWidget( {
data: 'delete',
- icon: 'close',
+ icon: 'trash',
label: mw.msg( 'rcfilters-savedqueries-remove' )
} ),
new OO.ui.MenuOptionWidget( {
=> "$testDir/phpunit/mocks/session/DummySessionBackend.php",
'DummySessionProvider' => "$testDir/phpunit/mocks/session/DummySessionProvider.php",
'MockMessageLocalizer' => "$testDir/phpunit/mocks/MockMessageLocalizer.php",
+ 'MockCompletionSearchEngine' => "$testDir/phpunit/mocks/search/MockCompletionSearchEngine.php",
+ 'MockSearchEngine' => "$testDir/phpunit/mocks/search/MockSearchEngine.php",
+ 'MockSearchResultSet' => "$testDir/phpunit/mocks/search/MockSearchResultSet.php",
+ 'MockSearchResult' => "$testDir/phpunit/mocks/search/MockSearchResult.php",
# tests/suites
'ParserTestFileSuite' => "$testDir/phpunit/suites/ParserTestFileSuite.php",
public function __construct() {
global $wgDjvuRenderer, $wgDjvuDump, $wgDjvuToXML, $wgFileExtensions, $wgDjvuTxt;
- $wgDjvuRenderer = $wgDjvuRenderer ? $wgDjvuRenderer : '/usr/bin/ddjvu';
- $wgDjvuDump = $wgDjvuDump ? $wgDjvuDump : '/usr/bin/djvudump';
- $wgDjvuToXML = $wgDjvuToXML ? $wgDjvuToXML : '/usr/bin/djvutoxml';
- $wgDjvuTxt = $wgDjvuTxt ? $wgDjvuTxt : '/usr/bin/djvutxt';
+ $wgDjvuRenderer = $wgDjvuRenderer ?: '/usr/bin/ddjvu';
+ $wgDjvuDump = $wgDjvuDump ?: '/usr/bin/djvudump';
+ $wgDjvuToXML = $wgDjvuToXML ?: '/usr/bin/djvutoxml';
+ $wgDjvuTxt = $wgDjvuTxt ?: '/usr/bin/djvutxt';
if ( !in_array( 'djvu', $wgFileExtensions ) ) {
$wgFileExtensions[] = 'djvu';
<span typeof="mw:Transclusion" data-mw='{"parts":[{"template":{"target":{"wt":"padright:","function":"padright"},"params":{"1":{"wt":"3"},"2":{"wt":"abcde"}},"i":0}}]}'>abc</span></p>
!! end
+!! test
+Padleft and padright with non-numerical length (T180403)
+!! wikitext
+{{padleft:abcdef|junk}}
+{{padright:abcdef|junk}}
+!! html/php
+<p>abcdef
+abcdef
+</p>
+!! end
+
!!test
Special parser function
!! wikitext
* Should be called from addDBData().
*
* @since 1.25 ($namespace in 1.28)
- * @param string|title $pageName Page name or title
+ * @param string|Title $pageName Page name or title
* @param string $text Page's content
* @param int $namespace Namespace id (name cannot already contain namespace)
* @return array Title object and page id
<?php
-use MediaWiki\MediaWikiServices;
/**
* @group Database
* @dataProvider provideLinkBeginHook
*/
public function testLinkBeginHook( $callback, $expected ) {
+ $this->hideDeprecated( 'LinkBegin hook (used in hook-LinkBegin-closure)' );
$this->setMwGlobals( [
'wgArticlePath' => '/wiki/$1',
'wgServer' => '//example.org',
* @dataProvider provideLinkEndHook
*/
public function testLinkEndHook( $callback, $expected ) {
+ $this->hideDeprecated( 'LinkEnd hook (used in hook-LinkEnd-closure)' );
$this->setMwGlobals( [
'wgArticlePath' => '/wiki/$1',
] );
$out = Linker::link( $title );
$this->assertEquals( $expected, $out );
}
-
- /**
- * @covers Linker::getLinkColour
- */
- public function testGetLinkColour() {
- $this->hideDeprecated( 'Linker::getLinkColour' );
- $linkCache = MediaWikiServices::getInstance()->getLinkCache();
- $foobarTitle = Title::makeTitle( NS_MAIN, 'FooBar' );
- $redirectTitle = Title::makeTitle( NS_MAIN, 'Redirect' );
- $userTitle = Title::makeTitle( NS_USER, 'Someuser' );
- $linkCache->addGoodLinkObj(
- 1, // id
- $foobarTitle,
- 10, // len
- 0 // redir
- );
- $linkCache->addGoodLinkObj(
- 2, // id
- $redirectTitle,
- 10, // len
- 1 // redir
- );
-
- $linkCache->addGoodLinkObj(
- 3, // id
- $userTitle,
- 10, // len
- 0 // redir
- );
-
- $this->assertEquals(
- '',
- Linker::getLinkColour( $foobarTitle, 0 )
- );
-
- $this->assertEquals(
- 'stub',
- Linker::getLinkColour( $foobarTitle, 20 )
- );
-
- $this->assertEquals(
- 'mw-redirect',
- Linker::getLinkColour( $redirectTitle, 0 )
- );
-
- $this->assertEquals(
- '',
- Linker::getLinkColour( $userTitle, 20 )
- );
- }
}
$context->getOutput()->getRedirect()
);
}
+
+ /**
+ * Test a post-send job can not set cookies (T191537).
+ */
+ public function testPostSendJobDoesNotSetCookie() {
+ // Prevent updates from running
+ $this->setMwGlobals( 'wgCommandLineMode', false );
+
+ $response = new WebResponse;
+
+ // A job that attempts to set a cookie
+ $jobHasRun = false;
+ DeferredUpdates::addCallableUpdate( function () use ( $response, &$jobHasRun ) {
+ $jobHasRun = true;
+ $response->setCookie( 'JobCookie', 'yes' );
+ $response->header( 'Foo: baz' );
+ } );
+
+ $hookWasRun = false;
+ $this->setTemporaryHook( 'WebResponseSetCookie', function () use ( &$hookWasRun ) {
+ $hookWasRun = true;
+ return true;
+ } );
+
+ $logger = new TestLogger();
+ $logger->setCollect( true );
+ $this->setLogger( 'cookie', $logger );
+ $this->setLogger( 'header', $logger );
+
+ $mw = new MediaWiki();
+ $mw->doPostOutputShutdown();
+ // restInPeace() might have been registered to a callback of
+ // register_postsend_function() and thus can not be triggered from
+ // PHPUnit.
+ if ( $jobHasRun === false ) {
+ $mw->restInPeace();
+ }
+
+ $this->assertTrue( $jobHasRun, 'post-send job has run' );
+ $this->assertFalse( $hookWasRun,
+ 'post-send job must not trigger WebResponseSetCookie hook' );
+ $this->assertEquals(
+ [
+ [ 'info', 'ignored post-send cookie {cookie}' ],
+ [ 'info', 'ignored post-send header {header}' ],
+ ],
+ $logger->getBuffer()
+ );
+ }
}
$WANObjectCache = null
) {
return new RevisionStore(
- $loadBalancer ? $loadBalancer : $this->getMockLoadBalancer(),
- $blobStore ? $blobStore : $this->getMockSqlBlobStore(),
- $WANObjectCache ? $WANObjectCache : $this->getHashWANObjectCache(),
+ $loadBalancer ?: $this->getMockLoadBalancer(),
+ $blobStore ?: $this->getMockSqlBlobStore(),
+ $WANObjectCache ?: $this->getHashWANObjectCache(),
MediaWikiServices::getInstance()->getCommentStore(),
MediaWikiServices::getInstance()->getActorMigration()
);
* @covers Title::checkUserBlock
*/
public function testUserBlock() {
- global $wgEmailConfirmToEdit, $wgEmailAuthentication;
- $wgEmailConfirmToEdit = true;
- $wgEmailAuthentication = true;
+ $this->setMwGlobals( [
+ 'wgEmailConfirmToEdit' => true,
+ 'wgEmailAuthentication' => true,
+ ] );
$this->setUserPerm( [ "createpage", "move" ] );
$this->setTitle( NS_HELP, "test page" );
- # $short
- $this->assertEquals( [ [ 'confirmedittext' ] ],
+ # $wgEmailConfirmToEdit only applies to 'edit' action
+ $this->assertEquals( [],
$this->title->getUserPermissionsErrors( 'move-target', $this->user ) );
- $wgEmailConfirmToEdit = false;
- $this->assertEquals( true, $this->title->userCan( 'move-target', $this->user ) );
+ $this->assertContains( [ 'confirmedittext' ],
+ $this->title->getUserPermissionsErrors( 'edit', $this->user ) );
+
+ $this->setMwGlobals( 'wgEmailConfirmToEdit', false );
+ $this->assertNotContains( [ 'confirmedittext' ],
+ $this->title->getUserPermissionsErrors( 'edit', $this->user ) );
# $wgEmailConfirmToEdit && !$user->isEmailConfirmed() && $action != 'createaccount'
$this->assertEquals( [],
--- /dev/null
+<?php
+
+/**
+ * @group API
+ * @group medium
+ *
+ * @covers ApiQueryPrefixSearch
+ */
+class ApiQueryPrefixSearchTest extends ApiTestCase {
+ const TEST_QUERY = 'unittest';
+
+ public function setUp() {
+ parent::setUp();
+ $this->setMwGlobals( [
+ 'wgSearchType' => MockCompletionSearchEngine::class,
+ ] );
+ MockCompletionSearchEngine::clearMockResults();
+ $results = [];
+ foreach ( range( 0, 10 ) as $i ) {
+ $title = "Search_Result_$i";
+ $results[] = $title;
+ $this->editPage( $title, 'hi there' );
+ }
+ MockCompletionSearchEngine::addMockResults( self::TEST_QUERY, $results );
+ }
+
+ public function offsetContinueProvider() {
+ return [
+ 'no offset' => [ 2, 2, 0, 2 ],
+ 'with offset' => [ 7, 2, 5, 2 ],
+ 'past end, no offset' => [ null, 11, 0, 20 ],
+ 'past end, with offset' => [ null, 5, 6, 10 ],
+ ];
+ }
+
+ /**
+ * @dataProvider offsetContinueProvider
+ */
+ public function testOffsetContinue( $expectedOffset, $expectedResults, $offset, $limit ) {
+ $response = $this->doApiRequest( [
+ 'action' => 'query',
+ 'list' => 'prefixsearch',
+ 'pssearch' => self::TEST_QUERY,
+ 'psoffset' => $offset,
+ 'pslimit' => $limit,
+ ] );
+ $result = $response[0];
+ $this->assertArrayNotHasKey( 'warnings', $result );
+ $suggestions = $result['query']['prefixsearch'];
+ $this->assertCount( $expectedResults, $suggestions );
+ if ( $expectedOffset == null ) {
+ $this->assertArrayNotHasKey( 'continue', $result );
+ } else {
+ $this->assertArrayHasKey( 'continue', $result );
+ $this->assertEquals( $expectedOffset, $result['continue']['psoffset'] );
+ }
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * @group medium
+ * @covers ApiQuerySearch
+ */
+class ApiQuerySearchTest extends ApiTestCase {
+ public function provideSearchResults() {
+ return [
+ 'empty search result' => [ [], [] ],
+ 'has search results' => [
+ [ 'Zomg' ],
+ [ $this->mockResult( 'Zomg' ) ],
+ ],
+ 'filters broken search results' => [
+ [ 'A', 'B' ],
+ [
+ $this->mockResult( 'a' ),
+ $this->mockResult( 'Zomg' )->setBrokenTitle( true ),
+ $this->mockResult( 'b' ),
+ ],
+ ],
+ 'filters results with missing revision' => [
+ [ 'B', 'A' ],
+ [
+ $this->mockResult( 'Zomg' )->setMissingRevision( true ),
+ $this->mockResult( 'b' ),
+ $this->mockResult( 'a' ),
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideSearchResults
+ */
+ public function testSearchResults( $expect, $hits, array $params = [] ) {
+ MockSearchEngine::addMockResults( 'my query', $hits );
+ list( $response, $request ) = $this->doApiRequest( $params + [
+ 'action' => 'query',
+ 'list' => 'search',
+ 'srsearch' => 'my query',
+ ] );
+ $titles = [];
+ foreach ( $response['query']['search'] as $result ) {
+ $titles[] = $result['title'];
+ }
+ $this->assertEquals( $expect, $titles );
+ }
+
+ public function provideInterwikiResults() {
+ return [
+ 'empty' => [ [], [] ],
+ 'one wiki response' => [
+ [ 'utwiki' => [ 'Qwerty' ] ],
+ [
+ SearchResultSet::SECONDARY_RESULTS => [
+ 'utwiki' => new MockSearchResultSet( [
+ $this->mockResult( 'Qwerty' )->setInterwikiPrefix( 'utwiki' ),
+ ] ),
+ ],
+ ]
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideInterwikiResults
+ */
+ public function testInterwikiResults( $expect, $hits, array $params = [] ) {
+ MockSearchEngine::setMockInterwikiResults( $hits );
+ list( $response, $request ) = $this->doApiRequest( $params + [
+ 'action' => 'query',
+ 'list' => 'search',
+ 'srsearch' => 'my query',
+ 'srinterwiki' => true,
+ ] );
+ if ( !$expect ) {
+ $this->assertArrayNotHasKey( 'interwikisearch', $response['query'] );
+ return;
+ }
+ $results = [];
+ $this->assertArrayHasKey( 'interwikisearchinfo', $response['query'] );
+ foreach ( $response['query']['interwikisearch'] as $wiki => $wikiResults ) {
+ $results[$wiki] = [];
+ foreach ( $wikiResults as $wikiResult ) {
+ $results[$wiki][] = $wikiResult['title'];
+ }
+ }
+ $this->assertEquals( $expect, $results );
+ }
+
+ public function setUp() {
+ parent::setUp();
+ MockSearchEngine::clearMockResults();
+ $this->registerMockSearchEngine();
+ }
+
+ private function registerMockSearchEngine() {
+ $this->setMwGlobals( [
+ 'wgSearchType' => MockSearchEngine::class,
+ ] );
+ }
+
+ private function mockResult( $title ) {
+ return MockSearchResult::newFromtitle( Title::newFromText( $title ) );
+ }
+
+}
'1@542#c47dcfb0566e7d7bc110a6128a45c93a',
LBFactory::makeCookieValueFromCPIndex( 1, 542, $agentId )
);
+
$this->assertSame(
- 5,
- LBFactory::getCPInfoFromCookieValue( "5", $time - 10 )['index'],
+ null,
+ LBFactory::getCPInfoFromCookieValue( "5#$agentId", $time - 10 )['index'],
'No time set'
);
$this->assertSame(
null,
- LBFactory::getCPInfoFromCookieValue( "0", $time - 10 )['index'],
+ LBFactory::getCPInfoFromCookieValue( "5@$time", $time - 10 )['index'],
+ 'No agent set'
+ );
+ $this->assertSame(
+ null,
+ LBFactory::getCPInfoFromCookieValue( "0@$time#$agentId", $time - 10 )['index'],
'Bad index'
);
$this->assertSame(
2,
- LBFactory::getCPInfoFromCookieValue( "2@$time", $time - 10 )['index'],
+ LBFactory::getCPInfoFromCookieValue( "2@$time#$agentId", $time - 10 )['index'],
'Fresh'
);
$this->assertSame(
2,
- LBFactory::getCPInfoFromCookieValue( "2@$time", $time + 9 - 10 )['index'],
+ LBFactory::getCPInfoFromCookieValue( "2@$time#$agentId", $time + 9 - 10 )['index'],
'Almost stale'
);
$this->assertSame(
null,
- LBFactory::getCPInfoFromCookieValue( "0@$time", $time + 9 - 10 )['index'],
+ LBFactory::getCPInfoFromCookieValue( "0@$time#$agentId", $time + 9 - 10 )['index'],
'Almost stale; bad index'
);
$this->assertSame(
null,
- LBFactory::getCPInfoFromCookieValue( "2@$time", $time + 11 - 10 )['index'],
+ LBFactory::getCPInfoFromCookieValue( "2@$time#$agentId", $time + 11 - 10 )['index'],
'Stale'
);
);
$this->assertSame(
null,
- LBFactory::getCPInfoFromCookieValue( "5@$time", $time + 11 - 10 )['clientId'],
+ LBFactory::getCPInfoFromCookieValue( "5@$time#$agentId", $time + 11 - 10 )['clientId'],
'Stale (client ID)'
);
}
. '(page does not exist)"><script>evil()</script></a>',
$linkRenderer->makeLink( $foobar, new HtmlArmor( '<script>evil()</script>' ) )
);
+
+ $this->assertEquals(
+ '<a href="#fragment">fragment</a>',
+ $linkRenderer->makeLink( Title::newFromText( '#fragment' ) )
+ );
}
public function testGetLinkClasses() {
$this->insertPage( 'Talk:Example' );
$this->insertPage( 'User:Example' );
+ $this->insertPage( 'Barcelona' );
+ $this->insertPage( 'Barbara' );
+ $this->insertPage( 'External' );
}
protected function setUp() {
],
] ],
[ [
- 'Exact match not on top (T72958)',
+ 'Exact match not in first result should be moved to the first result (T72958)',
'provision' => [
'Barcelona',
'Bar',
],
] ],
[ [
- 'Exact match missing (T72958)',
+ 'Exact match missing from results should be added as first result (T72958)',
'provision' => [
'Barcelona',
'Barbara',
],
] ],
[ [
- 'Exact match missing and not existing',
+ 'Exact match missing and not existing pages should be dropped',
'provision' => [
'Exile',
'Exist',
],
'query' => 'Ex',
'results' => [
- 'Exile',
- 'Exist',
'External',
],
] ],
'Redirect test',
],
] ],
+ [ [
+ "Extra results must not be returned",
+ 'provision' => [
+ 'Example',
+ 'Example Bar',
+ 'Example Foo',
+ 'Example Foo/Bar'
+ ],
+ 'query' => 'foo',
+ 'results' => [
+ 'Example',
+ 'Example Bar',
+ 'Example Foo',
+ ],
+ ] ],
];
}
* @covers PrefixSearch::searchBackend
*/
public function testSearchBackend( array $case ) {
- $search = $stub = $this->getMockBuilder( SearchEngine::class )
- ->setMethods( [ 'completionSearchBackend' ] )->getMock();
-
- $return = SearchSuggestionSet::fromStrings( $case['provision'] );
-
- $search->expects( $this->any() )
- ->method( 'completionSearchBackend' )
- ->will( $this->returnValue( $return ) );
-
- $search->setLimitOffset( 3 );
+ $search = $this->mockSearchWithResults( $case['provision'] );
$results = $search->completionSearch( $case['query'] );
$results = $results->map( function ( SearchSuggestion $s ) {
$case[0]
);
}
+
+ public function paginationProvider() {
+ $res = [ 'Example', 'Example Bar', 'Example Foo', 'Example Foo/Bar' ];
+ return [
+ 'With less than requested results no pagination' => [
+ false, array_slice( $res, 0, 2 ),
+ ],
+ 'With same as requested results no pagination' => [
+ false, array_slice( $res, 0, 3 ),
+ ],
+ 'With extra result returned offer pagination' => [
+ true, $res,
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider paginationProvider
+ */
+ public function testPagination( $hasMoreResults, $provision ) {
+ $search = $this->mockSearchWithResults( $provision );
+ $results = $search->completionSearch( 'irrelevant' );
+
+ $this->assertEquals( $hasMoreResults, $results->hasMoreResults() );
+ }
+
+ private function mockSearchWithResults( $titleStrings, $limit = 3 ) {
+ $search = $stub = $this->getMockBuilder( SearchEngine::class )
+ ->setMethods( [ 'completionSearchBackend' ] )->getMock();
+
+ $return = SearchSuggestionSet::fromStrings( $titleStrings );
+
+ $search->expects( $this->any() )
+ ->method( 'completionSearchBackend' )
+ ->will( $this->returnValue( $return ) );
+
+ $search->setLimitOffset( $limit );
+ return $search;
+ }
}
$this->assertTrue( is_object( $results ) );
$matches = [];
- $row = $results->next();
- while ( $row ) {
+ foreach ( $results as $row ) {
$matches[] = $row->getTitle()->getPrefixedText();
- $row = $results->next();
}
$results->free();
# Search is not guaranteed to return results in a certain order;
public function testPhraseSearchHighlight() {
$phrase = "smithee is one who smiths";
$res = $this->search->searchText( "\"$phrase\"" );
- $match = $res->next();
+ $match = $res->getIterator()->current();
$snippet = "A <span class='searchmatch'>" . $phrase . "</span>";
$this->assertStringStartsWith( $snippet,
$match->getTextSnippet( $res->termMatches() ),
$this->mergeMwGlobalArrayValue( 'wgHooks',
[ 'SearchResultsAugment' => [ [ $this, 'addAugmentors' ] ] ] );
$this->search->augmentSearchResults( $resultSet );
- for ( $result = $resultSet->next(); $result; $result = $resultSet->next() ) {
+ foreach ( $resultSet as $result ) {
$id = $result->getTitle()->getArticleID();
$augmentData = "Result:$id:" . $result->getTitle()->getText();
$augmentData2 = "Result2:$id:" . $result->getTitle()->getText();
->method( 'augmentAll' )
->willReturnCallback( function ( SearchResultSet $resultSet ) {
$data = [];
- for ( $result = $resultSet->next(); $result; $result = $resultSet->next() ) {
+ foreach ( $resultSet as $result ) {
$id = $result->getTitle()->getArticleID();
$data[$id] = "Result:$id:" . $result->getTitle()->getText();
}
- $resultSet->rewind();
return $data;
} );
$setAugmentors['testSet'] = $setAugmentor;
} );
$rowAugmentors['testRow'] = $rowAugmentor;
}
+
+ public function testFiltersMissing() {
+ $availableResults = [];
+ foreach ( range( 0, 11 ) as $i ) {
+ $title = "Search_Result_$i";
+ $availableResults[] = $title;
+ // pages not created must be filtered
+ if ( $i % 2 == 0 ) {
+ $this->editPage( $title );
+ }
+ }
+ MockCompletionSearchEngine::addMockResults( 'foo', $availableResults );
+
+ $engine = new MockCompletionSearchEngine();
+ $engine->setLimitOffset( 10, 0 );
+ $results = $engine->completionSearch( 'foo' );
+ $this->assertEquals( 5, $results->getSize() );
+ $this->assertTrue( $results->hasMoreResults() );
+
+ $engine->setLimitOffset( 10, 10 );
+ $results = $engine->completionSearch( 'foo' );
+ $this->assertEquals( 1, $results->getSize() );
+ $this->assertFalse( $results->hasMoreResults() );
+ }
+
+ private function editPage( $title ) {
+ $page = WikiPage::factory( Title::newFromText( $title ) );
+ $page->doEditContent(
+ new WikitextContent( 'UTContent' ),
+ 'UTPageSummary',
+ EDIT_NEW | EDIT_SUPPRESS_RC
+ );
+ }
}
--- /dev/null
+<?php
+
+class SearchNearMatchResultSetTest extends PHPUnit\Framework\TestCase {
+ /**
+ * @covers SearchNearMatchResultSet::__construct
+ * @covers SearchNearMatchResultSet::numRows
+ */
+ public function testNumRows() {
+ $resultSet = new SearchNearMatchResultSet( null );
+ $this->assertEquals( 0, $resultSet->numRows() );
+
+ $resultSet = new SearchNearMatchResultSet( Title::newMainPage() );
+ $this->assertEquals( 1, $resultSet->numRows() );
+ }
+}
--- /dev/null
+<?php
+
+class SearchResultSetTest extends MediaWikiTestCase {
+ /**
+ * @covers SearchResultSet::getIterator
+ * @covers SearchResultSet::next
+ * @covers SearchResultSet::rewind
+ */
+ public function testIterate() {
+ $result = SearchResult::newFromTitle( Title::newMainPage() );
+ $resultSet = new MockSearchResultSet( [ $result ] );
+ $this->assertEquals( 1, $resultSet->numRows() );
+ $count = 0;
+ foreach ( $resultSet as $iterResult ) {
+ $this->assertEquals( $result, $iterResult );
+ $count++;
+ }
+ $this->assertEquals( 1, $count );
+
+ $this->hideDeprecated( 'SearchResultSet::rewind' );
+ $this->hideDeprecated( 'SearchResultSet::next' );
+ $resultSet->rewind();
+ $count = 0;
+ while ( false !== ( $iterResult = $resultSet->next() ) ) {
+ $this->assertEquals( $result, $iterResult );
+ $count++;
+ }
+ $this->assertEquals( 1, $count );
+ }
+
+ /**
+ * @covers SearchResultSet::augmentResult
+ * @covers SearchResultSet::setAugmentedData
+ */
+ public function testDelayedResultAugment() {
+ $result = SearchResult::newFromTitle( Title::newMainPage() );
+ $resultSet = new MockSearchResultSet( [ $result ] );
+ $resultSet->augmentResult( $result );
+ $this->assertEquals( [], $result->getExtensionData() );
+ $resultSet->setAugmentedData( 'foo', [
+ $result->getTitle()->getArticleID() => 'bar'
+ ] );
+ $this->assertEquals( [ 'foo' => 'bar' ], $result->getExtensionData() );
+ }
+
+ /**
+ * @covers SearchResultSet::shrink
+ * @covers SearchResultSet::count
+ * @covers SearchResultSet::hasMoreResults
+ */
+ public function testHasMoreResults() {
+ $result = SearchResult::newFromTitle( Title::newMainPage() );
+ $resultSet = new MockSearchResultSet( array_fill( 0, 3, $result ) );
+ $this->assertEquals( 3, count( $resultSet ) );
+ $this->assertFalse( $resultSet->hasMoreResults() );
+ $resultSet->shrink( 3 );
+ $this->assertFalse( $resultSet->hasMoreResults() );
+ $resultSet->shrink( 2 );
+ $this->assertTrue( $resultSet->hasMoreResults() );
+ }
+}
--- /dev/null
+<?php
+
+class SearchResultTest extends MediawikiTestCase {
+ /**
+ * @covers SearchResult::getExtensionData
+ * @covers SearchResult::setExtensionData
+ */
+ public function testExtensionData() {
+ $result = SearchResult::newFromTitle( Title::newMainPage() );
+ $this->assertEquals( [], $result->getExtensionData(), 'starts empty' );
+
+ $data = [ 'hello' => 'world' ];
+ $result->setExtensionData( function () use ( &$data ) {
+ return $data;
+ } );
+ $this->assertEquals( $data, $result->getExtensionData(), 'can set extension data' );
+ $data['this'] = 'that';
+ $this->assertEquals( $data, $result->getExtensionData(), 'refetches from callback' );
+ }
+
+ /**
+ * @covers SearchResult::getExtensionData
+ * @covers SearchResult::setExtensionData
+ */
+ public function testExtensionDataArrayBC() {
+ $result = SearchResult::newFromTitle( Title::newMainPage() );
+ $data = [ 'hello' => 'world' ];
+ $this->hideDeprecated( 'SearchResult::setExtensionData with array argument' );
+ $this->assertEquals( [], $result->getExtensionData(), 'starts empty' );
+ $result->setExtensionData( $data );
+ $this->assertEquals( $data, $result->getExtensionData(), 'can set extension data' );
+ $data['this'] = 'that';
+ $this->assertNotEquals( $data, $result->getExtensionData(), 'shouldnt hold any reference' );
+
+ $result->setExtensionData( $data );
+ $this->assertEquals( $data, $result->getExtensionData(), 'can replace extension data' );
+ }
+}
$this->containedSyntax = $containedSyntax;
}
- public function numRows() {
- return count( $this->results );
+ public function expandResults() {
+ return $this->results;
}
public function getTotalHits() {
* @group SpecialPageAliases
* @group SystemTest
* @group medium
+ * @todo This should be a structure test
*
* @author Katie Filbert < aude.wiki@gmail.com >
*/
class SpecialPageAliasTest extends MediaWikiTestCase {
/**
+ * @coversNothing
* @dataProvider validSpecialPageAliasesProvider
*/
public function testValidSpecialPageAliases( $code, $specialPageAliases ) {
--- /dev/null
+<?php
+
+/**
+ * SearchEngine implementation for returning mocked completion search results.
+ */
+class MockCompletionSearchEngine extends SearchEngine {
+ /** @var string[][] */
+ private static $results = [];
+
+ /**
+ * Reset any mocked results
+ */
+ public static function clearMockResults() {
+ self::$results = [];
+ }
+
+ /**
+ * Allows returning arbitrary lists of titles for completion search.
+ * Provided results will be sliced based on offset/limit of query.
+ *
+ * For results to exit the search engine they must pass Title::isKnown.
+ * Injecting into link cache is not enough, as LinkBatch will mark them
+ * bad, they need to be injected into the DB.
+ *
+ * @param string $query Search term as seen in completionSearchBackend
+ * @param string[] $result List of titles to respond to query with
+ */
+ public static function addMockResults( $query, array $result ) {
+ // Leading : ensures we don't treat another : as a namespace separator
+ $normalized = Title::newFromText( ":$query" )->getText();
+ self::$results[$normalized] = $result;
+ }
+
+ public function completionSearchBackend( $search ) {
+ if ( !isset( self::$results[$search] ) ) {
+ return SearchSuggestionSet::emptySuggestionSet();
+ }
+ $results = array_slice( self::$results[$search], $this->offset, $this->limit );
+
+ return SearchSuggestionSet::fromStrings( $results );
+ }
+}
--- /dev/null
+<?php
+
+use MediaWiki\MediaWikiServices;
+
+class MockSearchEngine extends SearchEngine {
+ /** @var SearchResult[][] */
+ private static $results = [];
+ /** @var SearchResultSet[][] */
+ private static $interwikiResults = [];
+
+ public static function clearMockResults() {
+ self::$results = [];
+ self::$interwikiResults = [];
+ }
+
+ /**
+ * @param string $query The query searched for *after* initial
+ * transformations have been applied.
+ * @param SearchResult[] $results The results to return for $query
+ */
+ public static function addMockResults( $query, array $results ) {
+ self::$results[$query] = $results;
+ $lc = MediaWikiServices::getInstance()->getLinkCache();
+ foreach ( $results as $result ) {
+ // TODO: better page ids? Does it matter?
+ $lc->addGoodLinkObj( mt_rand(), $result->getTitle() );
+ }
+ }
+
+ /**
+ * @param SearchResultSet[][] $interwikiResults
+ */
+ public static function setMockInterwikiResults( array $interwikiResults ) {
+ self::$interwikiResults = $interwikiResults;
+ }
+
+ protected function doSearchText( $term ) {
+ if ( isset( self::$results[ $term ] ) ) {
+ $results = array_slice( self::$results[ $term ], $this->offset, $this->limit );
+ } else {
+ $results = [];
+ }
+ return new MockSearchResultSet( $results, self::$interwikiResults );
+ }
+}
--- /dev/null
+<?php
+
+class MockSearchResult extends SearchResult {
+ private $isMissingRevision = false;
+ private $isBrokenTitle = false;
+
+ public function isMissingRevision() {
+ return $this->isMissingRevision;
+ }
+ public function setMissingRevision( $isMissingRevision ) {
+ $this->isMissingRevision = $isMissingRevision;
+ return $this;
+ }
+
+ public function isBrokenTitle() {
+ return $this->isBrokenTitle;
+ }
+
+ public function setBrokenTitle( $isBrokenTitle ) {
+ $this->isBrokenTitle = $isBrokenTitle;
+ return $this;
+ }
+
+ public function getInterwikiPrefix() {
+ return $this->interwikiPrefix;
+ }
+
+ public function setInterwikiPrefix( $interwikiPrefix ) {
+ $this->interwikiPrefix = $interwikiPrefix;
+ return $this;
+ }
+}
--- /dev/null
+<?php
+
+class MockSearchResultSet extends SearchResultSet {
+ /*
+ * @var SearchResultSet[][] Map from result type to list of results for
+ * that type.
+ */
+ private $interwikiResults;
+
+ /**
+ * @param SearchResult[] $results
+ * @param SearchResultSet[][] $interwikiResults Map from result type
+ * to list of results for that type.
+ */
+ public function __construct( array $results, array $interwikiResults = [] ) {
+ parent::__construct( false, false );
+ $this->results = $results;
+ $this->interwikiResults = $interwikiResults;
+ }
+
+ public function numRows() {
+ return count( $this->results );
+ }
+
+ public function hasInterwikiResults( $type = self::SECONDARY_RESULTS ) {
+ return isset( $this->interwikiResults[$type] ) &&
+ count( $this->interwikiResults[$type] ) > 0;
+ }
+
+ public function getInterwikiResults( $type = self::SECONDARY_RESULTS ) {
+ if ( $this->hasInterwikiResults( $type ) ) {
+ return $this->interwikiResults[$type];
+ } else {
+ return null;
+ }
+ }
+}
return $rights;
}
+ /**
+ * @coversNothing
+ */
public function testAvailableRights() {
$missingRights = array_diff(
$this->getAllVisibleRights(),
}
/**
+ * @coversNothing
* @dataProvider provideHandlers
* @param ContentHandler $handler
*/
/**
* Validates all loaded extensions and skins using the ExtensionRegistry
* against the extension.json schema in the docs/ folder.
+ *
+ * @coversNothing
*/
class ExtensionJsonValidationTest extends PHPUnit\Framework\TestCase {
* @copyright © 2012, Niklas Laxström
* @copyright © 2012, Santhosh Thottingal
* @copyright © 2012, Timo Tijhof
+ * @coversNothing
*/
class ResourcesTest extends MediaWikiTestCase {
* Verify all files that appear to be tests have file names ending in
* Test. If the file names do not end in Test, they will not be run.
* @group medium
+ * @coversNothing
*/
public function testUnitTestFileNamesEndWithTest() {
if ( wfIsWindows() ) {
* when no extensions with tests are used.
*/
class DummyExtensionsTest extends MediaWikiTestCase {
+ /**
+ * @coversNothing
+ */
public function testNothing() {
$this->assertTrue( true );
}
# Check the command before running in background so
# that it can actually fail and have a descriptive error
hash chromedriver
-chromedriver --url-base=/wd/hub --port=4444 &
+chromedriver --url-base=wd/hub --port=4444 &
+CHROME_DRIVER_PID=$!
+echo chromedriver running with PID $CHROME_DRIVER_PID
# Make sure it is killed to prevent file descriptors leak
function kill_chromedriver() {
- killall chromedriver > /dev/null
+ # Use kill instead of killall to increase chances of this working on Windows
+ kill $CHROME_DRIVER_PID > /dev/null
}
trap kill_chromedriver EXIT
npm run selenium-test
var content,
name;
- function getTestString() {
- return Math.random().toString() + '-öäü-♠♣♥♦';
+ function getTestString( suffix = 'defaultsuffix' ) {
+ return Math.random().toString() + '-Iñtërnâtiônàlizætiøn☃-' + suffix;
}
before( function () {
beforeEach( function () {
browser.deleteCookie();
- content = getTestString();
- name = getTestString();
+ content = getTestString( 'beforeEach-content' );
+ name = getTestString( 'beforeEach-name' );
} );
it( 'should be creatable', function () {
} );
it( 'should be re-creatable', function () {
- let initialContent = getTestString();
+ let initialContent = getTestString( 'initialContent' );
// create
browser.call( function () {
} );
// edit
- EditPage.edit( name, content );
+ let editContent = getTestString( 'editContent' );
+ EditPage.edit( name, editContent );
// check
assert.strictEqual( EditPage.heading.getText(), name );
- assert.strictEqual( EditPage.displayedContent.getText(), content );
+ assert.strictEqual( EditPage.displayedContent.getText(), editContent );
} );
it( 'should have history', function () {