From: jenkins-bot Date: Wed, 25 Sep 2019 06:10:12 +0000 (+0000) Subject: Merge "resourceloader: Convert ImageModule test to stricter unit test" X-Git-Tag: 1.34.0-rc.0~98 X-Git-Url: http://git.heureux-cyclage.org/?a=commitdiff_plain;h=732cf4d5bf47402c859cb96a23a717908a76eb56;hp=d85ea2b0305ff62ab42c2c724a707963c3ed3848;p=lhc%2Fweb%2Fwiklou.git Merge "resourceloader: Convert ImageModule test to stricter unit test" --- diff --git a/RELEASE-NOTES-1.34 b/RELEASE-NOTES-1.34 index 508ba08955..6b89ff6633 100644 --- a/RELEASE-NOTES-1.34 +++ b/RELEASE-NOTES-1.34 @@ -126,6 +126,9 @@ $wgPasswordPolicy['policies']['default']['PasswordNotInLargeBlacklist'] = false; * (T222388) API modules can now be specified as an ObjectFactory spec, allowing the construction of modules that require services to be injected in their constructor. +* (T117736) The function signature of SpecialContributions::getForm::filters + has changed. It now expects definitions of additional filter fields as array + rather than string. === External library changes in 1.34 === diff --git a/docs/hooks.txt b/docs/hooks.txt index 55ba06e605..4261b0b0ef 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -3177,7 +3177,7 @@ $row: Revision information from the database 'SpecialContributions::getForm::filters': Called with a list of filters to render on Special:Contributions. $sp: SpecialContributions object, for context -&$filters: List of filters rendered as HTML +&$filters: List of filter object definitions (compatible with OOUI form) 'SpecialListusersDefaultQuery': Called right before the end of UsersPager::getDefaultQuery(). diff --git a/includes/Html.php b/includes/Html.php index c4b57af978..a8f349606c 100644 --- a/includes/Html.php +++ b/includes/Html.php @@ -704,7 +704,7 @@ class Html { * Return the HTML for a message box. * @since 1.31 * @param string $html of contents of box - * @param string $className corresponding to box + * @param string|array $className corresponding to box * @param string $heading (optional) * @return string of HTML representing a box. */ @@ -718,32 +718,38 @@ class Html { /** * Return a warning box. * @since 1.31 + * @since 1.34 $className optional parameter added * @param string $html of contents of box + * @param string $className (optional) corresponding to box * @return string of HTML representing a warning box. */ - public static function warningBox( $html ) { - return self::messageBox( $html, 'warningbox' ); + public static function warningBox( $html, $className = '' ) { + return self::messageBox( $html, [ 'warningbox', $className ] ); } /** * Return an error box. * @since 1.31 + * @since 1.34 $className optional parameter added * @param string $html of contents of error box * @param string $heading (optional) + * @param string $className (optional) corresponding to box * @return string of HTML representing an error box. */ - public static function errorBox( $html, $heading = '' ) { - return self::messageBox( $html, 'errorbox', $heading ); + public static function errorBox( $html, $heading = '', $className = '' ) { + return self::messageBox( $html, [ 'errorbox', $className ], $heading ); } /** * Return a success box. * @since 1.31 + * @since 1.34 $className optional parameter added * @param string $html of contents of box + * @param string $className (optional) corresponding to box * @return string of HTML representing a success box. */ - public static function successBox( $html ) { - return self::messageBox( $html, 'successbox' ); + public static function successBox( $html, $className = '' ) { + return self::messageBox( $html, [ 'successbox', $className ] ); } /** diff --git a/includes/Revision/RevisionStore.php b/includes/Revision/RevisionStore.php index 3ecef76fa0..a5cf8404cb 100644 --- a/includes/Revision/RevisionStore.php +++ b/includes/Revision/RevisionStore.php @@ -1907,7 +1907,11 @@ class RevisionStore * 'content'- whether the actual content of the slots should be * preloaded. * @param int $queryFlags - * @param Title|null $title + * @param Title|null $title The title to which all the revision rows belong, if there + * is such a title and the caller has it handy, so we don't have to look it up again. + * If this parameter is given and any of the rows has a rev_page_id that is different + * from $title->getArticleID(), an InvalidArgumentException is thrown. + * * @return StatusValue a status with a RevisionRecord[] of successfully fetched revisions * and an array of errors for the revisions failed to fetch. */ diff --git a/includes/exception/UserNotLoggedIn.php b/includes/exception/UserNotLoggedIn.php index 7a99765e9c..246c944f2e 100644 --- a/includes/exception/UserNotLoggedIn.php +++ b/includes/exception/UserNotLoggedIn.php @@ -80,9 +80,10 @@ class UserNotLoggedIn extends ErrorPageError { // since the message may not be compatible. if ( !in_array( $this->msg, LoginHelper::getValidErrorMessages() ) ) { parent::report(); + return; } - // Message is valid. Redirec to Special:Userlogin + // Message is valid. Redirect to Special:Userlogin $context = RequestContext::getMain(); diff --git a/includes/http/HttpRequestFactory.php b/includes/http/HttpRequestFactory.php index 5315cede41..84e7b739bd 100644 --- a/includes/http/HttpRequestFactory.php +++ b/includes/http/HttpRequestFactory.php @@ -59,13 +59,12 @@ class HttpRequestFactory { * - originalRequest Information about the original request (as a WebRequest object or * an associative array with 'ip' and 'userAgent'). * @codingStandardsIgnoreStart - * @phan-param array{timeout?:int|string,connectTimeout?:int|string,postData?:array,proxy?:string,noProxy?:bool,sslVerifyHost?:bool,sslVerifyCert?:bool,caInfo?:string,maxRedirects?:int,followRedirects?:bool,userAgent?:string,method?:string,logger?:\Psr\Log\LoggerInterface,username?:string,password?:string,originalRequest?:WebRequest|array{ip:string,userAgent:string}} $options + * @phan-param array{timeout?:int|string,connectTimeout?:int|string,postData?:array,proxy?:string,noProxy?:bool,sslVerifyHost?:bool,sslVerifyCert?:bool,caInfo?:string,maxRedirects?:int,followRedirects?:bool,userAgent?:string,method?:string,logger?:\Psr\Log\LoggerInterface,username?:string,password?:string,originalRequest?:\WebRequest|array{ip:string,userAgent:string}} $options * @codingStandardsIgnoreEnd * @param string $caller The method making this request, for profiling * @throws RuntimeException * @return MWHttpRequest * @see MWHttpRequest::__construct - * @suppress PhanUndeclaredTypeParameter */ public function create( $url, array $options = [], $caller = __METHOD__ ) { if ( !Http::$httpEngine ) { diff --git a/includes/installer/DatabaseInstaller.php b/includes/installer/DatabaseInstaller.php index ac8c9e6745..ce7e29ddb5 100644 --- a/includes/installer/DatabaseInstaller.php +++ b/includes/installer/DatabaseInstaller.php @@ -690,7 +690,7 @@ abstract class DatabaseInstaller { $this->getPasswordBox( 'wgDBpassword', 'config-db-password' ) . $this->parent->getHelpBox( 'config-db-web-help' ); if ( $noCreateMsg ) { - $s .= $this->parent->getWarningBox( wfMessage( $noCreateMsg )->plain() ); + $s .= Html::warningBox( wfMessage( $noCreateMsg )->plain(), 'config-warning-box' ); } else { $s .= $this->getCheckBox( '_CreateDBAccount', 'config-db-web-create' ); } diff --git a/includes/installer/WebInstaller.php b/includes/installer/WebInstaller.php index b6e90a9b45..21ad210f5e 100644 --- a/includes/installer/WebInstaller.php +++ b/includes/installer/WebInstaller.php @@ -390,7 +390,8 @@ class WebInstaller extends Installer { ); } $text = $msg->useDatabase( false )->plain(); - $this->output->addHTML( $this->getErrorBox( $text ) ); + $box = Html::errorBox( $text, '', 'config-error-box' ); + $this->output->addHTML( $box ); } /** @@ -1046,9 +1047,9 @@ class WebInstaller extends Installer { $text = $status->getWikiText(); if ( $status->isOK() ) { - $box = $this->getWarningBox( $text ); + $box = Html::warningBox( $text, 'config-warning-box' ); } else { - $box = $this->getErrorBox( $text ); + $box = Html::errorBox( $text, '', 'config-error-box' ); } $this->output->addHTML( $box ); diff --git a/includes/installer/WebInstallerOptions.php b/includes/installer/WebInstallerOptions.php index 7bec49a369..3521fa188f 100644 --- a/includes/installer/WebInstallerOptions.php +++ b/includes/installer/WebInstallerOptions.php @@ -137,7 +137,7 @@ class WebInstallerOptions extends WebInstallerPage { } } else { $skinHtml .= - $this->parent->getWarningBox( wfMessage( 'config-skins-missing' )->plain() ) . + Html::warningBox( wfMessage( 'config-skins-missing' )->plain(), 'config-warning-box' ) . Html::hidden( 'config_wgDefaultSkin', $chosenSkinName ); } diff --git a/includes/installer/WebInstallerRestart.php b/includes/installer/WebInstallerRestart.php index be55c32fd0..07e2e7513f 100644 --- a/includes/installer/WebInstallerRestart.php +++ b/includes/installer/WebInstallerRestart.php @@ -36,7 +36,7 @@ class WebInstallerRestart extends WebInstallerPage { } $this->startForm(); - $s = $this->parent->getWarningBox( wfMessage( 'config-help-restart' )->plain() ); + $s = Html::warningBox( wfMessage( 'config-help-restart' )->plain(), '', 'config-warning-box' ); $this->addHTML( $s ); $this->endForm( 'restart' ); diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php index e8b85fa024..8f92cd5f9d 100644 --- a/includes/specials/SpecialContributions.php +++ b/includes/specials/SpecialContributions.php @@ -23,7 +23,6 @@ use MediaWiki\Block\DatabaseBlock; use MediaWiki\MediaWikiServices; -use MediaWiki\Widget\DateInputWidget; /** * Special:Contributions, show user contributions in a paged list @@ -43,11 +42,16 @@ class SpecialContributions extends IncludableSpecialPage { $out = $this->getOutput(); // Modules required for viewing the list of contributions (also when included on other pages) $out->addModuleStyles( [ + 'jquery.makeCollapsible.styles', 'mediawiki.interface.helpers.styles', 'mediawiki.special', 'mediawiki.special.changeslist', ] ); - $out->addModules( 'mediawiki.special.recentchanges' ); + $out->addModules( [ + 'mediawiki.special.recentchanges', + // Certain skins e.g. Minerva might have disabled this module. + 'mediawiki.page.ready' + ] ); $this->addHelpLink( 'Help:User contributions' ); $this->opts = []; @@ -59,7 +63,7 @@ class SpecialContributions extends IncludableSpecialPage { if ( !strlen( $target ) ) { if ( !$this->including() ) { - $out->addHTML( $this->getForm() ); + $out->addHTML( $this->getForm( $this->opts ) ); } return; @@ -77,7 +81,7 @@ class SpecialContributions extends IncludableSpecialPage { if ( ExternalUserNames::isExternal( $target ) ) { $userObj = User::newFromName( $target, false ); if ( !$userObj ) { - $out->addHTML( $this->getForm() ); + $out->addHTML( $this->getForm( $this->opts ) ); return; } @@ -89,12 +93,12 @@ class SpecialContributions extends IncludableSpecialPage { } else { $nt = Title::makeTitleSafe( NS_USER, $target ); if ( !$nt ) { - $out->addHTML( $this->getForm() ); + $out->addHTML( $this->getForm( $this->opts ) ); return; } $userObj = User::newFromName( $nt->getText(), false ); if ( !$userObj ) { - $out->addHTML( $this->getForm() ); + $out->addHTML( $this->getForm( $this->opts ) ); return; } $id = $userObj->getId(); @@ -114,14 +118,23 @@ class SpecialContributions extends IncludableSpecialPage { } $ns = $request->getVal( 'namespace', null ); - if ( $ns !== null && $ns !== '' ) { + if ( $ns !== null && $ns !== '' && $ns !== 'all' ) { $this->opts['namespace'] = intval( $ns ); } else { $this->opts['namespace'] = ''; } + // Backwards compatibility: Before using OOUI form the old HTML form had + // fields for nsInvert and associated. These have now been replaced with the + // wpFilters query string parameters. These are retained to keep old URIs working. $this->opts['associated'] = $request->getBool( 'associated' ); $this->opts['nsInvert'] = (bool)$request->getVal( 'nsInvert' ); + $nsFilters = $request->getArray( 'wpfilters', null ); + if ( $nsFilters !== null ) { + $this->opts['associated'] = in_array( 'associated', $nsFilters ); + $this->opts['nsInvert'] = in_array( 'nsInvert', $nsFilters ); + } + $this->opts['tagfilter'] = (string)$request->getVal( 'tagfilter' ); // Allows reverts to have the bot flag in recent changes. It is just here to @@ -149,7 +162,7 @@ class SpecialContributions extends IncludableSpecialPage { "
\n\$1\n
", [ 'negative-namespace-not-supported' ] ); - $out->addHTML( $this->getForm() ); + $out->addHTML( $this->getForm( $this->opts ) ); return; } @@ -201,9 +214,6 @@ class SpecialContributions extends IncludableSpecialPage { $this->addFeedLinks( $feedParams ); if ( Hooks::run( 'SpecialContributionsBeforeMainOutput', [ $id, $userObj, $this ] ) ) { - if ( !$this->including() ) { - $out->addHTML( $this->getForm() ); - } $pager = new ContribsPager( $this->getContext(), [ 'target' => $target, 'namespace' => $this->opts['namespace'], @@ -217,6 +227,9 @@ class SpecialContributions extends IncludableSpecialPage { 'nsInvert' => $this->opts['nsInvert'], 'associated' => $this->opts['associated'], ], $this->getLinkRenderer() ); + if ( !$this->including() ) { + $out->addHTML( $this->getForm( $this->opts ) ); + } if ( IP::isValidRange( $target ) && !$pager->isQueryableRange( $target ) ) { // Valid range, but outside CIDR limit. @@ -458,52 +471,12 @@ class SpecialContributions extends IncludableSpecialPage { /** * Generates the namespace selector form with hidden attributes. + * @param array $pagerOptions with keys contribs, user, deletedOnly, limit, target, topOnly, + * newOnly, hideMinor, namespace, associated, nsInvert, tagfilter, year, start, end * @return string HTML fragment */ - protected function getForm() { + protected function getForm( array $pagerOptions ) { $this->opts['title'] = $this->getPageTitle()->getPrefixedText(); - if ( !isset( $this->opts['target'] ) ) { - $this->opts['target'] = ''; - } else { - $this->opts['target'] = str_replace( '_', ' ', $this->opts['target'] ); - } - - if ( !isset( $this->opts['namespace'] ) ) { - $this->opts['namespace'] = ''; - } - - if ( !isset( $this->opts['nsInvert'] ) ) { - $this->opts['nsInvert'] = ''; - } - - if ( !isset( $this->opts['associated'] ) ) { - $this->opts['associated'] = false; - } - - if ( !isset( $this->opts['start'] ) ) { - $this->opts['start'] = ''; - } - - if ( !isset( $this->opts['end'] ) ) { - $this->opts['end'] = ''; - } - - if ( !isset( $this->opts['tagfilter'] ) ) { - $this->opts['tagfilter'] = ''; - } - - if ( !isset( $this->opts['topOnly'] ) ) { - $this->opts['topOnly'] = false; - } - - if ( !isset( $this->opts['newOnly'] ) ) { - $this->opts['newOnly'] = false; - } - - if ( !isset( $this->opts['hideMinor'] ) ) { - $this->opts['hideMinor'] = false; - } - // Modules required only for the form $this->getOutput()->addModules( [ 'mediawiki.userSuggest', @@ -511,15 +484,7 @@ class SpecialContributions extends IncludableSpecialPage { ] ); $this->getOutput()->addModuleStyles( 'mediawiki.widgets.DateInputWidget.styles' ); $this->getOutput()->enableOOUI(); - - $form = Html::openElement( - 'form', - [ - 'method' => 'get', - 'action' => wfScript(), - 'class' => 'mw-contributions-form' - ] - ); + $fields = []; # Add hidden params for tracking except for parameters in $skipParameters $skipParameters = [ @@ -527,7 +492,6 @@ class SpecialContributions extends IncludableSpecialPage { 'nsInvert', 'deletedOnly', 'target', - 'contribs', 'year', 'month', 'start', @@ -543,207 +507,171 @@ class SpecialContributions extends IncludableSpecialPage { if ( in_array( $name, $skipParameters ) ) { continue; } - $form .= "\t" . Html::hidden( $name, $value ) . "\n"; - } - - $tagFilter = ChangeTags::buildTagFilterSelector( - $this->opts['tagfilter'], false, $this->getContext() ); - - if ( $tagFilter ) { - $filterSelection = Html::rawElement( - 'div', - [], - implode( "\u{00A0}", $tagFilter ) - ); - } else { - $filterSelection = Html::rawElement( 'div', [], '' ); - } - - $labelUsername = Xml::label( - $this->msg( 'sp-contributions-username' )->text(), - 'mw-target-user-or-ip' - ); - $input = Html::input( - 'target', - $this->opts['target'], - 'text', - [ - 'id' => 'mw-target-user-or-ip', - 'size' => '40', - 'class' => [ - 'mw-input', - 'mw-ui-input-inline', - 'mw-autocomplete-user', // used by mediawiki.userSuggest - ], - ] + ( - // Only autofocus if target hasn't been specified - $this->opts['target'] ? [] : [ 'autofocus' => true ] - ) - ); - - $targetSelection = Html::rawElement( - 'div', - [], - $labelUsername . ' ' . $input . ' ' - ); - $hidden = $this->opts['namespace'] === '' ? ' mw-input-hidden' : ''; - $namespaceSelection = Xml::tags( - 'div', - [], - Xml::label( - $this->msg( 'namespace' )->text(), - 'namespace' - ) . "\u{00A0}" . - Html::namespaceSelector( - [ 'selected' => $this->opts['namespace'], 'all' => '', 'in-user-lang' => true ], - [ - 'name' => 'namespace', - 'id' => 'namespace', - 'class' => 'namespaceselector', - ] - ) . "\u{00A0}" . - Html::rawElement( - 'span', - [ 'class' => 'mw-input-with-label' . $hidden ], - Xml::checkLabel( - $this->msg( 'invert' )->text(), - 'nsInvert', - 'nsinvert', - $this->opts['nsInvert'], - [ - 'title' => $this->msg( 'tooltip-invert' )->text(), - 'class' => 'mw-input' - ] - ) . "\u{00A0}" - ) . - Html::rawElement( 'span', [ 'class' => 'mw-input-with-label' . $hidden ], - Xml::checkLabel( - $this->msg( 'namespace_association' )->text(), - 'associated', - 'nsassociated', - $this->opts['associated'], - [ - 'title' => $this->msg( 'tooltip-namespace_association' )->text(), - 'class' => 'mw-input' - ] - ) . "\u{00A0}" - ) - ); + $fields[$name] = [ + 'name' => $name, + 'type' => 'hidden', + 'default' => $value, + ]; + } + + $target = $this->opts['target'] ?? null; + $fields['target'] = [ + 'type' => 'text', + 'cssclass' => 'mw-autocomplete-user mw-ui-input-inline mw-input', + 'default' => $target ? + str_replace( '_', ' ', $target ) : '' , + 'label' => $this->msg( 'sp-contributions-username' )->text(), + 'name' => 'target', + 'id' => 'mw-target-user-or-ip', + 'size' => 40, + 'autofocus' => !$target, + 'section' => 'contribs-top', + ]; - $filters = []; + $ns = $this->opts['namespace'] ?? null; + $fields['namespace'] = [ + 'type' => 'namespaceselect', + 'label' => $this->msg( 'namespace' )->text(), + 'name' => 'namespace', + 'cssclass' => 'namespaceselector', + 'default' => $ns, + 'id' => 'namespace', + 'section' => 'contribs-top', + ]; + $request = $this->getRequest(); + $nsFilters = $request->getArray( 'wpfilters' ); + $fields['nsFilters'] = [ + 'class' => 'HTMLMultiSelectField', + 'label' => '', + 'name' => 'wpfilters', + 'flatlist' => true, + // Only shown when namespaces are selected. + 'cssclass' => $ns === '' ? + 'contribs-ns-filters mw-input-with-label mw-input-hidden' : + 'contribs-ns-filters mw-input-with-label', + // `contribs-ns-filters` class allows these fields to be toggled on/off by JavaScript. + // See resources/src/mediawiki.special.recentchanges.js + 'infusable' => true, + 'options' => [ + $this->msg( 'invert' )->text() => 'nsInvert', + $this->msg( 'namespace_association' )->text() => 'associated', + ], + 'default' => $nsFilters, + 'section' => 'contribs-top', + ]; + $fields['tagfilter'] = [ + 'type' => 'tagfilter', + 'cssclass' => 'mw-tagfilter-input', + 'id' => 'tagfilter', + 'label-message' => [ 'tag-filter', 'parse' ], + 'name' => 'tagfilter', + 'size' => 20, + 'section' => 'contribs-top', + ]; if ( MediaWikiServices::getInstance() - ->getPermissionManager() - ->userHasRight( $this->getUser(), 'deletedhistory' ) + ->getPermissionManager() + ->userHasRight( $this->getUser(), 'deletedhistory' ) ) { - $filters[] = Html::rawElement( - 'span', - [ 'class' => 'mw-input-with-label' ], - Xml::checkLabel( - $this->msg( 'history-show-deleted' )->text(), - 'deletedOnly', - 'mw-show-deleted-only', - $this->opts['deletedOnly'], - [ 'class' => 'mw-input' ] - ) - ); - } - - $filters[] = Html::rawElement( - 'span', - [ 'class' => 'mw-input-with-label' ], - Xml::checkLabel( - $this->msg( 'sp-contributions-toponly' )->text(), - 'topOnly', - 'mw-show-top-only', - $this->opts['topOnly'], - [ 'class' => 'mw-input' ] - ) - ); - $filters[] = Html::rawElement( - 'span', - [ 'class' => 'mw-input-with-label' ], - Xml::checkLabel( - $this->msg( 'sp-contributions-newonly' )->text(), - 'newOnly', - 'mw-show-new-only', - $this->opts['newOnly'], - [ 'class' => 'mw-input' ] - ) - ); - $filters[] = Html::rawElement( - 'span', - [ 'class' => 'mw-input-with-label' ], - Xml::checkLabel( - $this->msg( 'sp-contributions-hideminor' )->text(), - 'hideMinor', - 'mw-hide-minor-edits', - $this->opts['hideMinor'], - [ 'class' => 'mw-input' ] - ) - ); + $fields['deletedOnly'] = [ + 'type' => 'check', + 'id' => 'mw-show-deleted-only', + 'label' => $this->msg( 'history-show-deleted' )->text(), + 'name' => 'deletedOnly', + 'section' => 'contribs-top', + ]; + } + + $fields['topOnly'] = [ + 'type' => 'check', + 'id' => 'mw-show-top-only', + 'label' => $this->msg( 'sp-contributions-toponly' )->text(), + 'name' => 'topOnly', + 'section' => 'contribs-top', + ]; + $fields['newOnly'] = [ + 'type' => 'check', + 'id' => 'mw-show-new-only', + 'label' => $this->msg( 'sp-contributions-newonly' )->text(), + 'name' => 'newOnly', + 'section' => 'contribs-top', + ]; + $fields['hideMinor'] = [ + 'type' => 'check', + 'cssclass' => 'mw-hide-minor-edits', + 'id' => 'mw-show-new-only', + 'label' => $this->msg( 'sp-contributions-hideminor' )->text(), + 'name' => 'hideMinor', + 'section' => 'contribs-top', + ]; + // Allow additions at this point to the filters. + $rawFilters = []; Hooks::run( 'SpecialContributions::getForm::filters', - [ $this, &$filters ] - ); - - $extraOptions = Html::rawElement( - 'div', - [], - implode( '', $filters ) + [ $this, &$rawFilters ] ); + foreach ( $rawFilters as $filter ) { + // Backwards compatibility support for previous hook function signature. + if ( is_string( $filter ) ) { + $fields[] = [ + 'type' => 'info', + 'default' => $filter, + 'raw' => true, + 'section' => 'contribs-top', + ]; + wfDeprecated( + __METHOD__ . + ' returning string[]', + '1.33' + ); + } else { + // Preferred append method. + $fields[] = $filter; + } + } - $dateRangeSelection = Html::rawElement( - 'div', - [], - Xml::label( $this->msg( 'date-range-from' )->text(), 'mw-date-start' ) . ' ' . - new DateInputWidget( [ - 'infusable' => true, - 'id' => 'mw-date-start', - 'name' => 'start', - 'value' => $this->opts['start'], - 'longDisplayFormat' => true, - ] ) . '
' . - Xml::label( $this->msg( 'date-range-to' )->text(), 'mw-date-end' ) . ' ' . - new DateInputWidget( [ - 'infusable' => true, - 'id' => 'mw-date-end', - 'name' => 'end', - 'value' => $this->opts['end'], - 'longDisplayFormat' => true, - ] ) - ); + $fields['start'] = [ + 'type' => 'date', + 'default' => '', + 'id' => 'mw-date-start', + 'label' => $this->msg( 'date-range-from' )->text(), + 'name' => 'start', + 'section' => 'contribs-date', + ]; + $fields['end'] = [ + 'type' => 'date', + 'default' => '', + 'id' => 'mw-date-end', + 'label' => $this->msg( 'date-range-to' )->text(), + 'name' => 'end', + 'section' => 'contribs-date', + ]; - $submit = Xml::tags( 'div', [], - Html::submitButton( - $this->msg( 'sp-contributions-submit' )->text(), - [ 'class' => 'mw-submit' ], [ 'mw-ui-progressive' ] + $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() ); + $htmlForm + ->setMethod( 'get' ) + // When offset is defined, the user is paging through results + // so we hide the form by default to allow users to focus on browsing + // rather than defining search parameters + ->setCollapsibleOptions( + ( $pagerOptions['target'] ?? null ) || + ( $pagerOptions['start'] ?? null ) || + ( $pagerOptions['end'] ?? null ) ) - ); - - $form .= Xml::fieldset( - $this->msg( 'sp-contributions-search' )->text(), - $targetSelection . - $namespaceSelection . - $filterSelection . - $extraOptions . - $dateRangeSelection . - $submit, - [ 'class' => 'mw-contributions-table' ] - ); + ->setAction( wfScript() ) + ->setSubmitText( $this->msg( 'sp-contributions-submit' )->text() ) + ->setWrapperLegend( $this->msg( 'sp-contributions-search' )->text() ); $explain = $this->msg( 'sp-contributions-explain' ); if ( !$explain->isBlank() ) { - $form .= Html::rawElement( - 'p', [ 'id' => 'mw-sp-contributions-explain' ], $explain->parse() - ); + $htmlForm->addFooterText( "

{$explain->parse()}

" ); } - $form .= Xml::closeElement( 'form' ); + $htmlForm->loadData(); - return $form; + return $htmlForm->getHTML( false ); } /** diff --git a/includes/specials/pagers/NewFilesPager.php b/includes/specials/pagers/NewFilesPager.php index be4a1bdf1e..9a78c5dda7 100644 --- a/includes/specials/pagers/NewFilesPager.php +++ b/includes/specials/pagers/NewFilesPager.php @@ -64,11 +64,11 @@ class NewFilesPager extends RangeChronologicalPager { function getQueryInfo() { $opts = $this->opts; $conds = []; - $imgQuery = LocalFile::getQueryInfo(); - $tables = $imgQuery['tables']; - $fields = [ 'img_name', 'img_timestamp' ] + $imgQuery['fields']; + $actorQuery = ActorMigration::newMigration()->getJoin( 'img_user' ); + $tables = [ 'image' ] + $actorQuery['tables']; + $fields = [ 'img_name', 'img_timestamp' ] + $actorQuery['fields']; $options = []; - $jconds = $imgQuery['joins']; + $jconds = $actorQuery['joins']; $user = $opts->getValue( 'user' ); if ( $user !== '' ) { @@ -89,7 +89,7 @@ class NewFilesPager extends RangeChronologicalPager { 'LEFT JOIN', [ 'ug_group' => $groupsWithBotPermission, - 'ug_user = ' . $imgQuery['fields']['img_user'], + 'ug_user = ' . $actorQuery['fields']['img_user'], 'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() ) ] ]; @@ -107,7 +107,7 @@ class NewFilesPager extends RangeChronologicalPager { 'JOIN', [ 'rc_title = img_name', - 'rc_actor = ' . $imgQuery['fields']['img_actor'], + 'rc_actor = ' . $actorQuery['fields']['img_actor'], 'rc_timestamp = img_timestamp' ] ]; diff --git a/maintenance/Maintenance.php b/maintenance/Maintenance.php index f89fa6281a..f741cd2653 100644 --- a/maintenance/Maintenance.php +++ b/maintenance/Maintenance.php @@ -587,7 +587,7 @@ abstract class Maintenance { "server name detection may fail in command line scripts.", false, true ); $this->addOption( 'profiler', 'Profiler output format (usually "text")', false, true ); // This is named --mwdebug, because --debug would conflict in the phpunit.php CLI script. - $this->addOption( 'mwdebug', 'Enable built-in MediaWiki development settings', false, true ); + $this->addOption( 'mwdebug', 'Enable built-in MediaWiki development settings', false, false ); # Save generic options to display them separately in help $this->mGenericParameters = $this->mParams; diff --git a/maintenance/cleanupTable.inc b/maintenance/cleanupTable.inc index b78e691e8f..a7a6465727 100644 --- a/maintenance/cleanupTable.inc +++ b/maintenance/cleanupTable.inc @@ -87,7 +87,7 @@ class TableCleanup extends Maintenance { $this->output( sprintf( "%s %s: %6.2f%% done on %s; ETA %s [%d/%d] %.2f/sec <%.2f%% updated>\n", - wfWikiID(), + WikiMap::getCurrentWikiDbDomain()->getId(), wfTimestamp( TS_DB, intval( $now ) ), $portion * 100.0, $this->table, diff --git a/maintenance/userDupes.inc b/maintenance/userDupes.inc index 5f7f9d5e43..cbe5d27dc0 100644 --- a/maintenance/userDupes.inc +++ b/maintenance/userDupes.inc @@ -110,8 +110,9 @@ class UserDupes { * @return bool */ private function checkDupes( $doDelete = false ) { + $dbDomain = WikiMap::getCurrentWikiDbDomain()->getId(); if ( $this->hasUniqueIndex() ) { - echo wfWikiID() . " already has a unique index on its user table.\n"; + echo "$dbDomain already has a unique index on its user table.\n"; return true; } @@ -122,7 +123,7 @@ class UserDupes { $dupes = $this->getDupes(); $count = count( $dupes ); - $this->out( "Found $count accounts with duplicate records on " . wfWikiID() . ".\n" ); + $this->out( "Found $count accounts with duplicate records on $dbDomain.\n" ); $this->trimmed = 0; $this->reassigned = 0; $this->failed = 0; @@ -145,11 +146,13 @@ class UserDupes { if ( $this->trimmed > 0 ) { if ( $doDelete ) { - $this->out( "$this->trimmed duplicate user records were deleted from " - . wfWikiID() . ".\n" ); + $this->out( + "$this->trimmed duplicate user records were deleted from $dbDomain.\n" ); } else { - $this->out( "$this->trimmed duplicate user accounts were found on " - . wfWikiID() . " which can be removed safely.\n" ); + $this->out( + "$this->trimmed duplicate user accounts were found on $dbDomain " . + "which can be removed safely.\n" + ); } } diff --git a/resources/Resources.php b/resources/Resources.php index c8eae03120..0b0e485197 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -1983,6 +1983,7 @@ return [ 'resources/src/mediawiki.special/special.less', 'resources/src/mediawiki.special/apisandbox.css', 'resources/src/mediawiki.special/comparepages.less', + 'resources/src/mediawiki.special/contributions.less', 'resources/src/mediawiki.special/edittags.css', 'resources/src/mediawiki.special/movePage.css', 'resources/src/mediawiki.special/newpages.less', @@ -2146,9 +2147,12 @@ return [ 'mediawiki.special.contributions' => [ 'scripts' => 'resources/src/mediawiki.special.contributions.js', 'dependencies' => [ + 'jquery.makeCollapsible', + 'oojs-ui', 'mediawiki.widgets.DateInputWidget', 'mediawiki.jqueryMsg', - ] + ], + 'targets' => [ 'desktop', 'mobile' ], ], 'mediawiki.special.edittags' => [ 'scripts' => 'resources/src/mediawiki.special.edittags.js', @@ -2195,6 +2199,9 @@ return [ 'styles' => 'resources/src/mediawiki.special.preferences.styles.ooui.less', ], 'mediawiki.special.recentchanges' => [ + 'dependencies' => [ + 'mediawiki.widgets' + ], 'scripts' => 'resources/src/mediawiki.special.recentchanges.js', 'targets' => [ 'desktop', 'mobile' ], ], diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less index dff7881bbb..9559d3af4a 100644 --- a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less @@ -181,6 +181,18 @@ font-weight: bold; } +// on smaller screen, set .watchlistDetail to full width +// so that the spinner doesn't appear beside it. T225127#5518870 +@media screen and ( max-width: @width-breakpoint-tablet ) { + .client-js { + /* stylelint-disable-next-line selector-class-pattern */ + .watchlistDetails { + float: none; + width: auto; + } + } +} + @-webkit-keyframes rcfiltersBouncedelay { // 50% equals 800ms 0%, diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.WatchlistTopSectionWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.WatchlistTopSectionWidget.less index 52f7ff20fc..75da5ddc5d 100644 --- a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.WatchlistTopSectionWidget.less +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.WatchlistTopSectionWidget.less @@ -31,3 +31,19 @@ border-top: 2px solid @colorGray14; } } + +// On small screens, remove the table properties from the +// top section. T225127#5518870 +@media screen and ( max-width: @width-breakpoint-tablet ) { + .mw-rcfilters-ui-watchlistTopSectionWidget { + .mw-rcfilters-ui-table, + .mw-rcfilters-ui-row, + .mw-rcfilters-ui-cell { + display: block; + } + + &-editWatchlistButton { + margin-top: 1em; + } + } +} diff --git a/resources/src/mediawiki.special.recentchanges.js b/resources/src/mediawiki.special.recentchanges.js index 310832defb..c62acd90ca 100644 --- a/resources/src/mediawiki.special.recentchanges.js +++ b/resources/src/mediawiki.special.recentchanges.js @@ -2,7 +2,7 @@ * JavaScript for Special:RecentChanges */ ( function () { - var rc, $checkboxes, $select; + var rc, $checkboxes, $select, namespaceDropdown; /** * @class mw.special.recentchanges @@ -15,19 +15,29 @@ */ updateCheckboxes: function () { // The option element for the 'all' namespace has an empty value - var isAllNS = $select.val() === ''; + var value = $select.val(), + isAllNS = value === 'all' || value === ''; // Iterates over checkboxes and propagate the selected option $checkboxes.toggleClass( 'mw-input-hidden', isAllNS ); }, init: function () { - $select = $( '#namespace' ); - $checkboxes = $( '#nsassociated, #nsinvert' ).closest( '.mw-input-with-label' ); + $select = $( 'select#namespace' ); + $checkboxes = $( '#nsassociated, #nsinvert, .contribs-ns-filters' ) + .closest( '.mw-input-with-label' ); - // Bind to change event of the checkboxes. - // The initial state is already set in HTML. - $select.on( 'change', rc.updateCheckboxes ); + if ( $select.length === 0 ) { + $select = $( '#namespace select' ); + if ( $select.length > 0 ) { + namespaceDropdown = OO.ui.infuse( $( '#namespace' ).closest( '[data-ooui]' ) ); + namespaceDropdown.on( 'change', rc.updateCheckboxes ); + } + } else { + // Bind to change event of the checkboxes. + // The initial state is already set in HTML. + $select.on( 'change', rc.updateCheckboxes ); + } } }; diff --git a/resources/src/mediawiki.special/contributions.less b/resources/src/mediawiki.special/contributions.less new file mode 100644 index 0000000000..cc0e5386db --- /dev/null +++ b/resources/src/mediawiki.special/contributions.less @@ -0,0 +1,65 @@ +/*! + * Styling for Special:Contributions + */ +@import 'mediawiki.ui/variables.less'; + +// OOUIHTMLForm styles. +@ooui-font-size-browser: 16; // Assumed browser default of `16px`. +@ooui-font-size-base: 0.875em; // Equals `14px` at browser default of `16px`. + +@ooui-spacing-small: 8 / @ooui-font-size-browser / @ooui-font-size-base; // Equals `0.57142857em`≈`8px`. +@ooui-spacing-medium: 12 / @ooui-font-size-browser / @ooui-font-size-base; // Equals `0.8571429em`≈`12px`. +@ooui-spacing-large: 16 / @ooui-font-size-browser / @ooui-font-size-base; // Equals `1.1428571em`≈`16px`. + +.oo-ui-fieldsetLayout-group { + max-width: 50em; + + .oo-ui-panelLayout-padded.oo-ui-panelLayout-framed { + margin: 0; + border: 0; + padding: 0; + } + + // Hide extra `legend`s when grouping form in sections. + .oo-ui-fieldsetLayout.oo-ui-labelElement > .oo-ui-fieldsetLayout-header { + display: none; + } +} + +.mw-autocomplete-user.oo-ui-fieldLayout { + margin-top: @ooui-spacing-small; +} + +// Higher specificity needed to override OOUIHTMLForm styles. +.mw-htmlform-field-HTMLMultiSelectField.mw-htmlform-flatlist.oo-ui-fieldLayout { + margin-top: @ooui-spacing-small; +} + +.mw-htmlform-field-HTMLTagFilter ~ .mw-htmlform-field-HTMLCheckField.oo-ui-fieldLayout { + display: inline-block; + padding-right: @ooui-spacing-large; +} + +// Clearfix for floated `.mw-htmlform-field-HTMLDateTimeField` below. +#mw-htmlform-contribs-date:after { + content: ''; + clear: both; + display: block; +} + +.mw-htmlform-field-HTMLDateTimeField { + margin-right: @ooui-spacing-large; + margin-bottom: @ooui-spacing-small; + + .oo-ui-fieldLayout.oo-ui-labelElement&:first-child { + margin-top: @ooui-spacing-medium; + } +} + +@media all and ( min-width: @width-breakpoint-tablet ) { + .mw-htmlform-field-HTMLDateTimeField { + float: left; + // Same `width` as DateInputWidget. + width: 21em; + } +} diff --git a/resources/src/mediawiki.special/special.less b/resources/src/mediawiki.special/special.less index 3f76cf068a..40a2ec2d31 100644 --- a/resources/src/mediawiki.special/special.less +++ b/resources/src/mediawiki.special/special.less @@ -88,10 +88,6 @@ font-weight: bold; } -.mw-contributions-form select { - vertical-align: middle; -} - /* Special:EditWatchlist */ .watchlistredir { font-style: italic; diff --git a/resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js b/resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js index 0eb1134f11..89de1551f9 100644 --- a/resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js +++ b/resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js @@ -73,7 +73,7 @@ // Highlight matching parts of link suggestion if ( config.query ) { - this.setHighlightedQuery( config.data, config.query, config.compare ); + this.setHighlightedQuery( config.data, config.query, config.compare, true ); } this.$label.attr( 'title', config.data ); diff --git a/tests/phpunit/includes/HtmlTest.php b/tests/phpunit/includes/HtmlTest.php index 4401410ef3..35d06dd63d 100644 --- a/tests/phpunit/includes/HtmlTest.php +++ b/tests/phpunit/includes/HtmlTest.php @@ -566,11 +566,11 @@ class HtmlTest extends MediaWikiTestCase { '
err
' ); $this->assertEquals( - Html::errorBox( 'err', 'heading' ), - '

heading

err
' + Html::errorBox( 'err', 'heading', 'errorbox-custom-class' ), + '

heading

err
' ); $this->assertEquals( - Html::errorBox( 'err', '0' ), + Html::errorBox( 'err', '0', '' ), '

0

err
' ); } diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js index 59672f4b1c..16c3183d54 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js @@ -186,7 +186,7 @@ [ 'test.load.circleB', '0', [ 'test.load.circleC' ] ], [ 'test.load.circleC', '0', [ 'test.load.circleA' ] ] ] ); - this.sandbox.stub( mw, 'track', function ( topic, data ) { + this.sandbox.stub( mw, 'trackError', function ( topic, data ) { capture.push( { topic: topic, error: data.exception && data.exception.message, @@ -211,7 +211,7 @@ mw.loader.register( [ [ 'test.load.circleDirect', '0', [ 'test.load.circleDirect' ] ] ] ); - this.sandbox.stub( mw, 'track', function ( topic, data ) { + this.sandbox.stub( mw, 'trackError', function ( topic, data ) { capture.push( { topic: topic, error: data.exception && data.exception.message, @@ -257,7 +257,7 @@ // Regression test for T36853 QUnit.test( '.load() - Error: Missing dependency', function ( assert ) { var capture = []; - this.sandbox.stub( mw, 'track', function ( topic, data ) { + this.sandbox.stub( mw, 'trackError', function ( topic, data ) { capture.push( { topic: topic, error: data.exception && data.exception.message, @@ -673,7 +673,7 @@ this.useStubClock(); // Don't actually emit an error event - this.sandbox.stub( mw, 'track' ); + this.sandbox.stub( mw, 'trackError' ); mw.loader.register( [ [ 'test.module1', '0' ], @@ -685,11 +685,11 @@ }, {}, {} ); this.tick(); - assert.strictEqual( mw.loader.getState( 'test.module1' ), 'error', 'Expected "error" state for test.module1' ); - assert.strictEqual( mw.loader.getState( 'test.module2' ), 'error', 'Expected "error" state for test.module2' ); - assert.strictEqual( mw.loader.getState( 'test.module3' ), 'error', 'Expected "error" state for test.module3' ); + assert.strictEqual( mw.loader.getState( 'test.module1' ), 'error', 'State of test.module1' ); + assert.strictEqual( mw.loader.getState( 'test.module2' ), 'error', 'State of test.module2' ); + assert.strictEqual( mw.loader.getState( 'test.module3' ), 'error', 'State of test.module3' ); - assert.strictEqual( mw.track.callCount, 1 ); + assert.strictEqual( mw.trackError.callCount, 1 ); } ); QUnit.test( 'Out-of-order implementation', function ( assert ) { @@ -703,21 +703,21 @@ mw.loader.implement( 'test.module4', function () {} ); this.tick(); - assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' ); - assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' ); - assert.strictEqual( mw.loader.getState( 'test.module6' ), 'registered', 'Expected "registered" state for test.module6' ); + assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'State of test.module4' ); + assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'State of test.module5' ); + assert.strictEqual( mw.loader.getState( 'test.module6' ), 'registered', 'State of test.module6' ); mw.loader.implement( 'test.module6', function () {} ); this.tick(); - assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' ); - assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' ); - assert.strictEqual( mw.loader.getState( 'test.module6' ), 'loaded', 'Expected "loaded" state for test.module6' ); + assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'State of test.module4' ); + assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'State of test.module5' ); + assert.strictEqual( mw.loader.getState( 'test.module6' ), 'loaded', 'State of test.module6' ); mw.loader.implement( 'test.module5', function () {} ); this.tick(); - assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' ); - assert.strictEqual( mw.loader.getState( 'test.module5' ), 'ready', 'Expected "ready" state for test.module5' ); - assert.strictEqual( mw.loader.getState( 'test.module6' ), 'ready', 'Expected "ready" state for test.module6' ); + assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'State of test.module4' ); + assert.strictEqual( mw.loader.getState( 'test.module5' ), 'ready', 'State of test.module5' ); + assert.strictEqual( mw.loader.getState( 'test.module6' ), 'ready', 'State of test.module6' ); } ); QUnit.test( 'Missing dependency', function ( assert ) {