From: jenkins-bot Date: Fri, 2 Dec 2016 13:00:22 +0000 (+0000) Subject: Merge "mediawiki.notification: Improve scroll performance" X-Git-Tag: 1.31.0-rc.0~4686 X-Git-Url: http://git.cyclocoop.org/%22%20.%20generer_url_ecrire%28%22suivi_revisions%22%2C%22id_auteur=%24connecte%22%29%20.%20%22?a=commitdiff_plain;h=3315354fd39f9bd60351ca27c73bafa05d449308;hp=f1ac8022d503a904dc66685e9b979814730ef3bc;p=lhc%2Fweb%2Fwiklou.git Merge "mediawiki.notification: Improve scroll performance" --- diff --git a/RELEASE-NOTES-1.29 b/RELEASE-NOTES-1.29 index 5b5640fffb..98d2a172c1 100644 --- a/RELEASE-NOTES-1.29 +++ b/RELEASE-NOTES-1.29 @@ -54,6 +54,12 @@ changes to languages because of Phabricator reports. === Other changes in 1.29 === * Database::getSearchEngine() (deprecated in 1.28) was removed. Use SearchEngineFactory::getSearchEngineClass() instead. +* $wgSessionsInMemcached (deprecated in 1.20) was removed. No replacement is + required as all sessions are stored in Object Cache now. +* MWHttpRequest::execute() should be considered to return a StatusValue; the + Status return type is deprecated. +* User::edits() (deprecated in 1.21) was removed. +* Xml::escapeJsString() (deprecated in 1.21) was removed. == Compatibility == diff --git a/autoload.php b/autoload.php index 30ef985be0..0d6407bc20 100644 --- a/autoload.php +++ b/autoload.php @@ -875,6 +875,7 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\Logger\\Monolog\\LegacyFormatter' => __DIR__ . '/includes/debug/logger/monolog/LegacyFormatter.php', 'MediaWiki\\Logger\\Monolog\\LegacyHandler' => __DIR__ . '/includes/debug/logger/monolog/LegacyHandler.php', 'MediaWiki\\Logger\\Monolog\\LineFormatter' => __DIR__ . '/includes/debug/logger/monolog/LineFormatter.php', + 'MediaWiki\\Logger\\Monolog\\LogstashFormatter' => __DIR__ . '/includes/debug/logger/monolog/LogstashFormatter.php', 'MediaWiki\\Logger\\Monolog\\SyslogHandler' => __DIR__ . '/includes/debug/logger/monolog/SyslogHandler.php', 'MediaWiki\\Logger\\Monolog\\WikiProcessor' => __DIR__ . '/includes/debug/logger/monolog/WikiProcessor.php', 'MediaWiki\\Logger\\NullSpi' => __DIR__ . '/includes/debug/logger/NullSpi.php', @@ -1569,6 +1570,8 @@ $wgAutoloadLocalClasses = [ 'WikiRevision' => __DIR__ . '/includes/import/WikiRevision.php', 'WikiStatsOutput' => __DIR__ . '/maintenance/language/StatOutputs.php', 'WikiTextStructure' => __DIR__ . '/includes/content/WikiTextStructure.php', + 'Wikimedia\\Rdbms\\ConnectionManager' => __DIR__ . '/includes/libs/rdbms/connectionmanager/ConnectionManager.php', + 'Wikimedia\\Rdbms\\SessionConsistentConnectionManager' => __DIR__ . '/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManager.php', 'WikitextContent' => __DIR__ . '/includes/content/WikitextContent.php', 'WikitextContentHandler' => __DIR__ . '/includes/content/WikitextContentHandler.php', 'WinCacheBagOStuff' => __DIR__ . '/includes/libs/objectcache/WinCacheBagOStuff.php', diff --git a/composer.json b/composer.json index e1d9f47962..19ca2380fd 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "ext-xml": "*", "liuggio/statsd-php-client": "1.0.18", "mediawiki/at-ease": "1.1.0", - "oojs/oojs-ui": "0.18.0", + "oojs/oojs-ui": "0.18.1", "oyejorge/less.php": "1.7.0.10", "php": ">=5.5.9", "psr/log": "1.0.0", diff --git a/docs/hooks.txt b/docs/hooks.txt index a73d50f9bd..da12d8c367 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -2554,8 +2554,6 @@ $user: the User considering the edit 'PasswordPoliciesForUser': Alter the effective password policy for a user. $user: User object whose policy you are modifying &$effectivePolicy: Array of policy statements that apply to this user -$purpose: string indicating purpose of the check, one of 'login', 'create', - or 'reset' 'PerformRetroactiveAutoblock': Called before a retroactive autoblock is applied to a user. diff --git a/includes/CategoryViewer.php b/includes/CategoryViewer.php index b95f274406..4c4b8bbc87 100644 --- a/includes/CategoryViewer.php +++ b/includes/CategoryViewer.php @@ -197,7 +197,11 @@ class CategoryViewer extends ContextSource { $link = null; Hooks::run( 'CategoryViewer::generateLink', [ $type, $title, $html, &$link ] ); if ( $link === null ) { - $link = Linker::link( $title, $html ); + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); + if ( $html !== null ) { + $html = new HtmlArmor( $html ); + } + $link = $linkRenderer->makeLink( $title, $html ); } if ( $isRedirect ) { $link = '' . $link . ''; diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index eb778b5ae1..5557dca875 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -2365,13 +2365,6 @@ $wgMainStash = 'db-replicated'; */ $wgParserCacheExpireTime = 86400; -/** - * Deprecated alias for $wgSessionsInObjectCache. - * - * @deprecated since 1.20; Use $wgSessionsInObjectCache - */ -$wgSessionsInMemcached = true; - /** * @deprecated since 1.27, session data is always stored in object cache. */ diff --git a/includes/Message.php b/includes/Message.php index 85c78d640c..7e5cc7dca2 100644 --- a/includes/Message.php +++ b/includes/Message.php @@ -168,6 +168,17 @@ class Message implements MessageSpecifier, Serializable { /** Transform {{..}} constructs, HTML-escape the result */ const FORMAT_ESCAPED = 'escaped'; + /** + * Mapping from Message::listParam() types to Language methods. + * @var array + */ + protected static $listTypeMap = [ + 'comma' => 'commaList', + 'semicolon' => 'semicolonList', + 'pipe' => 'pipeList', + 'text' => 'listToText', + ]; + /** * In which language to get this message. True, which is the default, * means the current user language, false content language. @@ -1070,6 +1081,22 @@ class Message implements MessageSpecifier, Serializable { return [ 'plaintext' => $plaintext ]; } + /** + * @since 1.29 + * + * @param array $list + * @param string $type 'comma', 'semicolon', 'pipe', 'text' + * @return array Array with "list" and "type" keys. + */ + public static function listParam( array $list, $type = 'text' ) { + if ( !isset( self::$listTypeMap[$type] ) ) { + throw new InvalidArgumentException( + "Invalid type '$type'. Known types are: " . join( ', ', array_keys( self::$listTypeMap ) ) + ); + } + return [ 'list' => $list, 'type' => $type ]; + } + /** * Substitutes any parameters into the message text. * @@ -1123,6 +1150,8 @@ class Message implements MessageSpecifier, Serializable { return [ 'before', $this->getLanguage()->formatBitrate( $param['bitrate'] ) ]; } elseif ( isset( $param['plaintext'] ) ) { return [ 'after', $this->formatPlaintext( $param['plaintext'], $format ) ]; + } elseif ( isset( $param['list'] ) ) { + return $this->formatListParam( $param['list'], $param['type'], $format ); } else { $warning = 'Invalid parameter for message "' . $this->getKey() . '": ' . htmlspecialchars( serialize( $param ) ); @@ -1251,6 +1280,54 @@ class Message implements MessageSpecifier, Serializable { } } + + /** + * Formats a list of parameters as a concatenated string. + * @since 1.29 + * @param array $params + * @param string $listType + * @param string $format One of the FORMAT_* constants. + * @return array Array with the parameter type (either "before" or "after") and the value. + */ + protected function formatListParam( array $params, $listType, $format ) { + if ( !isset( self::$listTypeMap[$listType] ) ) { + $warning = 'Invalid list type for message "' . $this->getKey() . '": ' . + htmlspecialchars( serialize( $param ) ); + trigger_error( $warning, E_USER_WARNING ); + $e = new Exception; + wfDebugLog( 'Bug58676', $warning . "\n" . $e->getTraceAsString() ); + return [ 'before', '[INVALID]' ]; + } + $func = self::$listTypeMap[$listType]; + + // Handle an empty list sensibly + if ( !$params ) { + return [ 'before', $this->getLanguage()->$func( [] ) ]; + } + + // First, determine what kinds of list items we have + $types = []; + $vars = []; + $list = []; + foreach ( $params as $n => $p ) { + list( $type, $value ) = $this->extractParam( $p, $format ); + $types[$type] = true; + $list[] = $value; + $vars[] = '$' . ( $n + 1 ); + } + + // Easy case: all are 'before' or 'after', so just join the + // values and use the same type. + if ( count( $types ) === 1 ) { + return [ key( $types ), $this->getLanguage()->$func( $list ) ]; + } + + // Hard case: We need to process each value per its type, then + // return the concatenated values as 'after'. We handle this by turning + // the list into a RawMessage and processing that as a parameter. + $vars = $this->getLanguage()->$func( $vars ); + return $this->extractParam( new RawMessage( $vars, $params ), $format ); + } } /** diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 43d71abca0..60701141a7 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -21,6 +21,7 @@ */ use MediaWiki\Logger\LoggerFactory; +use MediaWiki\MediaWikiServices; use MediaWiki\Session\SessionManager; use WrappedString\WrappedString; use WrappedString\WrappedStringList; @@ -1010,8 +1011,9 @@ class OutputPage extends ContextSource { if ( $title->isRedirect() ) { $query['redirect'] = 'no'; } + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); return wfMessage( 'backlinksubtitle' ) - ->rawParams( Linker::link( $title, null, [], $query ) ); + ->rawParams( $linkRenderer->makeLink( $title, null, [], $query ) ); } /** @@ -1269,6 +1271,7 @@ class OutputPage extends ContextSource { 'OutputPageMakeCategoryLinks', [ &$this, $categories, &$this->mCategoryLinks ] ) ) { + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); foreach ( $categories as $category => $type ) { // array keys will cast numeric category names to ints, so cast back to string $category = (string)$category; @@ -1283,7 +1286,7 @@ class OutputPage extends ContextSource { } $text = $wgContLang->convertHtml( $title->getText() ); $this->mCategories[$type][] = $title->getText(); - $this->mCategoryLinks[$type][] = Linker::link( $title, $text ); + $this->mCategoryLinks[$type][] = $linkRenderer->makeLink( $title, new HtmlArmor( $text ) ); } } } @@ -2653,8 +2656,10 @@ class OutputPage extends ContextSource { * @param array $options Options array to pass to Linker */ public function addReturnTo( $title, array $query = [], $text = null, $options = [] ) { + $linkRenderer = MediaWikiServices::getInstance() + ->getLinkRendererFactory()->createFromLegacyOptions( $options ); $link = $this->msg( 'returnto' )->rawParams( - Linker::link( $title, $text, [], $query, $options ) )->escaped(); + $linkRenderer->makeLink( $title, $text, [], $query ) )->escaped(); $this->addHTML( "

{$link}

\n" ); } diff --git a/includes/Preferences.php b/includes/Preferences.php index d86b19a5ce..d40e0c1323 100644 --- a/includes/Preferences.php +++ b/includes/Preferences.php @@ -21,6 +21,7 @@ */ use MediaWiki\Auth\AuthManager; use MediaWiki\Auth\PasswordAuthenticationRequest; +use MediaWiki\MediaWikiServices; /** * We're now using the HTMLForm object with some customisation to generate the @@ -253,7 +254,9 @@ class Preferences { 'section' => 'personal/info', ]; - $editCount = Linker::link( SpecialPage::getTitleFor( "Contributions", $userName ), + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); + + $editCount = $linkRenderer->makeLink( SpecialPage::getTitleFor( "Contributions", $userName ), $lang->formatNum( $user->getEditCount() ) ); $defaultPreferences['editcount'] = [ @@ -297,8 +300,8 @@ class Preferences { if ( $canEditPrivateInfo && $authManager->allowsAuthenticationDataChange( new PasswordAuthenticationRequest(), false )->isGood() ) { - $link = Linker::link( SpecialPage::getTitleFor( 'ChangePassword' ), - $context->msg( 'prefs-resetpass' )->escaped(), [], + $link = $linkRenderer->makeLink( SpecialPage::getTitleFor( 'ChangePassword' ), + $context->msg( 'prefs-resetpass' )->text(), [], [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] ); $defaultPreferences['password'] = [ @@ -448,9 +451,9 @@ class Preferences { $emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : ''; if ( $canEditPrivateInfo && $authManager->allowsPropertyChange( 'emailaddress' ) ) { - $link = Linker::link( + $link = $linkRenderer->makeLink( SpecialPage::getTitleFor( 'ChangeEmail' ), - $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->escaped(), + $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(), [], [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] ); @@ -601,14 +604,15 @@ class Preferences { $linkTools = []; $userName = $user->getName(); + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); if ( $allowUserCss ) { $cssPage = Title::makeTitleSafe( NS_USER, $userName . '/common.css' ); - $linkTools[] = Linker::link( $cssPage, $context->msg( 'prefs-custom-css' )->escaped() ); + $linkTools[] = $linkRenderer->makeLink( $cssPage, $context->msg( 'prefs-custom-css' )->text() ); } if ( $allowUserJs ) { $jsPage = Title::makeTitleSafe( NS_USER, $userName . '/common.js' ); - $linkTools[] = Linker::link( $jsPage, $context->msg( 'prefs-custom-js' )->escaped() ); + $linkTools[] = $linkRenderer->makeLink( $jsPage, $context->msg( 'prefs-custom-js' )->text() ); } $defaultPreferences['commoncssjs'] = [ @@ -1110,6 +1114,8 @@ class Preferences { $mptitle = Title::newMainPage(); $previewtext = $context->msg( 'skin-preview' )->escaped(); + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); + # Only show skins that aren't disabled in $wgSkipSkins $validSkinNames = Skin::getAllowedSkins(); @@ -1145,12 +1151,12 @@ class Preferences { # Create links to user CSS/JS pages if ( $allowUserCss ) { $cssPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.css' ); - $linkTools[] = Linker::link( $cssPage, $context->msg( 'prefs-custom-css' )->escaped() ); + $linkTools[] = $linkRenderer->makeLink( $cssPage, $context->msg( 'prefs-custom-css' )->text() ); } if ( $allowUserJs ) { $jsPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.js' ); - $linkTools[] = Linker::link( $jsPage, $context->msg( 'prefs-custom-js' )->escaped() ); + $linkTools[] = $linkRenderer->makeLink( $jsPage, $context->msg( 'prefs-custom-js' )->text() ); } $display = $sn . ' ' . $context->msg( 'parentheses' ) @@ -1624,7 +1630,8 @@ class PreferencesForm extends HTMLForm { if ( $this->getModifiedUser()->isAllowed( 'editmyoptions' ) ) { $t = SpecialPage::getTitleFor( 'Preferences', 'reset' ); - $html .= "\n" . Linker::link( $t, $this->msg( 'restoreprefs' )->escaped(), + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); + $html .= "\n" . $linkRenderer->makeLink( $t, $this->msg( 'restoreprefs' )->text(), Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ) ); $html = Xml::tags( 'div', [ 'class' => 'mw-prefs-buttons' ], $html ); diff --git a/includes/PrefixSearch.php b/includes/PrefixSearch.php index f6c4147118..04c17e4021 100644 --- a/includes/PrefixSearch.php +++ b/includes/PrefixSearch.php @@ -239,7 +239,7 @@ abstract class PrefixSearch { // canonical and alias title forms... $keys = []; foreach ( SpecialPageFactory::getNames() as $page ) { - $keys[$wgContLang->caseFold( $page )] = $page; + $keys[$wgContLang->caseFold( $page )] = [ 'page' => $page, 'rank' => 0 ]; } foreach ( $wgContLang->getSpecialPageAliases() as $page => $aliases ) { @@ -247,33 +247,35 @@ abstract class PrefixSearch { continue; } - foreach ( $aliases as $alias ) { - $keys[$wgContLang->caseFold( $alias )] = $alias; + foreach ( $aliases as $key => $alias ) { + $keys[$wgContLang->caseFold( $alias )] = [ 'page' => $alias, 'rank' => $key ]; } } ksort( $keys ); - $srchres = []; - $skipped = 0; + $matches = []; foreach ( $keys as $pageKey => $page ) { if ( $searchKey === '' || strpos( $pageKey, $searchKey ) === 0 ) { // bug 27671: Don't use SpecialPage::getTitleFor() here because it // localizes its input leading to searches for e.g. Special:All // returning Spezial:MediaWiki-Systemnachrichten and returning // Spezial:Alle_Seiten twice when $wgLanguageCode == 'de' - if ( $offset > 0 && $skipped < $offset ) { - $skipped++; - continue; + $matches[$page['rank']][] = Title::makeTitleSafe( NS_SPECIAL, $page['page'] ); + + if ( isset( $matches[0] ) && count( $matches[0] ) >= $limit + $offset ) { + // We have enough items in primary rank, no use to continue + break; } - $srchres[] = Title::makeTitleSafe( NS_SPECIAL, $page ); } - if ( count( $srchres ) >= $limit ) { - break; - } } - return $srchres; + // Ensure keys are in order + ksort( $matches ); + // Flatten the array + $matches = array_reduce( $matches, 'array_merge', [] ); + + return array_slice( $matches, $offset, $limit ); } /** diff --git a/includes/Setup.php b/includes/Setup.php index 357c76d8d1..9f722afd86 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -462,7 +462,7 @@ if ( $wgMaximalPasswordLength !== false ) { } // Backwards compatibility warning -if ( !$wgSessionsInObjectCache && !$wgSessionsInMemcached ) { +if ( !$wgSessionsInObjectCache ) { wfDeprecated( '$wgSessionsInObjectCache = false', '1.27' ); if ( $wgSessionHandler ) { wfDeprecated( '$wgSessionsHandler', '1.27' ); diff --git a/includes/Xml.php b/includes/Xml.php index 4c6b0715b0..e124c38b75 100644 --- a/includes/Xml.php +++ b/includes/Xml.php @@ -613,42 +613,6 @@ class Xml { $content, false ); } - /** - * Returns an escaped string suitable for inclusion in a string literal - * for JavaScript source code. - * Illegal control characters are assumed not to be present. - * - * @deprecated since 1.21; use Xml::encodeJsVar() or Xml::encodeJsCall() instead - * @param string $string String to escape - * @return string - */ - public static function escapeJsString( $string ) { - // See ECMA 262 section 7.8.4 for string literal format - $pairs = [ - "\\" => "\\\\", - "\"" => "\\\"", - '\'' => '\\\'', - "\n" => "\\n", - "\r" => "\\r", - - # To avoid closing the element or CDATA section - "<" => "\\x3c", - ">" => "\\x3e", - - # To avoid any complaints about bad entity refs - "&" => "\\x26", - - # Work around https://bugzilla.mozilla.org/show_bug.cgi?id=274152 - # Encode certain Unicode formatting chars so affected - # versions of Gecko don't misinterpret our strings; - # this is a common problem with Farsi text. - "\xe2\x80\x8c" => "\\u200c", // ZERO WIDTH NON-JOINER - "\xe2\x80\x8d" => "\\u200d", // ZERO WIDTH JOINER - ]; - - return strtr( $string, $pairs ); - } - /** * Encode a variable of arbitrary type to JavaScript. * If the value is an XmlJsCode object, pass through the object's value verbatim. diff --git a/includes/actions/CreditsAction.php b/includes/actions/CreditsAction.php index 1332ab489f..803695a77f 100644 --- a/includes/actions/CreditsAction.php +++ b/includes/actions/CreditsAction.php @@ -23,6 +23,8 @@ * @author */ +use MediaWiki\MediaWikiServices; + /** * @ingroup Actions */ @@ -198,14 +200,15 @@ class CreditsAction extends FormlessAction { if ( $this->canShowRealUserName() && !$user->isAnon() ) { $real = $user->getRealName(); } else { - $real = false; + $real = $user->getName(); } $page = $user->isAnon() ? SpecialPage::getTitleFor( 'Contributions', $user->getName() ) : $user->getUserPage(); - return Linker::link( $page, htmlspecialchars( $real ? $real : $user->getName() ) ); + return MediaWikiServices::getInstance() + ->getLinkRenderer()->makeLink( $page, $real ); } /** @@ -231,9 +234,9 @@ class CreditsAction extends FormlessAction { * @return string HTML link */ protected function othersLink() { - return Linker::linkKnown( + return MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink( $this->getTitle(), - $this->msg( 'others' )->escaped(), + $this->msg( 'others' )->text(), [], [ 'action' => 'credits' ] ); diff --git a/includes/actions/HistoryAction.php b/includes/actions/HistoryAction.php index c1763fa1be..9573cac3ed 100644 --- a/includes/actions/HistoryAction.php +++ b/includes/actions/HistoryAction.php @@ -23,6 +23,8 @@ * @ingroup Actions */ +use MediaWiki\MediaWikiServices; + /** * This class handles printing the history page for an article. In order to * be efficient, it uses timestamps rather than offsets for paging, to avoid @@ -58,9 +60,9 @@ class HistoryAction extends FormlessAction { protected function getDescription() { // Creation of a subtitle link pointing to [[Special:Log]] - return Linker::linkKnown( + return MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink( SpecialPage::getTitleFor( 'Log' ), - $this->msg( 'viewpagelogs' )->escaped(), + $this->msg( 'viewpagelogs' )->text(), [], [ 'page' => $this->getTitle()->getPrefixedText() ] ); @@ -734,9 +736,9 @@ class HistoryPager extends ReverseChronologicalPager { $undoTooltip = $latest ? [ 'title' => $this->msg( 'tooltip-undo' )->text() ] : []; - $undolink = Linker::linkKnown( + $undolink = MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink( $this->getTitle(), - $this->msg( 'editundo' )->escaped(), + $this->msg( 'editundo' )->text(), $undoTooltip, [ 'action' => 'edit', @@ -788,16 +790,15 @@ class HistoryPager extends ReverseChronologicalPager { */ function revLink( $rev ) { $date = $this->getLanguage()->userTimeAndDate( $rev->getTimestamp(), $this->getUser() ); - $date = htmlspecialchars( $date ); if ( $rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { - $link = Linker::linkKnown( + $link = MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink( $this->getTitle(), $date, [ 'class' => 'mw-changeslist-date' ], [ 'oldid' => $rev->getId() ] ); } else { - $link = $date; + $link = htmlspecialchars( $date ); } if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { $link = "$link"; @@ -818,7 +819,7 @@ class HistoryPager extends ReverseChronologicalPager { if ( $latest || !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { return $cur; } else { - return Linker::linkKnown( + return MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink( $this->getTitle(), $cur, [], @@ -847,9 +848,10 @@ class HistoryPager extends ReverseChronologicalPager { return $last; } + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); if ( $next === 'unknown' ) { # Next row probably exists but is unknown, use an oldid=prev link - return Linker::linkKnown( + return $linkRenderer->makeKnownLink( $this->getTitle(), $last, [], @@ -868,7 +870,7 @@ class HistoryPager extends ReverseChronologicalPager { return $last; } - return Linker::linkKnown( + return $linkRenderer->makeKnownLink( $this->getTitle(), $last, [], diff --git a/includes/actions/InfoAction.php b/includes/actions/InfoAction.php index be3be85e43..49b9ab7a33 100644 --- a/includes/actions/InfoAction.php +++ b/includes/actions/InfoAction.php @@ -230,11 +230,11 @@ class InfoAction extends FormlessAction { if ( $title->isRedirect() ) { $pageInfo['header-basic'][] = [ $this->msg( 'pageinfo-redirectsto' ), - Linker::link( $this->page->getRedirectTarget() ) . + $linkRenderer->makeLink( $this->page->getRedirectTarget() ) . $this->msg( 'word-separator' )->escaped() . - $this->msg( 'parentheses' )->rawParams( Linker::link( + $this->msg( 'parentheses' )->rawParams( $linkRenderer->makeLink( $this->page->getRedirectTarget(), - $this->msg( 'pageinfo-redirectsto-info' )->escaped(), + $this->msg( 'pageinfo-redirectsto-info' )->text(), [], [ 'action' => 'info' ] ) )->escaped() @@ -266,9 +266,9 @@ class InfoAction extends FormlessAction { ) { // Link to Special:PageLanguage with pre-filled page title if user has permissions $titleObj = SpecialPage::getTitleFor( 'PageLanguage', $title->getPrefixedText() ); - $langDisp = Linker::link( + $langDisp = $linkRenderer->makeLink( $titleObj, - $this->msg( 'pageinfo-language' )->escaped() + $this->msg( 'pageinfo-language' )->text() ); } else { // Display just the message @@ -360,9 +360,9 @@ class InfoAction extends FormlessAction { // Redirects to this page $whatLinksHere = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() ); $pageInfo['header-basic'][] = [ - Linker::link( + $linkRenderer->makeLink( $whatLinksHere, - $this->msg( 'pageinfo-redirects-name' )->escaped(), + $this->msg( 'pageinfo-redirects-name' )->text(), [], [ 'hidelinks' => 1, @@ -436,7 +436,7 @@ class InfoAction extends FormlessAction { foreach ( $sources as $sourceTitle ) { $cascadingFrom .= Html::rawElement( - 'li', [], Linker::linkKnown( $sourceTitle ) ); + 'li', [], $linkRenderer->makeKnownLink( $sourceTitle ) ); } $cascadingFrom = Html::rawElement( 'ul', [], $cascadingFrom ); @@ -525,9 +525,9 @@ class InfoAction extends FormlessAction { // Date of page creation $pageInfo['header-edits'][] = [ $this->msg( 'pageinfo-firsttime' ), - Linker::linkKnown( + $linkRenderer->makeKnownLink( $title, - htmlspecialchars( $lang->userTimeAndDate( $firstRev->getTimestamp(), $user ) ), + $lang->userTimeAndDate( $firstRev->getTimestamp(), $user ), [], [ 'oldid' => $firstRev->getId() ] ) @@ -544,11 +544,9 @@ class InfoAction extends FormlessAction { // Date of latest edit $pageInfo['header-edits'][] = [ $this->msg( 'pageinfo-lasttime' ), - Linker::linkKnown( + $linkRenderer->makeKnownLink( $title, - htmlspecialchars( - $lang->userTimeAndDate( $this->page->getTimestamp(), $user ) - ), + $lang->userTimeAndDate( $this->page->getTimestamp(), $user ), [], [ 'oldid' => $this->page->getLatest() ] ) @@ -655,9 +653,9 @@ class InfoAction extends FormlessAction { if ( !$config->get( 'MiserMode' ) && $pageCounts['transclusion']['to'] > 0 ) { if ( $pageCounts['transclusion']['to'] > count( $transcludedTargets ) ) { - $more = Linker::link( + $more = $linkRenderer->makeLink( $whatLinksHere, - $this->msg( 'moredotdotdot' )->escaped(), + $this->msg( 'moredotdotdot' )->text(), [], [ 'hidelinks' => 1, 'hideredirs' => 1 ] ); @@ -836,6 +834,7 @@ class InfoAction extends FormlessAction { $real_names = []; $user_names = []; $anon_ips = []; + $linkRenderer = MediaWikiServices::getLinkRenderer(); # Sift for real versus user names /** @var $user User */ @@ -846,11 +845,11 @@ class InfoAction extends FormlessAction { $hiddenPrefs = $this->context->getConfig()->get( 'HiddenPrefs' ); if ( $user->getId() == 0 ) { - $anon_ips[] = Linker::link( $page, htmlspecialchars( $user->getName() ) ); + $anon_ips[] = $linkRenderer->makeLink( $page, $user->getName() ); } elseif ( !in_array( 'realname', $hiddenPrefs ) && $user->getRealName() ) { - $real_names[] = Linker::link( $page, htmlspecialchars( $user->getRealName() ) ); + $real_names[] = $linkRenderer->makeLink( $page, $user->getRealName() ); } else { - $user_names[] = Linker::link( $page, htmlspecialchars( $user->getName() ) ); + $user_names[] = $linkRenderer->makeLink( $page, $user->getName() ); } } diff --git a/includes/actions/MarkpatrolledAction.php b/includes/actions/MarkpatrolledAction.php index 8df60445da..611e6837f5 100644 --- a/includes/actions/MarkpatrolledAction.php +++ b/includes/actions/MarkpatrolledAction.php @@ -20,6 +20,8 @@ * @ingroup Actions */ +use MediaWiki\MediaWikiServices; + /** * Mark a revision as patrolled on a page * @@ -56,6 +58,7 @@ class MarkpatrolledAction extends FormAction { protected function preText() { $rc = $this->getRecentChange(); $title = $rc->getTitle(); + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); // Based on logentry-patrol-patrol (see PatrolLogFormatter) $revId = $rc->getAttribute( 'rc_this_oldid' ); @@ -64,8 +67,8 @@ class MarkpatrolledAction extends FormAction { 'diff' => $revId, 'oldid' => $rc->getAttribute( 'rc_last_oldid' ) ]; - $revlink = Linker::link( $title, htmlspecialchars( $revId ), [], $query ); - $pagelink = Linker::link( $title, htmlspecialchars( $title->getPrefixedText() ) ); + $revlink = $linkRenderer->makeLink( $title, $revId, [], $query ); + $pagelink = $linkRenderer->makeLink( $title, $title->getPrefixedText() ); return $this->msg( 'confirm-markpatrolled-top' )->params( $title->getPrefixedText(), diff --git a/includes/api/ApiCheckToken.php b/includes/api/ApiCheckToken.php index 3d2159cf50..dd88b5fe3a 100644 --- a/includes/api/ApiCheckToken.php +++ b/includes/api/ApiCheckToken.php @@ -22,6 +22,8 @@ * @file */ +use MediaWiki\Session\Token; + /** * @since 1.25 * @ingroup API @@ -39,6 +41,13 @@ class ApiCheckToken extends ApiBase { $tokenObj = ApiQueryTokens::getToken( $this->getUser(), $this->getRequest()->getSession(), $salts[$params['type']] ); + + if ( substr( $token, -strlen( urldecode( Token::SUFFIX ) ) ) === urldecode( Token::SUFFIX ) ) { + $this->setWarning( + "Check that symbols such as \"+\" in the token are properly percent-encoded in the URL." + ); + } + if ( $tokenObj->match( $token, $maxage ) ) { $res['result'] = 'valid'; } elseif ( $maxage !== null && $tokenObj->match( $token ) ) { @@ -47,7 +56,7 @@ class ApiCheckToken extends ApiBase { $res['result'] = 'invalid'; } - $ts = MediaWiki\Session\Token::getTimestamp( $token ); + $ts = Token::getTimestamp( $token ); if ( $ts !== null ) { $mwts = new MWTimestamp(); $mwts->timestamp->setTimestamp( $ts ); diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php index 1a509c5c1e..853a8056be 100644 --- a/includes/api/ApiPageSet.php +++ b/includes/api/ApiPageSet.php @@ -1412,13 +1412,7 @@ class ApiPageSet extends ApiBase { ApiBase::PARAM_DFLT => false, ApiBase::PARAM_HELP_MSG => [ 'api-pageset-param-converttitles', - new DeferredStringifier( - function ( IContextSource $context ) { - return $context->getLanguage() - ->commaList( LanguageConverter::$languagesWithVariants ); - }, - $this - ) + [ Message::listParam( LanguageConverter::$languagesWithVariants, 'text' ) ], ], ], ]; diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php index 6be51986a6..9962d5ec20 100644 --- a/includes/api/ApiQuerySearch.php +++ b/includes/api/ApiQuerySearch.php @@ -107,10 +107,25 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $matches = $search->searchText( $query ); } } - if ( is_null( $matches ) ) { + + if ( $matches instanceof Status ) { + $status = $matches; + $matches = $status->getValue(); + } else { + $status = null; + } + + if ( $status ) { + if ( $status->isOK() ) { + $this->getMain()->getErrorFormatter()->addMessagesFromStatus( + $this->getModuleName(), + $status + ); + } else { + $this->dieUsage( $status->getWikiText( false, false, 'en' ), 'search-error' ); + } + } elseif ( is_null( $matches ) ) { $this->dieUsage( "{$what} search is disabled", "search-{$what}-disabled" ); - } elseif ( $matches instanceof Status && !$matches->isGood() ) { - $this->dieUsage( $matches->getWikiText( false, false, 'en' ), 'search-error' ); } if ( $resultPageSet === null ) { diff --git a/includes/auth/LocalPasswordPrimaryAuthenticationProvider.php b/includes/auth/LocalPasswordPrimaryAuthenticationProvider.php index 859fd0c650..fd36887c06 100644 --- a/includes/auth/LocalPasswordPrimaryAuthenticationProvider.php +++ b/includes/auth/LocalPasswordPrimaryAuthenticationProvider.php @@ -242,14 +242,14 @@ class LocalPasswordPrimaryAuthenticationProvider $pwhash = null; - if ( $this->loginOnly ) { - $pwhash = $this->getPasswordFactory()->newFromCiphertext( null ); - $expiry = null; - // @codeCoverageIgnoreStart - } elseif ( get_class( $req ) === PasswordAuthenticationRequest::class ) { - // @codeCoverageIgnoreEnd - $pwhash = $this->getPasswordFactory()->newFromPlaintext( $req->password ); - $expiry = $this->getNewPasswordExpiry( $username ); + if ( get_class( $req ) === PasswordAuthenticationRequest::class ) { + if ( $this->loginOnly ) { + $pwhash = $this->getPasswordFactory()->newFromCiphertext( null ); + $expiry = null; + } else { + $pwhash = $this->getPasswordFactory()->newFromPlaintext( $req->password ); + $expiry = $this->getNewPasswordExpiry( $username ); + } } if ( $pwhash ) { diff --git a/includes/cache/CacheHelper.php b/includes/cache/CacheHelper.php index fb461b7a91..8c70be24a1 100644 --- a/includes/cache/CacheHelper.php +++ b/includes/cache/CacheHelper.php @@ -82,6 +82,8 @@ interface ICacheHelper { function setExpiry( $cacheExpiry ); } +use MediaWiki\MediaWikiServices; + /** * Helper class for caching various elements in a single cache entry. * @@ -217,9 +219,9 @@ class CacheHelper implements ICacheHelper { $subPage = explode( '/', $subPage, 2 ); $subPage = count( $subPage ) > 1 ? $subPage[1] : false; - $message .= ' ' . Linker::link( + $message .= ' ' . MediaWikiServices::getInstance()->getLinkRenderer()->makeLink( $context->getTitle( $subPage ), - $context->msg( 'cachedspecial-refresh-now' )->escaped(), + $context->msg( 'cachedspecial-refresh-now' )->text(), [], $refreshArgs ); diff --git a/includes/cache/MessageCache.php b/includes/cache/MessageCache.php index 0c2f9de9b5..4e6b2fd396 100644 --- a/includes/cache/MessageCache.php +++ b/includes/cache/MessageCache.php @@ -496,6 +496,7 @@ class MessageCache { if ( $text === false ) { // Failed to fetch data; possible ES errors? // Store a marker to fetch on-demand as a workaround... + // TODO Use a differnt marker $entry = '!TOO BIG'; wfDebugLog( 'MessageCache', @@ -510,6 +511,10 @@ class MessageCache { $cache['VERSION'] = MSG_CACHE_VERSION; ksort( $cache ); + + # Hash for validating local cache (APC). No need to take into account + # messages larger than $wgMaxMsgCacheEntrySize, since those are only + # stored and fetched from memcache. $cache['HASH'] = md5( serialize( $cache ) ); $cache['EXPIRY'] = wfTimestamp( TS_MW, time() + $this->mExpiry ); @@ -568,7 +573,8 @@ class MessageCache { } ScopedCallback::consume( $scopedLock ); - // Relay the purge to APC and other DCs + // Relay the purge. Touching this check key expires cache contents + // and local cache (APC) validation hash across all datacenters. $this->wanCache->touchCheckKey( wfMemcKey( 'messages', $code ) ); // Also delete cached sidebar... just in case it is affected diff --git a/includes/changetags/ChangeTagsLogItem.php b/includes/changetags/ChangeTagsLogItem.php index 2dc953c360..b78efafa65 100644 --- a/includes/changetags/ChangeTagsLogItem.php +++ b/includes/changetags/ChangeTagsLogItem.php @@ -19,6 +19,8 @@ * @ingroup Change tagging */ +use MediaWiki\MediaWikiServices; + /** * Item class for a logging table row with its associated change tags. * @todo Abstract out a base class for this and RevDelLogItem, similar to the @@ -70,9 +72,9 @@ class ChangeTagsLogItem extends RevisionItemBase { $formatter->setAudience( LogFormatter::FOR_THIS_USER ); // Log link for this page - $loglink = Linker::link( + $loglink = MediaWikiServices::getInstance()->getLinkRenderer()->makeLink( SpecialPage::getTitleFor( 'Log' ), - $this->list->msg( 'log' )->escaped(), + $this->list->msg( 'log' )->text(), [], [ 'page' => $title->getPrefixedText() ] ); diff --git a/includes/debug/logger/monolog/LogstashFormatter.php b/includes/debug/logger/monolog/LogstashFormatter.php new file mode 100644 index 0000000000..553cbf61c4 --- /dev/null +++ b/includes/debug/logger/monolog/LogstashFormatter.php @@ -0,0 +1,83 @@ +contextPrefix ) { + return parent::formatV0( $record ); + } + + $context = !empty( $record['context'] ) ? $record['context'] : []; + $record['context'] = []; + $formatted = parent::formatV0( $record ); + + $formatted['@fields'] = $this->fixKeyConflicts( $formatted['@fields'], $context ); + return $formatted; + } + + /** + * Prevent key conflicts + * @param array $record + * @return array + */ + protected function formatV1( array $record ) { + if ( $this->contextPrefix ) { + return parent::formatV1( $record ); + } + + $context = !empty( $record['context'] ) ? $record['context'] : []; + $record['context'] = []; + $formatted = parent::formatV1( $record ); + + $formatted = $this->fixKeyConflicts( $formatted, $context ); + return $formatted; + } + + /** + * Check whether some context field would overwrite another message key. If so, rename + * and flag. + * @param array $fields Fields to be sent to logstash + * @param array $context Copy of the original $record['context'] + * @return array Updated version of $fields + */ + protected function fixKeyConflicts( array $fields, array $context ) { + foreach ( $context as $key => $val ) { + if ( + in_array( $key, $this->reservedKeys, true ) && + isset( $fields[$key] ) && $fields[$key] !== $val + ) { + $fields['logstash_formatter_key_conflict'][] = $key; + $key = 'c_' . $key; + } + $fields[$key] = $val; + } + return $fields; + } +} diff --git a/includes/debug/logger/monolog/WikiProcessor.php b/includes/debug/logger/monolog/WikiProcessor.php index 81e1e14763..ad939a0932 100644 --- a/includes/debug/logger/monolog/WikiProcessor.php +++ b/includes/debug/logger/monolog/WikiProcessor.php @@ -29,17 +29,6 @@ namespace MediaWiki\Logger\Monolog; * @copyright © 2013 Bryan Davis and Wikimedia Foundation. */ class WikiProcessor { - /** @var array Keys which should not be used in log context */ - protected $reservedKeys = [ - // from monolog:src/Monolog/Formatter/LogstashFormatter.php#L71-L88 - 'message', 'channel', 'level', 'type', - // from WebProcessor - 'url', 'ip', 'http_method', 'server', 'referrer', - // from WikiProcessor - 'host', 'wiki', 'reqId', 'mwversion', - // from config magic - 'normalized_message', - ]; /** * @param array $record @@ -47,15 +36,6 @@ class WikiProcessor { */ public function __invoke( array $record ) { global $wgVersion; - - // some log aggregators such as Logstash will merge the log context into the main - // metadata and end up overwriting the data coming from processors - foreach ( $this->reservedKeys as $key ) { - if ( isset( $record['context'][$key] ) ) { - wfLogWarning( __METHOD__ . ": '$key' key overwritten in log context." ); - } - } - $record['extra'] = array_merge( $record['extra'], [ @@ -67,4 +47,5 @@ class WikiProcessor { ); return $record; } + } diff --git a/includes/filerepo/file/File.php b/includes/filerepo/file/File.php index c1d5573b68..9188cd9140 100644 --- a/includes/filerepo/file/File.php +++ b/includes/filerepo/file/File.php @@ -1018,7 +1018,7 @@ abstract class File implements IDBAccessObject { return $handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); } else { return new MediaTransformError( 'thumbnail_error', - $params['width'], 0, wfMessage( 'thumbnail-dest-create' )->text() ); + $params['width'], 0, wfMessage( 'thumbnail-dest-create' ) ); } } diff --git a/includes/http/CurlHttpRequest.php b/includes/http/CurlHttpRequest.php index f58c3a9a5b..7fd3e835c4 100644 --- a/includes/http/CurlHttpRequest.php +++ b/includes/http/CurlHttpRequest.php @@ -38,11 +38,10 @@ class CurlHttpRequest extends MWHttpRequest { } public function execute() { - - parent::execute(); + $this->prepare(); if ( !$this->status->isOK() ) { - return $this->status; + return Status::wrap( $this->status ); // TODO B/C; move this to callers } $this->curlOptions[CURLOPT_PROXY] = $this->proxy; @@ -102,7 +101,7 @@ class CurlHttpRequest extends MWHttpRequest { $curlHandle = curl_init( $this->url ); if ( !curl_setopt_array( $curlHandle, $this->curlOptions ) ) { - throw new MWException( "Error setting curl options." ); + throw new InvalidArgumentException( "Error setting curl options." ); } if ( $this->followRedirects && $this->canFollowRedirects() ) { @@ -140,7 +139,7 @@ class CurlHttpRequest extends MWHttpRequest { $this->parseHeader(); $this->setStatus(); - return $this->status; + return Status::wrap( $this->status ); // TODO B/C; move this to callers } /** diff --git a/includes/http/Http.php b/includes/http/Http.php index 43ae2d0e8f..8255bb344e 100644 --- a/includes/http/Http.php +++ b/includes/http/Http.php @@ -51,6 +51,8 @@ class Http { * - userAgent A user agent, if you want to override the default * MediaWiki/$wgVersion * - logger A \Psr\Logger\LoggerInterface instance for debug logging + * - username Username for HTTP Basic Authentication + * - password Password for HTTP Basic Authentication * @param string $caller The method making this request, for profiling * @return string|bool (bool)false on failure or a string on success */ @@ -74,7 +76,7 @@ class Http { } else { $errors = $status->getErrorsByType( 'error' ); $logger = LoggerFactory::getInstance( 'http' ); - $logger->warning( $status->getWikiText( false, false, 'en' ), + $logger->warning( Status::wrap( $status )->getWikiText( false, false, 'en' ), [ 'error' => $errors, 'caller' => $caller, 'content' => $req->getContent() ] ); return false; } diff --git a/includes/http/MWHttpRequest.php b/includes/http/MWHttpRequest.php index 08883ae44f..fac052fffc 100644 --- a/includes/http/MWHttpRequest.php +++ b/includes/http/MWHttpRequest.php @@ -46,9 +46,11 @@ class MWHttpRequest implements LoggerAwareInterface { protected $reqHeaders = []; protected $url; protected $parsedUrl; + /** @var callable */ protected $callback; protected $maxRedirects = 5; protected $followRedirects = false; + protected $connectTimeout; /** * @var CookieJar @@ -60,7 +62,8 @@ class MWHttpRequest implements LoggerAwareInterface { protected $respStatus = "200 Ok"; protected $respHeaders = []; - public $status; + /** @var StatusValue */ + protected $status; /** * @var Profiler @@ -98,9 +101,9 @@ class MWHttpRequest implements LoggerAwareInterface { } if ( !$this->parsedUrl || !Http::isValidURI( $this->url ) ) { - $this->status = Status::newFatal( 'http-invalid-url', $url ); + $this->status = StatusValue::newFatal( 'http-invalid-url', $url ); } else { - $this->status = Status::newGood( 100 ); // continue + $this->status = StatusValue::newGood( 100 ); // continue } if ( isset( $options['timeout'] ) && $options['timeout'] != 'default' ) { @@ -116,6 +119,12 @@ class MWHttpRequest implements LoggerAwareInterface { if ( isset( $options['userAgent'] ) ) { $this->setUserAgent( $options['userAgent'] ); } + if ( isset( $options['username'] ) && isset( $options['password'] ) ) { + $this->setHeader( + 'Authorization', + 'Basic ' . base64_encode( $options['username'] . ':' . $options['password'] ) + ); + } $members = [ "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo", "method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" ]; @@ -161,7 +170,7 @@ class MWHttpRequest implements LoggerAwareInterface { * @param string $url Url to use * @param array $options (optional) extra params to pass (see Http::request()) * @param string $caller The method making this request, for profiling - * @throws MWException + * @throws DomainException * @return CurlHttpRequest|PhpHttpRequest * @see MWHttpRequest::__construct */ @@ -169,7 +178,7 @@ class MWHttpRequest implements LoggerAwareInterface { if ( !Http::$httpEngine ) { Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php'; } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) { - throw new MWException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' . + throw new DomainException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' . ' Http::$httpEngine is set to "curl"' ); } @@ -186,7 +195,7 @@ class MWHttpRequest implements LoggerAwareInterface { return new CurlHttpRequest( $url, $options, $caller, Profiler::instance() ); case 'php': if ( !wfIniGetBool( 'allow_url_fopen' ) ) { - throw new MWException( __METHOD__ . ': allow_url_fopen ' . + throw new DomainException( __METHOD__ . ': allow_url_fopen ' . 'needs to be enabled for pure PHP http requests to ' . 'work. If possible, curl should be used instead. See ' . 'http://php.net/curl.' @@ -194,7 +203,7 @@ class MWHttpRequest implements LoggerAwareInterface { } return new PhpHttpRequest( $url, $options, $caller, Profiler::instance() ); default: - throw new MWException( __METHOD__ . ': The setting of Http::$httpEngine is not valid.' ); + throw new DomainException( __METHOD__ . ': The setting of Http::$httpEngine is not valid.' ); } } @@ -222,7 +231,7 @@ class MWHttpRequest implements LoggerAwareInterface { * * @return void */ - public function proxySetup() { + protected function proxySetup() { // If there is an explicit proxy set and proxies are not disabled, then use it if ( $this->proxy && !$this->noProxy ) { return; @@ -300,7 +309,7 @@ class MWHttpRequest implements LoggerAwareInterface { * Get an array of the headers * @return array */ - public function getHeaderList() { + protected function getHeaderList() { $list = []; if ( $this->cookieJar ) { @@ -333,12 +342,14 @@ class MWHttpRequest implements LoggerAwareInterface { * bytes are reported handled than were passed to you, the HTTP fetch * will be aborted. * - * @param callable $callback - * @throws MWException + * @param callable|null $callback + * @throws InvalidArgumentException */ public function setCallback( $callback ) { - if ( !is_callable( $callback ) ) { - throw new MWException( 'Invalid MwHttpRequest callback' ); + if ( is_null( $callback ) ) { + $callback = [ $this, 'read' ]; + } elseif ( !is_callable( $callback ) ) { + throw new InvalidArgumentException( __METHOD__ . ': invalid callback' ); } $this->callback = $callback; } @@ -350,6 +361,7 @@ class MWHttpRequest implements LoggerAwareInterface { * @param resource $fh * @param string $content * @return int + * @internal */ public function read( $fh, $content ) { $this->content .= $content; @@ -359,9 +371,14 @@ class MWHttpRequest implements LoggerAwareInterface { /** * Take care of whatever is necessary to perform the URI request. * - * @return Status + * @return StatusValue + * @note currently returns Status for B/C */ public function execute() { + throw new LogicException( 'children must override this' ); + } + + protected function prepare() { $this->content = ""; if ( strtoupper( $this->method ) == "HEAD" ) { @@ -371,7 +388,7 @@ class MWHttpRequest implements LoggerAwareInterface { $this->proxySetup(); // set up any proxy as needed if ( !$this->callback ) { - $this->setCallback( [ $this, 'read' ] ); + $this->setCallback( null ); } if ( !isset( $this->reqHeaders['User-Agent'] ) ) { @@ -494,6 +511,8 @@ class MWHttpRequest implements LoggerAwareInterface { /** * Tells the MWHttpRequest object to use this pre-loaded CookieJar. * + * To read response cookies from the jar, getCookieJar must be called first. + * * @param CookieJar $jar */ public function setCookieJar( $jar ) { @@ -519,14 +538,18 @@ class MWHttpRequest implements LoggerAwareInterface { * Set-Cookie headers. * @see Cookie::set * @param string $name - * @param mixed $value + * @param string $value * @param array $attr */ - public function setCookie( $name, $value = null, $attr = null ) { + public function setCookie( $name, $value, $attr = [] ) { if ( !$this->cookieJar ) { $this->cookieJar = new CookieJar; } + if ( $this->parsedUrl && !isset( $attr['domain'] ) ) { + $attr['domain'] = $this->parsedUrl['host']; + } + $this->cookieJar->setCookie( $name, $value, $attr ); } diff --git a/includes/http/PhpHttpRequest.php b/includes/http/PhpHttpRequest.php index 2af000fac0..d8a9949d2f 100644 --- a/includes/http/PhpHttpRequest.php +++ b/includes/http/PhpHttpRequest.php @@ -87,6 +87,7 @@ class PhpHttpRequest extends MWHttpRequest { * is completely useless (something like "fopen: failed to open stream") * so normal methods of handling errors programmatically * like get_last_error() don't work. + * @internal */ public function errorHandler( $errno, $errstr ) { $n = count( $this->fopenErrors ) + 1; @@ -94,8 +95,7 @@ class PhpHttpRequest extends MWHttpRequest { } public function execute() { - - parent::execute(); + $this->prepare(); if ( is_array( $this->postData ) ) { $this->postData = wfArrayToCgi( $this->postData ); @@ -227,12 +227,12 @@ class PhpHttpRequest extends MWHttpRequest { . ': error opening connection: {errstr1}', $this->fopenErrors ); } $this->status->fatal( 'http-request-error' ); - return $this->status; + return Status::wrap( $this->status ); // TODO B/C; move this to callers } if ( $result['timed_out'] ) { $this->status->fatal( 'http-timed-out', $this->url ); - return $this->status; + return Status::wrap( $this->status ); // TODO B/C; move this to callers } // If everything went OK, or we received some error code @@ -253,6 +253,6 @@ class PhpHttpRequest extends MWHttpRequest { } fclose( $fh ); - return $this->status; + return Status::wrap( $this->status ); // TODO B/C; move this to callers } } diff --git a/includes/installer/DatabaseUpdater.php b/includes/installer/DatabaseUpdater.php index 6a702e9fbb..6a8a99ff09 100644 --- a/includes/installer/DatabaseUpdater.php +++ b/includes/installer/DatabaseUpdater.php @@ -32,8 +32,6 @@ require_once __DIR__ . '/../../maintenance/Maintenance.php'; * @since 1.17 */ abstract class DatabaseUpdater { - protected static $updateCounter = 0; - /** * Array of updates to perform on the database * @@ -423,8 +421,6 @@ abstract class DatabaseUpdater { * @param array $what What updates to perform */ public function doUpdates( $what = [ 'core', 'extensions', 'stats' ] ) { - global $wgVersion; - $this->db->setSchemaVars( $this->getSchemaVars() ); $what = array_flip( $what ); @@ -441,12 +437,9 @@ abstract class DatabaseUpdater { $this->checkStats(); } - $this->setAppliedUpdates( $wgVersion, $this->updates ); - if ( $this->fileHandle ) { $this->skipSchema = false; $this->writeSchemaUpdateFile(); - $this->setAppliedUpdates( "$wgVersion-schema", $this->updatesSkipped ); } } @@ -482,23 +475,6 @@ abstract class DatabaseUpdater { $this->updates = array_merge( $this->updates, $updatesDone ); } - /** - * @param string $version - * @param array $updates - */ - protected function setAppliedUpdates( $version, $updates = [] ) { - $this->db->clearFlag( DBO_DDLMODE ); - if ( !$this->canUseNewUpdatelog() ) { - return; - } - $key = "updatelist-$version-" . time() . self::$updateCounter; - self::$updateCounter++; - $this->db->insert( 'updatelog', - [ 'ul_key' => $key, 'ul_value' => serialize( $updates ) ], - __METHOD__ ); - $this->db->setFlag( DBO_DDLMODE ); - } - /** * Helper function: check if the given key is present in the updatelog table. * Obviously, only use this for updates that occur after the updatelog table was diff --git a/includes/installer/i18n/en.json b/includes/installer/i18n/en.json index b25ff2c635..95d2ba31c4 100644 --- a/includes/installer/i18n/en.json +++ b/includes/installer/i18n/en.json @@ -308,5 +308,5 @@ "config-nofile": "File \"$1\" could not be found. Has it been deleted?", "config-extension-link": "Did you know that your wiki supports [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions extensions]?\n\nYou can browse [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category extensions by category] or the [https://www.mediawiki.org/wiki/Extension_Matrix Extension Matrix] to see the full list of extensions.", "mainpagetext": "MediaWiki has been installed.", - "mainpagedocfooter": "Consult the [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents] for information on using the wiki software.\n\n== Getting started ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Configuration settings list]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Localise MediaWiki for your language]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Learn how to combat spam on your wiki]" + "mainpagedocfooter": "Consult the [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents User's Guide] for information on using the wiki software.\n\n== Getting started ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Configuration settings list]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki FAQ]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki release mailing list]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources Localise MediaWiki for your language]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Learn how to combat spam on your wiki]" } diff --git a/includes/libs/CookieJar.php b/includes/libs/CookieJar.php index 910a7ca82d..8f5700abb9 100644 --- a/includes/libs/CookieJar.php +++ b/includes/libs/CookieJar.php @@ -19,7 +19,11 @@ * @ingroup HTTP */ +/** + * Cookie jar to use with MWHttpRequest. Does not handle cookie unsetting. + */ class CookieJar { + /** @var Cookie[] */ private $cookie = []; /** diff --git a/includes/libs/rdbms/connectionmanager/ConnectionManager.php b/includes/libs/rdbms/connectionmanager/ConnectionManager.php new file mode 100644 index 0000000000..4f72f77afd --- /dev/null +++ b/includes/libs/rdbms/connectionmanager/ConnectionManager.php @@ -0,0 +1,141 @@ +loadBalancer = $loadBalancer; + $this->domain = $domain; + $this->groups = $groups; + } + + /** + * @param int $i + * @param string[]|null $groups + * + * @return Database + */ + private function getConnection( $i, array $groups = null ) { + $groups = $groups === null ? $this->groups : $groups; + return $this->loadBalancer->getConnection( $i, $groups, $this->domain ); + } + + /** + * @param int $i + * @param string[]|null $groups + * + * @return DBConnRef + */ + private function getConnectionRef( $i, array $groups = null ) { + $groups = $groups === null ? $this->groups : $groups; + return $this->loadBalancer->getConnectionRef( $i, $groups, $this->domain ); + } + + /** + * Returns a connection to the master DB, for updating. The connection should later be released + * by calling releaseConnection(). + * + * @since 1.29 + * + * @return Database + */ + public function getWriteConnection() { + return $this->getConnection( DB_MASTER ); + } + + /** + * Returns a database connection for reading. The connection should later be released by + * calling releaseConnection(). + * + * @since 1.29 + * + * @param string[]|null $groups + * + * @return Database + */ + public function getReadConnection( array $groups = null ) { + $groups = $groups === null ? $this->groups : $groups; + return $this->getConnection( DB_REPLICA, $groups ); + } + + /** + * @since 1.29 + * + * @param IDatabase $db + */ + public function releaseConnection( IDatabase $db ) { + $this->loadBalancer->reuseConnection( $db ); + } + + /** + * Returns a connection ref to the master DB, for updating. + * + * @since 1.29 + * + * @return DBConnRef + */ + public function getWriteConnectionRef() { + return $this->getConnectionRef( DB_MASTER ); + } + + /** + * Returns a database connection ref for reading. + * + * @since 1.29 + * + * @param string[]|null $groups + * + * @return DBConnRef + */ + public function getReadConnectionRef( array $groups = null ) { + $groups = $groups === null ? $this->groups : $groups; + return $this->getConnectionRef( DB_REPLICA, $groups ); + } + +} diff --git a/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManager.php b/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManager.php new file mode 100644 index 0000000000..fb031822e7 --- /dev/null +++ b/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManager.php @@ -0,0 +1,97 @@ +forceWriteConnection = true; + } + + /** + * @since 1.29 + * + * @param string[]|null $groups + * + * @return Database + */ + public function getReadConnection( array $groups = null ) { + if ( $this->forceWriteConnection ) { + return parent::getWriteConnection(); + } + + return parent::getReadConnection( $groups ); + } + + /** + * @since 1.29 + * + * @return Database + */ + public function getWriteConnection() { + $this->prepareForUpdates(); + return parent::getWriteConnection(); + } + + /** + * @since 1.29 + * + * @param string[]|null $groups + * + * @return DBConnRef + */ + public function getReadConnectionRef( array $groups = null ) { + if ( $this->forceWriteConnection ) { + return parent::getWriteConnectionRef(); + } + + return parent::getReadConnectionRef( $groups ); + } + + /** + * @since 1.29 + * + * @return DBConnRef + */ + public function getWriteConnectionRef() { + $this->prepareForUpdates(); + return parent::getWriteConnectionRef(); + } + +} diff --git a/includes/libs/rdbms/database/IDatabase.php b/includes/libs/rdbms/database/IDatabase.php index 48d76c4023..c6055dbdbd 100644 --- a/includes/libs/rdbms/database/IDatabase.php +++ b/includes/libs/rdbms/database/IDatabase.php @@ -1183,7 +1183,7 @@ interface IDatabase { /** * DELETE query wrapper. * - * @param array $table Table name + * @param string $table Table name * @param string|array $conds Array of conditions. See $conds in IDatabase::select() * for the format. Use $conds == "*" to delete all rows * @param string $fname Name of the calling function diff --git a/includes/logging/BlockLogFormatter.php b/includes/logging/BlockLogFormatter.php index 21e40ec5fa..c3902326c6 100644 --- a/includes/logging/BlockLogFormatter.php +++ b/includes/logging/BlockLogFormatter.php @@ -22,6 +22,8 @@ * @since 1.25 */ +use MediaWiki\MediaWikiServices; + /** * This class formats block log entries. * @@ -91,6 +93,7 @@ class BlockLogFormatter extends LogFormatter { public function getActionLinks() { $subtype = $this->entry->getSubtype(); + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); if ( $this->entry->isDeleted( LogPage::DELETED_ACTION ) // Action is hidden || !( $subtype === 'block' || $subtype === 'reblock' ) || !$this->context->getUser()->isAllowed( 'block' ) @@ -101,13 +104,13 @@ class BlockLogFormatter extends LogFormatter { // Show unblock/change block link $title = $this->entry->getTarget(); $links = [ - Linker::linkKnown( + $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'Unblock', $title->getDBkey() ), - $this->msg( 'unblocklink' )->escaped() + $this->msg( 'unblocklink' )->text() ), - Linker::linkKnown( + $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'Block', $title->getDBkey() ), - $this->msg( 'change-blocklink' )->escaped() + $this->msg( 'change-blocklink' )->text() ) ]; diff --git a/includes/logging/ContentModelLogFormatter.php b/includes/logging/ContentModelLogFormatter.php index f130740a8d..861ea3021b 100644 --- a/includes/logging/ContentModelLogFormatter.php +++ b/includes/logging/ContentModelLogFormatter.php @@ -1,5 +1,7 @@ context->getLanguage(); @@ -18,9 +20,9 @@ class ContentModelLogFormatter extends LogFormatter { } $params = $this->extractParameters(); - $revert = Linker::linkKnown( + $revert = MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink( SpecialPage::getTitleFor( 'ChangeContentModel' ), - $this->msg( 'logentry-contentmodel-change-revertlink' )->escaped(), + $this->msg( 'logentry-contentmodel-change-revertlink' )->text(), [], [ 'pagetitle' => $this->entry->getTarget()->getPrefixedText(), diff --git a/includes/logging/DeleteLogFormatter.php b/includes/logging/DeleteLogFormatter.php index dc9378fd36..05973df325 100644 --- a/includes/logging/DeleteLogFormatter.php +++ b/includes/logging/DeleteLogFormatter.php @@ -23,6 +23,8 @@ * @since 1.22 */ +use MediaWiki\MediaWikiServices; + /** * This class formats delete log entries. * @@ -114,6 +116,7 @@ class DeleteLogFormatter extends LogFormatter { public function getActionLinks() { $user = $this->context->getUser(); + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); if ( !$user->isAllowed( 'deletedhistory' ) || $this->entry->isDeleted( LogPage::DELETED_ACTION ) ) { @@ -128,9 +131,9 @@ class DeleteLogFormatter extends LogFormatter { } else { $message = 'undeleteviewlink'; } - $revert = Linker::linkKnown( + $revert = $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'Undelete' ), - $this->msg( $message )->escaped(), + $this->msg( $message )->text(), [], [ 'target' => $this->entry->getTarget()->getPrefixedDBkey() ] ); @@ -156,9 +159,9 @@ class DeleteLogFormatter extends LogFormatter { if ( count( $ids ) == 1 ) { // Live revision diffs... if ( $key == 'oldid' || $key == 'revision' ) { - $links[] = Linker::linkKnown( + $links[] = $linkRenderer->makeKnownLink( $this->entry->getTarget(), - $this->msg( 'diff' )->escaped(), + $this->msg( 'diff' )->text(), [], [ 'diff' => intval( $ids[0] ), @@ -167,9 +170,9 @@ class DeleteLogFormatter extends LogFormatter { ); // Deleted revision diffs... } elseif ( $key == 'artimestamp' || $key == 'archive' ) { - $links[] = Linker::linkKnown( + $links[] = $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'Undelete' ), - $this->msg( 'diff' )->escaped(), + $this->msg( 'diff' )->text(), [], [ 'target' => $this->entry->getTarget()->getPrefixedDBkey(), @@ -181,9 +184,9 @@ class DeleteLogFormatter extends LogFormatter { } // View/modify link... - $links[] = Linker::linkKnown( + $links[] = $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'Revisiondelete' ), - $this->msg( 'revdel-restore' )->escaped(), + $this->msg( 'revdel-restore' )->text(), [], [ 'target' => $this->entry->getTarget()->getPrefixedText(), @@ -206,9 +209,9 @@ class DeleteLogFormatter extends LogFormatter { $query = implode( ',', $query ); } // Link to each hidden object ID, $params[1] is the url param - $revert = Linker::linkKnown( + $revert = $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'Revisiondelete' ), - $this->msg( 'revdel-restore' )->escaped(), + $this->msg( 'revdel-restore' )->text(), [], [ 'target' => $this->entry->getTarget()->getPrefixedText(), diff --git a/includes/logging/LogEventsList.php b/includes/logging/LogEventsList.php index 0cf584b0a2..57a7597fe4 100644 --- a/includes/logging/LogEventsList.php +++ b/includes/logging/LogEventsList.php @@ -23,6 +23,8 @@ * @file */ +use MediaWiki\MediaWikiServices; + class LogEventsList extends ContextSource { const NO_ACTION_LINK = 1; const NO_EXTRA_USER_LINKS = 2; @@ -142,10 +144,11 @@ class LogEventsList extends ContextSource { */ private function getFilterLinks( $filter ) { // show/hide links - $messages = [ $this->msg( 'show' )->escaped(), $this->msg( 'hide' )->escaped() ]; + $messages = [ $this->msg( 'show' )->text(), $this->msg( 'hide' )->text() ]; // Option value -> message mapping $links = []; $hiddens = ''; // keep track for "go" button + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); foreach ( $filter as $type => $val ) { // Should the below assignment be outside the foreach? // Then it would have to be copied. Not certain what is more expensive. @@ -155,7 +158,7 @@ class LogEventsList extends ContextSource { $hideVal = 1 - intval( $val ); $query[$queryKey] = $hideVal; - $link = Linker::linkKnown( + $link = $linkRenderer->makeKnownLink( $this->getTitle(), $messages[$hideVal], [], @@ -672,9 +675,9 @@ class LogEventsList extends ContextSource { $urlParam = array_merge( $urlParam, $extraUrlParams ); } - $s .= Linker::linkKnown( + $s .= MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink( SpecialPage::getTitleFor( 'Log' ), - $context->msg( 'log-fulllog' )->escaped(), + $context->msg( 'log-fulllog' )->text(), [], $urlParam ); diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php index c86eabdec0..ac0564d2e8 100644 --- a/includes/media/Bitmap.php +++ b/includes/media/Bitmap.php @@ -541,7 +541,7 @@ class BitmapHandler extends TransformationalImageHandler { * @param array $params Rotate parameters. * 'rotation' clockwise rotation in degrees, allowed are multiples of 90 * @since 1.21 - * @return bool + * @return bool|MediaTransformError */ public function rotate( $file, $params ) { global $wgImageMagickConvertCommand; diff --git a/includes/media/DjVu.php b/includes/media/DjVu.php index 18f75ece66..a852215ca8 100644 --- a/includes/media/DjVu.php +++ b/includes/media/DjVu.php @@ -170,7 +170,7 @@ class DjVuHandler extends ImageHandler { 'thumbnail_error', $width, $height, - wfMessage( 'thumbnail_dest_directory' )->text() + wfMessage( 'thumbnail_dest_directory' ) ); } @@ -197,7 +197,7 @@ class DjVuHandler extends ImageHandler { return new MediaTransformError( 'thumbnail_error', $params['width'], $params['height'], - wfMessage( 'filemissing' )->text() + wfMessage( 'filemissing' ) ); } diff --git a/includes/media/Jpeg.php b/includes/media/Jpeg.php index b8b6f6c987..6c857a8565 100644 --- a/includes/media/Jpeg.php +++ b/includes/media/Jpeg.php @@ -130,7 +130,7 @@ class JpegHandler extends ExifBitmapHandler { * @param array $params Rotate parameters. * 'rotation' clockwise rotation in degrees, allowed are multiples of 90 * @since 1.21 - * @return bool + * @return bool|MediaTransformError */ public function rotate( $file, $params ) { global $wgJpegTran; diff --git a/includes/media/MediaTransformOutput.php b/includes/media/MediaTransformOutput.php index 46b96745fa..5366c4fa44 100644 --- a/includes/media/MediaTransformOutput.php +++ b/includes/media/MediaTransformOutput.php @@ -439,19 +439,12 @@ class ThumbnailImage extends MediaTransformOutput { * @ingroup Media */ class MediaTransformError extends MediaTransformOutput { - /** @var string HTML formatted version of the error */ - private $htmlMsg; - - /** @var string Plain text formatted version of the error */ - private $textMsg; + /** @var Message */ + private $msg; function __construct( $msg, $width, $height /*, ... */ ) { $args = array_slice( func_get_args(), 3 ); - $htmlArgs = array_map( 'htmlspecialchars', $args ); - $htmlArgs = array_map( 'nl2br', $htmlArgs ); - - $this->htmlMsg = wfMessage( $msg )->rawParams( $htmlArgs )->escaped(); - $this->textMsg = wfMessage( $msg )->rawParams( $htmlArgs )->text(); + $this->msg = wfMessage( $msg )->params( $args ); $this->width = intval( $width ); $this->height = intval( $height ); $this->url = false; @@ -461,16 +454,20 @@ class MediaTransformError extends MediaTransformOutput { function toHtml( $options = [] ) { return "
width}px; height: {$this->height}px; display:inline-block;\">" . - $this->htmlMsg . + $this->getHtmlMsg() . "
"; } function toText() { - return $this->textMsg; + return $this->msg->text(); } function getHtmlMsg() { - return $this->htmlMsg; + return $this->msg->escaped(); + } + + function getMsg() { + return $this->msg; } function isError() { @@ -492,7 +489,8 @@ class TransformParameterError extends MediaTransformError { parent::__construct( 'thumbnail_error', max( isset( $params['width'] ) ? $params['width'] : 0, 120 ), max( isset( $params['height'] ) ? $params['height'] : 0, 120 ), - wfMessage( 'thumbnail_invalid_params' )->text() ); + wfMessage( 'thumbnail_invalid_params' ) + ); } function getHttpStatusCode() { @@ -509,15 +507,15 @@ class TransformParameterError extends MediaTransformError { class TransformTooBigImageAreaError extends MediaTransformError { function __construct( $params, $maxImageArea ) { $msg = wfMessage( 'thumbnail_toobigimagearea' ); + $msg->rawParams( + $msg->getLanguage()->formatComputingNumbers( $maxImageArea, 1000, "size-$1pixel" ) + ); parent::__construct( 'thumbnail_error', max( isset( $params['width'] ) ? $params['width'] : 0, 120 ), max( isset( $params['height'] ) ? $params['height'] : 0, 120 ), - $msg->rawParams( - $msg->getLanguage()->formatComputingNumbers( - $maxImageArea, 1000, "size-$1pixel" ) - )->text() - ); + $msg + ); } function getHttpStatusCode() { diff --git a/includes/media/SVG.php b/includes/media/SVG.php index f3b33ace35..0cea6d899f 100644 --- a/includes/media/SVG.php +++ b/includes/media/SVG.php @@ -178,14 +178,14 @@ class SvgHandler extends ImageHandler { $metadata = $this->unpackMetadata( $image->getMetadata() ); if ( isset( $metadata['error'] ) ) { // sanity check - $err = wfMessage( 'svg-long-error', $metadata['error']['message'] )->text(); + $err = wfMessage( 'svg-long-error', $metadata['error']['message'] ); return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err ); } if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) { return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, - wfMessage( 'thumbnail_dest_directory' )->text() ); + wfMessage( 'thumbnail_dest_directory' ) ); } $srcPath = $image->getLocalRefPath(); @@ -196,7 +196,7 @@ class SvgHandler extends ImageHandler { return new MediaTransformError( 'thumbnail_error', $params['width'], $params['height'], - wfMessage( 'filemissing' )->text() + wfMessage( 'filemissing' ) ); } @@ -219,7 +219,7 @@ class SvgHandler extends ImageHandler { wfHostname(), $lnPath, $srcPath ) ); return new MediaTransformError( 'thumbnail_error', $params['width'], $params['height'], - wfMessage( 'thumbnail-temp-create' )->text() + wfMessage( 'thumbnail-temp-create' ) ); } diff --git a/includes/media/TransformationalImageHandler.php b/includes/media/TransformationalImageHandler.php index e33c27e99d..60aec45729 100644 --- a/includes/media/TransformationalImageHandler.php +++ b/includes/media/TransformationalImageHandler.php @@ -217,7 +217,7 @@ abstract class TransformationalImageHandler extends ImageHandler { return new MediaTransformError( 'thumbnail_error', $scalerParams['clientWidth'], $scalerParams['clientHeight'], - wfMessage( 'filemissing' )->text() + wfMessage( 'filemissing' ) ); } @@ -267,7 +267,7 @@ abstract class TransformationalImageHandler extends ImageHandler { # Thumbnail was zero-byte and had to be removed return new MediaTransformError( 'thumbnail_error', $scalerParams['clientWidth'], $scalerParams['clientHeight'], - wfMessage( 'unknown-error' )->text() + wfMessage( 'unknown-error' ) ); } elseif ( $mto ) { return $mto; @@ -565,7 +565,7 @@ abstract class TransformationalImageHandler extends ImageHandler { * @param array $params Rotate parameters. * 'rotation' clockwise rotation in degrees, allowed are multiples of 90 * @since 1.24 Is non-static. From 1.21 it was static - * @return bool + * @return bool|MediaTransformError */ public function rotate( $file, $params ) { return new MediaTransformError( 'thumbnail_error', 0, 0, diff --git a/includes/page/ImageHistoryPseudoPager.php b/includes/page/ImageHistoryPseudoPager.php index 6ab3ffc930..f4880d1dd4 100644 --- a/includes/page/ImageHistoryPseudoPager.php +++ b/includes/page/ImageHistoryPseudoPager.php @@ -42,6 +42,10 @@ class ImageHistoryPseudoPager extends ReverseChronologicalPager { $this->mImg = null; $this->mHist = []; $this->mRange = [ 0, 0 ]; // display range + + // Only display 10 revisions at once by default, otherwise the list is overwhelming + $this->mLimitsShown = array_merge( [ 10 ], $this->mLimitsShown ); + $this->setLimit( 10 ); } /** diff --git a/includes/password/ParameterizedPassword.php b/includes/password/ParameterizedPassword.php index 954d403bf8..c92979798f 100644 --- a/includes/password/ParameterizedPassword.php +++ b/includes/password/ParameterizedPassword.php @@ -90,7 +90,9 @@ abstract class ParameterizedPassword extends Password { $str .= $this->getDelimiter(); } - return $str . $this->hash; + $res = $str . $this->hash; + $this->assertIsSafeSize( $res ); + return $res; } /** diff --git a/includes/password/Password.php b/includes/password/Password.php index 4e395b5182..13d1e6deba 100644 --- a/includes/password/Password.php +++ b/includes/password/Password.php @@ -81,6 +81,11 @@ abstract class Password { */ protected $config; + /** + * Hash must fit in user_password, which is a tinyblob + */ + const MAX_HASH_SIZE = 255; + /** * Construct the Password object using a string hash * @@ -168,9 +173,28 @@ abstract class Password { * are considered equivalent. * * @return string + * @throws PasswordError if password cannot be serialized to fit a tinyblob. */ public function toString() { - return ':' . $this->config['type'] . ':' . $this->hash; + $result = ':' . $this->config['type'] . ':' . $this->hash; + $this->assertIsSafeSize( $result ); + return $result; + } + + /** + * Assert that hash will fit in a tinyblob field. + * + * This prevents MW from inserting it into the DB + * and having MySQL silently truncating it, locking + * the user out of their account. + * + * @param string $hash The hash in question. + * @throws PasswordError If hash does not fit in DB. + */ + final protected function assertIsSafeSize( $hash ) { + if ( strlen( $hash ) > self::MAX_HASH_SIZE ) { + throw new PasswordError( "Password hash is too big" ); + } } /** diff --git a/includes/password/UserPasswordPolicy.php b/includes/password/UserPasswordPolicy.php index 5584f6f162..bf1f8acfb7 100644 --- a/includes/password/UserPasswordPolicy.php +++ b/includes/password/UserPasswordPolicy.php @@ -67,12 +67,11 @@ class UserPasswordPolicy { * Check if a passwords meets the effective password policy for a User. * @param User $user who's policy we are checking * @param string $password the password to check - * @param string $purpose one of 'login', 'create', 'reset' * @return Status error to indicate the password didn't meet the policy, or fatal to * indicate the user shouldn't be allowed to login. */ - public function checkUserPassword( User $user, $password, $purpose = 'login' ) { - $effectivePolicy = $this->getPoliciesForUser( $user, $purpose ); + public function checkUserPassword( User $user, $password ) { + $effectivePolicy = $this->getPoliciesForUser( $user ); return $this->checkPolicies( $user, $password, @@ -134,20 +133,16 @@ class UserPasswordPolicy { * Get the policy for a user, based on their group membership. Public so * UI elements can access and inform the user. * @param User $user - * @param string $purpose one of 'login', 'create', 'reset' * @return array the effective policy for $user */ - public function getPoliciesForUser( User $user, $purpose = 'login' ) { - $effectivePolicy = $this->policies['default']; - if ( $purpose !== 'create' ) { - $effectivePolicy = self::getPoliciesForGroups( - $this->policies, - $user->getEffectiveGroups(), - $this->policies['default'] - ); - } + public function getPoliciesForUser( User $user ) { + $effectivePolicy = self::getPoliciesForGroups( + $this->policies, + $user->getEffectiveGroups(), + $this->policies['default'] + ); - Hooks::run( 'PasswordPoliciesForUser', [ $user, &$effectivePolicy, $purpose ] ); + Hooks::run( 'PasswordPoliciesForUser', [ $user, &$effectivePolicy ] ); return $effectivePolicy; } diff --git a/includes/specials/SpecialActiveusers.php b/includes/specials/SpecialActiveusers.php index 7e29be0a29..a01e9b2675 100644 --- a/includes/specials/SpecialActiveusers.php +++ b/includes/specials/SpecialActiveusers.php @@ -86,7 +86,7 @@ class SpecialActiveUsers extends SpecialPage { $groups = User::getAllGroups(); foreach ( $groups as $group ) { - $msg = User::getGroupName( $group ); + $msg = htmlspecialchars( User::getGroupName( $group ) ); $options[$msg] = $group; } diff --git a/includes/specials/SpecialAllPages.php b/includes/specials/SpecialAllPages.php index 4a2a619716..4b8446a795 100644 --- a/includes/specials/SpecialAllPages.php +++ b/includes/specials/SpecialAllPages.php @@ -204,6 +204,7 @@ class SpecialAllPages extends IncludableSpecialPage { ] ); + $linkRenderer = $this->getLinkRenderer(); if ( $res->numRows() > 0 ) { $out = Html::openElement( 'ul', [ 'class' => 'mw-allpages-chunk' ] ); @@ -213,7 +214,7 @@ class SpecialAllPages extends IncludableSpecialPage { $out .= 'page_is_redirect ? ' class="allpagesredirect"' : '' ) . '>' . - Linker::link( $t ) . + $linkRenderer->makeLink( $t ) . "\n"; } else { $out .= '
  • [[' . htmlspecialchars( $s->page_title ) . "]]
  • \n"; @@ -269,6 +270,7 @@ class SpecialAllPages extends IncludableSpecialPage { $navLinks = []; $self = $this->getPageTitle(); + $linkRenderer = $this->getLinkRenderer(); // Generate a "previous page" link if needed if ( $prevTitle ) { $query = [ 'from' => $prevTitle->getText() ]; @@ -281,9 +283,9 @@ class SpecialAllPages extends IncludableSpecialPage { $query['hideredirects'] = $hideredirects; } - $navLinks[] = Linker::linkKnown( + $navLinks[] = $linkRenderer->makeKnownLink( $self, - $this->msg( 'prevpage', $prevTitle->getText() )->escaped(), + $this->msg( 'prevpage', $prevTitle->getText() )->text(), [], $query ); @@ -304,9 +306,9 @@ class SpecialAllPages extends IncludableSpecialPage { $query['hideredirects'] = $hideredirects; } - $navLinks[] = Linker::linkKnown( + $navLinks[] = $linkRenderer->makeKnownLink( $self, - $this->msg( 'nextpage', $t->getText() )->escaped(), + $this->msg( 'nextpage', $t->getText() )->text(), [], $query ); diff --git a/includes/specials/SpecialAncientpages.php b/includes/specials/SpecialAncientpages.php index 9ee1b75a06..ecc030e638 100644 --- a/includes/specials/SpecialAncientpages.php +++ b/includes/specials/SpecialAncientpages.php @@ -78,9 +78,10 @@ class AncientPagesPage extends QueryPage { $d = $this->getLanguage()->userTimeAndDate( $result->value, $this->getUser() ); $title = Title::makeTitle( $result->namespace, $result->title ); - $link = Linker::linkKnown( + $linkRenderer = $this->getLinkRenderer(); + $link = $linkRenderer->makeKnownLink( $title, - htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) + $wgContLang->convert( $title->getPrefixedText() ) ); return $this->getLanguage()->specialList( $link, htmlspecialchars( $d ) ); diff --git a/includes/specials/SpecialBlock.php b/includes/specials/SpecialBlock.php index ce7d24e57b..585f70b86b 100644 --- a/includes/specials/SpecialBlock.php +++ b/includes/specials/SpecialBlock.php @@ -372,12 +372,13 @@ class SpecialBlock extends FormSpecialPage { $this->getOutput()->addModuleStyles( 'mediawiki.special' ); + $linkRenderer = $this->getLinkRenderer(); # Link to the user's contributions, if applicable if ( $this->target instanceof User ) { $contribsPage = SpecialPage::getTitleFor( 'Contributions', $this->target->getName() ); - $links[] = Linker::link( + $links[] = $linkRenderer->makeLink( $contribsPage, - $this->msg( 'ipb-blocklist-contribs', $this->target->getName() )->escaped() + $this->msg( 'ipb-blocklist-contribs', $this->target->getName() )->text() ); } @@ -392,21 +393,24 @@ class SpecialBlock extends FormSpecialPage { $message = $this->msg( 'ipb-unblock' )->parse(); $list = SpecialPage::getTitleFor( 'Unblock' ); } - $links[] = Linker::linkKnown( $list, $message, [] ); + $links[] = $linkRenderer->makeKnownLink( + $list, + new HtmlArmor( $message ) + ); # Link to the block list - $links[] = Linker::linkKnown( + $links[] = $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'BlockList' ), - $this->msg( 'ipb-blocklist' )->escaped() + $this->msg( 'ipb-blocklist' )->text() ); $user = $this->getUser(); # Link to edit the block dropdown reasons, if applicable if ( $user->isAllowed( 'editinterface' ) ) { - $links[] = Linker::linkKnown( + $links[] = $linkRenderer->makeKnownLink( $this->msg( 'ipbreason-dropdown' )->inContentLanguage()->getTitle(), - $this->msg( 'ipb-edit-dropdown' )->escaped(), + $this->msg( 'ipb-edit-dropdown' )->text(), [], [ 'action' => 'edit' ] ); diff --git a/includes/specials/SpecialBrokenRedirects.php b/includes/specials/SpecialBrokenRedirects.php index 17533968a0..b730ecd789 100644 --- a/includes/specials/SpecialBrokenRedirects.php +++ b/includes/specials/SpecialBrokenRedirects.php @@ -109,12 +109,13 @@ class BrokenRedirectsPage extends QueryPage { } } + $linkRenderer = $this->getLinkRenderer(); // $toObj may very easily be false if the $result list is cached if ( !is_object( $toObj ) ) { - return '' . Linker::link( $fromObj ) . ''; + return '' . $linkRenderer->makeLink( $fromObj ) . ''; } - $from = Linker::linkKnown( + $from = $linkRenderer->makeKnownLink( $fromObj, null, [], @@ -128,28 +129,22 @@ class BrokenRedirectsPage extends QueryPage { // check, if the content model is editable through action=edit ContentHandler::getForTitle( $fromObj )->supportsDirectEditing() ) { - $links[] = Linker::linkKnown( + $links[] = $linkRenderer->makeKnownLink( $fromObj, - $this->msg( 'brokenredirects-edit' )->escaped(), + $this->msg( 'brokenredirects-edit' )->text(), [], [ 'action' => 'edit' ] ); } - $to = Linker::link( - $toObj, - null, - [], - [], - [ 'broken' ] - ); + $to = $linkRenderer->makeBrokenLink( $toObj ); $arr = $this->getLanguage()->getArrow(); $out = $from . $this->msg( 'word-separator' )->escaped(); if ( $this->getUser()->isAllowed( 'delete' ) ) { - $links[] = Linker::linkKnown( + $links[] = $linkRenderer->makeKnownLink( $fromObj, - $this->msg( 'brokenredirects-delete' )->escaped(), + $this->msg( 'brokenredirects-delete' )->text(), [], [ 'action' => 'delete' ] ); diff --git a/includes/specials/SpecialDoubleRedirects.php b/includes/specials/SpecialDoubleRedirects.php index 0cec9d076c..9140bf1426 100644 --- a/includes/specials/SpecialDoubleRedirects.php +++ b/includes/specials/SpecialDoubleRedirects.php @@ -137,14 +137,15 @@ class DoubleRedirectsPage extends QueryPage { $result = $dbr->fetchObject( $res ); } } + $linkRenderer = $this->getLinkRenderer(); if ( !$result ) { - return '' . Linker::link( $titleA, null, [], [ 'redirect' => 'no' ] ) . ''; + return '' . $linkRenderer->makeLink( $titleA, null, [], [ 'redirect' => 'no' ] ) . ''; } $titleB = Title::makeTitle( $result->nsb, $result->tb ); $titleC = Title::makeTitle( $result->nsc, $result->tc, '', $result->iwc ); - $linkA = Linker::linkKnown( + $linkA = $linkRenderer->makeKnownLink( $titleA, null, [], @@ -158,26 +159,24 @@ class DoubleRedirectsPage extends QueryPage { // check, if the content model is editable through action=edit ContentHandler::getForTitle( $titleA )->supportsDirectEditing() ) { - $edit = Linker::linkKnown( + $edit = $linkRenderer->makeKnownLink( $titleA, - $this->msg( 'parentheses', $this->msg( 'editlink' )->text() )->escaped(), + $this->msg( 'parentheses', $this->msg( 'editlink' )->text() )->text(), [], - [ - 'action' => 'edit' - ] + [ 'action' => 'edit' ] ); } else { $edit = ''; } - $linkB = Linker::linkKnown( + $linkB = $linkRenderer->makeKnownLink( $titleB, null, [], [ 'redirect' => 'no' ] ); - $linkC = Linker::linkKnown( $titleC ); + $linkC = $linkRenderer->makeKnownLink( $titleC ); $lang = $this->getLanguage(); $arr = $lang->getArrow() . $lang->getDirMark(); diff --git a/includes/specials/SpecialEditTags.php b/includes/specials/SpecialEditTags.php index 252d07670e..476c452ad8 100644 --- a/includes/specials/SpecialEditTags.php +++ b/includes/specials/SpecialEditTags.php @@ -158,10 +158,11 @@ class SpecialEditTags extends UnlistedSpecialPage { // Also set header tabs to be for the target. $this->getSkin()->setRelevantTitle( $this->targetObj ); + $linkRenderer = $this->getLinkRenderer(); $links = []; - $links[] = Linker::linkKnown( + $links[] = $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'Log' ), - $this->msg( 'viewpagelogs' )->escaped(), + $this->msg( 'viewpagelogs' )->text(), [], [ 'page' => $this->targetObj->getPrefixedText(), @@ -170,17 +171,17 @@ class SpecialEditTags extends UnlistedSpecialPage { ); if ( !$this->targetObj->isSpecialPage() ) { // Give a link to the page history - $links[] = Linker::linkKnown( + $links[] = $linkRenderer->makeKnownLink( $this->targetObj, - $this->msg( 'pagehist' )->escaped(), + $this->msg( 'pagehist' )->text(), [], [ 'action' => 'history' ] ); } // Link to Special:Tags - $links[] = Linker::linkKnown( + $links[] = $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'Tags' ), - $this->msg( 'tags-edit-manage-link' )->escaped() + $this->msg( 'tags-edit-manage-link' )->text() ); // Logs themselves don't have histories or archived revisions $this->getOutput()->addSubtitle( $this->getLanguage()->pipeList( $links ) ); diff --git a/includes/specials/SpecialEmailuser.php b/includes/specials/SpecialEmailuser.php index a550e8853b..9692dd096a 100644 --- a/includes/specials/SpecialEmailuser.php +++ b/includes/specials/SpecialEmailuser.php @@ -53,13 +53,14 @@ class SpecialEmailUser extends UnlistedSpecialPage { } protected function getFormFields() { + $linkRenderer = $this->getLinkRenderer(); return [ 'From' => [ 'type' => 'info', 'raw' => 1, - 'default' => Linker::link( + 'default' => $linkRenderer->makeLink( $this->getUser()->getUserPage(), - htmlspecialchars( $this->getUser()->getName() ) + $this->getUser()->getName() ), 'label-message' => 'emailfrom', 'id' => 'mw-emailuser-sender', @@ -67,9 +68,9 @@ class SpecialEmailUser extends UnlistedSpecialPage { 'To' => [ 'type' => 'info', 'raw' => 1, - 'default' => Linker::link( + 'default' => $linkRenderer->makeLink( $this->mTargetObj->getUserPage(), - htmlspecialchars( $this->mTargetObj->getName() ) + $this->mTargetObj->getName() ), 'label-message' => 'emailto', 'id' => 'mw-emailuser-recipient', diff --git a/includes/specials/SpecialFewestrevisions.php b/includes/specials/SpecialFewestrevisions.php index b86a95e8aa..f20829fd64 100644 --- a/includes/specials/SpecialFewestrevisions.php +++ b/includes/specials/SpecialFewestrevisions.php @@ -53,12 +53,6 @@ class FewestrevisionsPage extends QueryPage { 'page_namespace' => MWNamespace::getContentNamespaces(), 'page_id = rev_page' ], 'options' => [ - 'HAVING' => 'COUNT(*) > 1', - // ^^^ This was probably here to weed out redirects. - // Since we mark them as such now, it might be - // useful to remove this. People _do_ create pages - // and never revise them, they aren't necessarily - // redirects. 'GROUP BY' => [ 'page_namespace', 'page_title', 'page_is_redirect' ] ] ]; @@ -88,14 +82,14 @@ class FewestrevisionsPage extends QueryPage { ) ); } + $linkRenderer = $this->getLinkRenderer(); + $text = $wgContLang->convert( $nt->getPrefixedText() ); + $plink = $linkRenderer->makeLink( $nt, $text ); - $text = htmlspecialchars( $wgContLang->convert( $nt->getPrefixedText() ) ); - $plink = Linker::linkKnown( $nt, $text ); - - $nl = $this->msg( 'nrevisions' )->numParams( $result->value )->escaped(); + $nl = $this->msg( 'nrevisions' )->numParams( $result->value )->text(); $redirect = isset( $result->redirect ) && $result->redirect ? ' - ' . $this->msg( 'isredirect' )->escaped() : ''; - $nlink = Linker::linkKnown( + $nlink = $linkRenderer->makeKnownLink( $nt, $nl, [], diff --git a/includes/specials/SpecialFileDuplicateSearch.php b/includes/specials/SpecialFileDuplicateSearch.php index 6de127d3b8..8021bc2c3a 100644 --- a/includes/specials/SpecialFileDuplicateSearch.php +++ b/includes/specials/SpecialFileDuplicateSearch.php @@ -208,11 +208,12 @@ class FileDuplicateSearchPage extends QueryPage { function formatResult( $skin, $result ) { global $wgContLang; + $linkRenderer = $this->getLinkRenderer(); $nt = $result->getTitle(); $text = $wgContLang->convert( $nt->getText() ); - $plink = Linker::link( + $plink = $linkRenderer->makeLink( $nt, - htmlspecialchars( $text ) + $text ); $userText = $result->getUser( 'text' ); diff --git a/includes/specials/SpecialImport.php b/includes/specials/SpecialImport.php index c58af603d2..ce886247a9 100644 --- a/includes/specials/SpecialImport.php +++ b/includes/specials/SpecialImport.php @@ -24,6 +24,8 @@ * @ingroup SpecialPage */ +use MediaWiki\MediaWikiServices; + /** * MediaWiki page data importer * @@ -592,12 +594,12 @@ class ImportReporter extends ContextSource { } $this->mPageCount++; - + $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); if ( $successCount > 0 ) { // prevents jumbling of the versions count // in RTL wikis in case the page title is LTR $this->getOutput()->addHTML( - "
  • " . Linker::linkKnown( $title ) . " " . + "
  • " . $linkRenderer->makeLink( $title ) . " " . "" . $this->msg( 'import-revision-count' )->numParams( $successCount )->escaped() . "" . @@ -656,7 +658,7 @@ class ImportReporter extends ContextSource { ); } } else { - $this->getOutput()->addHTML( "
  • " . Linker::linkKnown( $title ) . " " . + $this->getOutput()->addHTML( "
  • " . $linkRenderer->makeKnownLink( $title ) . " " . $this->msg( 'import-nonewrevisions' )->escaped() . "
  • \n" ); } } diff --git a/includes/specials/SpecialListgrants.php b/includes/specials/SpecialListgrants.php index 39c8ae8a93..2c92410cbc 100644 --- a/includes/specials/SpecialListgrants.php +++ b/includes/specials/SpecialListgrants.php @@ -71,8 +71,14 @@ class SpecialListGrants extends SpecialPage { $id = \Sanitizer::escapeId( $grant ); $out->addHTML( \Html::rawElement( 'tr', [ 'id' => $id ], - "" . $this->msg( "grant-$grant" )->escaped() . "" . - "" . $grantCellHtml . '' + "" . + $this->msg( + "listgrants-grant-display", + \User::getGrantName( $grant ), + "" . $id . "" + )->parse() . + "" . + "" . $grantCellHtml . "" ) ); } diff --git a/includes/specials/SpecialListgrouprights.php b/includes/specials/SpecialListgrouprights.php index d64306beab..f3d3a776e6 100644 --- a/includes/specials/SpecialListgrouprights.php +++ b/includes/specials/SpecialListgrouprights.php @@ -71,6 +71,8 @@ class SpecialListGroupRights extends SpecialPage { ) ); asort( $allGroups ); + $linkRenderer = $this->getLinkRenderer(); + foreach ( $allGroups as $group ) { $permissions = isset( $groupPermissions[$group] ) ? $groupPermissions[$group] @@ -92,22 +94,22 @@ class SpecialListGroupRights extends SpecialPage { // Do not make a link for the generic * group or group with invalid group page $grouppage = htmlspecialchars( $groupnameLocalized ); } else { - $grouppage = Linker::link( + $grouppage = $linkRenderer->makeLink( $grouppageLocalizedTitle, - htmlspecialchars( $groupnameLocalized ) + $groupnameLocalized ); } if ( $group === 'user' ) { // Link to Special:listusers for implicit group 'user' - $grouplink = '
    ' . Linker::linkKnown( + $grouplink = '
    ' . $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'Listusers' ), - $this->msg( 'listgrouprights-members' )->escaped() + $this->msg( 'listgrouprights-members' )->text() ); } elseif ( !in_array( $group, $config->get( 'ImplicitGroups' ) ) ) { - $grouplink = '
    ' . Linker::linkKnown( + $grouplink = '
    ' . $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'Listusers' ), - $this->msg( 'listgrouprights-members' )->escaped(), + $this->msg( 'listgrouprights-members' )->text(), [], [ 'group' => $group ] ); @@ -165,7 +167,7 @@ class SpecialListGroupRights extends SpecialPage { $this->msg( 'listgrouprights-namespaceprotection-restrictedto' )->text() ) ); - + $linkRenderer = $this->getLinkRenderer(); ksort( $namespaceProtection ); foreach ( $namespaceProtection as $namespace => $rights ) { if ( !in_array( $namespace, MWNamespace::getValidNamespaces() ) ) { @@ -183,9 +185,9 @@ class SpecialListGroupRights extends SpecialPage { Html::rawElement( 'td', [], - Linker::link( + $linkRenderer->makeLink( SpecialPage::getTitleFor( 'Allpages' ), - htmlspecialchars( $namespaceText ), + $namespaceText, [], [ 'namespace' => $namespace ] ) diff --git a/includes/specials/SpecialMIMEsearch.php b/includes/specials/SpecialMIMEsearch.php index 1d02a4fd45..c61609dec5 100644 --- a/includes/specials/SpecialMIMEsearch.php +++ b/includes/specials/SpecialMIMEsearch.php @@ -153,11 +153,12 @@ class MIMEsearchPage extends QueryPage { function formatResult( $skin, $result ) { global $wgContLang; + $linkRenderer = $this->getLinkRenderer(); $nt = Title::makeTitle( $result->namespace, $result->title ); $text = $wgContLang->convert( $nt->getText() ); - $plink = Linker::link( + $plink = $linkRenderer->makeLink( Title::newFromText( $nt->getPrefixedText() ), - htmlspecialchars( $text ) + $text ); $download = Linker::makeMediaLinkObj( $nt, $this->msg( 'download' )->escaped() ); @@ -166,9 +167,9 @@ class MIMEsearchPage extends QueryPage { $bytes = htmlspecialchars( $lang->formatSize( $result->img_size ) ); $dimensions = $this->msg( 'widthheight' )->numParams( $result->img_width, $result->img_height )->escaped(); - $user = Linker::link( + $user = $linkRenderer->makeLink( Title::makeTitle( NS_USER, $result->img_user_text ), - htmlspecialchars( $result->img_user_text ) + $result->img_user_text ); $time = $lang->userTimeAndDate( $result->img_timestamp, $this->getUser() ); diff --git a/includes/specials/SpecialMediaStatistics.php b/includes/specials/SpecialMediaStatistics.php index 7683ad8beb..e11977fda3 100644 --- a/includes/specials/SpecialMediaStatistics.php +++ b/includes/specials/SpecialMediaStatistics.php @@ -174,10 +174,11 @@ class MediaStatisticsPage extends QueryPage { */ protected function outputTableRow( $mime, $count, $bytes ) { $mimeSearch = SpecialPage::getTitleFor( 'MIMEsearch', $mime ); + $linkRenderer = $this->getLinkRenderer(); $row = Html::rawElement( 'td', [], - Linker::link( $mimeSearch, htmlspecialchars( $mime ) ) + $linkRenderer->makeLink( $mimeSearch, $mime ) ); $row .= Html::element( 'td', diff --git a/includes/specials/SpecialMergeHistory.php b/includes/specials/SpecialMergeHistory.php index b916c1fc78..f122db8a37 100644 --- a/includes/specials/SpecialMergeHistory.php +++ b/includes/specials/SpecialMergeHistory.php @@ -277,6 +277,8 @@ class SpecialMergeHistory extends SpecialPage { function formatRevisionRow( $row ) { $rev = new Revision( $row ); + $linkRenderer = $this->getLinkRenderer(); + $stxt = ''; $last = $this->msg( 'last' )->escaped(); @@ -285,9 +287,9 @@ class SpecialMergeHistory extends SpecialPage { $user = $this->getUser(); - $pageLink = Linker::linkKnown( + $pageLink = $linkRenderer->makeKnownLink( $rev->getTitle(), - htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) ), + $this->getLanguage()->userTimeAndDate( $ts, $user ), [], [ 'oldid' => $rev->getId() ] ); @@ -299,9 +301,9 @@ class SpecialMergeHistory extends SpecialPage { if ( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) { $last = $this->msg( 'last' )->escaped(); } elseif ( isset( $this->prevId[$row->rev_id] ) ) { - $last = Linker::linkKnown( + $last = $linkRenderer->makeKnownLink( $rev->getTitle(), - $this->msg( 'last' )->escaped(), + $this->msg( 'last' )->text(), [], [ 'diff' => $row->rev_id, @@ -359,7 +361,9 @@ class SpecialMergeHistory extends SpecialPage { return false; } - $targetLink = Linker::link( + $linkRenderer = $this->getLinkRenderer(); + + $targetLink = $linkRenderer->makeLink( $targetTitle, null, [], diff --git a/includes/specials/SpecialMostcategories.php b/includes/specials/SpecialMostcategories.php index 015701d877..6095412ac7 100644 --- a/includes/specials/SpecialMostcategories.php +++ b/includes/specials/SpecialMostcategories.php @@ -91,10 +91,11 @@ class MostcategoriesPage extends QueryPage { ); } + $linkRenderer = $this->getLinkRenderer(); if ( $this->isCached() ) { - $link = Linker::link( $title ); + $link = $linkRenderer->makeLink( $title ); } else { - $link = Linker::linkKnown( $title ); + $link = $linkRenderer->makeKnownLink( $title ); } $count = $this->msg( 'ncategories' )->numParams( $result->value )->escaped(); diff --git a/includes/specials/SpecialMostinterwikis.php b/includes/specials/SpecialMostinterwikis.php index 3e78352a50..210c4a2808 100644 --- a/includes/specials/SpecialMostinterwikis.php +++ b/includes/specials/SpecialMostinterwikis.php @@ -97,10 +97,11 @@ class MostinterwikisPage extends QueryPage { ); } + $linkRenderer = $this->getLinkRenderer(); if ( $this->isCached() ) { - $link = Linker::link( $title ); + $link = $linkRenderer->makeLink( $title ); } else { - $link = Linker::linkKnown( $title ); + $link = $linkRenderer->makeKnownLink( $title ); } $count = $this->msg( 'ninterwikis' )->numParams( $result->value )->escaped(); diff --git a/includes/specials/SpecialMostlinked.php b/includes/specials/SpecialMostlinked.php index 01eb39e3f6..712574cf14 100644 --- a/includes/specials/SpecialMostlinked.php +++ b/includes/specials/SpecialMostlinked.php @@ -91,7 +91,8 @@ class MostlinkedPage extends QueryPage { function makeWlhLink( $title, $caption ) { $wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedDBkey() ); - return Linker::linkKnown( $wlh, $caption ); + $linkRenderer = $this->getLinkRenderer(); + return $linkRenderer->makeKnownLink( $wlh, $caption ); } /** @@ -115,10 +116,11 @@ class MostlinkedPage extends QueryPage { ); } - $link = Linker::link( $title ); + $linkRenderer = $this->getLinkRenderer(); + $link = $linkRenderer->makeLink( $title ); $wlh = $this->makeWlhLink( $title, - $this->msg( 'nlinks' )->numParams( $result->value )->escaped() + $this->msg( 'nlinks' )->numParams( $result->value )->text() ); return $this->getLanguage()->specialList( $link, $wlh ); diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index 9280b0478f..9f83832e13 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -295,12 +295,12 @@ class SpecialSearch extends SpecialPage { $textStatus = null; if ( $textMatches instanceof Status ) { $textStatus = $textMatches; - $textMatches = null; + $textMatches = $textStatus->getValue(); } // did you mean... suggestions $didYouMeanHtml = ''; - if ( $showSuggestion && $textMatches && !$textStatus ) { + if ( $showSuggestion && $textMatches ) { if ( $textMatches->hasRewrittenQuery() ) { $didYouMeanHtml = $this->getDidYouMeanRewrittenHtml( $term, $textMatches ); } elseif ( $textMatches->hasSuggestion() ) { @@ -360,6 +360,25 @@ class SpecialSearch extends SpecialPage { $out->addHTML( "
    " ); + $hasErrors = $textStatus && $textStatus->getErrors(); + if ( $hasErrors ) { + list( $error, $warning ) = $textStatus->splitByErrorType(); + if ( $error->getErrors() ) { + $out->addHTML( Html::rawElement( + 'div', + [ 'class' => 'errorbox' ], + $error->getHTML( 'search-error' ) + ) ); + } + if ( $warning->getErrors() ) { + $out->addHTML( Html::rawElement( + 'div', + [ 'class' => 'warningbox' ], + $warning->getHTML( 'search-warning' ) + ) ); + } + } + // prev/next links $prevnext = null; if ( $num || $this->offset ) { @@ -388,7 +407,8 @@ class SpecialSearch extends SpecialPage { } $titleMatches->free(); } - if ( $textMatches && !$textStatus ) { + + if ( $textMatches ) { // output appropriate heading if ( $numTextMatches > 0 && $numTitleMatches > 0 ) { $out->addHTML( '
    ' ); @@ -412,22 +432,18 @@ class SpecialSearch extends SpecialPage { $hasOtherResults = $textMatches && $textMatches->hasInterwikiResults( SearchResultSet::INLINE_RESULTS ); - if ( $num === 0 ) { - if ( $textStatus ) { - $out->addHTML( '
    ' . - $textStatus->getMessage( 'search-error' ) . '
    ' ); - } else { - if ( !$this->offset ) { - // If we have an offset the create link was rendered earlier in this function. - // This class needs a good de-spaghettification, but for now this will - // do the job. - $this->showCreateLink( $title, $num, $titleMatches, $textMatches ); - } - $out->wrapWikiMsg( "

    \n$1

    ", - [ $hasOtherResults ? 'search-nonefound-thiswiki' : 'search-nonefound', - wfEscapeWikiText( $term ) - ] ); + // If we have no results and we have not already displayed an error message + if ( $num === 0 && !$hasErrors ) { + if ( !$this->offset ) { + // If we have an offset the create link was rendered earlier in this function. + // This class needs a good de-spaghettification, but for now this will + // do the job. + $this->showCreateLink( $title, $num, $titleMatches, $textMatches ); } + $out->wrapWikiMsg( "

    \n$1

    ", [ + $hasOtherResults ? 'search-nonefound-thiswiki' : 'search-nonefound', + wfEscapeWikiText( $term ) + ] ); } if ( $hasOtherResults ) { diff --git a/includes/specials/SpecialUserrights.php b/includes/specials/SpecialUserrights.php index 6ded6d9dd7..5b4f1f897f 100644 --- a/includes/specials/SpecialUserrights.php +++ b/includes/specials/SpecialUserrights.php @@ -331,7 +331,7 @@ class UserrightsPage extends SpecialPage { * @param bool $writing * @return Status */ - public function fetchUser( $username, $writing ) { + public function fetchUser( $username, $writing = true ) { $parts = explode( $this->getConfig()->get( 'UserrightsInterwikiDelimiter' ), $username ); if ( count( $parts ) < 2 ) { $name = trim( $username ); diff --git a/includes/specials/pagers/CategoryPager.php b/includes/specials/pagers/CategoryPager.php index b78fed89d5..345577d6ea 100644 --- a/includes/specials/pagers/CategoryPager.php +++ b/includes/specials/pagers/CategoryPager.php @@ -52,7 +52,6 @@ class CategoryPager extends AlphabeticPager { return [ 'tables' => [ 'category' ], 'fields' => [ 'cat_title', 'cat_pages' ], - 'conds' => [ 'cat_pages > 0' ], 'options' => [ 'USE INDEX' => 'cat_title' ], ]; } diff --git a/includes/user/User.php b/includes/user/User.php index 82d8806f75..df9dd3e628 100644 --- a/includes/user/User.php +++ b/includes/user/User.php @@ -1003,11 +1003,10 @@ class User implements IDBAccessObject { * able to set their password to this. * * @param string $password Desired password - * @param string $purpose one of 'login', 'create', 'reset' * @return Status * @since 1.23 */ - public function checkPasswordValidity( $password, $purpose = 'login' ) { + public function checkPasswordValidity( $password ) { global $wgPasswordPolicy; $upp = new UserPasswordPolicy( @@ -1024,7 +1023,7 @@ class User implements IDBAccessObject { } if ( $result === false ) { - $status->merge( $upp->checkUserPassword( $this, $password, $purpose ) ); + $status->merge( $upp->checkUserPassword( $this, $password ) ); return $status; } elseif ( $result === true ) { return $status; @@ -1098,20 +1097,6 @@ class User implements IDBAccessObject { return $name; } - /** - * Count the number of edits of a user - * - * @param int $uid User ID to check - * @return int The user's edit count - * - * @deprecated since 1.21 in favour of User::getEditCount - */ - public static function edits( $uid ) { - wfDeprecated( __METHOD__, '1.21' ); - $user = self::newFromId( $uid ); - return $user->getEditCount(); - } - /** * Return a random password. * @@ -5066,13 +5051,27 @@ class User implements IDBAccessObject { /** * Get the description of a given right * + * @since 1.29 * @param string $right Right to query * @return string Localized description of the right */ public static function getRightDescription( $right ) { $key = "right-$right"; $msg = wfMessage( $key ); - return $msg->isBlank() ? $right : $msg->text(); + return $msg->isDisabled() ? $right : $msg->text(); + } + + /** + * Get the name of a given grant + * + * @since 1.29 + * @param string $grant Grant to query + * @return string Localized name of the grant + */ + public static function getGrantName( $grant ) { + $key = "grant-$grant"; + $msg = wfMessage( $key ); + return $msg->isDisabled() ? $grant : $msg->text(); } /** diff --git a/languages/i18n/azb.json b/languages/i18n/azb.json index f1469056f8..f3bbf8352a 100644 --- a/languages/i18n/azb.json +++ b/languages/i18n/azb.json @@ -1962,6 +1962,7 @@ "whatlinkshere-hidelinks": "$1 باغلانتیلاری", "whatlinkshere-hideimages": "فایل باغلانتیلارینی $1", "whatlinkshere-filters": "سۆزگَجلر", + "whatlinkshere-submit": "گئت", "autoblockid": "اوتوماتیک باغلانما #$1", "block": "ایستیفادچینی باغلاما", "unblock": "ایستیفاده‌چی‌نین باغلانماسین گؤتور", diff --git a/languages/i18n/be-tarask.json b/languages/i18n/be-tarask.json index 73e2397807..ba40fdf7d6 100644 --- a/languages/i18n/be-tarask.json +++ b/languages/i18n/be-tarask.json @@ -2629,6 +2629,7 @@ "pageinfo-category-pages": "Колькасьць старонак", "pageinfo-category-subcats": "Колькасьць падкатэгорыяў", "pageinfo-category-files": "Колькасьць файлаў", + "pageinfo-user-id": "Ідэнтыфікатар удзельніка", "markaspatrolleddiff": "Пазначыць як «патруляваную»", "markaspatrolledtext": "Пазначыць гэтую старонку як «патруляваную»", "markaspatrolledtext-file": "Пазначыць гэтую вэрсію файлу як патруляваную", diff --git a/languages/i18n/diq.json b/languages/i18n/diq.json index 110d9b5c5e..fe9e024eb2 100644 --- a/languages/i18n/diq.json +++ b/languages/i18n/diq.json @@ -85,7 +85,7 @@ "tuesday": "Sêşeme", "wednesday": "Çarşeme", "thursday": "Pancşeme", - "friday": "Yene", + "friday": "Êne", "saturday": "Şeme", "sun": "Krê", "mon": "Dış", @@ -170,7 +170,7 @@ "morenotlisted": "Na lista qay kemi ya.", "mypage": "Pele", "mytalk": "Mesac", - "anontalk": "Werênayış", + "anontalk": "Vaten", "navigation": "Pusula", "and": " u", "qbfind": "Bıvêne", @@ -196,7 +196,7 @@ "history_short": "Tarix", "updatedmarker": "cıkewtena mına peyêne ra dıme biyo rocane", "printableversion": "Asayışê çapkerdışi", - "permalink": "Gıreyo daimi", + "permalink": "Gıreyo bêpeyni", "print": "Çap ke", "view": "Bıvêne", "view-foreign": "$1 de bıvêne", @@ -253,19 +253,19 @@ "pool-servererror": "Amordoğa xızmeti ya istifade nëbena $1", "poolcounter-usage-error": "Xırab karyayış:$1", "aboutsite": "Heqa {{SITENAME}} de", - "aboutpage": "Project:Heqa", + "aboutpage": "Proce:Heqa", "copyright": "Zerrekacı $1 bındı not biya.", "copyrightpage": "{{ns:project}}:Heqa telifi", "currentevents": "Hediseyê rocaneyi", "currentevents-url": "Project:Hediseyê rocaneyi", "disclaimers": "Redê mesuliyeti", - "disclaimerpage": "Project:Reddê mesuliyetê bıngey", + "disclaimerpage": "Project:Redê mesulêtê pêroyi", "edithelp": "Peştdariya vurnayışi", "helppage-top-gethelp": "Peşti", "mainpage": "Pela Seri", "mainpage-description": "Pela seri", "policy-url": "Project:Terzê hereketi", - "portal": "Meydanê cemaeti", + "portal": "Portalê cemaeti", "portal-url": "Project:Portalë şëlıgi", "privacy": "Politikaya nımıteyiye", "privacypage": "Project:Xısusiyetê nımıtışi", @@ -291,7 +291,7 @@ "editlink": "bıvurne", "viewsourcelink": "çımey bıvêne", "editsectionhint": "Leteyo ke bıvuriyo: $1", - "toc": "Sernameyê meselan", + "toc": "Tedeestey", "showtoc": "bımocne", "hidetoc": "bınımne", "collapsible-collapse": "Teng kı", @@ -317,14 +317,14 @@ "nstab-main": "Pele", "nstab-user": "Pella karberi", "nstab-media": "Pela medya", - "nstab-special": "Pella xısusi", + "nstab-special": "Pela xısusiye", "nstab-project": "Pela proceyi", "nstab-image": "Dosya", "nstab-mediawiki": "Mesac", "nstab-template": "Şablon", "nstab-help": "Pela peşti", "nstab-category": "Kategoriye", - "mainpage-nstab": "Pera esas", + "mainpage-nstab": "Pela seri", "nosuchaction": "Fealiyeto wınasi çıniyo", "nosuchactiontext": "URL ra kar qebul nêbı.\nŞıma belka URL şaş nuşt, ya zi gıreyi şaş ra ameyi.\nKeyepelê {{SITENAME}} eşkeno xeta eşkera bıkero.", "nosuchspecialpage": "Pella xısusi ya unasin çınya", @@ -367,7 +367,7 @@ "perfcached": "Datay cı ver hazır biye. No semedê ra nıkayin niyo! tewr zaf {{PLURAL:$1|netice|$1 netice}} debêno de", "perfcachedts": "Cêr de malumatê nımıteyi esti, demdê newe kerdışo peyın: $1. Tewr zaf {{PLURAL:$4|netice|$4 neticey cı}} debyayo de", "querypage-no-updates": "Rocanebiyayışê na pele nıka cadayiyê.\nDayiyi tiya nıka newe nêbenê.", - "viewsource": "Çemi bıvin", + "viewsource": "Çımey bıvêne", "viewsource-title": "Cı geyrayışê $1'i bıvin", "actionthrottled": "Kerden peysnaya", "actionthrottledtext": "Riyê tedbirê anti-spami ra, wextê do kılmek de şıma nê fealiyeti nêşkenê zaf zêde bıkerê, şıma ki no hedi viyarna ra.\nÇend deqey ra tepeya reyna bıcerrebnên.", @@ -604,13 +604,13 @@ "summary": "Xulasa:", "subject": "Mewzu:", "minoredit": "No yew vurnayışo werdiyo", - "watchthis": "Ena pele bıewne", - "savearticle": "Peller qeyd kı", + "watchthis": "Bıewni ena perrer", + "savearticle": "Perrer qeyd kı", "savechanges": "Vuryayışa qeyd kerê", "publishpage": "Perer bıhesırne", "publishchanges": "Vurnayışa vıla ke", "preview": "Verqayt", - "showpreview": "Var asani bıvinê", + "showpreview": "Ver asayışi bıvinê", "showdiff": "Vurriyayışa bıasne", "anoneditwarning": "İqaz: Şıma be hesabê xo nêkewtê cı. \nAdresê şımayê IP tarixê vırnayışê na pele de do qeyd bo. Eke şıma [$1 cıkewê] ya zi [$2 hesab vırazê], vurnayışê şıma be zewbina kare ra nameyê şıma rê bar beno.", "anonpreviewwarning": "\"Şıma be hesabê xo nêkewtê cı. Eke qeyd kerê, adresê şımaê IP tarixê vırnayışê na pele de do qeyd bo.\"", @@ -744,7 +744,7 @@ "nohistory": "Verê vurnayışanê na pele çıniyo.", "currentrev": "Çımraviyarnayışo rocane", "currentrev-asof": "$1 ra tepya mewcud weziyeta pela", - "revisionasof": "Verziyonê roca $1ine", + "revisionasof": "Çımraviyarnayışê $1", "revision-info": "Vurnayışo ke $1 de terefê {{GENDER:$6|$2}}$7 ra biyo", "previousrevision": "← Çımraviyarnayışo kıhanêr", "nextrevision": "Rewizyono newên →", @@ -864,13 +864,13 @@ "lineno": "Xeta $1:", "compareselectedversions": "Rewizyonanê weçineyan pêver ke", "showhideselectedversions": "Revizyonanê weçinıtan bımocne/bınımne", - "editundo": "peyser biya", + "editundo": "Peyser bıgêre", "diff-empty": "(Babetna niyo)", "diff-multi-sameuser": "(Terefê eyni karberi ra {{PLURAL:$1|yew revizyono miyanên nêmocno|$1 revizyonê miyanêni nêmocnê}})", "diff-multi-otherusers": "(Terefê {{PLURAL:$2|yew karberi|$2 karberan}} ra {{PLURAL:$1|yew revizyono miyanên nêmocno|$1 revizyonê miyanêni nêmocnê}})", "diff-multi-manyusers": "({{PLURAL:$1|jew timar kerdışo qıckeko|$1 timar kerdışo qıckeko}} timar kerdo, $2 {{PLURAL:$2|Karber|karberi}} memocne)", "difference-missing-revision": "Ferqê {{PLURAL:$2|Yew rewizyonê|$2 rewizyonê}} {{PLURAL:$2|dı|dı}} ($1) sero çıniyo.\n\nNo normal de werênayış dê pelanê besterneyan dı ena xırabin asena.\nDetayê besternayışi [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} tiya dı] aseno.", - "searchresults": "Neticeyê geyrayışi", + "searchresults": "Peyniyê cıgeyrayışi", "searchresults-title": "Qandê \"$1\" neticeyê geyrayışi", "titlematches": "Tekê (zewcê) sernameyê pele", "textmatches": "Tekê (zewcê) nuştey pele", @@ -881,17 +881,17 @@ "next-page": "Pela peyên", "prevn-title": "$1o verên {{PLURAL:$1|netice|neticeyan}}", "nextn-title": "$1o ke yeno {{PLURAL:$1|netice|neticey}}", - "shown-title": "Herg per sero $1 {{PLURAL:$1|netici|netica}} bıasne", + "shown-title": "Her pele sero $1 {{PLURAL:$1|netici|netica}} bımocne", "viewprevnext": "($1 {{int:pipe-separator}} $2) ($3) bıvênên", "searchmenu-exists": "''Ena 'Wikipediya de ser \"[[:$1]]\" yew pel esto'''", "searchmenu-new": "Na wiki de pela \"[[:$1]]\" vıraze! {{PLURAL:$2|0=|Sewbina pela ke şıma geyrayê cı aye bıvênê.|Yew zi neticanê cıgeyrayışê xo bıvênê.}}", - "searchprofile-articles": "Perrê muhteway", + "searchprofile-articles": "Pelê zerreki", "searchprofile-images": "Zafınmedya", "searchprofile-everything": "Pêro çi", "searchprofile-advanced": "Herayen", "searchprofile-articles-tooltip": "$1 de cı geyre", "searchprofile-images-tooltip": "Dosya cı geyre", - "searchprofile-everything-tooltip": "Tedeesteyan hemine cı geyre (pelanê mınaqeşeyi zi tey)", + "searchprofile-everything-tooltip": "Tedeesteyan hemine cı geyre (pelanê werênayışi zi tey)", "searchprofile-advanced-tooltip": "Cayê nameyanê xısusiyan de cı geyre", "search-result-size": "$1 ({{PLURAL:$2|1 çeku|$2 çekuy}})", "search-result-category-size": "{{PLURAL:$1|1 eza|$1 ezayan}} ({{PLURAL:$2|1 kategoriyê bini|$2 kategirayanê binan}}, {{PLURAL:$3|1 dosya|$3 dosyayan}})", @@ -1093,7 +1093,7 @@ "right-bot": "Zey yew kardê otomotiki kar bıvin", "right-nominornewtalk": "Pelanê werênayışan rê vurnayışê qıckeki çıniyê, qutiya mesacanê newiyan bıgurene", "right-apihighlimits": "Persanê API de sinoranê berzêran bıgurene", - "right-writeapi": "İstıfadey APIyê nuştey", + "right-writeapi": "Gurenayışê nuştey API", "right-delete": "Pele bestere", "right-bigdelete": "Pelanê be tarixanê dergan bestere", "right-deletelogentry": "Qeydanê cıkewtışanê xısusiyan bestere û peyser biya", @@ -1223,12 +1223,12 @@ "nchanges": "$1 {{PLURAL:$1|vurnayış|vurnayışi}}", "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|ziyaretê peyêni ra nata}}", "enhancedrc-history": "tarix", - "recentchanges": "Vurriyayışê peyêni", + "recentchanges": "Vuriyayışê peyêni", "recentchanges-legend": "Tercihê vurnayışanê peyênan", "recentchanges-summary": "Wiki sero vurriyayışê peyêni asenê.", "recentchanges-noresult": "Goreyê kriteranê kıfşkerdeyan ra qet yew vurnayış nêvêniya.", "recentchanges-feed-description": "Ena feed dı vurnayişanê tewr peniyan teqip bık.", - "recentchanges-label-newpage": "Enê vurnayışi ra yu pera newi vıraziya ya", + "recentchanges-label-newpage": "Enê vurnayışi yew pela newiye vıraşta.", "recentchanges-label-minor": "No yew vurnayışo werdiyo", "recentchanges-label-bot": "Eno vurnayış terefê yew boti ra vıraziyo", "recentchanges-label-unpatrolled": "Eno vurnayış hewna dewriya nêbiyo", @@ -1254,7 +1254,7 @@ "rcshowhidepatr": "$1 vurnayışê ke dewriya geyrayê", "rcshowhidepatr-show": "Bımocne", "rcshowhidepatr-hide": "Bınımne", - "rcshowhidemine": "vurnayışanê mı $1", + "rcshowhidemine": "vurnayışê mı $1", "rcshowhidemine-show": "Bımocne", "rcshowhidemine-hide": "Bınımne", "rcshowhidecategorization": "kategorizasyonê pele $1", @@ -1273,7 +1273,7 @@ "rc_categories": "Kategoriyan rêz kı ( \"|“ ya ciya yo):", "rc_categories_any": "Weçinayiyan ra her yew", "rc-change-size": "$1", - "rc-change-size-new": "Vurnayışa dıma $1 {{PLURAL:$1|bayt|bayt}}", + "rc-change-size-new": "$1 {{PLURAL:$1|bayt|bayt}} ra dıma vurnayış", "newsectionsummary": "/* $1 */ qısımo newe", "rc-enhanced-expand": "Detaya bıvin (JavaScript lazımo)", "rc-enhanced-hide": "Melumat bınımne", @@ -1664,7 +1664,7 @@ "listusers-desc": "Kemeyen rézed ratn", "usereditcount": "$1 {{PLURAL:$1|vurnayîş|vurnayîşî}}", "usercreated": "$2 de $1 {{GENDER:$3|viraziya}}", - "newpages": "Perrê newey", + "newpages": "Pelê newey", "newpages-submit": "Bımocne", "newpages-username": "Nameyê karberi:", "ancientpages": "Perrê kı rewnayo kı nêvuriya yê", @@ -1894,7 +1894,7 @@ "delete-warning-toobig": "no pel wayirê tarixê vurnayiş ê derg o, $1 {{PLURAL:$1|revizyonê|revizyonê}} seri de.\nhewn a kerdışê ıney {{SITENAME}} şuxul bıne gırano;\nbı diqqet dewam kerê.", "deleteprotected": "Şıma nêşenê ena perer esternê, çıkı per starya ya.", "rollback": "vurnayişan tepiya bıger", - "rollbacklink": "peyser biya", + "rollbacklink": "peyser biyare", "rollbacklinkcount": "$1 {{PLURAL:$1|vurnayış|vurnayışi}} peyd gıroti", "rollbacklinkcount-morethan": "$1 {{PLURAL:$1|vurnayış|vuranyışi}} tewr peyd gırot", "rollbackfailed": "Peyserardış nêbi", @@ -2031,7 +2031,7 @@ "sp-contributions-deleted": "iştırakê {{GENDER:$1|karberi}} esterdi", "sp-contributions-uploads": "Barkerdışi", "sp-contributions-logs": "qeydi", - "sp-contributions-talk": "werênayış", + "sp-contributions-talk": "vaten", "sp-contributions-userrights": "idareyê heqanê karberan", "sp-contributions-blocked-notice": "verniyê no/na karber/e geriyayo/a\nqê referansi qeydê vernigrewtışi cêr de eşkera biyo:", "sp-contributions-blocked-notice-anon": "Eno adresê IPi bloke biyo.\nCıkewtışo tewr peyêno ke bloke biyo, cêr seba referansi belikerdeyo:", @@ -2337,7 +2337,7 @@ "tooltip-pt-createaccount": "Şıma rê tewsiyey ma xorê jew hesab akerê. Fına zi hesab akerdış mecburi niyo.", "tooltip-ca-talk": "Heqa zerrekê pele de werênayış", "tooltip-ca-edit": "Ena pele bıvurne", - "tooltip-ca-addsection": "Zu bınnusteya newi ak", + "tooltip-ca-addsection": "Yew leteyo newe a ke", "tooltip-ca-viewsource": "Ena pele kılit biya.\nŞıma şenê çımeyê aye bıvênê", "tooltip-ca-history": "Versiyonê verênê ena pele", "tooltip-ca-protect": "Ena pele bışevekne", @@ -2373,7 +2373,7 @@ "tooltip-ca-nstab-media": "Pela medya bıvêne", "tooltip-ca-nstab-special": "Na yew pela xasa, şıma nêşenê sero vurnayış bıkerê", "tooltip-ca-nstab-project": "Pela proceyi bıvêne", - "tooltip-ca-nstab-image": "Pera dosyayer bıvin", + "tooltip-ca-nstab-image": "Pela dosya bıvêne", "tooltip-ca-nstab-mediawiki": "Mesacê sistemi bımocne", "tooltip-ca-nstab-template": "Şabloni bıvêne", "tooltip-ca-nstab-help": "Pela peşti bıvêne", @@ -2381,7 +2381,7 @@ "tooltip-minoredit": "Nay vırnayışa werdi nışan bıkeré", "tooltip-save": "Vurnayışanê xo qeyd ke", "tooltip-publish": "Vurnayışê xo vıla kı", - "tooltip-preview": "Vuryayışané xo çım ra ravyarné. Verdé qeyd kerdışi eneri bıkarné!", + "tooltip-preview": "Vurnayışanê xo çım ra bıviyarnê. Qeydkerdış ra ver bıgurê cı!", "tooltip-diff": "Metni sero vurnayışan mocneno", "tooltip-compareselectedversions": "Ena per de ferqê rewziyonan de dı weçinaya bıvinê", "tooltip-watch": "Ena pele lista xoya seyrkerdışi ke", @@ -2392,7 +2392,7 @@ "tooltip-rollback": "\"Peyser biya\" be yew tık pela iştıraqanê peyênan peyser ano", "tooltip-undo": "\"Undo\" ena vurnayışê newi iptal kena u vurnayışê verni a kena.\nTı eşkeno yew sebeb bınus.", "tooltip-preferences-save": "Terciha qeyd ke", - "tooltip-summary": "Yew xulasaya kilm binuse", + "tooltip-summary": "Yew xulasa kılmeke bınuse", "interlanguage-link-title": "$1 - $2", "common.css": "/************************************************\n * COMMON CSS\n *\n * Any CSS placed in this page will be used on \n * all skins, please think carefully about if it\n * belongs here (and not in one of the skin CSS\n * pages) before adding it. Thanks.\n ************************************************/\n\n/* */\ntable.highlighthovertable tr:hover,\ntable.highlighthovertable tr:hover td,\ntable.mw-ext-translate-groupstatistics tr:hover,\ntable.mw-ext-translate-groupstatistics tr:hover td {\n background-color: white;\n}\n\n\n/* Babel wrapper layout. */\n/* XXX: This is either redundant or should be in-core */\n/* @noflip */table.mw-babel-wrapper {\n\twidth: 238px;\n\tfloat: right;\n\tclear: right;\n\tmargin: 1em;\n\tborder-style: solid;\n\tborder-width: 1px;\n\tborder-color: #99B3FF;\n}\n\n/* Babel box layout. */\n/* @noflip */div.mw-babel-box {\n\tfloat: left;\n\tclear: left;\n\tmargin: 1px;\n}\n\ndiv.mw-babel-box table {\n\twidth: 238px;\n}\n\ndiv.mw-babel-box table th {\n\twidth: 238px;\n\twidth: 45px;\n\theight: 45px;\n\tfont-size: 14pt;\n\tfont-family: monospace;\n}\n\ndiv.mw-babel-box table td {\n\tfont-size: 8pt;\n\tpadding: 4pt;\n\tline-height: 1.25em;\n}\n\n/* Babel box colours. */\ndiv.mw-babel-box-0 {\n\tborder: solid #B7B7B7 1px;\n}\n\ndiv.mw-babel-box-1 {\n\tborder: solid #C0C8FF 1px;\n}\n\ndiv.mw-babel-box-2 {\n\tborder: solid #77E0E8 1px;\n}\n\ndiv.mw-babel-box-3 {\n\tborder: solid #99B3FF 1px;\n}\n\ndiv.mw-babel-box-4 {\n\tborder: solid #CCCC00 1px;\n}\n\ndiv.mw-babel-box-5 {\n\tborder: solid #F99C99 1px;\n}\n\ndiv.mw-babel-box-N {\n\tborder: solid #6EF7A7 1px;\n}\n\ndiv.mw-babel-box-0 table th {\n\tbackground-color: #B7B7B7;\n}\n\ndiv.mw-babel-box-1 table th {\n\tbackground-color: #C0C8FF;\n}\n\ndiv.mw-babel-box-2 table th {\n\tbackground-color: #77E0E8;\n}\n\ndiv.mw-babel-box-3 table th {\n\tbackground-color: #99B3FF;\n}\n\ndiv.mw-babel-box-4 table th {\n\tbackground-color: #CCCC00;\n}\n\ndiv.mw-babel-box-5 table th {\n\tbackground-color: #F99C99;\n}\n\ndiv.mw-babel-box-N table th{\n\tbackground-color: #6EF7A7;\n}\n\ndiv.mw-babel-box-0 table {\n\tbackground-color: #E8E8E8;\n}\n\ndiv.mw-babel-box-1 table {\n\tbackground-color: #F0F8FF;\n}\n\ndiv.mw-babel-box-2 table {\n\tbackground-color: #D0F8FF;\n}\n\ndiv.mw-babel-box-3 table {\n\tbackground-color: #E0E8FF;\n}\n\ndiv.mw-babel-box-4 table {\n\tbackground-color: #FFFF99;\n}\n\ndiv.mw-babel-box-5 table {\n\tbackground-color: #F9CBC9;\n}\n\ndiv.mw-babel-box-N table {\n\tbackground-color: #C5FCDC;\n}\n\n.babel-box td.babel-footer {\n\ttext-align: center;\n}\n\n/* Styling for portals. */\ndiv.table {\n display: table;\n vertical-align: top;\n width: 100%;\n}\n\ndiv.table-row {\n display: table-row;\n vertical-align: top;\n}\n\ndiv.table-cell {\n display: table-cell;\n vertical-align: top;\n}\n\nbody.ns-100 table.mw-babel-wrapper {\n border: solid 1px #bbbbbb;\n background-color: #f0f0f0;\n margin-left: 1em;\n}\n\n.graytext {\n color: #aaa;\n}\n\n/* On [[Special:RecentChanges]] and [[Special:Watchlist]] make the new pages symbol bold green and the minor edit symbol gray. */\n.newpage {\n color: green;\n font-weight: bold\n}\n\n.minoredit,\n.minor {\n color: gray;\n}\n\n/* Monospace diffs, this makes more sense since diffs show what would be seen in the edit box. */\n/* Note: Anno 2012 many browsers don't use monospace in the textarea anymore by default, notably Chrome and Safari don't (unless the user overrides this in the preferences) */\n.diff-context,\n.diff-deletedline,\n.diff-addedline {\n font-family: monospace, \"Courier New\";\n/* Just guess does the stupid wikidiff2 extensions add extra whitespace around..... */\n white-space: -moz-pre-wrap;\n white-space: pre-wrap;\n}\n \n.diffchange {\n border: 1px dotted rgb( 170, 170, 170 );\n}\n\n/* It is unclear what the following CSS does, please add comments if you can clarify. */\n/* The box which is 400px high and if its content is longer, it gets the scrollbar */\n.scrollme {\n overflow: scroll;\n width: 100%;\n height: 400px;\n}\n\n/* Standard Navigationsleisten, aka box hiding thingy from .de. Documentation at [[Wikipedia:NavFrame]]. */\ndiv.Boxmerge, div.NavFrame { margin: 0; padding: 4px; border-collapse: collapse;}\ndiv.Boxmerge div.NavFrame { border-style: none; border-style: hidden; }\ndiv.NavFrame + div.NavFrame { border-top-style: none; border-top-style: hidden; }\ndiv.NavFrame div.NavHead { height: 1.6em; position:relative; }\ndiv.NavEnd { margin: 0; padding: 0; line-height: 1px; clear: both; }\na.NavToggle { position: absolute; top: 0; right: 5px; }\n.note-flaggedrevs * a.NavToggle { right: 12px; } /* For [[Template:Flagged Revs]] */\n\n/* Template:Languages */\n.bw-languages {\n border: 1px solid #aaaaaa;\n padding: 0.2em;\n border-collapse: collapse;\n line-height: 1.2;\n font-size: 95%;\n margin: 1px 1px;\n}\n.bw-languages-title {\n width: 180px;\n border: 1px solid #aaaaaa;\n background: #EEF3E2;\n padding: 0.5em;\n font-weight: bold;\n}\n.bw-languages-links { padding:0.5em; background:#F6F9ED; }\n\n/* Senseless in this project */\n#editpage-copywarn { display: none; }\n\n/* Hide warnings about bad links on MediaWiki:Common.css */\n.page-MediaWiki_Common_css .mw-translate-messagechecks { display: none; }\n\n/*******************\n** Faciliate RTL translation\n*******************/\n/* @noflip */\n#bodyContent .arabic a {\n\tpadding-right:0;\n\tbackground:none;\n}\n\n.vatop tr, tr.vatop, .vatop td, .vatop th {\n vertical-align: top;\n}\n\n.bw-languages {\n direction: ltr;\n}\n\n/* prevent wrapping of lines in LQT TOC if not necessary */\ntable.lqt_toc {\n\twidth: auto;\n}\n\n/* [[m:MediaZilla:35337]] */\n@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 1.5dppx) {\n #p-logo a {\n background-image: url(\"//translatewiki.net/images/thumb/7/7c/Translatewiki-logo-bare.svg/152px-Translatewiki-logo-bare.svg.png\") !important;\n background-size: auto 135px;\n }\n}\n@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {\n #p-logo a {\n background-image: url(\"//translatewiki.net/images/thumb/7/7c/Translatewiki-logo-bare.svg/202px-Translatewiki-logo-bare.svg.png\") !important;\n background-size: auto 135px;\n }\n}\n\n/* qqq visibility, [[Thread:Support/Suggestion: Add this CSS to MediaWiki:Common.css]] */\n \n.mw-sp-translate-edit-info .mw-content-ltr {\n background-position:left center;\n padding-left:45px;\n}\nfieldset.mw-sp-translate-edit-info .mw-centent-rtl {\n background-position:right center;\n padding-right:45px;\n}\n\n/* Semantic MediaWiki - make special properties easier to identify */\n\n.smwbuiltin a,\n.smwbuiltin a.new {\n\tcolor: #FF8000;\n}\n\n/* Recentchangestext toggle link */\n.white-link a {\n color: #fff;\n}", "common.js": "/* Any JavaScript here will be loaded for all users on every page load. */", @@ -2491,14 +2491,14 @@ "widthheight": "$1 - $2", "widthheightpage": "$1 × $2, $3 {{PLURAL:$3|pele|peli}}", "file-info": "ebatê dosyayi: $1, MIME tip: $2", - "file-info-size": "$1 × $2 pixelan, ebatê dosya: $3, MIME type: $4", + "file-info-size": "$1 × $2 pikselan, ebatê dosya: $3, MIME tipê cı: $4", "file-info-size-pages": "$1 × $2 pikse, dergeya dosyay: $3, MIME tipiya cı: $4, $5 {{PLURAL:$5|pela|pela}}", "file-nohires": "Deha berz agozney cı çıniyo", "svg-long-desc": "Dosyay SVG, zek vanê $1 × $2 piksela, ebatê dosya: $3", "svg-long-desc-animated": "SVG dosya, nominalin $1 × $2 piksela, ebatê dosya: $3", "svg-long-error": "Nêmeqbul dosyaya SVG'i: $1", "show-big-image": "Dosyaya oricinale", - "show-big-image-preview": "Verqaytê dergiya: $1.", + "show-big-image-preview": "Vervênayışê ebatê : $1.", "show-big-image-other": "Zewmi{{PLURAL:$2|Vılêşnayış|Vılêşnayışê}}: $1.", "show-big-image-size": "$1 × $2 piksel", "file-info-gif-looped": "viyariye biyo", @@ -3184,7 +3184,7 @@ "fileduplicatesearch-result-1": "Dosyayê ''$1î'' de hem-kopya çini yo.", "fileduplicatesearch-result-n": "Dosyayê ''$1î'' de {{PLURAL:$2|1 hem-kopya|$2 hem-kopyayî'}} esto.", "fileduplicatesearch-noresults": "Ebe namey \"$1\" ra dosya nêdiyayê.", - "specialpages": "Pellê xısusiy", + "specialpages": "Perrê Hısusi", "specialpages-note-top": "Kıtabek", "specialpages-note": "* Pelê xasê normali.\n* Pelê xasê nımıtey.", "specialpages-group-maintenance": "Raporê pawıtışi", diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 3a8a7ae6b2..afd13f0016 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -1018,6 +1018,7 @@ "searchdisabled": "{{SITENAME}} search is disabled.\nYou can search via Google in the meantime.\nNote that their indexes of {{SITENAME}} content may be out of date.", "googlesearch": "\n\t\n\t\n\t\n\t\n\n\t\n\t\n
    \n\t\n\t\n
    \n", "search-error": "An error has occurred while searching: $1", + "search-warning": "A warning has occured while searching: $1", "opensearch-desc": "{{SITENAME}} ({{CONTENTLANGUAGE}})", "preferences": "Preferences", "preferences-summary": "", @@ -2023,6 +2024,7 @@ "listgrants-summary": "The following is a list of grants with their associated access to user rights. Users can authorize applications to use their account, but with limited permissions based on the grants the user gave to the application. An application acting on behalf of a user cannot actually use rights that the user does not have however.\nThere may be [[{{MediaWiki:Listgrouprights-helppage}}|additional information]] about individual rights.", "listgrants-grant": "Grant", "listgrants-rights": "Rights", + "listgrants-grant-display": "$1 ($2)", "trackingcategories": "Tracking categories", "trackingcategories-summary": "This page lists tracking categories which are automatically populated by the MediaWiki software. Their names can be changed by altering the relevant system messages in the {{ns:8}} namespace.", "trackingcategories-msg": "Tracking category", diff --git a/languages/i18n/fi.json b/languages/i18n/fi.json index 8cf7922f25..0ebbd472df 100644 --- a/languages/i18n/fi.json +++ b/languages/i18n/fi.json @@ -1127,12 +1127,9 @@ "userrights-reason": "Syy:", "userrights-no-interwiki": "Sinulla ei ole oikeutta muokata käyttöoikeuksia muissa wikeissä.", "userrights-nodatabase": "Tietokantaa $1 ei ole tai se ei ole paikallinen.", - "userrights-nologin": "Sinun täytyy [[Special:UserLogin|kirjautua sisään]] ylläpitäjän tunnuksella, jotta voisit muuttaa käyttöoikeuksia.", - "userrights-notallowed": "Sinulla ei ole oikeutta lisätä tai poistaa käyttäjien oikeuksia.", "userrights-changeable-col": "Ryhmät, joita voit muuttaa", "userrights-unchangeable-col": "Ryhmät, joita et voi muuttaa", "userrights-conflict": "Päällekkäinen käyttöoikeuksien muutos! Tarkista tekemäsi muutokset ja vahvista ne.", - "userrights-removed-self": "Poistit omat oikeutesi. Tämän vuoksi sinulla ei enää ole oikeutta päästä tälle sivulle.", "group": "Ryhmä", "group-user": "käyttäjät", "group-autoconfirmed": "automaattisesti hyväksytyt käyttäjät", @@ -1840,6 +1837,8 @@ "apisandbox-alert-field": "Tässä kentässä oleva arvo ei ole kelvollinen.", "apisandbox-continue": "Jatka", "apisandbox-continue-clear": "Tyhjennä", + "apisandbox-multivalue-all-namespaces": "$1 (Kaikki nimiavaruudet)", + "apisandbox-multivalue-all-values": "$1 (Kaikki arvot)", "booksources": "Kirjalähteet", "booksources-search-legend": "Etsi kirjalähteitä", "booksources-isbn": "ISBN", @@ -2069,7 +2068,7 @@ "changecontentmodel-legend": "Muuta sisältömallia", "changecontentmodel-title-label": "Sivun otsikko", "changecontentmodel-model-label": "Uusi sisältömalli", - "changecontentmodel-reason-label": "Syy:", + "changecontentmodel-reason-label": "Syy", "changecontentmodel-submit": "Tee muutos", "changecontentmodel-success-title": "Sisältömallia on muutettu", "changecontentmodel-success-text": "Sisältötyyppiä kohteessa [[:$1]] on muutettu.", @@ -3696,6 +3695,7 @@ "mw-widgets-dateinput-placeholder-month": "VVVV-KK", "mw-widgets-titleinput-description-new-page": "sivua ei ole olemassa vielä", "mw-widgets-titleinput-description-redirect": "ohjaus kohteeseen $1", + "mw-widgets-categoryselector-add-category-placeholder": "Lisää luokka...", "sessionmanager-tie": "!!FYZZ!!Cannot combine multiple request authentication types: $1.", "sessionprovider-generic": "$1 istuntoa", "sessionprovider-mediawiki-session-cookiesessionprovider": "istuntoja, joissa on evästeet käytössä", @@ -3820,7 +3820,5 @@ "unlinkaccounts-success": "Tunnuksen linkitys poistettiin.", "authenticationdatachange-ignored": "Varmennustietojen muutosta ei käsitelty. Ehkä palveluntarjoajaa ei määritelty?", "restrictionsfield-badip": "Virheellinen IP-osoite tai alue: $1", - "restrictionsfield-label": "Sallitut IP-alueet:", - "edit-error-short": "$1", - "edit-error-long": "Virheet:\n\n$1" + "restrictionsfield-label": "Sallitut IP-alueet:" } diff --git a/languages/i18n/hy.json b/languages/i18n/hy.json index ea3fd1e6f8..a4120c9430 100644 --- a/languages/i18n/hy.json +++ b/languages/i18n/hy.json @@ -354,7 +354,7 @@ "cannotdelete": "Չհաջողվեց ջնջել «$1» էջը կամ ֆայլը։\nՀավանաբար այն արդեն ջնջվել է մեկ այլ մասնակցի կողմից։", "cannotdelete-title": "Հնարավոր չէ ջնջել $1 էջը", "delete-hook-aborted": "Խմբագրել չեղյալ է.\nԼրացուցիչ պարզաբանումներ չի դրվել.", - "no-null-revision": "Չի հաջողվել ստեղծել նոր զրոյական правку համար էջը \"$1\"", + "no-null-revision": "Չի հաջողվել ստեղծել նոր զրոյական խմբագրում էջի համար \"$1\"", "badtitle": "Անընդունելի անվանում", "badtitletext": "Հարցված էջի անվանումը անընդունելի է, դատարկ է կամ սխալ միջ-լեզվական կամ ինտերվիքի անվանում է։ Հնարավոր է, որ այն պարունակում է անթույլատրելի սիմվոլներ։", "title-invalid-empty": "Էջի հայցվող վերնագիրը դատարկ է կամ պարունակում է միայն անվանատարածքի անունը։", diff --git a/languages/i18n/mr.json b/languages/i18n/mr.json index 6ef89e66cf..1596eb6236 100644 --- a/languages/i18n/mr.json +++ b/languages/i18n/mr.json @@ -206,7 +206,7 @@ "namespaces": "नामविश्वे", "variants": "चले(व्हेरियंट्स)", "navigation-heading": "दिक्चालन यादी", - "errorpagetitle": "चूक", + "errorpagetitle": "त्रुटी", "returnto": "$1 कडे परत चला.", "tagline": "{{SITENAME}} कडून", "help": "साहाय्य", @@ -1809,6 +1809,7 @@ "listgrants-rights": "अधिकार", "trackingcategories": "मागोवा घेणारे वर्ग", "trackingcategories-summary": "या पानात ते रेखापथनातील वर्ग(tracking categories) आहेत, जे, मिडियाविकि संचेतनाद्वारे स्वयंचलितरित्या वसविण्यात (तयार करण्यात) आले आहेत. त्यांची नावे, {{ns:8}} नामविश्वातील संबंधित प्रणाली संदेशात फेरफार करुन, बदलविता येतात.", + "trackingcategories-msg": "मागोवा घेणारा वर्ग", "trackingcategories-name": "संदेश नाम", "trackingcategories-desc": "वर्ग अंतर्भूत करण्याचे निकष", "trackingcategories-nodesc": "वर्णन उपलब्ध नाही.", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index d175c86fb1..936fd8b8ac 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -1202,6 +1202,7 @@ "searchdisabled": "{{doc-singularthey}}\nIn this sentence, \"their indexes\" refers to \"Google's indexes\".\n\nShown on [[Special:Search]] when the internal search is disabled.", "googlesearch": "{{notranslate}}\nShown when [[mw:Manual:$wgDisableTextSearch|$wgDisableTextSearch]] is set to true and no [[mw:Manual:$wgSearchForwardUrl|$wgSearchForwardUrl]] is set.\n\nParameters:\n* $1 - the search term\n* $2 - \"UTF-8\" (hard-coded)\n* $3 - the message {{msg-mw|Searchbutton}}", "search-error": "Shown when an error has occurred when performing a search. Parameters:\n* $1 - the localized error that was returned", + "search-warning": "Shown when a warning has occured when performing a search. Parameters:\n* $1 - the localized warning that was returned.", "opensearch-desc": "{{ignored}}Link description of the [www.opensearch.org/ OpenSearch] link in the HTML head of pages.", "preferences": "Title of the [[Special:Preferences]] page.\n{{Identical|Preferences}}", "preferences-summary": "{{doc-specialpagesummary|preferences}}", @@ -2207,6 +2208,7 @@ "listgrants-summary": "Explanatory text shown at the top of the grant/rights mapping table.\n\nRefers to {{msg-mw|Listgrouprights-helppage}}.", "listgrants-grant": "Used as table header for the grant/rights mapping table.\n{{Identical|Grant}}", "listgrants-rights": "Used as table header for the grant/rights mapping table.\n{{Identical|Right}}", + "listgrants-grant-display": "{{optional}}\nUsed to display the code name of a grant next to the grant. Parameters:\n* $1 - the text from the \"grant-...\" messages, i.e. {{msg-mw|Grant-highvolume}}\n* $2 - the codename of this grant", "trackingcategories": "[[Special:TrackingCategories]] page implementing list of Tracking categories [[mw:Special:MyLanguage/Help:Tracking categories|tracking category]].\n{{Identical|Tracking category}}", "trackingcategories-summary": "Description for [[Special:TrackingCategories]] page [[mw:Help:Tracking categories|tracking category]]", "trackingcategories-msg": "Header for the message column of the table on [[Special:TrackingCategories]]. This column lists the mediawiki message that controls the tracking category in question.\n{{Identical|Tracking category}}", diff --git a/languages/i18n/roa-tara.json b/languages/i18n/roa-tara.json index deeece5c76..807ced9781 100644 --- a/languages/i18n/roa-tara.json +++ b/languages/i18n/roa-tara.json @@ -375,7 +375,7 @@ "mypreferencesprotected": "Non ge tìne le permesse pe cangià le preferenze tune.", "ns-specialprotected": "Le pàgene speciale no ponne essere cangete.", "titleprotected": "Stu titele ha state prutette da 'a ccreazione da [[User:$1|$1]].\n'U mutive jè $2.", - "filereadonlyerror": "Non ge pozze cangià 'u file \"$1\" purcé l'archivije de le file \"$2\" ste in mode sola letture.\n\nL'amministratore ca l'ha bloccate dèje sta spiegazione: \"$3\".", + "filereadonlyerror": "Non ge pozze cangià 'u file \"$1\" purcé l'archivije de le file \"$2\" ste in sola letture.\n\nL'amministratore d'u sisteme ca l'ave bloccate dèje sta spiegazione: \"$3\".", "invalidtitle-knownnamespace": "Titole invalide cu 'u namespace \"$2\" e teste \"$3\"", "invalidtitle-unknownnamespace": "Titele invalide cu numere de namespace scanusciute $1 e teste \"$2\"", "exception-nologin": "Non ge sì collegate", @@ -451,7 +451,7 @@ "noname": "Non gìè specifichete 'nu nome utende valide.", "loginsuccesstitle": "Tutte a poste, è trasute!", "loginsuccess": "'''Mò tu si colleghete jndr'à {{SITENAME}} cumme \"$1\".'''", - "nosuchuser": "Non g'esiste n'utende cu 'u nome \"$1\".\nFà attenzione ca le nome de l'utinde so senzibbele a le lettere granne e piccenne.\nVide bbuene a cumme l'è scritte, o [[Special:CreateAccount|ccreje n'utende nuève]].", + "nosuchuser": "Non g'esiste n'utende cu 'u nome \"$1\".\nLe nome de l'utinde so senzibbele a le lettere granne e piccenne.\nVide bbuene a cumme l'è scritte, o [[Special:CreateAccount|ccreje n'utende nuève]].", "nosuchusershort": "Non ge ste nisciune utende cu 'u nome \"$1\".\nCondrolle accume l'è scritte.", "nouserspecified": "A scrivere pe forze 'u nome de l'utende.", "login-userblocked": "Stu utende jè bloccate. Non ge puè trasè.", @@ -467,7 +467,7 @@ "noemail": "Non ge stonne email reggistrete pe l'utende \"$1\".", "noemailcreate": "Tu ha mèttere 'n'indirizze e-mail valide", "passwordsent": "'Na nova passuord ha state mannete a l'indirizze e-mail reggistrete pe \"$1\".\nPe piacere, colleghete n'otra vota quanne l'è ricevute.", - "blocked-mailpassword": "L'indirizze IP tue jè blocchete pe le cangiaminde e accussì tu non ge puè ausà 'a funzione de recupere d'a password pe prevenìe l'abbuse.", + "blocked-mailpassword": "L'indirizze IP tune jè bloccate pe le cangiaminde. Tu non ge puè ausà 'a funzione de recupere d'a password pe prevenìe l'abbuse.", "eauthentsent": "'N'e-mail de conferme ha state mannate a l'indirizze ca tu è ditte.\nApprime ca otre e-mail avènene mannate a 'u cunde tune, tu ha seguì le 'struzione ca stonne jndr'à l'e-mail, pe confermà ca 'u cunde jè une de le tune.", "throttled-mailpassword": "'Nu arrecordatore de passuord ha stete già mannate jndr'à {{PLURAL:$1|l'urtema ore|l'urteme $1 ore}}.\nPe prevenì l'abbuse, sulamende 'nu arrecordatore de passuord avene mannate ogne {{PLURAL:$1|ore|$1 ore}}.", "mailerror": "Errore mannanne 'a mail: $1", @@ -525,8 +525,6 @@ "passwordreset-emaildisabled": "Le funziune de l'email onne state disabbilitate sus a sta uicchi.", "passwordreset-username": "Nome utende:", "passwordreset-domain": "Dominie:", - "passwordreset-capture": "Vide 'a mail resultande?", - "passwordreset-capture-help": "Ce tu signe sta sckatele, 'a mail (cu 'a passuord temboranèe) t'avène fatte vedè cumme adda essere mannate a l'utende.", "passwordreset-email": "Indirizze e-mail:", "passwordreset-emailtitle": "Dettaglie d'u cunde utende sus a {{SITENAME}}", "passwordreset-emailtext-ip": "Quacchedune (pò essere tu, da 'u 'ndirizze IP $1) ha richieste 'na mail pe arrecurdarse de le dettaglie d'u cunde sue pe {{SITENAME}} ($4). {{PLURAL:$3|'U cunde utende seguende jè|le cunde utinde seguende sonde}} associate cu st'indirizze e-mail:\n\n$2\n\n{{PLURAL:$3|Sta passuord temboranèe scade|Ste passuord temboranèe scadene}} 'mbrà {{PLURAL:$5|'nu sciurne|$5 sciurne}}.\nTu avissa trasè e scacchià 'na passuord nova. Ce quacchedun'otre ha fatte sta richieste, o ce tu t'è arrecurdate 'a passuord origgenale toje, e non g'a vuè ccu cange cchiù, tu puè ignorà stu messagge e condinuà ausanne 'a passuord vecchie.", @@ -1021,13 +1019,10 @@ "userrights-reason": "Mutive:", "userrights-no-interwiki": "Tu non ge tìne le permesse pe cangià le deritte utende sus a l'otre uicchi.", "userrights-nodatabase": "'U Database $1 non g'esiste o non g'è lochele.", - "userrights-nologin": "Tu à essere [[Special:UserLogin|colleghete]] cu 'nu cunde utende d'amministratore pe assignà le deritte utende.", - "userrights-notallowed": "Non ge tìne le permesse pe aggiungere o luà le deritte a le utinde.", "userrights-changeable-col": "Gruppe ca tu puè cangià", "userrights-unchangeable-col": "Gruppe ca tu non ge puè cangià", "userrights-irreversible-marker": "$1*", "userrights-conflict": "Conflitte sus a le cangiaminde de le deritte utende! Pe piacere revide e conferme le cangiaminde tune.", - "userrights-removed-self": "T'è luate le deritte tune. Mò non ge puè cchiù trasè jndr'à sta pàgene.", "group": "Gruppe:", "group-user": "Utinde", "group-autoconfirmed": "Utinde auto confermatarije", @@ -1117,7 +1112,6 @@ "right-siteadmin": "Blocche e sblocche 'u database", "right-override-export-depth": "L'esportazione de pàggene inglude pàggene collegate 'mbonde a 'na profonnetà de 5", "right-sendemail": "Manne 'a mail a otre utinde", - "right-passwordreset": "Vide l'e-mail de azzeramende d'a passuord", "right-managechangetags": "CCreje e scangìlle [[Special:Tags|tag]] da 'u database", "right-applychangetags": "Appleche [[Special:Tags|tag]] sus a 'u de le cangiaminde tune", "right-changetags": "Aggiunge e live arbitrariamende [[Special:Tags|tag]] sus a le revisiune individuale e vôsce de l'archivije", diff --git a/languages/i18n/sah.json b/languages/i18n/sah.json index 6e45545aea..66e53af262 100644 --- a/languages/i18n/sah.json +++ b/languages/i18n/sah.json @@ -1809,6 +1809,10 @@ "apisandbox-alert-field": "Хонуу суолтата алҕастаах.", "apisandbox-continue": "Салгыы", "apisandbox-continue-clear": "Сот", + "apisandbox-continue-help": "{{int:apisandbox-continue}} бүтэһик көрдөбүлү [https://www.mediawiki.org/wiki/API:Query#Continuing_queries салгыаҕа]; {{int:apisandbox-continue-clear}} салҕааһыны кытта ситимнээх туруоруулары ырастыа.", + "apisandbox-param-limit": "Муҥутуур болдьох муҥутуурдук туттулларын туоруор.", + "apisandbox-multivalue-all-namespaces": "$1 (Аат даллара барыта)", + "apisandbox-multivalue-all-values": "$1 (Бары суолталара)", "booksources": "Кинигэлэр источниктара", "booksources-search-legend": "Кинигэ туһунан көрдөө", "booksources-search": "Бул", diff --git a/languages/i18n/udm.json b/languages/i18n/udm.json index 3a5a537d15..75b503cc1e 100644 --- a/languages/i18n/udm.json +++ b/languages/i18n/udm.json @@ -257,6 +257,7 @@ "databaseerror-query": "Курон: $1", "databaseerror-function": "Функция: $1", "databaseerror-error": "Янгыш: $1", + "badtitle": "Умойтэм ним", "badtitletext": "Курем бам ним луэ мыдлань, буш либо кылъёс куспын яке викиос куспын нимыз умойтэм герӟамын.\nНимын, вылды, ярантэм символъёс вань.", "viewsource": "Кодзэ учкыны", "viewsource-title": "Кодзэ учкыны бам $1", @@ -278,6 +279,7 @@ "createacct-another-username-ph": "Учётной книга нимъёс пыртэмын", "yourpassword": "Лушкемкыл:", "userlogin-yourpassword": "Лушкемкыл", + "userlogin-yourpassword-ph": "Гожтэ асьтэлэсь парольдэс", "createacct-yourpassword-ph": "Гожтэ паролез", "createacct-yourpasswordagain": "Пароль юнматэ", "createacct-yourpasswordagain-ph": "Гожтэ паролез эшшо одӥг пол", @@ -291,6 +293,7 @@ "logout": "Кошкыны", "userlogout": "Потыны", "notloggedin": "Тон эн тусбуяськыны сӧзнэтэз", + "userlogin-noaccount": "Ас учётной записьты ӧвӧл?", "nologin": "Учётной книга ӧвӧл-а? $1.", "nologinlink": "Выль вики-авторлэн регистрациез", "createaccount": "выль вики-авторлэн регистрациез", @@ -304,6 +307,9 @@ "createacct-submit": "Выль вики-авторлэн регистрациез", "createacct-another-submit": "Выль вики-авторлэн регистрациез", "createacct-benefit-heading": "{{SITENAME}} — тӥ выллем адямиослэн валче ужамзы.", + "createacct-benefit-body1": "{{PLURAL:$1|тупатон}}", + "createacct-benefit-body2": "{{PLURAL:$1|бам}}", + "createacct-benefit-body3": "{{PLURAL:$1|викиавтор}} берло дыре", "loginerror": "Янгышъёс пырон", "createacct-error": "Янгышъёс бордын учётной книга кылдытыны", "createaccounterror": "Уг быгатиськы гожъян учётной кылдоз: $1", @@ -356,8 +362,10 @@ "editing": "Тупатон: $1", "creating": "«$1» бамез кылдытон", "editingsection": "Тупатон: $1 (люкет)", + "templatesused": "Та бам пушкы пыртэм {{PLURAL:$1|шаблон|шаблонъёс}}:", "template-protected": "(утемын)", "template-semiprotected": "(полуутемын)", + "hiddencategories": "Та бам пыре {{PLURAL:$1|$1 ватэм категорие}}:", "nocreatetext": "Та сайтлэн бамаз выль сюбегатэм луонлыкъёсын кылдытон.\nТон улыса, берлань вуэ быгатэ бам отредактировать, [[Special:UserLogin|тусбуяськыны книгае яке выль система кылдыто учётной]].", "nocreate-loggedin": "Тон доразы юаськыны кылдӥз выль бам ӧвӧл.", "permissionserrors": "Янгышъёс юаське", @@ -368,8 +376,12 @@ "cantcreateaccount-text": "Та книгаез кылдытонлы учётной IP-адрес ($1) заблокировать луизы [[User:$3|$3]].\n\nМугез, вайиз $3 возьматэ $2", "cantcreateaccount-range-text": "Учётной кылдытон - гожъян IP-адрес диапазонын $1, Тон пыриське со IP-адрес ($4), заблокировать луизы [[User:$3|$3]].\n\nМугез, вайиз $3 возьматэ $2", "viewpagelogs": "Та бамлы журналъёсыз возьматыны", + "currentrev-asof": "Алиез версия $1", "revisionasof": "Версия $1", + "revision-info": "Версия $1; {{GENDER:$6|$2}}$7", "previousrevision": "← Вужгем", + "nextrevision": "Выльгем →", + "currentrevisionlink": "Алиез версия", "cur": "али", "last": "азьв.", "history-show-deleted": "Ӵушылэмъёссэ гинэ", @@ -380,6 +392,7 @@ "revdelete-radio-unset": "Адӟымон", "revdelete-reason-dropdown": "*Вӧлскем палэнскон мугъёсты\n** Авторской правоосты тӥян\n** Яке кулэтэм информациез личной комментарий\n** Логин несоответствовать\n** Курла информациез Потенциально", "history-title": "$1 — воштонъёслэн историзы", + "difference-title": "$1 — версиосыз куспын пӧртэмлык", "lineno": "$1-тӥ чур:", "compareselectedversions": "Быръем версиосыз ӵошатыны", "showhideselectedversions": "Возьматыны/ватыны быръем версиосыз", @@ -401,6 +414,7 @@ "search-result-size": "$1 ({{PLURAL:$2|$2 кыл}})", "search-redirect": "($1 бамысь ыстон)", "search-section": "(«$1» люкет)", + "search-suggest": "Тӥ, вылды, утчаллямды «$1».", "search-interwiki-more": "(эшшо)", "searchall": "Ваньзэ", "search-showingresults": "{{PLURAL:$4|$3 пӧлысь $1-тӥ шедьтэм|$3 пӧлысь $1—$2 шедьтэмъёс}}", @@ -447,6 +461,7 @@ "rcshowhideminor-hide": "Ватыны", "rcshowhidebots": "$1 ботъёсыз", "rcshowhidebots-show": "Возьматыны", + "rcshowhidebots-hide": "Ватыны", "rcshowhideliu": "$1 пырем викиавторъёсыз", "rcshowhideliu-show": "Возьматыны", "rcshowhideliu-hide": "Ватыны", @@ -486,6 +501,7 @@ "upload-dialog-button-cancel": "Берытсконо", "license-header": "Лицензия", "nolicense": "Ӧвӧл", + "imgfile": "файл", "file-anchor-link": "Файл", "filehist": "Файллэн историез", "filehist-help": "Зӥбе дата/дыр шоры, кызьы файл со дырын адӟиськемез учкыны вылысь.", @@ -504,11 +520,15 @@ "randompage": "Олокыӵе статья", "withoutinterwiki-submit": "Возьматыны", "nbytes": "{{PLURAL:$1|$1 байт}}", + "nmembers": "$1 {{PLURAL:$1|объект}}", "prefixindex-submit": "Возьматыны", "newpages": "Выль бамъёс", "newpages-submit": "Возьматыны", "move": "Нимзэ воштыны", + "pager-older-n": "{{PLURAL:$1|вужгес $1}}", "booksources": "Книгаосын источникъёс", + "booksources-search-legend": "Книга сярысь информациез утчан", + "booksources-search": "Утчаны", "log": "Журналъёс", "logeventslist-submit": "Возьматыны", "showhideselectedlogentries": "Возьматыны/ватыны быръем журналъёсысь гожъямъёсыз", @@ -516,7 +536,9 @@ "checkbox-all": "Ваньзэ", "checkbox-none": "Номыре", "checkbox-invert": "Воштыны интыен", + "allarticles": "Ваньмыз бамъёс", "allpagessubmit": "Быдэстоно", + "categories": "Категориос", "categories-submit": "Возьматыны", "sp-deletedcontributions-contribs": "тупатонъёсыз", "listusers-submit": "Возьматыны", @@ -535,8 +557,10 @@ "watchlist-options": "Чаклан списокез тупатыны", "enotif_reset": "Вань бамъёсыз лыдӟем пусйыны", "historyaction-submit": "Возьматыны", + "dellogpage": "Быдтонъёсын журнал", "deletionlog": "палэнэ журнал", "rollbacklink": "ӝог берыктыны", + "rollbacklinkcount": "$1 {{PLURAL:$1|тупатонэз}} ӝог берыктыны", "revertpage": "Откат шонертон [[Special:Contributions/$2|$2]] ([[User talk:$2|обсуждение]]) доры версия [[User:$1|$1]]", "revertpage-nouser": "Откат шонертон (пыриськисьёс ватэм нимъёссы) доры версия {{GENDER:$1|[[User:$1|$1]]}}", "restriction-edit": "Тупатон", @@ -549,6 +573,8 @@ "mycontris": "Гожтэмъёс", "anoncontribs": "Гожтэмъёс", "nocontribs": "Критерии нокыӵе воштӥськонъёс та соответствующий шедьтыны уг луы.", + "month": "Толэзьысен (вазен но):", + "year": "Арысен (вазен но):", "sp-contributions-blocklog": "блокировка", "sp-contributions-deleted": "шонертон палэнтыны {{GENDER:$1|участник|куакеч}}", "sp-contributions-blocked-notice": "Пользователь заблокирован сётӥз та учырлы. Справка понна радъяськылӥсь журнал блокировка лапег берпуметӥ гожтэт:", @@ -595,6 +621,7 @@ "block-log-flags-nousertalk": "тупатъяны ачиз уггес быгаты бамлэн обсуждениосаз", "range_block_disabled": "Администратор диапазонэз блокировать али.", "move-watch": "Чаклан списоке пыртоно инъет но валтӥсь бамъёсыз", + "movelogpage": "Нимъёсты воштонъёсын журнал", "export": "Бамъёсты поттон", "allmessagesname": "Ивортон", "allmessages-filter-all": "Ваньзэ", @@ -635,6 +662,7 @@ "tooltip-ca-nstab-main": "Валтӥсь бамез учконо", "tooltip-ca-nstab-user": "Викиавторлэн бамез", "tooltip-ca-nstab-special": "Та бам нимысьтыз, сое тупатон луонтэм", + "tooltip-ca-nstab-project": "Проектлэн бамез", "tooltip-ca-nstab-image": "Файллэн бамез", "tooltip-ca-nstab-template": "Шаблонлэн бамез", "tooltip-ca-nstab-category": "Категорилэн бамез", @@ -648,6 +676,7 @@ "pageinfo-header-edits": "Воштонъёслэн историзы", "pageinfo-toolboxlink": "Бам сярысь тодэтъёс", "previousdiff": "← Вужгес тупатон", + "nextdiff": "Выльгес тупатон →", "file-info-size": "$1 × $2 пиксель, файллэн быдӟалаез: $3, MIME-тип: $4", "file-nohires": "Бадӟымгес быдӟалаен суред ӧвӧл.", "svg-long-desc": "SVG файл, номинально $1 × $2 пиксель, файллэн быдӟалаез: $3", @@ -681,7 +710,9 @@ "tags-title": "Меткаос", "logentry-delete-delete": "$1 {{GENDER:$2|палэнтыны|палэнтыны}} бам $3", "logentry-delete-restore": "$1 {{GENDER:$2|выльысь}} бам $3", + "logentry-move-move": "$1 $3 бамлы $4 выль ним {{GENDER:$2|сётӥз}}", "logentry-newusers-create": "$1 нимо учётной запись {{GENDER:$2|кылдытэмын}} вал", + "logentry-upload-upload": "$1 {{GENDER:$2|понӥз}} $3", "searchsuggest-search": "Утчано {{SITENAME}}", "searchsuggest-containing": "кудъёсаз вань...", "api-error-autoblocked": "Тон IP-адрес заблокировать эрказ луи, малы ке шуоно со заблокировать пользователь кутыны луоз.", diff --git a/languages/messages/MessagesMdf.php b/languages/messages/MessagesMdf.php index 954d911a22..84b107ef40 100644 --- a/languages/messages/MessagesMdf.php +++ b/languages/messages/MessagesMdf.php @@ -13,7 +13,7 @@ * @author Numulunj pilgae */ -$fallbak = 'ru'; +$fallback = 'ru'; $namespaceNames = [ NS_MEDIA => 'Медиа', diff --git a/maintenance/jsduck/categories.json b/maintenance/jsduck/categories.json index aad85da4a4..9fe5009acd 100644 --- a/maintenance/jsduck/categories.json +++ b/maintenance/jsduck/categories.json @@ -27,6 +27,7 @@ "mw.notification", "mw.Notification_", "mw.storage", + "mw.storage.session", "mw.user", "mw.util", "mw.plugin.*", diff --git a/maintenance/mergeMessageFileList.php b/maintenance/mergeMessageFileList.php index a650aa0b0e..bb47631357 100644 --- a/maintenance/mergeMessageFileList.php +++ b/maintenance/mergeMessageFileList.php @@ -36,11 +36,6 @@ $mmfl = false; * @ingroup Maintenance */ class MergeMessageFileList extends Maintenance { - /** - * @var bool - */ - protected $hasError; - function __construct() { parent::__construct(); $this->addOption( @@ -106,7 +101,6 @@ class MergeMessageFileList extends Maintenance { } if ( !$found ) { - $this->hasError = true; $this->error( "Extension {$extname} in {$extdir} lacks expected entry point: " . "extension.json, skin.json, or {$extname}.php." ); } @@ -119,10 +113,6 @@ class MergeMessageFileList extends Maintenance { $mmfl['setupFiles'] = array_merge( $mmfl['setupFiles'], $extensionPaths ); } - if ( $this->hasError ) { - $this->error( "Some files are missing (see above). Giving up.", 1 ); - } - if ( $this->hasOption( 'output' ) ) { $mmfl['output'] = $this->getOption( 'output' ); } diff --git a/maintenance/protect.php b/maintenance/protect.php index 31b2101c68..f6bb253202 100644 --- a/maintenance/protect.php +++ b/maintenance/protect.php @@ -41,8 +41,8 @@ class Protect extends Maintenance { } public function execute() { - $userName = $this->getOption( 'u', false ); - $reason = $this->getOption( 'r', '' ); + $userName = $this->getOption( 'user', false ); + $reason = $this->getOption( 'reason', '' ); $cascade = $this->hasOption( 'cascade' ); diff --git a/resources/lib/oojs-ui/i18n/el.json b/resources/lib/oojs-ui/i18n/el.json index 85384179b6..98835d538b 100644 --- a/resources/lib/oojs-ui/i18n/el.json +++ b/resources/lib/oojs-ui/i18n/el.json @@ -24,6 +24,7 @@ "ooui-dialog-process-dismiss": "Απόρριψη", "ooui-dialog-process-retry": "Δοκιμάστε ξανά", "ooui-dialog-process-continue": "Συνέχεια", + "ooui-selectfile-button-select": "Επιλέξτε ένα αρχείο", "ooui-selectfile-not-supported": "Επιλογή αρχείου δεν υποστηρίζεται", "ooui-selectfile-placeholder": "Κανένα αρχείο δεν είναι επιλεγμένο", "ooui-selectfile-dragdrop-placeholder": "Σύρετε το αρχείο εδώ" diff --git a/resources/lib/oojs-ui/oojs-ui-apex.js b/resources/lib/oojs-ui/oojs-ui-apex.js index 542447dcc3..a96ae13789 100644 --- a/resources/lib/oojs-ui/oojs-ui-apex.js +++ b/resources/lib/oojs-ui/oojs-ui-apex.js @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.18.0 + * OOjs UI v0.18.1 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2016 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2016-11-09T00:52:37Z + * Date: 2016-11-29T22:57:37Z */ ( function ( OO ) { diff --git a/resources/lib/oojs-ui/oojs-ui-core-apex.css b/resources/lib/oojs-ui/oojs-ui-core-apex.css index bcc3778cc2..e5e62520c6 100644 --- a/resources/lib/oojs-ui/oojs-ui-core-apex.css +++ b/resources/lib/oojs-ui/oojs-ui-core-apex.css @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.18.0 + * OOjs UI v0.18.1 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2016 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2016-11-09T00:52:42Z + * Date: 2016-11-29T22:57:42Z */ .oo-ui-element-hidden { display: none !important; diff --git a/resources/lib/oojs-ui/oojs-ui-core-mediawiki.css b/resources/lib/oojs-ui/oojs-ui-core-mediawiki.css index e7c2ee03c8..6a31fe8eae 100644 --- a/resources/lib/oojs-ui/oojs-ui-core-mediawiki.css +++ b/resources/lib/oojs-ui/oojs-ui-core-mediawiki.css @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.18.0 + * OOjs UI v0.18.1 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2016 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2016-11-09T00:52:42Z + * Date: 2016-11-29T22:57:42Z */ .oo-ui-element-hidden { display: none !important; @@ -143,14 +143,14 @@ box-shadow: none; } .oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button > .oo-ui-labelElement-label { - color: #c33; + color: #d33; } .oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:hover > .oo-ui-labelElement-label { - color: #e53939; + color: #ff4242; } .oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:active > .oo-ui-labelElement-label, .oo-ui-buttonElement-frameless.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button > .oo-ui-labelElement-label { - color: #873636; + color: #b32424; box-shadow: none; } .oo-ui-buttonElement-frameless.oo-ui-widget-enabled[class*='oo-ui-flaggedElement'] > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon, @@ -224,7 +224,7 @@ } .oo-ui-buttonElement-framed.oo-ui-widget-enabled > .oo-ui-buttonElement-button:active, .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button { - background-color: #d9d9d9; + background-color: #c8ccd1; color: #000; border-color: #72777d; } @@ -278,7 +278,7 @@ box-shadow: inset 0 0 0 1px #36c; } .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button { - color: #c33; + color: #d33; } .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:hover { background-color: #fff; @@ -288,14 +288,14 @@ .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:active:focus, .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button, .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive.oo-ui-buttonElement-active > .oo-ui-buttonElement-button { - background-color: #fbf4f4; - color: #873636; - border-color: #873636; + background-color: #ffffff; + color: #b32424; + border-color: #b32424; box-shadow: none; } .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:focus { - border-color: #c33; - box-shadow: inset 0 0 0 1px #c33; + border-color: #d33; + box-shadow: inset 0 0 0 1px #d33; } .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-progressive > .oo-ui-buttonElement-button { color: #fff; @@ -343,25 +343,25 @@ } .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button { color: #fff; - background-color: #c33; - border-color: #c33; + background-color: #d33; + border-color: #d33; } .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:hover { - background-color: #e53939; - border-color: #e53939; + background-color: #ff4242; + border-color: #ff4242; } .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:active, .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:active:focus, .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive.oo-ui-buttonElement-pressed > .oo-ui-buttonElement-button, .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive.oo-ui-buttonElement-active > .oo-ui-buttonElement-button { color: #fff; - background-color: #873636; - border-color: #873636; + background-color: #b32424; + border-color: #b32424; box-shadow: none; } .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary.oo-ui-flaggedElement-destructive > .oo-ui-buttonElement-button:focus { - border-color: #c33; - box-shadow: inset 0 0 0 1px #c33, inset 0 0 0 2px #fff; + border-color: #d33; + box-shadow: inset 0 0 0 1px #d33, inset 0 0 0 2px #fff; } .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary > .oo-ui-buttonElement-button > .oo-ui-iconElement-icon, .oo-ui-buttonElement-framed.oo-ui-widget-enabled.oo-ui-flaggedElement-primary > .oo-ui-buttonElement-button > .oo-ui-indicatorElement-indicator { @@ -403,7 +403,7 @@ } .oo-ui-fieldLayout { display: block; - margin-bottom: 1em; + margin-top: 1.640625em; } .oo-ui-fieldLayout:before, .oo-ui-fieldLayout:after { @@ -444,28 +444,33 @@ padding: 0.5em 0.75em; line-height: 1.5; } -.oo-ui-fieldLayout:last-child { - margin-bottom: 0; +.oo-ui-fieldLayout.oo-ui-labelElement, +.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline { + margin-top: 1.171875em; } -.oo-ui-fieldLayout.oo-ui-fieldLayout-align-left.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label, -.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label { - padding-top: 0.5em; - margin-right: 5%; - width: 35%; +.oo-ui-fieldLayout:first-child, +.oo-ui-fieldLayout.oo-ui-labelElement:first-child, +.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline:first-child { + margin-top: 0; } -.oo-ui-fieldLayout.oo-ui-fieldLayout-align-left > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field, -.oo-ui-fieldLayout.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field { - width: 60%; +.oo-ui-fieldLayout.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label { + padding-bottom: 0.3125em; } -.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline { - margin-bottom: 1.25em; +.oo-ui-fieldLayout.oo-ui-labelElement.oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label { + padding: 0.3125em 0.46875em; +} +.oo-ui-fieldLayout.oo-ui-labelElement.oo-ui-fieldLayout-align-left > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label, +.oo-ui-fieldLayout.oo-ui-labelElement.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label { + width: 35%; + margin-right: 5%; + padding-top: 0.3125em; } -.oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label { - padding: 0.25em 0.25em 0.25em 0.5em; +.oo-ui-fieldLayout.oo-ui-labelElement.oo-ui-fieldLayout-align-left > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field, +.oo-ui-fieldLayout.oo-ui-labelElement.oo-ui-fieldLayout-align-right > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field { + width: 60%; } -.oo-ui-fieldLayout.oo-ui-fieldLayout-align-top.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label { - padding-top: 0.25em; - padding-bottom: 0.5em; +.oo-ui-fieldLayout-disabled > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label { + color: #72777d; } .oo-ui-fieldLayout > .oo-ui-popupButtonWidget { margin-right: 0; @@ -473,9 +478,6 @@ .oo-ui-fieldLayout > .oo-ui-popupButtonWidget:last-child { margin-right: 0; } -.oo-ui-fieldLayout-disabled > .oo-ui-fieldLayout-body > .oo-ui-labelElement-label { - color: #72777d; -} .oo-ui-fieldLayout-messages { list-style: none none; margin: 0.25em 0 0 0.25em; @@ -488,11 +490,10 @@ } .oo-ui-fieldLayout-messages .oo-ui-iconWidget { display: table-cell; - border-right: 0.5em solid transparent; } .oo-ui-fieldLayout-messages .oo-ui-labelWidget { display: table-cell; - padding: 0.1em 0; + padding: 0.1em 0 0.1em 0.3125em; line-height: 1.5; vertical-align: middle; } @@ -552,7 +553,7 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout { margin-top: 2em; } .oo-ui-fieldsetLayout.oo-ui-labelElement > .oo-ui-labelElement-label { - margin-bottom: 0.5em; + margin-bottom: 0.56818em; font-size: 1.1em; font-weight: bold; } @@ -687,7 +688,7 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout { background-color: transparent; } .oo-ui-radioOptionWidget.oo-ui-labelElement .oo-ui-labelElement-label { - padding: 0.25em 0.25em 0.25em 0.5em; + padding: 0.25em 0.25em 0.25em 0.46875em; } .oo-ui-radioOptionWidget .oo-ui-radioInputWidget { margin-right: 0; @@ -1469,12 +1470,17 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout { } .oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle:hover { background-color: #fff; + color: #444; border-color: #a2a9b1; } .oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle:hover .oo-ui-iconElement-icon, .oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle:hover .oo-ui-indicatorElement-indicator { opacity: 0.73; } +.oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle:active { + color: #000; + border-color: #72777d; +} .oo-ui-dropdownWidget.oo-ui-widget-enabled .oo-ui-dropdownWidget-handle:focus { border-color: #36c; outline: 0; @@ -1610,7 +1616,7 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout { vertical-align: middle; } .oo-ui-checkboxMultioptionWidget.oo-ui-labelElement .oo-ui-labelElement-label { - padding: 0.25em 0.25em 0.25em 0.5em; + padding: 0.25em 0.25em 0.25em 0.46875em; } .oo-ui-checkboxMultioptionWidget .oo-ui-checkboxInputWidget { margin-right: 0; diff --git a/resources/lib/oojs-ui/oojs-ui-core.js b/resources/lib/oojs-ui/oojs-ui-core.js index fd4e033ab3..66dfbe84ce 100644 --- a/resources/lib/oojs-ui/oojs-ui-core.js +++ b/resources/lib/oojs-ui/oojs-ui-core.js @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.18.0 + * OOjs UI v0.18.1 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2016 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2016-11-09T00:52:37Z + * Date: 2016-11-29T22:57:37Z */ ( function ( OO ) { @@ -414,7 +414,7 @@ OO.ui.infuse = function ( idOrNode ) { } return message; }; -} )(); +}() ); /** * Package a message and arguments for deferred resolution. @@ -4308,8 +4308,7 @@ OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.ClippableElement ); OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) { if ( this.isVisible() && - !$.contains( this.$element[ 0 ], e.target ) && - ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length ) + !OO.ui.contains( this.$element.add( this.$autoCloseIgnore ).get(), e.target, true ) ) { this.toggle( false ); } @@ -4597,7 +4596,7 @@ OO.ui.mixin.PopupElement = function OoUiMixinPopupElement( config ) { this.popup = new OO.ui.PopupWidget( $.extend( { autoClose: true }, config.popup, - { $autoCloseIgnore: this.$element } + { $autoCloseIgnore: this.$element.add( config.popup && config.popup.$autoCloseIgnore ) } ) ); }; @@ -9797,7 +9796,7 @@ OO.ui.ComboBoxInputWidget.prototype.setOptions = function ( options ) { * @throws {Error} An error is thrown if no widget is specified */ OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) { - var hasInputWidget, div; + var hasInputWidget, $div; // Allow passing positional parameters inside the config object if ( OO.isPlainObject( fieldWidget ) && config === undefined ) { @@ -9837,14 +9836,14 @@ OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) { icon: 'info' } ); - div = $( '
    ' ); + $div = $( '
    ' ); if ( config.help instanceof OO.ui.HtmlSnippet ) { - div.html( config.help.toString() ); + $div.html( config.help.toString() ); } else { - div.text( config.help ); + $div.text( config.help ); } this.popupButtonWidget.getPopup().$body.append( - div.addClass( 'oo-ui-fieldLayout-help-content' ) + $div.addClass( 'oo-ui-fieldLayout-help-content' ) ); this.$help = this.popupButtonWidget.$element; } else { @@ -10138,8 +10137,13 @@ OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout ); * @constructor * @param {Object} [config] Configuration options * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields. + * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear + * in the upper-right corner of the rendered field; clicking it will display the text in a popup. + * For important messages, you are advised to use `notices`, as they are always shown. */ OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) { + var $div; + // Configuration initialization config = config || {}; @@ -10158,10 +10162,14 @@ OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) { icon: 'info' } ); + $div = $( '
    ' ); + if ( config.help instanceof OO.ui.HtmlSnippet ) { + $div.html( config.help.toString() ); + } else { + $div.text( config.help ); + } this.popupButtonWidget.getPopup().$body.append( - $( '
    ' ) - .text( config.help ) - .addClass( 'oo-ui-fieldsetLayout-help-content' ) + $div.addClass( 'oo-ui-fieldsetLayout-help-content' ) ); this.$help = this.popupButtonWidget.$element; } else { diff --git a/resources/lib/oojs-ui/oojs-ui-mediawiki.js b/resources/lib/oojs-ui/oojs-ui-mediawiki.js index 17bca7e491..962db9abc3 100644 --- a/resources/lib/oojs-ui/oojs-ui-mediawiki.js +++ b/resources/lib/oojs-ui/oojs-ui-mediawiki.js @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.18.0 + * OOjs UI v0.18.1 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2016 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2016-11-09T00:52:37Z + * Date: 2016-11-29T22:57:37Z */ ( function ( OO ) { diff --git a/resources/lib/oojs-ui/oojs-ui-toolbars-apex.css b/resources/lib/oojs-ui/oojs-ui-toolbars-apex.css index 7fb36c4d8d..4b598766ce 100644 --- a/resources/lib/oojs-ui/oojs-ui-toolbars-apex.css +++ b/resources/lib/oojs-ui/oojs-ui-toolbars-apex.css @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.18.0 + * OOjs UI v0.18.1 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2016 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2016-11-09T00:52:42Z + * Date: 2016-11-29T22:57:42Z */ .oo-ui-popupTool .oo-ui-popupWidget-popup, .oo-ui-popupTool .oo-ui-popupWidget-anchor { diff --git a/resources/lib/oojs-ui/oojs-ui-toolbars-mediawiki.css b/resources/lib/oojs-ui/oojs-ui-toolbars-mediawiki.css index cb9660a05d..c3b0c98b05 100644 --- a/resources/lib/oojs-ui/oojs-ui-toolbars-mediawiki.css +++ b/resources/lib/oojs-ui/oojs-ui-toolbars-mediawiki.css @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.18.0 + * OOjs UI v0.18.1 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2016 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2016-11-09T00:52:42Z + * Date: 2016-11-29T22:57:42Z */ .oo-ui-tool.oo-ui-widget-enabled { -webkit-transition: background-color 100ms; diff --git a/resources/lib/oojs-ui/oojs-ui-toolbars.js b/resources/lib/oojs-ui/oojs-ui-toolbars.js index e17f511a16..f57e2dbd8a 100644 --- a/resources/lib/oojs-ui/oojs-ui-toolbars.js +++ b/resources/lib/oojs-ui/oojs-ui-toolbars.js @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.18.0 + * OOjs UI v0.18.1 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2016 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2016-11-09T00:52:37Z + * Date: 2016-11-29T22:57:37Z */ ( function ( OO ) { diff --git a/resources/lib/oojs-ui/oojs-ui-widgets-apex.css b/resources/lib/oojs-ui/oojs-ui-widgets-apex.css index d6ba00b71b..884e48e95f 100644 --- a/resources/lib/oojs-ui/oojs-ui-widgets-apex.css +++ b/resources/lib/oojs-ui/oojs-ui-widgets-apex.css @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.18.0 + * OOjs UI v0.18.1 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2016 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2016-11-09T00:52:42Z + * Date: 2016-11-29T22:57:42Z */ .oo-ui-draggableElement-handle, .oo-ui-draggableElement-handle.oo-ui-widget { diff --git a/resources/lib/oojs-ui/oojs-ui-widgets-mediawiki.css b/resources/lib/oojs-ui/oojs-ui-widgets-mediawiki.css index bf50532b0b..cfbea3ea00 100644 --- a/resources/lib/oojs-ui/oojs-ui-widgets-mediawiki.css +++ b/resources/lib/oojs-ui/oojs-ui-widgets-mediawiki.css @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.18.0 + * OOjs UI v0.18.1 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2016 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2016-11-09T00:52:42Z + * Date: 2016-11-29T22:57:42Z */ .oo-ui-draggableElement-handle, .oo-ui-draggableElement-handle.oo-ui-widget { @@ -824,6 +824,23 @@ .oo-ui-capsuleMultiselectWidget-handle > .oo-ui-iconElement-icon { position: absolute; } +.oo-ui-capsuleMultiselectWidget-handle > .oo-ui-capsuleMultiselectWidget-content :-moz-placeholder { + color: #72777d; + opacity: 1; +} +.oo-ui-capsuleMultiselectWidget-handle > .oo-ui-capsuleMultiselectWidget-content ::-moz-placeholder { + color: #72777d; + opacity: 1; +} +.oo-ui-capsuleMultiselectWidget-handle > .oo-ui-capsuleMultiselectWidget-content :-ms-input-placeholder { + color: #72777d; +} +.oo-ui-capsuleMultiselectWidget-handle > .oo-ui-capsuleMultiselectWidget-content ::-webkit-input-placeholder { + color: #72777d; +} +.oo-ui-capsuleMultiselectWidget-handle > .oo-ui-capsuleMultiselectWidget-content :placeholder-shown { + color: #72777d; +} .oo-ui-capsuleMultiselectWidget-handle > .oo-ui-capsuleMultiselectWidget-content > input { border: 0; line-height: 1.675; diff --git a/resources/lib/oojs-ui/oojs-ui-widgets.js b/resources/lib/oojs-ui/oojs-ui-widgets.js index 6962c92909..8242c86c7d 100644 --- a/resources/lib/oojs-ui/oojs-ui-widgets.js +++ b/resources/lib/oojs-ui/oojs-ui-widgets.js @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.18.0 + * OOjs UI v0.18.1 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2016 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2016-11-09T00:52:37Z + * Date: 2016-11-29T22:57:37Z */ ( function ( OO ) { @@ -4074,8 +4074,7 @@ OO.ui.CapsuleMultiselectWidget.prototype.onPopupFocusOut = function () { setTimeout( function () { if ( widget.isVisible() && - !OO.ui.contains( widget.$element[ 0 ], document.activeElement, true ) && - ( !widget.$autoCloseIgnore || !widget.$autoCloseIgnore.has( document.activeElement ).length ) + !OO.ui.contains( widget.$element.add( widget.$autoCloseIgnore ).get(), document.activeElement, true ) ) { widget.toggle( false ); } diff --git a/resources/lib/oojs-ui/oojs-ui-windows-apex.css b/resources/lib/oojs-ui/oojs-ui-windows-apex.css index 6258b84372..40de1d763e 100644 --- a/resources/lib/oojs-ui/oojs-ui-windows-apex.css +++ b/resources/lib/oojs-ui/oojs-ui-windows-apex.css @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.18.0 + * OOjs UI v0.18.1 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2016 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2016-11-09T00:52:42Z + * Date: 2016-11-29T22:57:42Z */ .oo-ui-actionWidget.oo-ui-pendingElement-pending { background-image: /* @embed */ url(themes/apex/images/textures/pending.gif); diff --git a/resources/lib/oojs-ui/oojs-ui-windows-mediawiki.css b/resources/lib/oojs-ui/oojs-ui-windows-mediawiki.css index 359c469340..d1b35e870b 100644 --- a/resources/lib/oojs-ui/oojs-ui-windows-mediawiki.css +++ b/resources/lib/oojs-ui/oojs-ui-windows-mediawiki.css @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.18.0 + * OOjs UI v0.18.1 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2016 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2016-11-09T00:52:42Z + * Date: 2016-11-29T22:57:42Z */ .oo-ui-window { background: transparent; diff --git a/resources/lib/oojs-ui/oojs-ui-windows.js b/resources/lib/oojs-ui/oojs-ui-windows.js index 8b614c650e..f6e2a39a10 100644 --- a/resources/lib/oojs-ui/oojs-ui-windows.js +++ b/resources/lib/oojs-ui/oojs-ui-windows.js @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.18.0 + * OOjs UI v0.18.1 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2016 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2016-11-09T00:52:37Z + * Date: 2016-11-29T22:57:37Z */ ( function ( OO ) { @@ -1286,7 +1286,8 @@ OO.ui.WindowManager.prototype.getCurrentWindow = function () { * * @param {OO.ui.Window|string} win Window object or symbolic name of window to open * @param {Object} [data] Window opening data - * @param {jQuery} [data.$returnFocusTo] Element to which the window will return focus when closed. + * @param {jQuery|null} [data.$returnFocusTo] Element to which the window will return focus when closed. + * Defaults the current activeElement. If set to null, focus isn't changed on close. * @return {jQuery.Promise} An `opening` promise resolved when the window is done opening. * See {@link #event-opening 'opening' event} for more information about `opening` promises. * @fires opening @@ -1418,7 +1419,9 @@ OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) { manager.toggleGlobalEvents( false ); manager.toggleAriaIsolation( false ); } - manager.$returnFocusTo[ 0 ].focus(); + if ( manager.$returnFocusTo && manager.$returnFocusTo.length ) { + manager.$returnFocusTo[ 0 ].focus(); + } manager.closing = null; manager.currentWindow = null; closing.resolve( data ); diff --git a/resources/lib/oojs-ui/themes/mediawiki/icons-alerts.json b/resources/lib/oojs-ui/themes/mediawiki/icons-alerts.json index f5694a1567..e6fa863c12 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/icons-alerts.json +++ b/resources/lib/oojs-ui/themes/mediawiki/icons-alerts.json @@ -15,7 +15,7 @@ "color": "#36c" }, "destructive": { - "color": "#c33" + "color": "#d33" }, "warning": { "color": "#ff5d00" diff --git a/resources/lib/oojs-ui/themes/mediawiki/icons-content.json b/resources/lib/oojs-ui/themes/mediawiki/icons-content.json index 651cddfa06..80bbcaf2b3 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/icons-content.json +++ b/resources/lib/oojs-ui/themes/mediawiki/icons-content.json @@ -15,7 +15,7 @@ "color": "#36c" }, "destructive": { - "color": "#c33" + "color": "#d33" }, "warning": { "color": "#ff5d00" diff --git a/resources/lib/oojs-ui/themes/mediawiki/icons-editing-advanced.json b/resources/lib/oojs-ui/themes/mediawiki/icons-editing-advanced.json index 6b9e490493..21efb82e48 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/icons-editing-advanced.json +++ b/resources/lib/oojs-ui/themes/mediawiki/icons-editing-advanced.json @@ -15,7 +15,7 @@ "color": "#36c" }, "destructive": { - "color": "#c33" + "color": "#d33" }, "warning": { "color": "#ff5d00" diff --git a/resources/lib/oojs-ui/themes/mediawiki/icons-editing-core.json b/resources/lib/oojs-ui/themes/mediawiki/icons-editing-core.json index 11fcef7fd2..4515405b41 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/icons-editing-core.json +++ b/resources/lib/oojs-ui/themes/mediawiki/icons-editing-core.json @@ -15,7 +15,7 @@ "color": "#36c" }, "destructive": { - "color": "#c33" + "color": "#d33" }, "warning": { "color": "#ff5d00" diff --git a/resources/lib/oojs-ui/themes/mediawiki/icons-editing-list.json b/resources/lib/oojs-ui/themes/mediawiki/icons-editing-list.json index cd4087efb7..3edb545470 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/icons-editing-list.json +++ b/resources/lib/oojs-ui/themes/mediawiki/icons-editing-list.json @@ -15,7 +15,7 @@ "color": "#36c" }, "destructive": { - "color": "#c33" + "color": "#d33" }, "warning": { "color": "#ff5d00" diff --git a/resources/lib/oojs-ui/themes/mediawiki/icons-editing-styling.json b/resources/lib/oojs-ui/themes/mediawiki/icons-editing-styling.json index d168364924..c97d770273 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/icons-editing-styling.json +++ b/resources/lib/oojs-ui/themes/mediawiki/icons-editing-styling.json @@ -15,7 +15,7 @@ "color": "#36c" }, "destructive": { - "color": "#c33" + "color": "#d33" }, "warning": { "color": "#ff5d00" diff --git a/resources/lib/oojs-ui/themes/mediawiki/icons-interactions.json b/resources/lib/oojs-ui/themes/mediawiki/icons-interactions.json index 7efe53169e..f110a0462a 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/icons-interactions.json +++ b/resources/lib/oojs-ui/themes/mediawiki/icons-interactions.json @@ -15,7 +15,7 @@ "color": "#36c" }, "destructive": { - "color": "#c33" + "color": "#d33" }, "warning": { "color": "#ff5d00" diff --git a/resources/lib/oojs-ui/themes/mediawiki/icons-layout.json b/resources/lib/oojs-ui/themes/mediawiki/icons-layout.json index 765b8fefe0..6ff4a0e0d3 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/icons-layout.json +++ b/resources/lib/oojs-ui/themes/mediawiki/icons-layout.json @@ -15,7 +15,7 @@ "color": "#36c" }, "destructive": { - "color": "#c33" + "color": "#d33" }, "warning": { "color": "#ff5d00" diff --git a/resources/lib/oojs-ui/themes/mediawiki/icons-location.json b/resources/lib/oojs-ui/themes/mediawiki/icons-location.json index c844449a81..7098f2304b 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/icons-location.json +++ b/resources/lib/oojs-ui/themes/mediawiki/icons-location.json @@ -15,7 +15,7 @@ "color": "#36c" }, "destructive": { - "color": "#c33" + "color": "#d33" }, "warning": { "color": "#ff5d00" diff --git a/resources/lib/oojs-ui/themes/mediawiki/icons-media.json b/resources/lib/oojs-ui/themes/mediawiki/icons-media.json index 8c7b84554a..afdb9e527f 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/icons-media.json +++ b/resources/lib/oojs-ui/themes/mediawiki/icons-media.json @@ -15,7 +15,7 @@ "color": "#36c" }, "destructive": { - "color": "#c33" + "color": "#d33" }, "warning": { "color": "#ff5d00" diff --git a/resources/lib/oojs-ui/themes/mediawiki/icons-moderation.json b/resources/lib/oojs-ui/themes/mediawiki/icons-moderation.json index e98012fd93..3779ae3fc0 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/icons-moderation.json +++ b/resources/lib/oojs-ui/themes/mediawiki/icons-moderation.json @@ -15,7 +15,7 @@ "color": "#36c" }, "destructive": { - "color": "#c33" + "color": "#d33" }, "warning": { "color": "#ff5d00" diff --git a/resources/lib/oojs-ui/themes/mediawiki/icons-movement.json b/resources/lib/oojs-ui/themes/mediawiki/icons-movement.json index c545a49dbb..059073f408 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/icons-movement.json +++ b/resources/lib/oojs-ui/themes/mediawiki/icons-movement.json @@ -15,7 +15,7 @@ "color": "#36c" }, "destructive": { - "color": "#c33" + "color": "#d33" }, "warning": { "color": "#ff5d00" diff --git a/resources/lib/oojs-ui/themes/mediawiki/icons-user.json b/resources/lib/oojs-ui/themes/mediawiki/icons-user.json index 39fdda5d2f..5a70c5ead8 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/icons-user.json +++ b/resources/lib/oojs-ui/themes/mediawiki/icons-user.json @@ -15,7 +15,7 @@ "color": "#36c" }, "destructive": { - "color": "#c33" + "color": "#d33" }, "warning": { "color": "#ff5d00" diff --git a/resources/lib/oojs-ui/themes/mediawiki/icons-wikimedia.json b/resources/lib/oojs-ui/themes/mediawiki/icons-wikimedia.json index bac97683ee..61aec85d21 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/icons-wikimedia.json +++ b/resources/lib/oojs-ui/themes/mediawiki/icons-wikimedia.json @@ -15,7 +15,7 @@ "color": "#36c" }, "destructive": { - "color": "#c33" + "color": "#d33" }, "warning": { "color": "#ff5d00" diff --git a/resources/lib/oojs-ui/themes/mediawiki/icons.json b/resources/lib/oojs-ui/themes/mediawiki/icons.json index 6a7c5659fa..4666fd1a59 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/icons.json +++ b/resources/lib/oojs-ui/themes/mediawiki/icons.json @@ -15,7 +15,7 @@ "color": "#36c" }, "destructive": { - "color": "#c33" + "color": "#d33" }, "warning": { "color": "#ff5d00" diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/block-destructive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/block-destructive.png index 53460bea18..54b9f62ffa 100644 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/block-destructive.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/block-destructive.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/block-destructive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/block-destructive.svg index abf656f313..f01a7790d4 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/block-destructive.svg +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/block-destructive.svg @@ -1,4 +1,4 @@ - + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/cancel-destructive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/cancel-destructive.png index 000e529e3c..f574a39231 100644 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/cancel-destructive.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/cancel-destructive.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/cancel-destructive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/cancel-destructive.svg index b2b01790ca..3391d4e8eb 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/cancel-destructive.svg +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/cancel-destructive.svg @@ -1,5 +1,5 @@ - + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/check-destructive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/check-destructive.png index 0cc9169ee5..305f41d5e6 100644 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/check-destructive.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/check-destructive.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/check-destructive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/check-destructive.svg index 7e3dc53dc9..059f0bd146 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/check-destructive.svg +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/check-destructive.svg @@ -1,4 +1,4 @@ - + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/lock-ltr-destructive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/lock-ltr-destructive.png index 42311de116..e16f04292f 100644 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/lock-ltr-destructive.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/lock-ltr-destructive.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/lock-ltr-destructive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/lock-ltr-destructive.svg index a9900c18e2..2cfa62e84f 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/lock-ltr-destructive.svg +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/lock-ltr-destructive.svg @@ -1,4 +1,4 @@ - + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/lock-rtl-destructive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/lock-rtl-destructive.png index 72d6a7b6b4..29cd2b57a2 100644 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/lock-rtl-destructive.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/lock-rtl-destructive.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/lock-rtl-destructive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/lock-rtl-destructive.svg index 2811b252d6..2daea47b1a 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/lock-rtl-destructive.svg +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/lock-rtl-destructive.svg @@ -1,4 +1,4 @@ - + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/tag-destructive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/tag-destructive.png index 55ab6c435e..cf85c4d052 100644 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/tag-destructive.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/tag-destructive.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/tag-destructive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/tag-destructive.svg index 7048a402b2..0732f2ebe6 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/tag-destructive.svg +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/tag-destructive.svg @@ -1,4 +1,4 @@ - + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/trash-destructive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/trash-destructive.png index c1d2a66790..c367e77b0f 100644 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/trash-destructive.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/trash-destructive.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/trash-destructive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/trash-destructive.svg index 3ebc63b862..59ad3f280b 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/trash-destructive.svg +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/trash-destructive.svg @@ -1,4 +1,4 @@ - + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/unLock-ltr-destructive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/unLock-ltr-destructive.png index 8fb039ca5c..2cb27f157c 100644 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/unLock-ltr-destructive.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/unLock-ltr-destructive.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/unLock-ltr-destructive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/unLock-ltr-destructive.svg index 7ee7522a4f..d45ebac277 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/unLock-ltr-destructive.svg +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/unLock-ltr-destructive.svg @@ -1,4 +1,4 @@ - + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/unLock-rtl-destructive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/unLock-rtl-destructive.png index 7c2786d4c4..fac51b9fdd 100644 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/unLock-rtl-destructive.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/unLock-rtl-destructive.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/unLock-rtl-destructive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/unLock-rtl-destructive.svg index a5f2721672..5e6d2055a2 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/unLock-rtl-destructive.svg +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/unLock-rtl-destructive.svg @@ -1,4 +1,4 @@ - + diff --git a/resources/lib/oojs-ui/themes/mediawiki/indicators.json b/resources/lib/oojs-ui/themes/mediawiki/indicators.json index 349227a4f5..91d0358d19 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/indicators.json +++ b/resources/lib/oojs-ui/themes/mediawiki/indicators.json @@ -15,7 +15,7 @@ "color": "#36c" }, "destructive": { - "color": "#c33" + "color": "#d33" }, "warning": { "color": "#ff5d00" diff --git a/resources/src/mediawiki.action/mediawiki.action.history.styles.css b/resources/src/mediawiki.action/mediawiki.action.history.styles.css index 5b08f95ae5..cad530bf12 100644 --- a/resources/src/mediawiki.action/mediawiki.action.history.styles.css +++ b/resources/src/mediawiki.action/mediawiki.action.history.styles.css @@ -10,8 +10,8 @@ } #pagehistory li.selected { - background-color: #f9f9f9; - border: 1px dashed #aaa; + background-color: #f8f9fa; + border: 1px dashed #a2a9b1; } .mw-history-revisionactions { diff --git a/resources/src/mediawiki.action/mediawiki.action.view.filepage.css b/resources/src/mediawiki.action/mediawiki.action.view.filepage.css index d5520a1f97..1f0365eced 100644 --- a/resources/src/mediawiki.action/mediawiki.action.view.filepage.css +++ b/resources/src/mediawiki.action/mediawiki.action.view.filepage.css @@ -46,8 +46,8 @@ */ #filetoc { text-align: center; - border: 1px solid #aaa; - background-color: #f9f9f9; + border: 1px solid #a2a9b1; + background-color: #f8f9fa; padding: 5px; font-size: 95%; margin-bottom: 0.5em; @@ -99,13 +99,13 @@ .mw_metadata td, .mw_metadata th { - border: 1px solid #aaa; + border: 1px solid #a2a9b1; padding-left: 5px; padding-right: 5px; } .mw_metadata th { - background-color: #f9f9f9; + background-color: #f8f9fa; } .mw_metadata td { diff --git a/resources/src/mediawiki.language/mediawiki.language.numbers.js b/resources/src/mediawiki.language/mediawiki.language.numbers.js index 11926508b5..10ceecc48d 100644 --- a/resources/src/mediawiki.language/mediawiki.language.numbers.js +++ b/resources/src/mediawiki.language/mediawiki.language.numbers.js @@ -158,6 +158,31 @@ return valueParts.join( options.decimal ); } + /** + * Helper function to flip transformation tables. + * + * @param {...Object} Transformation tables + * @return {Object} + */ + function flipTransform() { + var i, key, table, flipped = {}; + + // Ensure we strip thousand separators. This might be overwritten. + flipped[ ',' ] = ''; + + for ( i = 0; i < arguments.length; i++ ) { + table = arguments[ i ]; + for ( key in table ) { + if ( table.hasOwnProperty( key ) ) { + // The thousand separator should be deleted + flipped[ table[ key ] ] = key === ',' ? '' : key; + } + } + } + + return flipped; + } + $.extend( mw.language, { /** @@ -168,46 +193,51 @@ * @return {number|string} Formatted number */ convertNumber: function ( num, integer ) { - var i, tmp, transformTable, numberString, convertedNumber, pattern; - - pattern = mw.language.getData( mw.config.get( 'wgUserLanguage' ), - 'digitGroupingPattern' ) || '#,##0.###'; + var transformTable, digitTransformTable, separatorTransformTable, + i, numberString, convertedNumber, pattern; - // Set the target transform table: - transformTable = mw.language.getDigitTransformTable(); - - if ( !transformTable ) { + // Quick shortcut for plain numbers + if ( integer && parseInt( num, 10 ) === num ) { return num; } - // Check if the 'restore' to Latin number flag is set: + // Load the transformation tables (can be empty) + digitTransformTable = mw.language.getDigitTransformTable(); + separatorTransformTable = mw.language.getSeparatorTransformTable(); + if ( integer ) { - if ( parseInt( num, 10 ) === num ) { - return num; - } - tmp = []; - for ( i in transformTable ) { - tmp[ transformTable[ i ] ] = i; - } - transformTable = tmp; + // Reverse the digit transformation tables if we are doing unformatting + transformTable = flipTransform( separatorTransformTable, digitTransformTable ); numberString = String( num ); } else { - // Ignore transform table if wgTranslateNumerals is false - if ( !mw.config.get( 'wgTranslateNumerals' ) ) { - transformTable = []; + // This check being here means that digits can still be unformatted + // even if we do not produce them. This seems sane behavior. + if ( mw.config.get( 'wgTranslateNumerals' ) ) { + transformTable = digitTransformTable; } + + // Commaying is more complex, so we handle it here separately. + // When unformatting, we just use separatorTransformTable. + pattern = mw.language.getData( mw.config.get( 'wgUserLanguage' ), + 'digitGroupingPattern' ) || '#,##0.###'; numberString = mw.language.commafy( num, pattern ); } convertedNumber = ''; for ( i = 0; i < numberString.length; i++ ) { - if ( transformTable[ numberString[ i ] ] ) { + if ( transformTable.hasOwnProperty( numberString[ i ] ) ) { convertedNumber += transformTable[ numberString[ i ] ]; } else { convertedNumber += numberString[ i ]; } } - return integer ? parseInt( convertedNumber, 10 ) : convertedNumber; + + if ( integer ) { + // Parse string to integer. This loses decimals! + convertedNumber = parseInt( convertedNumber, 10 ); + } + + return convertedNumber; }, /** diff --git a/resources/src/mediawiki.legacy/shared.css b/resources/src/mediawiki.legacy/shared.css index 1522de1e75..59f892e0f0 100644 --- a/resources/src/mediawiki.legacy/shared.css +++ b/resources/src/mediawiki.legacy/shared.css @@ -343,8 +343,8 @@ a.new { */ table.wikitable { margin: 1em 0; - background-color: #f9f9f9; - border: 1px solid #aaa; + background-color: #f8f9fa; + border: 1px solid #a2a9b1; border-collapse: collapse; color: #000; } @@ -359,7 +359,7 @@ table.wikitable > * > tr > td { table.wikitable > tr > th, table.wikitable > * > tr > th { - background-color: #f2f2f2; + background-color: #eaecf0; text-align: center; } diff --git a/resources/src/mediawiki.skinning/content.css b/resources/src/mediawiki.skinning/content.css index d4c93fd266..e1df01d896 100644 --- a/resources/src/mediawiki.skinning/content.css +++ b/resources/src/mediawiki.skinning/content.css @@ -10,8 +10,8 @@ .toc, .mw-warning, .toccolours { - border: 1px solid #aaa; - background-color: #f9f9f9; + border: 1px solid #a2a9b1; + background-color: #f8f9fa; padding: 5px; font-size: 95%; } @@ -147,9 +147,9 @@ div.thumb { } div.thumbinner { - border: 1px solid #ccc; + border: 1px solid #c8ccd1; padding: 3px; - background-color: #f9f9f9; + background-color: #f8f9fa; font-size: 94%; text-align: center; /* new block formatting context, @@ -158,7 +158,7 @@ div.thumbinner { } html .thumbimage { - border: 1px solid #ccc; + border: 1px solid #c8ccd1; } html .thumbcaption { @@ -199,7 +199,7 @@ div.magnify a { } img.thumbborder { - border: 1px solid #ddd; + border: 1px solid #eaecf0; } /* Directionality-specific styles for thumbnails - their positioning depends on content language */ diff --git a/resources/src/mediawiki.skinning/elements.css b/resources/src/mediawiki.skinning/elements.css index 9c52b2a00d..33ceb482d0 100644 --- a/resources/src/mediawiki.skinning/elements.css +++ b/resources/src/mediawiki.skinning/elements.css @@ -204,8 +204,8 @@ pre, code, tt, kbd, samp, .mw-code { code { color: #000; - background-color: #f9f9f9; - border: 1px solid #ddd; + background-color: #f8f9fa; + border: 1px solid #eaecf0; border-radius: 2px; padding: 1px 4px; } @@ -213,8 +213,8 @@ code { pre, .mw-code { color: #000; - background-color: #f9f9f9; - border: 1px solid #ddd; + background-color: #f8f9fa; + border: 1px solid #eaecf0; padding: 1em; /* Wrap lines in overflow. T2260, T103780 */ white-space: pre-wrap; diff --git a/resources/src/mediawiki.skinning/interface.css b/resources/src/mediawiki.skinning/interface.css index 4ca2096661..7dbcd4dc28 100644 --- a/resources/src/mediawiki.skinning/interface.css +++ b/resources/src/mediawiki.skinning/interface.css @@ -8,8 +8,8 @@ /* Categories */ .catlinks { - border: 1px solid #aaa; - background-color: #f9f9f9; + border: 1px solid #a2a9b1; + background-color: #f8f9fa; padding: 5px; margin-top: 1em; clear: both; diff --git a/resources/src/mediawiki.special/mediawiki.special.apisandbox.js b/resources/src/mediawiki.special/mediawiki.special.apisandbox.js index d2015d8966..60155fd50a 100644 --- a/resources/src/mediawiki.special/mediawiki.special.apisandbox.js +++ b/resources/src/mediawiki.special/mediawiki.special.apisandbox.js @@ -51,9 +51,7 @@ tokenWidget: { alertTokenError: function ( code, error ) { windowManager.openWindow( 'errorAlert', { - title: mw.message( - 'apisandbox-results-fixtoken-fail', this.paramInfo.tokentype - ).parse(), + title: Util.parseMsg( 'apisandbox-results-fixtoken-fail', this.paramInfo.tokentype ), message: error, actions: [ { @@ -218,7 +216,7 @@ }; /** - * @class mw.special.ApiSandbox.Utils + * @class mw.special.ApiSandbox.Util * @private */ Util = { @@ -591,17 +589,42 @@ }, /** - * Parse an HTML string, adding target="_blank" to any links + * Parse an HTML string and call Util.fixupHTML() * * @param {string} html HTML to parse * @return {jQuery} */ parseHTML: function ( html ) { var $ret = $( $.parseHTML( html ) ); - $ret.filter( 'a' ).add( $ret.find( 'a' ) ) + return Util.fixupHTML( $ret ); + }, + + /** + * Parse an i18n message and call Util.fixupHTML() + * + * @param {string} key Key of message to get + * @param {...Mixed} parameters Values for $N replacements + * @return {jQuery} + */ + parseMsg: function () { + var $ret = mw.message.apply( mw.message, arguments ).parseDom(); + return Util.fixupHTML( $ret ); + }, + + /** + * Fix HTML for ApiSandbox display + * + * Fixes are: + * - Add target="_blank" to any links + * + * @param {jQuery} $html DOM to process + * @return {jQuery} + */ + fixupHTML: function ( $html ) { + $html.filter( 'a' ).add( $html.find( 'a' ) ) .filter( '[href]:not([target])' ) .attr( 'target', '_blank' ); - return $ret; + return $html; } }; @@ -683,7 +706,7 @@ $content .empty() - .append( $( '

    ' ).append( mw.message( 'apisandbox-intro' ).parse() ) ) + .append( $( '

    ' ).append( Util.parseMsg( 'apisandbox-intro' ) ) ) .append( $( '

    ', { id: 'mw-apisandbox-ui' } ) .append( $toolbar ) @@ -896,8 +919,8 @@ $.when.apply( $, deferreds ).done( function () { if ( $.inArray( false, arguments ) !== -1 ) { windowManager.openWindow( 'errorAlert', { - title: mw.message( 'apisandbox-submit-invalid-fields-title' ).parse(), - message: mw.message( 'apisandbox-submit-invalid-fields-message' ).parse(), + title: Util.parseMsg( 'apisandbox-submit-invalid-fields-title' ), + message: Util.parseMsg( 'apisandbox-submit-invalid-fields-message' ), actions: [ { action: 'accept', @@ -942,7 +965,7 @@ readOnly: true, value: mw.util.wikiScript( 'api' ) + '?' + query } ), { - label: mw.message( 'apisandbox-request-url-label' ).parse() + label: Util.parseMsg( 'apisandbox-request-url-label' ) } ).$element, $result @@ -1012,9 +1035,7 @@ if ( data.status && data.status !== 200 ) { $( '
    ' ) .addClass( 'api-pretty-header api-pretty-status' ) - .append( - mw.message( 'api-format-prettyprint-status', data.status, data.statustext ).parse() - ) + .append( Util.parseMsg( 'api-format-prettyprint-status', data.status, data.statustext ) ) .appendTo( $result ); } $result.append( Util.parseHTML( data.html ) ); @@ -1049,7 +1070,7 @@ framed: false, icon: 'info', popup: { - $content: $( '
    ' ).append( mw.message( 'apisandbox-continue-help' ).parse() ), + $content: $( '
    ' ).append( Util.parseMsg( 'apisandbox-continue-help' ) ), padded: true } } ).$element @@ -1198,9 +1219,7 @@ if ( that.widgets[ name ] !== undefined ) { windowManager.openWindow( 'errorAlert', { - title: mw.message( - 'apisandbox-dynamic-error-exists', name - ).parse(), + title: Util.parseMsg( 'apisandbox-dynamic-error-exists', name ), actions: [ { action: 'accept', @@ -1380,22 +1399,20 @@ dl.append( $( '
    ', { addClass: 'info', append: [ - Util.parseHTML( mw.message( + Util.parseMsg( 'api-help-param-limit2', pi.parameters[ i ].max, pi.parameters[ i ].highmax - ).parse() ), + ), ' ', - Util.parseHTML( mw.message( 'apisandbox-param-limit' ).parse() ) + Util.parseMsg( 'apisandbox-param-limit' ) ] } ) ); } else { dl.append( $( '
    ', { addClass: 'info', append: [ - Util.parseHTML( mw.message( - 'api-help-param-limit', pi.parameters[ i ].max - ).parse() ), + Util.parseMsg( 'api-help-param-limit', pi.parameters[ i ].max ), ' ', - Util.parseHTML( mw.message( 'apisandbox-param-limit' ).parse() ) + Util.parseMsg( 'apisandbox-param-limit' ) ] } ) ); } @@ -1412,11 +1429,11 @@ if ( tmp !== '' ) { dl.append( $( '
    ', { addClass: 'info', - append: Util.parseHTML( mw.message( + append: Util.parseMsg( 'api-help-param-integer-' + tmp, Util.apiBool( pi.parameters[ i ].multi ) ? 2 : 1, pi.parameters[ i ].min, pi.parameters[ i ].max - ).parse() ) + ) } ) ); } break; @@ -1496,7 +1513,7 @@ items.push( new OO.ui.FieldLayout( new OO.ui.Widget( {} ).toggle( false ), { align: 'top', - label: Util.parseHTML( mw.message( 'apisandbox-no-parameters' ).parse() ) + label: Util.parseMsg( 'apisandbox-no-parameters' ) } ) ); } diff --git a/resources/src/mediawiki.special/mediawiki.special.search.styles.css b/resources/src/mediawiki.special/mediawiki.special.search.styles.css index 5191f92b16..ebe9ed94ce 100644 --- a/resources/src/mediawiki.special/mediawiki.special.search.styles.css +++ b/resources/src/mediawiki.special/mediawiki.special.search.styles.css @@ -53,9 +53,9 @@ font-size: 97%; } .mw-search-profile-tabs { - background-color: #f3f3f3; + background-color: #f8f9fa; margin-top: 1em; - border: 1px solid #c0c0c0; + border: 1px solid #c8ccd1; } .search-types { float: left; @@ -76,7 +76,7 @@ padding: 0.5em; } .search-types .current a { - color: #333; + color: #222; cursor: default; } .search-types .current a:hover { @@ -86,7 +86,7 @@ float: right; padding: 0.5em; padding-right: 0.75em; - color: #666; + color: #54595d; font-size: 95%; } #mw-search-top-table div.oo-ui-actionFieldLayout { @@ -96,8 +96,8 @@ #mw-searchoptions { margin: 0; padding: 0.5em 0.75em 0.75em 0.75em; - background-color: #f9f9f9; - border: 1px solid #c0c0c0; + background-color: #f8f9fa; + border: 1px solid #c8ccd1; border-top-width: 0; } #mw-searchoptions legend { @@ -119,7 +119,7 @@ } #mw-searchoptions .divider { clear: both; - border-bottom: 1px solid #ddd; + border-bottom: 1px solid #eaecf0; padding-top: 0.5em; margin-bottom: 0.5em; } @@ -130,7 +130,7 @@ #mw-search-interwiki { float: right; width: 18em; - border: 1px solid #aaa; + border: 1px solid #a2a9b1; margin-top: 2ex; } .searchalttitle, @@ -150,12 +150,12 @@ font-size: 97%; text-align: left; padding: 0.15em 0.15em 0.2em 0.2em; - background-color: #ececec; - border-top: 1px solid #bbb; + background-color: #eaecf0; + border-top: 1px solid #c8ccd1; } .searchdidyoumean { font-size: 127%; margin-top: 0.8em; /* Note that this color won't affect the link, as desired. */ - color: #c00; + color: #d33; } diff --git a/resources/src/mediawiki.widgets/mw.widgets.SearchInputWidget.js b/resources/src/mediawiki.widgets/mw.widgets.SearchInputWidget.js index 39bee7c119..2ac75c59f2 100755 --- a/resources/src/mediawiki.widgets/mw.widgets.SearchInputWidget.js +++ b/resources/src/mediawiki.widgets/mw.widgets.SearchInputWidget.js @@ -78,7 +78,7 @@ * @inheritdoc mw.widgets.TitleWidget */ mw.widgets.SearchInputWidget.prototype.getSuggestionsPromise = function () { - var api = new mw.Api(), + var api = this.getApi(), promise, self = this; diff --git a/resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js b/resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js index e1e50ea17a..0e5e0c5ecf 100644 --- a/resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js +++ b/resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js @@ -6,16 +6,6 @@ */ ( function ( $, mw ) { - var interwikiPrefixesPromise = new mw.Api().get( { - action: 'query', - meta: 'siteinfo', - siprop: 'interwikimap' - } ).then( function ( data ) { - return $.map( data.query.interwikimap, function ( interwiki ) { - return interwiki.prefix; - } ); - } ); - /** * Mixin for title widgets * @@ -36,6 +26,7 @@ * @cfg {boolean} [validateTitle=true] Whether the input must be a valid title (if set to true, * the widget will marks itself red for invalid inputs, including an empty query). * @cfg {Object} [cache] Result cache which implements a 'set' method, taking keyed values as an argument + * @cfg {mw.Api} [api] API object to use, creates a default mw.Api instance if not specified */ mw.widgets.TitleWidget = function MwWidgetsTitleWidget( config ) { // Config initialization @@ -56,6 +47,7 @@ this.excludeCurrentPage = !!config.excludeCurrentPage; this.validateTitle = config.validateTitle !== undefined ? config.validateTitle : true; this.cache = config.cache; + this.api = config.api || new mw.Api(); // Initialization this.$element.addClass( 'mw-widget-titleWidget' ); @@ -65,6 +57,10 @@ OO.initClass( mw.widgets.TitleWidget ); + /* Static properties */ + + mw.widgets.TitleWidget.static.interwikiPrefixesPromiseCache = {}; + /* Methods */ /** @@ -93,6 +89,24 @@ this.namespace = namespace; }; + mw.widgets.TitleWidget.prototype.getInterwikiPrefixesPromise = function () { + var api = this.getApi(), + cache = this.constructor.static.interwikiPrefixesPromiseCache, + key = api.defaults.ajax.url; + if ( !cache.hasOwnProperty( key ) ) { + cache[ key ] = api.get( { + action: 'query', + meta: 'siteinfo', + siprop: 'interwikimap' + } ).then( function ( data ) { + return $.map( data.query.interwikimap, function ( interwiki ) { + return interwiki.prefix; + } ); + } ); + } + return cache[ key ]; + }; + /** * Get a promise which resolves with an API repsonse for suggested * links for the current query. @@ -101,6 +115,7 @@ */ mw.widgets.TitleWidget.prototype.getSuggestionsPromise = function () { var req, + api = this.getApi(), query = this.getQueryValue(), widget = this, promiseAbortObject = { abort: function () { @@ -108,7 +123,7 @@ } }; if ( mw.Title.newFromText( query ) ) { - return interwikiPrefixesPromise.then( function ( interwikiPrefixes ) { + return this.getInterwikiPrefixesPromise().then( function ( interwikiPrefixes ) { var params, interwiki = query.substring( 0, query.indexOf( ':' ) ); if ( @@ -142,11 +157,11 @@ params.prop.push( 'pageterms' ); params.wbptterms = 'description'; } - req = new mw.Api().get( params ); + req = api.get( params ); promiseAbortObject.abort = req.abort.bind( req ); // TODO ew return req.then( function ( ret ) { if ( ret.query === undefined ) { - ret = new mw.Api().get( { action: 'query', titles: query } ); + ret = api.get( { action: 'query', titles: query } ); promiseAbortObject.abort = ret.abort.bind( ret ); } return ret; @@ -160,6 +175,15 @@ } }; + /** + * Get the API object for title requests + * + * @return {mw.Api} MediaWiki API + */ + mw.widgets.TitleWidget.prototype.getApi = function () { + return this.api; + }; + /** * Get option widgets from the server response * diff --git a/resources/src/mediawiki/api.js b/resources/src/mediawiki/api.js index b9e05c368f..0c08ca4b69 100644 --- a/resources/src/mediawiki/api.js +++ b/resources/src/mediawiki/api.js @@ -334,8 +334,8 @@ } ); } - // Different error, pass on to let caller handle the error code - return this; + // Let caller handle the error code + return $.Deferred().rejectWith( this, arguments ); } ); } ).promise( { abort: function () { @@ -363,6 +363,10 @@ promiseGroup = promises[ this.defaults.ajax.url ]; d = promiseGroup && promiseGroup[ type + 'Token' ]; + if ( !promiseGroup ) { + promiseGroup = promises[ this.defaults.ajax.url ] = {}; + } + if ( !d ) { apiPromise = this.get( { action: 'query', @@ -382,16 +386,13 @@ // Clear promise. Do not cache errors. delete promiseGroup[ type + 'Token' ]; - // Pass on to allow the caller to handle the error - return this; + // Let caller handle the error code + return $.Deferred().rejectWith( this, arguments ); } ) // Attach abort handler .promise( { abort: apiPromise.abort } ); // Store deferred now so that we can use it again even if it isn't ready yet - if ( !promiseGroup ) { - promiseGroup = promises[ this.defaults.ajax.url ] = {}; - } promiseGroup[ type + 'Token' ] = d; } diff --git a/resources/src/mediawiki/mediawiki.storage.js b/resources/src/mediawiki/mediawiki.storage.js index a9d17ff70d..20f8efb659 100644 --- a/resources/src/mediawiki/mediawiki.storage.js +++ b/resources/src/mediawiki/mediawiki.storage.js @@ -1,65 +1,91 @@ ( function ( mw ) { 'use strict'; - /** - * Library for storing device specific information. It should be used for storing simple - * strings and is not suitable for storing large chunks of data. - * - * @class mw.storage - * @singleton - */ - mw.storage = { - - localStorage: ( function () { - // Catch exceptions to avoid fatal in Chrome's "Block data storage" mode - // which throws when accessing the localStorage property itself, as opposed - // to the standard behaviour of throwing on getItem/setItem. (T148998) + // Catch exceptions to avoid fatal in Chrome's "Block data storage" mode + // which throws when accessing the localStorage property itself, as opposed + // to the standard behaviour of throwing on getItem/setItem. (T148998) + var + localStorage = ( function () { try { return window.localStorage; } catch ( e ) {} }() ), - - /** - * Retrieve value from device storage. - * - * @param {string} key Key of item to retrieve - * @return {string|boolean} False when localStorage not available, otherwise string - */ - get: function ( key ) { + sessionStorage = ( function () { try { - return mw.storage.localStorage.getItem( key ); + return window.sessionStorage; } catch ( e ) {} - return false; - }, + }() ); - /** - * Set a value in device storage. - * - * @param {string} key Key name to store under - * @param {string} value Value to be stored - * @return {boolean} Whether the save succeeded or not - */ - set: function ( key, value ) { - try { - mw.storage.localStorage.setItem( key, value ); - return true; - } catch ( e ) {} - return false; - }, + /** + * A wrapper for an HTML5 Storage interface (`localStorage` or `sessionStorage`) + * that is safe to call on all browsers. + * + * @class mw.SafeStorage + * @private + */ - /** - * Remove a value from device storage. - * - * @param {string} key Key of item to remove - * @return {boolean} Whether the save succeeded or not - */ - remove: function ( key ) { - try { - mw.storage.localStorage.removeItem( key ); - return true; - } catch ( e ) {} - return false; - } + /** + * @ignore + * @param {Object|undefined} store The Storage instance to wrap around + */ + function SafeStorage( store ) { + this.store = store; + } + + /** + * Retrieve value from device storage. + * + * @param {string} key Key of item to retrieve + * @return {string|boolean} False when localStorage not available, otherwise string + */ + SafeStorage.prototype.get = function ( key ) { + try { + return this.store.getItem( key ); + } catch ( e ) {} + return false; + }; + + /** + * Set a value in device storage. + * + * @param {string} key Key name to store under + * @param {string} value Value to be stored + * @return {boolean} Whether the save succeeded or not + */ + SafeStorage.prototype.set = function ( key, value ) { + try { + this.store.setItem( key, value ); + return true; + } catch ( e ) {} + return false; + }; + + /** + * Remove a value from device storage. + * + * @param {string} key Key of item to remove + * @return {boolean} Whether the save succeeded or not + */ + SafeStorage.prototype.remove = function ( key ) { + try { + this.store.removeItem( key ); + return true; + } catch ( e ) {} + return false; }; + /** + * @class + * @singleton + * @extends mw.SafeStorage + */ + mw.storage = new SafeStorage( localStorage ); + + /** + * @class + * @singleton + * @extends mw.SafeStorage + */ + mw.storage.session = new SafeStorage( sessionStorage ); + }( mediaWiki ) ); diff --git a/resources/src/mediawiki/page/gallery.css b/resources/src/mediawiki/page/gallery.css index 474d541d83..b7a9132dfe 100644 --- a/resources/src/mediawiki/page/gallery.css +++ b/resources/src/mediawiki/page/gallery.css @@ -28,8 +28,8 @@ li.gallerycaption { li.gallerybox div.thumb { text-align: center; - border: 1px solid #ccc; - background-color: #f9f9f9; + border: 1px solid #c8ccd1; + background-color: #f8f9fa; margin: 2px; } diff --git a/tests/common/TestsAutoLoader.php b/tests/common/TestsAutoLoader.php index 66df315e10..b67c9abfee 100644 --- a/tests/common/TestsAutoLoader.php +++ b/tests/common/TestsAutoLoader.php @@ -29,11 +29,13 @@ $wgAutoloadClasses += [ # tests/common 'TestSetup' => "$testDir/common/TestSetup.php", + # tests/integration + 'MWHttpRequestTestCase' => "$testDir/integration/includes/http/MWHttpRequestTestCase.php", + # tests/parser 'DbTestPreviewer' => "$testDir/parser/DbTestPreviewer.php", 'DbTestRecorder' => "$testDir/parser/DbTestRecorder.php", 'DjVuSupport' => "$testDir/parser/DjVuSupport.php", - 'TestRecorder' => "$testDir/parser/TestRecorder.php", 'MultiTestRecorder' => "$testDir/parser/MultiTestRecorder.php", 'ParserTestMockParser' => "$testDir/parser/ParserTestMockParser.php", 'ParserTestRunner' => "$testDir/parser/ParserTestRunner.php", diff --git a/tests/integration/includes/http/CurlHttpRequestTest.php b/tests/integration/includes/http/CurlHttpRequestTest.php new file mode 100644 index 0000000000..04f80f434f --- /dev/null +++ b/tests/integration/includes/http/CurlHttpRequestTest.php @@ -0,0 +1,5 @@ +oldHttpEngine = Http::$httpEngine; + Http::$httpEngine = static::$httpEngine; + + try { + $request = MWHttpRequest::factory( 'null:' ); + } catch ( DomainException $e ) { + $this->markTestSkipped( static::$httpEngine . ' engine not supported' ); + } + + if ( static::$httpEngine === 'php' ) { + $this->assertInstanceOf( PhpHttpRequest::class, $request ); + } else { + $this->assertInstanceOf( CurlHttpRequest::class, $request ); + } + } + + public function tearDown() { + parent::tearDown(); + Http::$httpEngine = $this->oldHttpEngine; + } + + // -------------------- + + public function testIsRedirect() { + $request = MWHttpRequest::factory( 'http://httpbin.org/get' ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertFalse( $request->isRedirect() ); + + $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/1' ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertTrue( $request->isRedirect() ); + } + + public function testgetFinalUrl() { + $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/3' ); + if ( !$request->canFollowRedirects() ) { + $this->markTestSkipped( 'cannot follow redirects' ); + } + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertNotSame( 'http://httpbin.org/get', $request->getFinalUrl() ); + + $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/3', [ 'followRedirects' + => true ] ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertSame( 'http://httpbin.org/get', $request->getFinalUrl() ); + $this->assertResponseFieldValue( 'url', 'http://httpbin.org/get', $request ); + + $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/3', [ 'followRedirects' + => true ] ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertSame( 'http://httpbin.org/get', $request->getFinalUrl() ); + $this->assertResponseFieldValue( 'url', 'http://httpbin.org/get', $request ); + + if ( static::$httpEngine === 'curl' ) { + $this->markTestIncomplete( 'maxRedirects seems to be ignored by CurlHttpRequest' ); + return; + } + + $request = MWHttpRequest::factory( 'http://httpbin.org/redirect/3', [ 'followRedirects' + => true, 'maxRedirects' => 1 ] ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertNotSame( 'http://httpbin.org/get', $request->getFinalUrl() ); + } + + public function testSetCookie() { + $request = MWHttpRequest::factory( 'http://httpbin.org/cookies' ); + $request->setCookie( 'foo', 'bar' ); + $request->setCookie( 'foo2', 'bar2', [ 'domain' => 'example.com' ] ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertResponseFieldValue( 'cookies', [ 'foo' => 'bar' ], $request ); + } + + public function testSetCookieJar() { + $request = MWHttpRequest::factory( 'http://httpbin.org/cookies' ); + $cookieJar = new CookieJar(); + $cookieJar->setCookie( 'foo', 'bar', [ 'domain' => 'httpbin.org' ] ); + $cookieJar->setCookie( 'foo2', 'bar2', [ 'domain' => 'example.com' ] ); + $request->setCookieJar( $cookieJar ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertResponseFieldValue( 'cookies', [ 'foo' => 'bar' ], $request ); + + $request = MWHttpRequest::factory( 'http://httpbin.org/cookies/set?foo=bar' ); + $cookieJar = new CookieJar(); + $request->setCookieJar( $cookieJar ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertHasCookie( 'foo', 'bar', $request->getCookieJar() ); + + $this->markTestIncomplete( 'CookieJar does not handle deletion' ); + return; + + $request = MWHttpRequest::factory( 'http://httpbin.org/cookies/delete?foo' ); + $cookieJar = new CookieJar(); + $cookieJar->setCookie( 'foo', 'bar', [ 'domain' => 'httpbin.org' ] ); + $cookieJar->setCookie( 'foo2', 'bar2', [ 'domain' => 'httpbin.org' ] ); + $request->setCookieJar( $cookieJar ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertNotHasCookie( 'foo', $request->getCookieJar() ); + $this->assertHasCookie( 'foo2', 'bar2', $request->getCookieJar() ); + } + + public function testGetResponseHeaders() { + $request = MWHttpRequest::factory( 'http://httpbin.org/response-headers?Foo=bar' ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $headers = array_change_key_case( $request->getResponseHeaders(), CASE_LOWER ); + $this->assertArrayHasKey( 'foo', $headers ); + $this->assertSame( $request->getResponseHeader( 'Foo' ), 'bar' ); + } + + public function testSetHeader() { + $request = MWHttpRequest::factory( 'http://httpbin.org/headers' ); + $request->setHeader( 'Foo', 'bar' ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertResponseFieldValue( [ 'headers', 'Foo' ], 'bar', $request ); + } + + public function testGetStatus() { + $request = MWHttpRequest::factory( 'http://httpbin.org/status/418' ); + $status = $request->execute(); + $this->assertFalse( $status->isOK() ); + $this->assertSame( $request->getStatus(), 418 ); + } + + public function testSetUserAgent() { + $request = MWHttpRequest::factory( 'http://httpbin.org/user-agent' ); + $request->setUserAgent( 'foo' ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertResponseFieldValue( 'user-agent', 'foo', $request ); + } + + public function testSetData() { + $request = MWHttpRequest::factory( 'http://httpbin.org/post', [ 'method' => 'POST' ] ); + $request->setData( [ 'foo' => 'bar', 'foo2' => 'bar2' ] ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $this->assertResponseFieldValue( 'form', [ 'foo' => 'bar', 'foo2' => 'bar2' ], $request ); + } + + public function testSetCallback() { + if ( static::$httpEngine === 'php' ) { + $this->markTestIncomplete( 'PhpHttpRequest does not use setCallback()' ); + return; + } + + $request = MWHttpRequest::factory( 'http://httpbin.org/ip' ); + $data = ''; + $request->setCallback( function ( $fh, $content ) use ( &$data ) { + $data .= $content; + return strlen( $content ); + } ); + $status = $request->execute(); + $this->assertTrue( $status->isGood() ); + $data = json_decode( $data, true ); + $this->assertInternalType( 'array', $data ); + $this->assertArrayHasKey( 'origin', $data ); + } + + // -------------------- + + protected function assertResponseFieldValue( $key, $expectedValue, MWHttpRequest $response ) { + $this->assertSame( 200, $response->getStatus(), 'response status is not 200' ); + $data = json_decode( $response->getContent(), true ); + $this->assertInternalType( 'array', $data, 'response is not JSON' ); + $keyPath = ''; + foreach ( (array)$key as $keySegment ) { + $keyPath .= ( $keyPath ? '.' : '' ) . $keySegment; + $this->assertArrayHasKey( $keySegment, $data, $keyPath . ' not found' ); + $data = $data[$keySegment]; + } + $this->assertSame( $expectedValue, $data ); + } + + protected function assertHasCookie( $expectedName, $expectedValue, CookieJar $cookieJar ) { + $cookieJar = TestingAccessWrapper::newFromObject( $cookieJar ); + $cookies = array_change_key_case( $cookieJar->cookie, CASE_LOWER ); + $this->assertArrayHasKey( strtolower( $expectedName ), $cookies ); + $cookie = TestingAccessWrapper::newFromObject( + $cookies[strtolower( $expectedName )] ); + $this->assertSame( $expectedValue, $cookie->value ); + } + + protected function assertNotHasCookie( $name, CookieJar $cookieJar ) { + $cookieJar = TestingAccessWrapper::newFromObject( $cookieJar ); + $this->assertArrayNotHasKey( strtolower( $name ), + array_change_key_case( $cookieJar->cookie, CASE_LOWER ) ); + } +} + diff --git a/tests/integration/includes/http/PhpHttpRequestTest.php b/tests/integration/includes/http/PhpHttpRequestTest.php new file mode 100644 index 0000000000..d0222a5e76 --- /dev/null +++ b/tests/integration/includes/http/PhpHttpRequestTest.php @@ -0,0 +1,5 @@ +assertEquals( $expected, $ok, $msg ); - } - - public static function cookieDomains() { - return [ - [ false, "org" ], - [ false, ".org" ], - [ true, "wikipedia.org" ], - [ true, ".wikipedia.org" ], - [ false, "co.uk" ], - [ false, ".co.uk" ], - [ false, "gov.uk" ], - [ false, ".gov.uk" ], - [ true, "supermarket.uk" ], - [ false, "uk" ], - [ false, ".uk" ], - [ false, "127.0.0." ], - [ false, "127." ], - [ false, "127.0.0.1." ], - [ true, "127.0.0.1" ], - [ false, "333.0.0.1" ], - [ true, "example.com" ], - [ false, "example.com." ], - [ true, ".example.com" ], - - [ true, ".example.com", "www.example.com" ], - [ false, "example.com", "www.example.com" ], - [ true, "127.0.0.1", "127.0.0.1" ], - [ false, "127.0.0.1", "localhost" ], - ]; - } - - /** - * Test Http::isValidURI() - * @bug 27854 : Http::isValidURI is too lax - * @dataProvider provideURI - * @covers Http::isValidURI - */ - public function testIsValidUri( $expect, $URI, $message = '' ) { - $this->assertEquals( - $expect, - (bool)Http::isValidURI( $URI ), - $message - ); - } - - /** - * @covers Http::getProxy - */ - public function testGetProxy() { - $this->setMwGlobals( 'wgHTTPProxy', 'proxy.domain.tld' ); - $this->assertEquals( - 'proxy.domain.tld', - Http::getProxy() - ); - } - - /** - * Feeds URI to test a long regular expression in Http::isValidURI - */ - public static function provideURI() { - /** Format: 'boolean expectation', 'URI to test', 'Optional message' */ - return [ - [ false, '¿non sens before!! http://a', 'Allow anything before URI' ], - - # (http|https) - only two schemes allowed - [ true, 'http://www.example.org/' ], - [ true, 'https://www.example.org/' ], - [ true, 'http://www.example.org', 'URI without directory' ], - [ true, 'http://a', 'Short name' ], - [ true, 'http://étoile', 'Allow UTF-8 in hostname' ], # 'étoile' is french for 'star' - [ false, '\\host\directory', 'CIFS share' ], - [ false, 'gopher://host/dir', 'Reject gopher scheme' ], - [ false, 'telnet://host', 'Reject telnet scheme' ], - - # :\/\/ - double slashes - [ false, 'http//example.org', 'Reject missing colon in protocol' ], - [ false, 'http:/example.org', 'Reject missing slash in protocol' ], - [ false, 'http:example.org', 'Must have two slashes' ], - # Following fail since hostname can be made of anything - [ false, 'http:///example.org', 'Must have exactly two slashes, not three' ], - - # (\w+:{0,1}\w*@)? - optional user:pass - [ true, 'http://user@host', 'Username provided' ], - [ true, 'http://user:@host', 'Username provided, no password' ], - [ true, 'http://user:pass@host', 'Username and password provided' ], - - # (\S+) - host part is made of anything not whitespaces - // commented these out in order to remove @group Broken - // @todo are these valid tests? if so, fix Http::isValidURI so it can handle them - // [ false, 'http://!"èèè¿¿¿~~\'', 'hostname is made of any non whitespace' ], - // [ false, 'http://exam:ple.org/', 'hostname can not use colons!' ], - - # (:[0-9]+)? - port number - [ true, 'http://example.org:80/' ], - [ true, 'https://example.org:80/' ], - [ true, 'http://example.org:443/' ], - [ true, 'https://example.org:443/' ], - - # Part after the hostname is / or / with something else - [ true, 'http://example/#' ], - [ true, 'http://example/!' ], - [ true, 'http://example/:' ], - [ true, 'http://example/.' ], - [ true, 'http://example/?' ], - [ true, 'http://example/+' ], - [ true, 'http://example/=' ], - [ true, 'http://example/&' ], - [ true, 'http://example/%' ], - [ true, 'http://example/@' ], - [ true, 'http://example/-' ], - [ true, 'http://example//' ], - [ true, 'http://example/&' ], - - # Fragment - [ true, 'http://exam#ple.org', ], # This one is valid, really! - [ true, 'http://example.org:80#anchor' ], - [ true, 'http://example.org/?id#anchor' ], - [ true, 'http://example.org/?#anchor' ], - - [ false, 'http://a ¿non !!sens after', 'Allow anything after URI' ], - ]; - } - - /** - * Warning: - * - * These tests are for code that makes use of an artifact of how CURL - * handles header reporting on redirect pages, and will need to be - * rewritten when bug 29232 is taken care of (high-level handling of - * HTTP redirects). - */ - public function testRelativeRedirections() { - $h = MWHttpRequestTester::factory( 'http://oldsite/file.ext', [], __METHOD__ ); - - # Forge a Location header - $h->setRespHeaders( 'location', [ - 'http://newsite/file.ext', - '/newfile.ext', - ] - ); - # Verify we correctly fix the Location - $this->assertEquals( - 'http://newsite/newfile.ext', - $h->getFinalUrl(), - "Relative file path Location: interpreted as full URL" - ); - - $h->setRespHeaders( 'location', [ - 'https://oldsite/file.ext' - ] - ); - $this->assertEquals( - 'https://oldsite/file.ext', - $h->getFinalUrl(), - "Location to the HTTPS version of the site" - ); - - $h->setRespHeaders( 'location', [ - '/anotherfile.ext', - 'http://anotherfile/hoster.ext', - 'https://anotherfile/hoster.ext' - ] - ); - $this->assertEquals( - 'https://anotherfile/hoster.ext', - $h->getFinalUrl( "Relative file path Location: should keep the latest host and scheme!" ) - ); - } - - /** - * Constant values are from PHP 5.3.28 using cURL 7.24.0 - * @see https://secure.php.net/manual/en/curl.constants.php - * - * All constant values are present so that developers don’t need to remember - * to add them if added at a later date. The commented out constants were - * not found anywhere in the MediaWiki core code. - * - * Commented out constants that were not available in: - * HipHop VM 3.3.0 (rel) - * Compiler: heads/master-0-g08810d920dfff59e0774cf2d651f92f13a637175 - * Repo schema: 3214fc2c684a4520485f715ee45f33f2182324b1 - * Extension API: 20140829 - * - * Commented out constants that were removed in PHP 5.6.0 - * - * @covers CurlHttpRequest::execute - */ - public function provideCurlConstants() { - return [ - [ 'CURLAUTH_ANY' ], - [ 'CURLAUTH_ANYSAFE' ], - [ 'CURLAUTH_BASIC' ], - [ 'CURLAUTH_DIGEST' ], - [ 'CURLAUTH_GSSNEGOTIATE' ], - [ 'CURLAUTH_NTLM' ], - // [ 'CURLCLOSEPOLICY_CALLBACK' ], // removed in PHP 5.6.0 - // [ 'CURLCLOSEPOLICY_LEAST_RECENTLY_USED' ], // removed in PHP 5.6.0 - // [ 'CURLCLOSEPOLICY_LEAST_TRAFFIC' ], // removed in PHP 5.6.0 - // [ 'CURLCLOSEPOLICY_OLDEST' ], // removed in PHP 5.6.0 - // [ 'CURLCLOSEPOLICY_SLOWEST' ], // removed in PHP 5.6.0 - [ 'CURLE_ABORTED_BY_CALLBACK' ], - [ 'CURLE_BAD_CALLING_ORDER' ], - [ 'CURLE_BAD_CONTENT_ENCODING' ], - [ 'CURLE_BAD_FUNCTION_ARGUMENT' ], - [ 'CURLE_BAD_PASSWORD_ENTERED' ], - [ 'CURLE_COULDNT_CONNECT' ], - [ 'CURLE_COULDNT_RESOLVE_HOST' ], - [ 'CURLE_COULDNT_RESOLVE_PROXY' ], - [ 'CURLE_FAILED_INIT' ], - [ 'CURLE_FILESIZE_EXCEEDED' ], - [ 'CURLE_FILE_COULDNT_READ_FILE' ], - [ 'CURLE_FTP_ACCESS_DENIED' ], - [ 'CURLE_FTP_BAD_DOWNLOAD_RESUME' ], - [ 'CURLE_FTP_CANT_GET_HOST' ], - [ 'CURLE_FTP_CANT_RECONNECT' ], - [ 'CURLE_FTP_COULDNT_GET_SIZE' ], - [ 'CURLE_FTP_COULDNT_RETR_FILE' ], - [ 'CURLE_FTP_COULDNT_SET_ASCII' ], - [ 'CURLE_FTP_COULDNT_SET_BINARY' ], - [ 'CURLE_FTP_COULDNT_STOR_FILE' ], - [ 'CURLE_FTP_COULDNT_USE_REST' ], - [ 'CURLE_FTP_PORT_FAILED' ], - [ 'CURLE_FTP_QUOTE_ERROR' ], - [ 'CURLE_FTP_SSL_FAILED' ], - [ 'CURLE_FTP_USER_PASSWORD_INCORRECT' ], - [ 'CURLE_FTP_WEIRD_227_FORMAT' ], - [ 'CURLE_FTP_WEIRD_PASS_REPLY' ], - [ 'CURLE_FTP_WEIRD_PASV_REPLY' ], - [ 'CURLE_FTP_WEIRD_SERVER_REPLY' ], - [ 'CURLE_FTP_WEIRD_USER_REPLY' ], - [ 'CURLE_FTP_WRITE_ERROR' ], - [ 'CURLE_FUNCTION_NOT_FOUND' ], - [ 'CURLE_GOT_NOTHING' ], - [ 'CURLE_HTTP_NOT_FOUND' ], - [ 'CURLE_HTTP_PORT_FAILED' ], - [ 'CURLE_HTTP_POST_ERROR' ], - [ 'CURLE_HTTP_RANGE_ERROR' ], - [ 'CURLE_LDAP_CANNOT_BIND' ], - [ 'CURLE_LDAP_INVALID_URL' ], - [ 'CURLE_LDAP_SEARCH_FAILED' ], - [ 'CURLE_LIBRARY_NOT_FOUND' ], - [ 'CURLE_MALFORMAT_USER' ], - [ 'CURLE_OBSOLETE' ], - [ 'CURLE_OK' ], - [ 'CURLE_OPERATION_TIMEOUTED' ], - [ 'CURLE_OUT_OF_MEMORY' ], - [ 'CURLE_PARTIAL_FILE' ], - [ 'CURLE_READ_ERROR' ], - [ 'CURLE_RECV_ERROR' ], - [ 'CURLE_SEND_ERROR' ], - [ 'CURLE_SHARE_IN_USE' ], - // [ 'CURLE_SSH' ], // not present in HHVM 3.3.0-dev - [ 'CURLE_SSL_CACERT' ], - [ 'CURLE_SSL_CERTPROBLEM' ], - [ 'CURLE_SSL_CIPHER' ], - [ 'CURLE_SSL_CONNECT_ERROR' ], - [ 'CURLE_SSL_ENGINE_NOTFOUND' ], - [ 'CURLE_SSL_ENGINE_SETFAILED' ], - [ 'CURLE_SSL_PEER_CERTIFICATE' ], - [ 'CURLE_TELNET_OPTION_SYNTAX' ], - [ 'CURLE_TOO_MANY_REDIRECTS' ], - [ 'CURLE_UNKNOWN_TELNET_OPTION' ], - [ 'CURLE_UNSUPPORTED_PROTOCOL' ], - [ 'CURLE_URL_MALFORMAT' ], - [ 'CURLE_URL_MALFORMAT_USER' ], - [ 'CURLE_WRITE_ERROR' ], - [ 'CURLFTPAUTH_DEFAULT' ], - [ 'CURLFTPAUTH_SSL' ], - [ 'CURLFTPAUTH_TLS' ], - // [ 'CURLFTPMETHOD_MULTICWD' ], // not present in HHVM 3.3.0-dev - // [ 'CURLFTPMETHOD_NOCWD' ], // not present in HHVM 3.3.0-dev - // [ 'CURLFTPMETHOD_SINGLECWD' ], // not present in HHVM 3.3.0-dev - [ 'CURLFTPSSL_ALL' ], - [ 'CURLFTPSSL_CONTROL' ], - [ 'CURLFTPSSL_NONE' ], - [ 'CURLFTPSSL_TRY' ], - // [ 'CURLINFO_CERTINFO' ], // not present in HHVM 3.3.0-dev - [ 'CURLINFO_CONNECT_TIME' ], - [ 'CURLINFO_CONTENT_LENGTH_DOWNLOAD' ], - [ 'CURLINFO_CONTENT_LENGTH_UPLOAD' ], - [ 'CURLINFO_CONTENT_TYPE' ], - [ 'CURLINFO_EFFECTIVE_URL' ], - [ 'CURLINFO_FILETIME' ], - [ 'CURLINFO_HEADER_OUT' ], - [ 'CURLINFO_HEADER_SIZE' ], - [ 'CURLINFO_HTTP_CODE' ], - [ 'CURLINFO_NAMELOOKUP_TIME' ], - [ 'CURLINFO_PRETRANSFER_TIME' ], - [ 'CURLINFO_PRIVATE' ], - [ 'CURLINFO_REDIRECT_COUNT' ], - [ 'CURLINFO_REDIRECT_TIME' ], - // [ 'CURLINFO_REDIRECT_URL' ], // not present in HHVM 3.3.0-dev - [ 'CURLINFO_REQUEST_SIZE' ], - [ 'CURLINFO_SIZE_DOWNLOAD' ], - [ 'CURLINFO_SIZE_UPLOAD' ], - [ 'CURLINFO_SPEED_DOWNLOAD' ], - [ 'CURLINFO_SPEED_UPLOAD' ], - [ 'CURLINFO_SSL_VERIFYRESULT' ], - [ 'CURLINFO_STARTTRANSFER_TIME' ], - [ 'CURLINFO_TOTAL_TIME' ], - [ 'CURLMSG_DONE' ], - [ 'CURLM_BAD_EASY_HANDLE' ], - [ 'CURLM_BAD_HANDLE' ], - [ 'CURLM_CALL_MULTI_PERFORM' ], - [ 'CURLM_INTERNAL_ERROR' ], - [ 'CURLM_OK' ], - [ 'CURLM_OUT_OF_MEMORY' ], - [ 'CURLOPT_AUTOREFERER' ], - [ 'CURLOPT_BINARYTRANSFER' ], - [ 'CURLOPT_BUFFERSIZE' ], - [ 'CURLOPT_CAINFO' ], - [ 'CURLOPT_CAPATH' ], - // [ 'CURLOPT_CERTINFO' ], // not present in HHVM 3.3.0-dev - // [ 'CURLOPT_CLOSEPOLICY' ], // removed in PHP 5.6.0 - [ 'CURLOPT_CONNECTTIMEOUT' ], - [ 'CURLOPT_CONNECTTIMEOUT_MS' ], - [ 'CURLOPT_COOKIE' ], - [ 'CURLOPT_COOKIEFILE' ], - [ 'CURLOPT_COOKIEJAR' ], - [ 'CURLOPT_COOKIESESSION' ], - [ 'CURLOPT_CRLF' ], - [ 'CURLOPT_CUSTOMREQUEST' ], - [ 'CURLOPT_DNS_CACHE_TIMEOUT' ], - [ 'CURLOPT_DNS_USE_GLOBAL_CACHE' ], - [ 'CURLOPT_EGDSOCKET' ], - [ 'CURLOPT_ENCODING' ], - [ 'CURLOPT_FAILONERROR' ], - [ 'CURLOPT_FILE' ], - [ 'CURLOPT_FILETIME' ], - [ 'CURLOPT_FOLLOWLOCATION' ], - [ 'CURLOPT_FORBID_REUSE' ], - [ 'CURLOPT_FRESH_CONNECT' ], - [ 'CURLOPT_FTPAPPEND' ], - [ 'CURLOPT_FTPLISTONLY' ], - [ 'CURLOPT_FTPPORT' ], - [ 'CURLOPT_FTPSSLAUTH' ], - [ 'CURLOPT_FTP_CREATE_MISSING_DIRS' ], - // [ 'CURLOPT_FTP_FILEMETHOD' ], // not present in HHVM 3.3.0-dev - // [ 'CURLOPT_FTP_SKIP_PASV_IP' ], // not present in HHVM 3.3.0-dev - [ 'CURLOPT_FTP_SSL' ], - [ 'CURLOPT_FTP_USE_EPRT' ], - [ 'CURLOPT_FTP_USE_EPSV' ], - [ 'CURLOPT_HEADER' ], - [ 'CURLOPT_HEADERFUNCTION' ], - [ 'CURLOPT_HTTP200ALIASES' ], - [ 'CURLOPT_HTTPAUTH' ], - [ 'CURLOPT_HTTPGET' ], - [ 'CURLOPT_HTTPHEADER' ], - [ 'CURLOPT_HTTPPROXYTUNNEL' ], - [ 'CURLOPT_HTTP_VERSION' ], - [ 'CURLOPT_INFILE' ], - [ 'CURLOPT_INFILESIZE' ], - [ 'CURLOPT_INTERFACE' ], - [ 'CURLOPT_IPRESOLVE' ], - // [ 'CURLOPT_KEYPASSWD' ], // not present in HHVM 3.3.0-dev - [ 'CURLOPT_KRB4LEVEL' ], - [ 'CURLOPT_LOW_SPEED_LIMIT' ], - [ 'CURLOPT_LOW_SPEED_TIME' ], - [ 'CURLOPT_MAXCONNECTS' ], - [ 'CURLOPT_MAXREDIRS' ], - // [ 'CURLOPT_MAX_RECV_SPEED_LARGE' ], // not present in HHVM 3.3.0-dev - // [ 'CURLOPT_MAX_SEND_SPEED_LARGE' ], // not present in HHVM 3.3.0-dev - [ 'CURLOPT_NETRC' ], - [ 'CURLOPT_NOBODY' ], - [ 'CURLOPT_NOPROGRESS' ], - [ 'CURLOPT_NOSIGNAL' ], - [ 'CURLOPT_PORT' ], - [ 'CURLOPT_POST' ], - [ 'CURLOPT_POSTFIELDS' ], - [ 'CURLOPT_POSTQUOTE' ], - [ 'CURLOPT_POSTREDIR' ], - [ 'CURLOPT_PRIVATE' ], - [ 'CURLOPT_PROGRESSFUNCTION' ], - // [ 'CURLOPT_PROTOCOLS' ], // not present in HHVM 3.3.0-dev - [ 'CURLOPT_PROXY' ], - [ 'CURLOPT_PROXYAUTH' ], - [ 'CURLOPT_PROXYPORT' ], - [ 'CURLOPT_PROXYTYPE' ], - [ 'CURLOPT_PROXYUSERPWD' ], - [ 'CURLOPT_PUT' ], - [ 'CURLOPT_QUOTE' ], - [ 'CURLOPT_RANDOM_FILE' ], - [ 'CURLOPT_RANGE' ], - [ 'CURLOPT_READDATA' ], - [ 'CURLOPT_READFUNCTION' ], - // [ 'CURLOPT_REDIR_PROTOCOLS' ], // not present in HHVM 3.3.0-dev - [ 'CURLOPT_REFERER' ], - [ 'CURLOPT_RESUME_FROM' ], - [ 'CURLOPT_RETURNTRANSFER' ], - // [ 'CURLOPT_SSH_AUTH_TYPES' ], // not present in HHVM 3.3.0-dev - // [ 'CURLOPT_SSH_HOST_PUBLIC_KEY_MD5' ], // not present in HHVM 3.3.0-dev - // [ 'CURLOPT_SSH_PRIVATE_KEYFILE' ], // not present in HHVM 3.3.0-dev - // [ 'CURLOPT_SSH_PUBLIC_KEYFILE' ], // not present in HHVM 3.3.0-dev - [ 'CURLOPT_SSLCERT' ], - [ 'CURLOPT_SSLCERTPASSWD' ], - [ 'CURLOPT_SSLCERTTYPE' ], - [ 'CURLOPT_SSLENGINE' ], - [ 'CURLOPT_SSLENGINE_DEFAULT' ], - [ 'CURLOPT_SSLKEY' ], - [ 'CURLOPT_SSLKEYPASSWD' ], - [ 'CURLOPT_SSLKEYTYPE' ], - [ 'CURLOPT_SSLVERSION' ], - [ 'CURLOPT_SSL_CIPHER_LIST' ], - [ 'CURLOPT_SSL_VERIFYHOST' ], - [ 'CURLOPT_SSL_VERIFYPEER' ], - [ 'CURLOPT_STDERR' ], - [ 'CURLOPT_TCP_NODELAY' ], - [ 'CURLOPT_TIMECONDITION' ], - [ 'CURLOPT_TIMEOUT' ], - [ 'CURLOPT_TIMEOUT_MS' ], - [ 'CURLOPT_TIMEVALUE' ], - [ 'CURLOPT_TRANSFERTEXT' ], - [ 'CURLOPT_UNRESTRICTED_AUTH' ], - [ 'CURLOPT_UPLOAD' ], - [ 'CURLOPT_URL' ], - [ 'CURLOPT_USERAGENT' ], - [ 'CURLOPT_USERPWD' ], - [ 'CURLOPT_VERBOSE' ], - [ 'CURLOPT_WRITEFUNCTION' ], - [ 'CURLOPT_WRITEHEADER' ], - // [ 'CURLPROTO_ALL' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_DICT' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_FILE' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_FTP' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_FTPS' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_HTTP' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_HTTPS' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_LDAP' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_LDAPS' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_SCP' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_SFTP' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_TELNET' ], // not present in HHVM 3.3.0-dev - // [ 'CURLPROTO_TFTP' ], // not present in HHVM 3.3.0-dev - [ 'CURLPROXY_HTTP' ], - // [ 'CURLPROXY_SOCKS4' ], // not present in HHVM 3.3.0-dev - [ 'CURLPROXY_SOCKS5' ], - // [ 'CURLSSH_AUTH_DEFAULT' ], // not present in HHVM 3.3.0-dev - // [ 'CURLSSH_AUTH_HOST' ], // not present in HHVM 3.3.0-dev - // [ 'CURLSSH_AUTH_KEYBOARD' ], // not present in HHVM 3.3.0-dev - // [ 'CURLSSH_AUTH_NONE' ], // not present in HHVM 3.3.0-dev - // [ 'CURLSSH_AUTH_PASSWORD' ], // not present in HHVM 3.3.0-dev - // [ 'CURLSSH_AUTH_PUBLICKEY' ], // not present in HHVM 3.3.0-dev - [ 'CURLVERSION_NOW' ], - [ 'CURL_HTTP_VERSION_1_0' ], - [ 'CURL_HTTP_VERSION_1_1' ], - [ 'CURL_HTTP_VERSION_NONE' ], - [ 'CURL_IPRESOLVE_V4' ], - [ 'CURL_IPRESOLVE_V6' ], - [ 'CURL_IPRESOLVE_WHATEVER' ], - [ 'CURL_NETRC_IGNORED' ], - [ 'CURL_NETRC_OPTIONAL' ], - [ 'CURL_NETRC_REQUIRED' ], - [ 'CURL_TIMECOND_IFMODSINCE' ], - [ 'CURL_TIMECOND_IFUNMODSINCE' ], - [ 'CURL_TIMECOND_LASTMOD' ], - [ 'CURL_VERSION_IPV6' ], - [ 'CURL_VERSION_KERBEROS4' ], - [ 'CURL_VERSION_LIBZ' ], - [ 'CURL_VERSION_SSL' ], - ]; - } - - /** - * Added this test based on an issue experienced with HHVM 3.3.0-dev - * where it did not define a cURL constant. - * - * @bug 70570 - * @dataProvider provideCurlConstants - */ - public function testCurlConstants( $value ) { - $this->assertTrue( defined( $value ), $value . ' not defined' ); - } -} - -/** - * Class to let us overwrite MWHttpRequest respHeaders variable - */ -class MWHttpRequestTester extends MWHttpRequest { - // function derived from the MWHttpRequest factory function but - // returns appropriate tester class here - public static function factory( $url, $options = null, $caller = __METHOD__ ) { - if ( !Http::$httpEngine ) { - Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php'; - } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) { - throw new MWException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' . - 'Http::$httpEngine is set to "curl"' ); - } - - switch ( Http::$httpEngine ) { - case 'curl': - return new CurlHttpRequestTester( $url, $options, $caller ); - case 'php': - if ( !wfIniGetBool( 'allow_url_fopen' ) ) { - throw new MWException( __METHOD__ . - ': allow_url_fopen needs to be enabled for pure PHP HTTP requests to work. ' - . 'If possible, curl should be used instead. See http://php.net/curl.' ); - } - - return new PhpHttpRequestTester( $url, $options, $caller ); - default: - } - } -} - -class CurlHttpRequestTester extends CurlHttpRequest { - function setRespHeaders( $name, $value ) { - $this->respHeaders[$name] = $value; - } -} - -class PhpHttpRequestTester extends PhpHttpRequest { - function setRespHeaders( $name, $value ) { - $this->respHeaders[$name] = $value; - } -} diff --git a/tests/phpunit/includes/MessageTest.php b/tests/phpunit/includes/MessageTest.php index e8afb4ccee..4fe806c438 100644 --- a/tests/phpunit/includes/MessageTest.php +++ b/tests/phpunit/includes/MessageTest.php @@ -512,6 +512,108 @@ class MessageTest extends MediaWikiLangTestCase { ); } + public static function provideListParam() { + $lang = Language::factory( 'de' ); + $msg1 = new Message( 'mainpage', [], $lang ); + $msg2 = new RawMessage( "''link''", [], $lang ); + + return [ + 'Simple comma list' => [ + [ 'a', 'b', 'c' ], + 'comma', + 'text', + 'a, b, c' + ], + + 'Simple semicolon list' => [ + [ 'a', 'b', 'c' ], + 'semicolon', + 'text', + 'a; b; c' + ], + + 'Simple pipe list' => [ + [ 'a', 'b', 'c' ], + 'pipe', + 'text', + 'a | b | c' + ], + + 'Simple text list' => [ + [ 'a', 'b', 'c' ], + 'text', + 'text', + 'a, b and c' + ], + + 'Empty list' => [ + [], + 'comma', + 'text', + '' + ], + + 'List with all "before" params, ->text()' => [ + [ "''link''", Message::numParam( 12345678 ) ], + 'semicolon', + 'text', + '\'\'link\'\'; 12,345,678' + ], + + 'List with all "before" params, ->parse()' => [ + [ "''link''", Message::numParam( 12345678 ) ], + 'semicolon', + 'parse', + 'link; 12,345,678' + ], + + 'List with all "after" params, ->text()' => [ + [ $msg1, $msg2, Message::rawParam( '[[foo]]' ) ], + 'semicolon', + 'text', + 'Main Page; \'\'link\'\'; [[foo]]' + ], + + 'List with all "after" params, ->parse()' => [ + [ $msg1, $msg2, Message::rawParam( '[[foo]]' ) ], + 'semicolon', + 'parse', + 'Main Page; link; [[foo]]' + ], + + 'List with both "before" and "after" params, ->text()' => [ + [ $msg1, $msg2, Message::rawParam( '[[foo]]' ), "''link''", Message::numParam( 12345678 ) ], + 'semicolon', + 'text', + 'Main Page; \'\'link\'\'; [[foo]]; \'\'link\'\'; 12,345,678' + ], + + 'List with both "before" and "after" params, ->parse()' => [ + [ $msg1, $msg2, Message::rawParam( '[[foo]]' ), "''link''", Message::numParam( 12345678 ) ], + 'semicolon', + 'parse', + 'Main Page; link; [[foo]]; link; 12,345,678' + ], + ]; + } + + /** + * @covers Message::listParam + * @covers Message::extractParam + * @covers Message::formatListParam + * @dataProvider provideListParam + */ + public function testListParam( $list, $type, $format, $expect ) { + $lang = Language::factory( 'en' ); + + $msg = new RawMessage( '$1' ); + $msg->params( [ Message::listParam( $list, $type ) ] ); + $this->assertEquals( + $expect, + $msg->inLanguage( $lang )->$format() + ); + } + /** * @covers Message::extractParam */ diff --git a/tests/phpunit/includes/PrefixSearchTest.php b/tests/phpunit/includes/PrefixSearchTest.php index bc43709fde..c5a7e04e30 100644 --- a/tests/phpunit/includes/PrefixSearchTest.php +++ b/tests/phpunit/includes/PrefixSearchTest.php @@ -129,11 +129,11 @@ class PrefixSearchTest extends MediaWikiLangTestCase { 'results' => [ 'Special:ActiveUsers', 'Special:AllMessages', - 'Special:AllMyFiles', + 'Special:AllMyUploads', ], // Third result when testing offset 'offsetresult' => [ - 'Special:AllMyUploads', + 'Special:AllPages', ], ] ], [ [ @@ -146,7 +146,7 @@ class PrefixSearchTest extends MediaWikiLangTestCase { ], // Third result when testing offset 'offsetresult' => [ - 'Special:UncategorizedImages', + 'Special:UncategorizedPages', ], ] ], [ [ diff --git a/tests/phpunit/includes/XmlTest.php b/tests/phpunit/includes/XmlTest.php index dbd1299ebd..18ff1f4b11 100644 --- a/tests/phpunit/includes/XmlTest.php +++ b/tests/phpunit/includes/XmlTest.php @@ -305,17 +305,6 @@ class XmlTest extends MediaWikiTestCase { ); } - /** - * @covers Xml::escapeJsString - */ - public function testEscapeJsStringSpecialChars() { - $this->assertEquals( - '\\\\\r\n', - Xml::escapeJsString( "\\\r\n" ), - 'escapeJsString() with special characters' - ); - } - /** * @covers Xml::encodeJsVar */ diff --git a/tests/phpunit/includes/auth/LocalPasswordPrimaryAuthenticationProviderTest.php b/tests/phpunit/includes/auth/LocalPasswordPrimaryAuthenticationProviderTest.php index cb34be295d..72a03c311a 100644 --- a/tests/phpunit/includes/auth/LocalPasswordPrimaryAuthenticationProviderTest.php +++ b/tests/phpunit/includes/auth/LocalPasswordPrimaryAuthenticationProviderTest.php @@ -451,7 +451,7 @@ class LocalPasswordPrimaryAuthenticationProviderTest extends \MediaWikiTestCase $changeReq->password = $newpass; $provider->providerChangeAuthenticationData( $changeReq ); - if ( $loginOnly ) { + if ( $loginOnly && $changed ) { $old = 'fail'; $new = 'fail'; $expectExpiry = null; diff --git a/tests/phpunit/includes/debug/logger/monolog/LogstashFormatterTest.php b/tests/phpunit/includes/debug/logger/monolog/LogstashFormatterTest.php new file mode 100644 index 0000000000..8086b4bf23 --- /dev/null +++ b/tests/phpunit/includes/debug/logger/monolog/LogstashFormatterTest.php @@ -0,0 +1,55 @@ +format( $record ), true ); + foreach ( $expected as $key => $value ) { + $this->assertArrayHasKey( $key, $formatted ); + $this->assertSame( $value, $formatted[$key] ); + } + foreach ( $notExpected as $key ) { + $this->assertArrayNotHasKey( $key, $formatted ); + } + } + + public function provideV1() { + return [ + [ + [ 'extra' => [ 'foo' => 1 ], 'context' => [ 'bar' => 2 ] ], + [ 'foo' => 1, 'bar' => 2 ], + [ 'logstash_formatter_key_conflict' ], + ], + [ + [ 'extra' => [ 'url' => 1 ], 'context' => [ 'url' => 2 ] ], + [ 'url' => 1, 'c_url' => 2, 'logstash_formatter_key_conflict' => [ 'url' ] ], + [], + ], + [ + [ 'channel' => 'x', 'context' => [ 'channel' => 'y' ] ], + [ 'channel' => 'x', 'c_channel' => 'y', + 'logstash_formatter_key_conflict' => [ 'channel' ] ], + [], + ], + ]; + } + + public function testV1WithPrefix() { + $formatter = new LogstashFormatter( 'app', 'system', null, 'ctx_', LogstashFormatter::V1 ); + $record = [ 'extra' => [ 'url' => 1 ], 'context' => [ 'url' => 2 ] ]; + $formatted = json_decode( $formatter->format( $record ), true ); + $this->assertArrayHasKey( 'url', $formatted ); + $this->assertSame( 1, $formatted['url'] ); + $this->assertArrayHasKey( 'ctx_url', $formatted ); + $this->assertSame( 2, $formatted['ctx_url'] ); + $this->assertArrayNotHasKey( 'c_url', $formatted ); + } +} diff --git a/tests/phpunit/includes/http/HttpTest.php b/tests/phpunit/includes/http/HttpTest.php new file mode 100644 index 0000000000..7e98d1c069 --- /dev/null +++ b/tests/phpunit/includes/http/HttpTest.php @@ -0,0 +1,534 @@ +assertEquals( $expected, $ok, $msg ); + } + + public static function cookieDomains() { + return [ + [ false, "org" ], + [ false, ".org" ], + [ true, "wikipedia.org" ], + [ true, ".wikipedia.org" ], + [ false, "co.uk" ], + [ false, ".co.uk" ], + [ false, "gov.uk" ], + [ false, ".gov.uk" ], + [ true, "supermarket.uk" ], + [ false, "uk" ], + [ false, ".uk" ], + [ false, "127.0.0." ], + [ false, "127." ], + [ false, "127.0.0.1." ], + [ true, "127.0.0.1" ], + [ false, "333.0.0.1" ], + [ true, "example.com" ], + [ false, "example.com." ], + [ true, ".example.com" ], + + [ true, ".example.com", "www.example.com" ], + [ false, "example.com", "www.example.com" ], + [ true, "127.0.0.1", "127.0.0.1" ], + [ false, "127.0.0.1", "localhost" ], + ]; + } + + /** + * Test Http::isValidURI() + * @bug 27854 : Http::isValidURI is too lax + * @dataProvider provideURI + * @covers Http::isValidURI + */ + public function testIsValidUri( $expect, $URI, $message = '' ) { + $this->assertEquals( + $expect, + (bool)Http::isValidURI( $URI ), + $message + ); + } + + /** + * @covers Http::getProxy + */ + public function testGetProxy() { + $this->setMwGlobals( 'wgHTTPProxy', 'proxy.domain.tld' ); + $this->assertEquals( + 'proxy.domain.tld', + Http::getProxy() + ); + } + + /** + * Feeds URI to test a long regular expression in Http::isValidURI + */ + public static function provideURI() { + /** Format: 'boolean expectation', 'URI to test', 'Optional message' */ + return [ + [ false, '¿non sens before!! http://a', 'Allow anything before URI' ], + + # (http|https) - only two schemes allowed + [ true, 'http://www.example.org/' ], + [ true, 'https://www.example.org/' ], + [ true, 'http://www.example.org', 'URI without directory' ], + [ true, 'http://a', 'Short name' ], + [ true, 'http://étoile', 'Allow UTF-8 in hostname' ], # 'étoile' is french for 'star' + [ false, '\\host\directory', 'CIFS share' ], + [ false, 'gopher://host/dir', 'Reject gopher scheme' ], + [ false, 'telnet://host', 'Reject telnet scheme' ], + + # :\/\/ - double slashes + [ false, 'http//example.org', 'Reject missing colon in protocol' ], + [ false, 'http:/example.org', 'Reject missing slash in protocol' ], + [ false, 'http:example.org', 'Must have two slashes' ], + # Following fail since hostname can be made of anything + [ false, 'http:///example.org', 'Must have exactly two slashes, not three' ], + + # (\w+:{0,1}\w*@)? - optional user:pass + [ true, 'http://user@host', 'Username provided' ], + [ true, 'http://user:@host', 'Username provided, no password' ], + [ true, 'http://user:pass@host', 'Username and password provided' ], + + # (\S+) - host part is made of anything not whitespaces + // commented these out in order to remove @group Broken + // @todo are these valid tests? if so, fix Http::isValidURI so it can handle them + // [ false, 'http://!"èèè¿¿¿~~\'', 'hostname is made of any non whitespace' ], + // [ false, 'http://exam:ple.org/', 'hostname can not use colons!' ], + + # (:[0-9]+)? - port number + [ true, 'http://example.org:80/' ], + [ true, 'https://example.org:80/' ], + [ true, 'http://example.org:443/' ], + [ true, 'https://example.org:443/' ], + + # Part after the hostname is / or / with something else + [ true, 'http://example/#' ], + [ true, 'http://example/!' ], + [ true, 'http://example/:' ], + [ true, 'http://example/.' ], + [ true, 'http://example/?' ], + [ true, 'http://example/+' ], + [ true, 'http://example/=' ], + [ true, 'http://example/&' ], + [ true, 'http://example/%' ], + [ true, 'http://example/@' ], + [ true, 'http://example/-' ], + [ true, 'http://example//' ], + [ true, 'http://example/&' ], + + # Fragment + [ true, 'http://exam#ple.org', ], # This one is valid, really! + [ true, 'http://example.org:80#anchor' ], + [ true, 'http://example.org/?id#anchor' ], + [ true, 'http://example.org/?#anchor' ], + + [ false, 'http://a ¿non !!sens after', 'Allow anything after URI' ], + ]; + } + + /** + * Warning: + * + * These tests are for code that makes use of an artifact of how CURL + * handles header reporting on redirect pages, and will need to be + * rewritten when bug 29232 is taken care of (high-level handling of + * HTTP redirects). + */ + public function testRelativeRedirections() { + $h = MWHttpRequestTester::factory( 'http://oldsite/file.ext', [], __METHOD__ ); + + # Forge a Location header + $h->setRespHeaders( 'location', [ + 'http://newsite/file.ext', + '/newfile.ext', + ] + ); + # Verify we correctly fix the Location + $this->assertEquals( + 'http://newsite/newfile.ext', + $h->getFinalUrl(), + "Relative file path Location: interpreted as full URL" + ); + + $h->setRespHeaders( 'location', [ + 'https://oldsite/file.ext' + ] + ); + $this->assertEquals( + 'https://oldsite/file.ext', + $h->getFinalUrl(), + "Location to the HTTPS version of the site" + ); + + $h->setRespHeaders( 'location', [ + '/anotherfile.ext', + 'http://anotherfile/hoster.ext', + 'https://anotherfile/hoster.ext' + ] + ); + $this->assertEquals( + 'https://anotherfile/hoster.ext', + $h->getFinalUrl( "Relative file path Location: should keep the latest host and scheme!" ) + ); + } + + /** + * Constant values are from PHP 5.3.28 using cURL 7.24.0 + * @see https://secure.php.net/manual/en/curl.constants.php + * + * All constant values are present so that developers don’t need to remember + * to add them if added at a later date. The commented out constants were + * not found anywhere in the MediaWiki core code. + * + * Commented out constants that were not available in: + * HipHop VM 3.3.0 (rel) + * Compiler: heads/master-0-g08810d920dfff59e0774cf2d651f92f13a637175 + * Repo schema: 3214fc2c684a4520485f715ee45f33f2182324b1 + * Extension API: 20140829 + * + * Commented out constants that were removed in PHP 5.6.0 + * + * @covers CurlHttpRequest::execute + */ + public function provideCurlConstants() { + return [ + [ 'CURLAUTH_ANY' ], + [ 'CURLAUTH_ANYSAFE' ], + [ 'CURLAUTH_BASIC' ], + [ 'CURLAUTH_DIGEST' ], + [ 'CURLAUTH_GSSNEGOTIATE' ], + [ 'CURLAUTH_NTLM' ], + // [ 'CURLCLOSEPOLICY_CALLBACK' ], // removed in PHP 5.6.0 + // [ 'CURLCLOSEPOLICY_LEAST_RECENTLY_USED' ], // removed in PHP 5.6.0 + // [ 'CURLCLOSEPOLICY_LEAST_TRAFFIC' ], // removed in PHP 5.6.0 + // [ 'CURLCLOSEPOLICY_OLDEST' ], // removed in PHP 5.6.0 + // [ 'CURLCLOSEPOLICY_SLOWEST' ], // removed in PHP 5.6.0 + [ 'CURLE_ABORTED_BY_CALLBACK' ], + [ 'CURLE_BAD_CALLING_ORDER' ], + [ 'CURLE_BAD_CONTENT_ENCODING' ], + [ 'CURLE_BAD_FUNCTION_ARGUMENT' ], + [ 'CURLE_BAD_PASSWORD_ENTERED' ], + [ 'CURLE_COULDNT_CONNECT' ], + [ 'CURLE_COULDNT_RESOLVE_HOST' ], + [ 'CURLE_COULDNT_RESOLVE_PROXY' ], + [ 'CURLE_FAILED_INIT' ], + [ 'CURLE_FILESIZE_EXCEEDED' ], + [ 'CURLE_FILE_COULDNT_READ_FILE' ], + [ 'CURLE_FTP_ACCESS_DENIED' ], + [ 'CURLE_FTP_BAD_DOWNLOAD_RESUME' ], + [ 'CURLE_FTP_CANT_GET_HOST' ], + [ 'CURLE_FTP_CANT_RECONNECT' ], + [ 'CURLE_FTP_COULDNT_GET_SIZE' ], + [ 'CURLE_FTP_COULDNT_RETR_FILE' ], + [ 'CURLE_FTP_COULDNT_SET_ASCII' ], + [ 'CURLE_FTP_COULDNT_SET_BINARY' ], + [ 'CURLE_FTP_COULDNT_STOR_FILE' ], + [ 'CURLE_FTP_COULDNT_USE_REST' ], + [ 'CURLE_FTP_PORT_FAILED' ], + [ 'CURLE_FTP_QUOTE_ERROR' ], + [ 'CURLE_FTP_SSL_FAILED' ], + [ 'CURLE_FTP_USER_PASSWORD_INCORRECT' ], + [ 'CURLE_FTP_WEIRD_227_FORMAT' ], + [ 'CURLE_FTP_WEIRD_PASS_REPLY' ], + [ 'CURLE_FTP_WEIRD_PASV_REPLY' ], + [ 'CURLE_FTP_WEIRD_SERVER_REPLY' ], + [ 'CURLE_FTP_WEIRD_USER_REPLY' ], + [ 'CURLE_FTP_WRITE_ERROR' ], + [ 'CURLE_FUNCTION_NOT_FOUND' ], + [ 'CURLE_GOT_NOTHING' ], + [ 'CURLE_HTTP_NOT_FOUND' ], + [ 'CURLE_HTTP_PORT_FAILED' ], + [ 'CURLE_HTTP_POST_ERROR' ], + [ 'CURLE_HTTP_RANGE_ERROR' ], + [ 'CURLE_LDAP_CANNOT_BIND' ], + [ 'CURLE_LDAP_INVALID_URL' ], + [ 'CURLE_LDAP_SEARCH_FAILED' ], + [ 'CURLE_LIBRARY_NOT_FOUND' ], + [ 'CURLE_MALFORMAT_USER' ], + [ 'CURLE_OBSOLETE' ], + [ 'CURLE_OK' ], + [ 'CURLE_OPERATION_TIMEOUTED' ], + [ 'CURLE_OUT_OF_MEMORY' ], + [ 'CURLE_PARTIAL_FILE' ], + [ 'CURLE_READ_ERROR' ], + [ 'CURLE_RECV_ERROR' ], + [ 'CURLE_SEND_ERROR' ], + [ 'CURLE_SHARE_IN_USE' ], + // [ 'CURLE_SSH' ], // not present in HHVM 3.3.0-dev + [ 'CURLE_SSL_CACERT' ], + [ 'CURLE_SSL_CERTPROBLEM' ], + [ 'CURLE_SSL_CIPHER' ], + [ 'CURLE_SSL_CONNECT_ERROR' ], + [ 'CURLE_SSL_ENGINE_NOTFOUND' ], + [ 'CURLE_SSL_ENGINE_SETFAILED' ], + [ 'CURLE_SSL_PEER_CERTIFICATE' ], + [ 'CURLE_TELNET_OPTION_SYNTAX' ], + [ 'CURLE_TOO_MANY_REDIRECTS' ], + [ 'CURLE_UNKNOWN_TELNET_OPTION' ], + [ 'CURLE_UNSUPPORTED_PROTOCOL' ], + [ 'CURLE_URL_MALFORMAT' ], + [ 'CURLE_URL_MALFORMAT_USER' ], + [ 'CURLE_WRITE_ERROR' ], + [ 'CURLFTPAUTH_DEFAULT' ], + [ 'CURLFTPAUTH_SSL' ], + [ 'CURLFTPAUTH_TLS' ], + // [ 'CURLFTPMETHOD_MULTICWD' ], // not present in HHVM 3.3.0-dev + // [ 'CURLFTPMETHOD_NOCWD' ], // not present in HHVM 3.3.0-dev + // [ 'CURLFTPMETHOD_SINGLECWD' ], // not present in HHVM 3.3.0-dev + [ 'CURLFTPSSL_ALL' ], + [ 'CURLFTPSSL_CONTROL' ], + [ 'CURLFTPSSL_NONE' ], + [ 'CURLFTPSSL_TRY' ], + // [ 'CURLINFO_CERTINFO' ], // not present in HHVM 3.3.0-dev + [ 'CURLINFO_CONNECT_TIME' ], + [ 'CURLINFO_CONTENT_LENGTH_DOWNLOAD' ], + [ 'CURLINFO_CONTENT_LENGTH_UPLOAD' ], + [ 'CURLINFO_CONTENT_TYPE' ], + [ 'CURLINFO_EFFECTIVE_URL' ], + [ 'CURLINFO_FILETIME' ], + [ 'CURLINFO_HEADER_OUT' ], + [ 'CURLINFO_HEADER_SIZE' ], + [ 'CURLINFO_HTTP_CODE' ], + [ 'CURLINFO_NAMELOOKUP_TIME' ], + [ 'CURLINFO_PRETRANSFER_TIME' ], + [ 'CURLINFO_PRIVATE' ], + [ 'CURLINFO_REDIRECT_COUNT' ], + [ 'CURLINFO_REDIRECT_TIME' ], + // [ 'CURLINFO_REDIRECT_URL' ], // not present in HHVM 3.3.0-dev + [ 'CURLINFO_REQUEST_SIZE' ], + [ 'CURLINFO_SIZE_DOWNLOAD' ], + [ 'CURLINFO_SIZE_UPLOAD' ], + [ 'CURLINFO_SPEED_DOWNLOAD' ], + [ 'CURLINFO_SPEED_UPLOAD' ], + [ 'CURLINFO_SSL_VERIFYRESULT' ], + [ 'CURLINFO_STARTTRANSFER_TIME' ], + [ 'CURLINFO_TOTAL_TIME' ], + [ 'CURLMSG_DONE' ], + [ 'CURLM_BAD_EASY_HANDLE' ], + [ 'CURLM_BAD_HANDLE' ], + [ 'CURLM_CALL_MULTI_PERFORM' ], + [ 'CURLM_INTERNAL_ERROR' ], + [ 'CURLM_OK' ], + [ 'CURLM_OUT_OF_MEMORY' ], + [ 'CURLOPT_AUTOREFERER' ], + [ 'CURLOPT_BINARYTRANSFER' ], + [ 'CURLOPT_BUFFERSIZE' ], + [ 'CURLOPT_CAINFO' ], + [ 'CURLOPT_CAPATH' ], + // [ 'CURLOPT_CERTINFO' ], // not present in HHVM 3.3.0-dev + // [ 'CURLOPT_CLOSEPOLICY' ], // removed in PHP 5.6.0 + [ 'CURLOPT_CONNECTTIMEOUT' ], + [ 'CURLOPT_CONNECTTIMEOUT_MS' ], + [ 'CURLOPT_COOKIE' ], + [ 'CURLOPT_COOKIEFILE' ], + [ 'CURLOPT_COOKIEJAR' ], + [ 'CURLOPT_COOKIESESSION' ], + [ 'CURLOPT_CRLF' ], + [ 'CURLOPT_CUSTOMREQUEST' ], + [ 'CURLOPT_DNS_CACHE_TIMEOUT' ], + [ 'CURLOPT_DNS_USE_GLOBAL_CACHE' ], + [ 'CURLOPT_EGDSOCKET' ], + [ 'CURLOPT_ENCODING' ], + [ 'CURLOPT_FAILONERROR' ], + [ 'CURLOPT_FILE' ], + [ 'CURLOPT_FILETIME' ], + [ 'CURLOPT_FOLLOWLOCATION' ], + [ 'CURLOPT_FORBID_REUSE' ], + [ 'CURLOPT_FRESH_CONNECT' ], + [ 'CURLOPT_FTPAPPEND' ], + [ 'CURLOPT_FTPLISTONLY' ], + [ 'CURLOPT_FTPPORT' ], + [ 'CURLOPT_FTPSSLAUTH' ], + [ 'CURLOPT_FTP_CREATE_MISSING_DIRS' ], + // [ 'CURLOPT_FTP_FILEMETHOD' ], // not present in HHVM 3.3.0-dev + // [ 'CURLOPT_FTP_SKIP_PASV_IP' ], // not present in HHVM 3.3.0-dev + [ 'CURLOPT_FTP_SSL' ], + [ 'CURLOPT_FTP_USE_EPRT' ], + [ 'CURLOPT_FTP_USE_EPSV' ], + [ 'CURLOPT_HEADER' ], + [ 'CURLOPT_HEADERFUNCTION' ], + [ 'CURLOPT_HTTP200ALIASES' ], + [ 'CURLOPT_HTTPAUTH' ], + [ 'CURLOPT_HTTPGET' ], + [ 'CURLOPT_HTTPHEADER' ], + [ 'CURLOPT_HTTPPROXYTUNNEL' ], + [ 'CURLOPT_HTTP_VERSION' ], + [ 'CURLOPT_INFILE' ], + [ 'CURLOPT_INFILESIZE' ], + [ 'CURLOPT_INTERFACE' ], + [ 'CURLOPT_IPRESOLVE' ], + // [ 'CURLOPT_KEYPASSWD' ], // not present in HHVM 3.3.0-dev + [ 'CURLOPT_KRB4LEVEL' ], + [ 'CURLOPT_LOW_SPEED_LIMIT' ], + [ 'CURLOPT_LOW_SPEED_TIME' ], + [ 'CURLOPT_MAXCONNECTS' ], + [ 'CURLOPT_MAXREDIRS' ], + // [ 'CURLOPT_MAX_RECV_SPEED_LARGE' ], // not present in HHVM 3.3.0-dev + // [ 'CURLOPT_MAX_SEND_SPEED_LARGE' ], // not present in HHVM 3.3.0-dev + [ 'CURLOPT_NETRC' ], + [ 'CURLOPT_NOBODY' ], + [ 'CURLOPT_NOPROGRESS' ], + [ 'CURLOPT_NOSIGNAL' ], + [ 'CURLOPT_PORT' ], + [ 'CURLOPT_POST' ], + [ 'CURLOPT_POSTFIELDS' ], + [ 'CURLOPT_POSTQUOTE' ], + [ 'CURLOPT_POSTREDIR' ], + [ 'CURLOPT_PRIVATE' ], + [ 'CURLOPT_PROGRESSFUNCTION' ], + // [ 'CURLOPT_PROTOCOLS' ], // not present in HHVM 3.3.0-dev + [ 'CURLOPT_PROXY' ], + [ 'CURLOPT_PROXYAUTH' ], + [ 'CURLOPT_PROXYPORT' ], + [ 'CURLOPT_PROXYTYPE' ], + [ 'CURLOPT_PROXYUSERPWD' ], + [ 'CURLOPT_PUT' ], + [ 'CURLOPT_QUOTE' ], + [ 'CURLOPT_RANDOM_FILE' ], + [ 'CURLOPT_RANGE' ], + [ 'CURLOPT_READDATA' ], + [ 'CURLOPT_READFUNCTION' ], + // [ 'CURLOPT_REDIR_PROTOCOLS' ], // not present in HHVM 3.3.0-dev + [ 'CURLOPT_REFERER' ], + [ 'CURLOPT_RESUME_FROM' ], + [ 'CURLOPT_RETURNTRANSFER' ], + // [ 'CURLOPT_SSH_AUTH_TYPES' ], // not present in HHVM 3.3.0-dev + // [ 'CURLOPT_SSH_HOST_PUBLIC_KEY_MD5' ], // not present in HHVM 3.3.0-dev + // [ 'CURLOPT_SSH_PRIVATE_KEYFILE' ], // not present in HHVM 3.3.0-dev + // [ 'CURLOPT_SSH_PUBLIC_KEYFILE' ], // not present in HHVM 3.3.0-dev + [ 'CURLOPT_SSLCERT' ], + [ 'CURLOPT_SSLCERTPASSWD' ], + [ 'CURLOPT_SSLCERTTYPE' ], + [ 'CURLOPT_SSLENGINE' ], + [ 'CURLOPT_SSLENGINE_DEFAULT' ], + [ 'CURLOPT_SSLKEY' ], + [ 'CURLOPT_SSLKEYPASSWD' ], + [ 'CURLOPT_SSLKEYTYPE' ], + [ 'CURLOPT_SSLVERSION' ], + [ 'CURLOPT_SSL_CIPHER_LIST' ], + [ 'CURLOPT_SSL_VERIFYHOST' ], + [ 'CURLOPT_SSL_VERIFYPEER' ], + [ 'CURLOPT_STDERR' ], + [ 'CURLOPT_TCP_NODELAY' ], + [ 'CURLOPT_TIMECONDITION' ], + [ 'CURLOPT_TIMEOUT' ], + [ 'CURLOPT_TIMEOUT_MS' ], + [ 'CURLOPT_TIMEVALUE' ], + [ 'CURLOPT_TRANSFERTEXT' ], + [ 'CURLOPT_UNRESTRICTED_AUTH' ], + [ 'CURLOPT_UPLOAD' ], + [ 'CURLOPT_URL' ], + [ 'CURLOPT_USERAGENT' ], + [ 'CURLOPT_USERPWD' ], + [ 'CURLOPT_VERBOSE' ], + [ 'CURLOPT_WRITEFUNCTION' ], + [ 'CURLOPT_WRITEHEADER' ], + // [ 'CURLPROTO_ALL' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_DICT' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_FILE' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_FTP' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_FTPS' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_HTTP' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_HTTPS' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_LDAP' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_LDAPS' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_SCP' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_SFTP' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_TELNET' ], // not present in HHVM 3.3.0-dev + // [ 'CURLPROTO_TFTP' ], // not present in HHVM 3.3.0-dev + [ 'CURLPROXY_HTTP' ], + // [ 'CURLPROXY_SOCKS4' ], // not present in HHVM 3.3.0-dev + [ 'CURLPROXY_SOCKS5' ], + // [ 'CURLSSH_AUTH_DEFAULT' ], // not present in HHVM 3.3.0-dev + // [ 'CURLSSH_AUTH_HOST' ], // not present in HHVM 3.3.0-dev + // [ 'CURLSSH_AUTH_KEYBOARD' ], // not present in HHVM 3.3.0-dev + // [ 'CURLSSH_AUTH_NONE' ], // not present in HHVM 3.3.0-dev + // [ 'CURLSSH_AUTH_PASSWORD' ], // not present in HHVM 3.3.0-dev + // [ 'CURLSSH_AUTH_PUBLICKEY' ], // not present in HHVM 3.3.0-dev + [ 'CURLVERSION_NOW' ], + [ 'CURL_HTTP_VERSION_1_0' ], + [ 'CURL_HTTP_VERSION_1_1' ], + [ 'CURL_HTTP_VERSION_NONE' ], + [ 'CURL_IPRESOLVE_V4' ], + [ 'CURL_IPRESOLVE_V6' ], + [ 'CURL_IPRESOLVE_WHATEVER' ], + [ 'CURL_NETRC_IGNORED' ], + [ 'CURL_NETRC_OPTIONAL' ], + [ 'CURL_NETRC_REQUIRED' ], + [ 'CURL_TIMECOND_IFMODSINCE' ], + [ 'CURL_TIMECOND_IFUNMODSINCE' ], + [ 'CURL_TIMECOND_LASTMOD' ], + [ 'CURL_VERSION_IPV6' ], + [ 'CURL_VERSION_KERBEROS4' ], + [ 'CURL_VERSION_LIBZ' ], + [ 'CURL_VERSION_SSL' ], + ]; + } + + /** + * Added this test based on an issue experienced with HHVM 3.3.0-dev + * where it did not define a cURL constant. + * + * @bug 70570 + * @dataProvider provideCurlConstants + */ + public function testCurlConstants( $value ) { + $this->assertTrue( defined( $value ), $value . ' not defined' ); + } +} + +/** + * Class to let us overwrite MWHttpRequest respHeaders variable + */ +class MWHttpRequestTester extends MWHttpRequest { + // function derived from the MWHttpRequest factory function but + // returns appropriate tester class here + public static function factory( $url, $options = null, $caller = __METHOD__ ) { + if ( !Http::$httpEngine ) { + Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php'; + } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) { + throw new DomainException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' . + 'Http::$httpEngine is set to "curl"' ); + } + + switch ( Http::$httpEngine ) { + case 'curl': + return new CurlHttpRequestTester( $url, $options, $caller ); + case 'php': + if ( !wfIniGetBool( 'allow_url_fopen' ) ) { + throw new DomainException( __METHOD__ . + ': allow_url_fopen needs to be enabled for pure PHP HTTP requests to work. ' + . 'If possible, curl should be used instead. See http://php.net/curl.' ); + } + + return new PhpHttpRequestTester( $url, $options, $caller ); + default: + } + } +} + +class CurlHttpRequestTester extends CurlHttpRequest { + function setRespHeaders( $name, $value ) { + $this->respHeaders[$name] = $value; + } +} + +class PhpHttpRequestTester extends PhpHttpRequest { + function setRespHeaders( $name, $value ) { + $this->respHeaders[$name] = $value; + } +} diff --git a/tests/phpunit/includes/installer/DatabaseUpdaterTest.php b/tests/phpunit/includes/installer/DatabaseUpdaterTest.php deleted file mode 100644 index 2a75cf4020..0000000000 --- a/tests/phpunit/includes/installer/DatabaseUpdaterTest.php +++ /dev/null @@ -1,286 +0,0 @@ -setAppliedUpdates( "test", [] ); - $expected = "updatelist-test-" . time() . "0"; - $actual = $db->lastInsertData['ul_key']; - $this->assertEquals( $expected, $actual, var_export( $db->lastInsertData, true ) ); - $dbu->setAppliedUpdates( "test", [] ); - $expected = "updatelist-test-" . time() . "1"; - $actual = $db->lastInsertData['ul_key']; - $this->assertEquals( $expected, $actual, var_export( $db->lastInsertData, true ) ); - } -} - -class FakeDatabase extends Database { - public $lastInsertTable; - public $lastInsertData; - - function __construct() { - $this->cliMode = true; - $this->connLogger = new \Psr\Log\NullLogger(); - $this->queryLogger = new \Psr\Log\NullLogger(); - $this->errorLogger = function ( Exception $e ) { - wfWarn( get_class( $e ) . ": {$e->getMessage()}" ); - }; - $this->currentDomain = DatabaseDomain::newUnspecified(); - } - - function clearFlag( $arg, $remember = self::REMEMBER_NOTHING ) { - } - - function setFlag( $arg, $remember = self::REMEMBER_NOTHING ) { - } - - public function insert( $table, $a, $fname = __METHOD__, $options = [] ) { - $this->lastInsertTable = $table; - $this->lastInsertData = $a; - } - - /** - * Get the type of the DBMS, as it appears in $wgDBtype. - * - * @return string - */ - function getType() { - // TODO: Implement getType() method. - } - - /** - * Open a connection to the database. Usually aborts on failure - * - * @param string $server Database server host - * @param string $user Database user name - * @param string $password Database user password - * @param string $dbName Database name - * @return bool - * @throws DBConnectionError - */ - function open( $server, $user, $password, $dbName ) { - // TODO: Implement open() method. - } - - /** - * Fetch the next row from the given result object, in object form. - * Fields can be retrieved with $row->fieldname, with fields acting like - * member variables. - * If no more rows are available, false is returned. - * - * @param ResultWrapper|stdClass $res Object as returned from Database::query(), etc. - * @return stdClass|bool - * @throws DBUnexpectedError Thrown if the database returns an error - */ - function fetchObject( $res ) { - // TODO: Implement fetchObject() method. - } - - /** - * Fetch the next row from the given result object, in associative array - * form. Fields are retrieved with $row['fieldname']. - * If no more rows are available, false is returned. - * - * @param ResultWrapper $res Result object as returned from Database::query(), etc. - * @return array|bool - * @throws DBUnexpectedError Thrown if the database returns an error - */ - function fetchRow( $res ) { - // TODO: Implement fetchRow() method. - } - - /** - * Get the number of rows in a result object - * - * @param mixed $res A SQL result - * @return int - */ - function numRows( $res ) { - // TODO: Implement numRows() method. - } - - /** - * Get the number of fields in a result object - * @see https://secure.php.net/mysql_num_fields - * - * @param mixed $res A SQL result - * @return int - */ - function numFields( $res ) { - // TODO: Implement numFields() method. - } - - /** - * Get a field name in a result object - * @see https://secure.php.net/mysql_field_name - * - * @param mixed $res A SQL result - * @param int $n - * @return string - */ - function fieldName( $res, $n ) { - // TODO: Implement fieldName() method. - } - - /** - * Get the inserted value of an auto-increment row - * - * The value inserted should be fetched from nextSequenceValue() - * - * Example: - * $id = $dbw->nextSequenceValue( 'page_page_id_seq' ); - * $dbw->insert( 'page', [ 'page_id' => $id ] ); - * $id = $dbw->insertId(); - * - * @return int - */ - function insertId() { - // TODO: Implement insertId() method. - } - - /** - * Change the position of the cursor in a result object - * @see https://secure.php.net/mysql_data_seek - * - * @param mixed $res A SQL result - * @param int $row - */ - function dataSeek( $res, $row ) { - // TODO: Implement dataSeek() method. - } - - /** - * Get the last error number - * @see https://secure.php.net/mysql_errno - * - * @return int - */ - function lastErrno() { - // TODO: Implement lastErrno() method. - } - - /** - * Get a description of the last error - * @see https://secure.php.net/mysql_error - * - * @return string - */ - function lastError() { - // TODO: Implement lastError() method. - } - - /** - * mysql_fetch_field() wrapper - * Returns false if the field doesn't exist - * - * @param string $table Table name - * @param string $field Field name - * - * @return Field - */ - function fieldInfo( $table, $field ) { - // TODO: Implement fieldInfo() method. - } - - /** - * Get information about an index into an object - * @param string $table Table name - * @param string $index Index name - * @param string $fname Calling function name - * @return mixed Database-specific index description class or false if the index does not exist - */ - function indexInfo( $table, $index, $fname = __METHOD__ ) { - // TODO: Implement indexInfo() method. - } - - /** - * Get the number of rows affected by the last write query - * @see https://secure.php.net/mysql_affected_rows - * - * @return int - */ - function affectedRows() { - // TODO: Implement affectedRows() method. - } - - /** - * Wrapper for addslashes() - * - * @param string $s String to be slashed. - * @return string Slashed string. - */ - function strencode( $s ) { - // TODO: Implement strencode() method. - } - - /** - * Returns a wikitext link to the DB's website, e.g., - * return "[https://www.mysql.com/ MySQL]"; - * Should at least contain plain text, if for some reason - * your database has no website. - * - * @return string Wikitext of a link to the server software's web site - */ - function getSoftwareLink() { - // TODO: Implement getSoftwareLink() method. - } - - /** - * A string describing the current software version, like from - * mysql_get_server_info(). - * - * @return string Version information from the database server. - */ - function getServerVersion() { - // TODO: Implement getServerVersion() method. - } - - /** - * Closes underlying database connection - * @since 1.20 - * @return bool Whether connection was closed successfully - */ - protected function closeConnection() { - // TODO: Implement closeConnection() method. - } - - /** - * The DBMS-dependent part of query() - * - * @param string $sql SQL query. - * @return ResultWrapper|bool Result object to feed to fetchObject, - * fetchRow, ...; or false on failure - */ - protected function doQuery( $sql ) { - // TODO: Implement doQuery() method. - } -} - -class FakeDatabaseUpdater extends DatabaseUpdater { - function __construct( $db ) { - $this->db = $db; - self::$updateCounter = 0; - } - - /** - * Get an array of updates to perform on the database. Should return a - * multi-dimensional array. The main key is the MediaWiki version (1.12, - * 1.13...) with the values being arrays of updates, identical to how - * updaters.inc did it (for now) - * - * @return array - */ - protected function getCoreUpdateList() { - return []; - } - - public function canUseNewUpdatelog() { - return true; - } - - public function setAppliedUpdates( $version, $updates = [] ) { - parent::setAppliedUpdates( $version, $updates ); - } -} diff --git a/tests/phpunit/includes/libs/rdbms/connectionmanager/ConnectionManagerTest.php b/tests/phpunit/includes/libs/rdbms/connectionmanager/ConnectionManagerTest.php new file mode 100644 index 0000000000..1677851736 --- /dev/null +++ b/tests/phpunit/includes/libs/rdbms/connectionmanager/ConnectionManagerTest.php @@ -0,0 +1,139 @@ +getMock( IDatabase::class ); + } + + /** + * @return LoadBalancer|PHPUnit_Framework_MockObject_MockObject + */ + private function getLoadBalancerMock() { + $lb = $this->getMockBuilder( LoadBalancer::class ) + ->disableOriginalConstructor() + ->getMock(); + + return $lb; + } + + public function testGetReadConnection_nullGroups() { + $database = $this->getIDatabaseMock(); + $lb = $this->getLoadBalancerMock(); + + $lb->expects( $this->once() ) + ->method( 'getConnection' ) + ->with( DB_REPLICA, [ 'group1' ], 'someDbName' ) + ->will( $this->returnValue( $database ) ); + + $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] ); + $actual = $manager->getReadConnection(); + + $this->assertSame( $database, $actual ); + } + + public function testGetReadConnection_withGroups() { + $database = $this->getIDatabaseMock(); + $lb = $this->getLoadBalancerMock(); + + $lb->expects( $this->once() ) + ->method( 'getConnection' ) + ->with( DB_REPLICA, [ 'group2' ], 'someDbName' ) + ->will( $this->returnValue( $database ) ); + + $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] ); + $actual = $manager->getReadConnection( [ 'group2' ] ); + + $this->assertSame( $database, $actual ); + } + + public function testGetWriteConnection() { + $database = $this->getIDatabaseMock(); + $lb = $this->getLoadBalancerMock(); + + $lb->expects( $this->once() ) + ->method( 'getConnection' ) + ->with( DB_MASTER, [ 'group1' ], 'someDbName' ) + ->will( $this->returnValue( $database ) ); + + $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] ); + $actual = $manager->getWriteConnection(); + + $this->assertSame( $database, $actual ); + } + + public function testReleaseConnection() { + $database = $this->getIDatabaseMock(); + $lb = $this->getLoadBalancerMock(); + + $lb->expects( $this->once() ) + ->method( 'reuseConnection' ) + ->with( $database ) + ->will( $this->returnValue( null ) ); + + $manager = new ConnectionManager( $lb ); + $manager->releaseConnection( $database ); + } + + public function testGetReadConnectionRef_nullGroups() { + $database = $this->getIDatabaseMock(); + $lb = $this->getLoadBalancerMock(); + + $lb->expects( $this->once() ) + ->method( 'getConnectionRef' ) + ->with( DB_REPLICA, [ 'group1' ], 'someDbName' ) + ->will( $this->returnValue( $database ) ); + + $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] ); + $actual = $manager->getReadConnectionRef(); + + $this->assertSame( $database, $actual ); + } + + public function testGetReadConnectionRef_withGroups() { + $database = $this->getIDatabaseMock(); + $lb = $this->getLoadBalancerMock(); + + $lb->expects( $this->once() ) + ->method( 'getConnectionRef' ) + ->with( DB_REPLICA, [ 'group2' ], 'someDbName' ) + ->will( $this->returnValue( $database ) ); + + $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] ); + $actual = $manager->getReadConnectionRef( [ 'group2' ] ); + + $this->assertSame( $database, $actual ); + } + + public function testGetWriteConnectionRef() { + $database = $this->getIDatabaseMock(); + $lb = $this->getLoadBalancerMock(); + + $lb->expects( $this->once() ) + ->method( 'getConnectionRef' ) + ->with( DB_MASTER, [ 'group1' ], 'someDbName' ) + ->will( $this->returnValue( $database ) ); + + $manager = new ConnectionManager( $lb, 'someDbName', [ 'group1' ] ); + $actual = $manager->getWriteConnectionRef(); + + $this->assertSame( $database, $actual ); + } + +} diff --git a/tests/phpunit/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManagerTest.php b/tests/phpunit/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManagerTest.php new file mode 100644 index 0000000000..0d54659b5e --- /dev/null +++ b/tests/phpunit/includes/libs/rdbms/connectionmanager/SessionConsistentConnectionManagerTest.php @@ -0,0 +1,108 @@ +getMock( IDatabase::class ); + } + + /** + * @return LoadBalancer|PHPUnit_Framework_MockObject_MockObject + */ + private function getLoadBalancerMock() { + $lb = $this->getMockBuilder( LoadBalancer::class ) + ->disableOriginalConstructor() + ->getMock(); + + return $lb; + } + + public function testGetReadConnection() { + $database = $this->getIDatabaseMock(); + $lb = $this->getLoadBalancerMock(); + + $lb->expects( $this->once() ) + ->method( 'getConnection' ) + ->with( DB_REPLICA ) + ->will( $this->returnValue( $database ) ); + + $manager = new SessionConsistentConnectionManager( $lb ); + $actual = $manager->getReadConnection(); + + $this->assertSame( $database, $actual ); + } + + public function testGetReadConnectionReturnsWriteDbOnForceMatser() { + $database = $this->getIDatabaseMock(); + $lb = $this->getLoadBalancerMock(); + + $lb->expects( $this->once() ) + ->method( 'getConnection' ) + ->with( DB_MASTER ) + ->will( $this->returnValue( $database ) ); + + $manager = new SessionConsistentConnectionManager( $lb ); + $manager->prepareForUpdates(); + $actual = $manager->getReadConnection(); + + $this->assertSame( $database, $actual ); + } + + public function testGetWriteConnection() { + $database = $this->getIDatabaseMock(); + $lb = $this->getLoadBalancerMock(); + + $lb->expects( $this->once() ) + ->method( 'getConnection' ) + ->with( DB_MASTER ) + ->will( $this->returnValue( $database ) ); + + $manager = new SessionConsistentConnectionManager( $lb ); + $actual = $manager->getWriteConnection(); + + $this->assertSame( $database, $actual ); + } + + public function testForceMaster() { + $database = $this->getIDatabaseMock(); + $lb = $this->getLoadBalancerMock(); + + $lb->expects( $this->once() ) + ->method( 'getConnection' ) + ->with( DB_MASTER ) + ->will( $this->returnValue( $database ) ); + + $manager = new SessionConsistentConnectionManager( $lb ); + $manager->prepareForUpdates(); + $manager->getReadConnection(); + } + + public function testReleaseConnection() { + $database = $this->getIDatabaseMock(); + $lb = $this->getLoadBalancerMock(); + + $lb->expects( $this->once() ) + ->method( 'reuseConnection' ) + ->with( $database ) + ->will( $this->returnValue( null ) ); + + $manager = new SessionConsistentConnectionManager( $lb ); + $manager->releaseConnection( $database ); + } +} diff --git a/tests/phpunit/includes/registration/ExtensionRegistryTest.php b/tests/phpunit/includes/registration/ExtensionRegistryTest.php index 1de42650fd..9b57e1c3d1 100644 --- a/tests/phpunit/includes/registration/ExtensionRegistryTest.php +++ b/tests/phpunit/includes/registration/ExtensionRegistryTest.php @@ -259,7 +259,7 @@ class ExtensionRegistryTest extends MediaWikiTestCase { 'JsonZeroConfig' => [ 'namespace' => 480, 'nsName' => 'Zero', - 'isLocal' => false, + 'isLocal' => true, ], ], ], diff --git a/tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php b/tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php index 1093039f88..baf0b69e6c 100644 --- a/tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php +++ b/tests/phpunit/includes/resourceloader/ResourceLoaderContextTest.php @@ -104,4 +104,13 @@ class ResourceLoaderContextTest extends PHPUnit_Framework_TestCase { $this->assertSame( 'Example', $ctx->getUser() ); $this->assertEquals( 'Example', $ctx->getUserObj()->getName() ); } + + public function testMsg() { + $ctx = new ResourceLoaderContext( $this->getResourceLoader(), new FauxRequest( [ + 'lang' => 'en' + ] ) ); + $msg = $ctx->msg( 'mainpage' ); + $this->assertInstanceOf( Message::class, $msg ); + $this->assertSame( 'Main Page', $msg->useDatabase( false )->plain() ); + } } diff --git a/tests/phpunit/includes/search/SearchEnginePrefixTest.php b/tests/phpunit/includes/search/SearchEnginePrefixTest.php index e0de58866f..a88264bb78 100644 --- a/tests/phpunit/includes/search/SearchEnginePrefixTest.php +++ b/tests/phpunit/includes/search/SearchEnginePrefixTest.php @@ -126,11 +126,11 @@ class SearchEnginePrefixTest extends MediaWikiLangTestCase { 'results' => [ 'Special:ActiveUsers', 'Special:AllMessages', - 'Special:AllMyFiles', + 'Special:AllMyUploads', ], // Third result when testing offset 'offsetresult' => [ - 'Special:AllMyUploads', + 'Special:AllPages', ], ] ], [ [ @@ -143,7 +143,7 @@ class SearchEnginePrefixTest extends MediaWikiLangTestCase { ], // Third result when testing offset 'offsetresult' => [ - 'Special:UncategorizedImages', + 'Special:UncategorizedPages', ], ] ], [ [ diff --git a/tests/qunit/suites/resources/mediawiki.api/mediawiki.ForeignApi.test.js b/tests/qunit/suites/resources/mediawiki.api/mediawiki.ForeignApi.test.js index 9d0fdf54b0..1676130e98 100644 --- a/tests/qunit/suites/resources/mediawiki.api/mediawiki.ForeignApi.test.js +++ b/tests/qunit/suites/resources/mediawiki.api/mediawiki.ForeignApi.test.js @@ -3,16 +3,10 @@ setup: function () { this.server = this.sandbox.useFakeServer(); this.server.respondImmediately = true; - this.clock = this.sandbox.useFakeTimers(); - }, - teardown: function () { - // https://github.com/jquery/jquery/issues/2453 - this.clock.tick(); } } ) ); - QUnit.test( 'origin is included in GET requests', function ( assert ) { - QUnit.expect( 1 ); + QUnit.test( 'origin is included in GET requests', 1, function ( assert ) { var api = new mw.ForeignApi( '//localhost:4242/w/api.php' ); this.server.respond( function ( request ) { @@ -20,11 +14,10 @@ request.respond( 200, { 'Content-Type': 'application/json' }, '[]' ); } ); - api.get( {} ); + return api.get( {} ); } ); - QUnit.test( 'origin is included in POST requests', function ( assert ) { - QUnit.expect( 2 ); + QUnit.test( 'origin is included in POST requests', 2, function ( assert ) { var api = new mw.ForeignApi( '//localhost:4242/w/api.php' ); this.server.respond( function ( request ) { @@ -33,7 +26,7 @@ request.respond( 200, { 'Content-Type': 'application/json' }, '[]' ); } ); - api.post( {} ); + return api.post( {} ); } ); }( mediaWiki ) ); diff --git a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.category.test.js b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.category.test.js index a0c7daf1a0..a79bff698b 100644 --- a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.category.test.js +++ b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.category.test.js @@ -2,29 +2,24 @@ QUnit.module( 'mediawiki.api.category', QUnit.newMwEnvironment( { setup: function () { this.server = this.sandbox.useFakeServer(); + this.server.respondImmediately = true; } } ) ); - QUnit.test( '.getCategoriesByPrefix()', function ( assert ) { - QUnit.expect( 1 ); + QUnit.test( '.getCategoriesByPrefix()', 1, function ( assert ) { + this.server.respondWith( [ 200, { 'Content-Type': 'application/json' }, + '{ "query": { "allpages": [ ' + + '{ "title": "Category:Food" },' + + '{ "title": "Category:Fool Supermarine S.6" },' + + '{ "title": "Category:Fools" }' + + '] } }' + ] ); - var api = new mw.Api(); - - api.getCategoriesByPrefix( 'Foo' ).done( function ( matches ) { + return new mw.Api().getCategoriesByPrefix( 'Foo' ).then( function ( matches ) { assert.deepEqual( matches, [ 'Food', 'Fool Supermarine S.6', 'Fools' ] ); } ); - - this.server.respond( function ( req ) { - req.respond( 200, { 'Content-Type': 'application/json' }, - '{ "query": { "allpages": [ ' + - '{ "title": "Category:Food" },' + - '{ "title": "Category:Fool Supermarine S.6" },' + - '{ "title": "Category:Fools" }' + - '] } }' - ); - } ); } ); }( mediaWiki ) ); diff --git a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.messages.test.js b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.messages.test.js index 5880962a74..d8b5db88a3 100644 --- a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.messages.test.js +++ b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.messages.test.js @@ -2,14 +2,21 @@ QUnit.module( 'mediawiki.api.messages', QUnit.newMwEnvironment( { setup: function () { this.server = this.sandbox.useFakeServer(); + this.server.respondImmediately = true; } } ) ); - QUnit.test( '.getMessages()', function ( assert ) { - QUnit.expect( 1 ); + QUnit.test( '.getMessages()', 1, function ( assert ) { + this.server.respondWith( /ammessages=foo%7Cbaz/, [ + 200, + { 'Content-Type': 'application/json' }, + '{ "query": { "allmessages": [' + + '{ "name": "foo", "content": "Foo bar" },' + + '{ "name": "baz", "content": "Baz Quux" }' + + '] } }' + ] ); - var api = new mw.Api(); - api.getMessages( [ 'foo', 'baz' ] ).then( function ( messages ) { + return new mw.Api().getMessages( [ 'foo', 'baz' ] ).then( function ( messages ) { assert.deepEqual( messages, { @@ -18,14 +25,5 @@ } ); } ); - - this.server.respond( /ammessages=foo%7Cbaz/, [ - 200, - { 'Content-Type': 'application/json' }, - '{ "query": { "allmessages": [' + - '{ "name": "foo", "content": "Foo bar" },' + - '{ "name": "baz", "content": "Baz Quux" }' + - '] } }' - ] ); } ); }( mediaWiki ) ); diff --git a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js index 0797f32dfb..7ed1875036 100644 --- a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js +++ b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js @@ -2,14 +2,12 @@ QUnit.module( 'mediawiki.api.options', QUnit.newMwEnvironment( { setup: function () { this.server = this.sandbox.useFakeServer(); + this.server.respondImmediately = true; } } ) ); - QUnit.test( 'saveOption', function ( assert ) { - QUnit.expect( 2 ); - - var - api = new mw.Api(), + QUnit.test( 'saveOption', 2, function ( assert ) { + var api = new mw.Api(), stub = this.sandbox.stub( mw.Api.prototype, 'saveOptions' ); api.saveOption( 'foo', 'bar' ); @@ -18,9 +16,7 @@ assert.deepEqual( stub.getCall( 0 ).args, [ { foo: 'bar' } ], '#saveOptions called correctly' ); } ); - QUnit.test( 'saveOptions without Unit Separator', function ( assert ) { - QUnit.expect( 13 ); - + QUnit.test( 'saveOptions without Unit Separator', 13, function ( assert ) { var api = new mw.Api( { useUS: false } ); // We need to respond to the request for token first, otherwise the other requests won't be sent @@ -32,25 +28,6 @@ '{ "query": { "tokens": { "csrftoken": "+\\\\" } } }' ] ); - api.saveOptions( {} ).done( function () { - assert.ok( true, 'Request completed: empty case' ); - } ); - api.saveOptions( { foo: 'bar' } ).done( function () { - assert.ok( true, 'Request completed: simple' ); - } ); - api.saveOptions( { foo: 'bar', baz: 'quux' } ).done( function () { - assert.ok( true, 'Request completed: two options' ); - } ); - api.saveOptions( { foo: 'bar|quux', bar: 'a|b|c', baz: 'quux' } ).done( function () { - assert.ok( true, 'Request completed: not bundleable' ); - } ); - api.saveOptions( { foo: null } ).done( function () { - assert.ok( true, 'Request completed: reset an option' ); - } ); - api.saveOptions( { 'foo|bar=quux': null } ).done( function () { - assert.ok( true, 'Request completed: reset an option, not bundleable' ); - } ); - // Requests are POST, match requestBody instead of url this.server.respond( function ( request ) { switch ( request.requestBody ) { @@ -74,11 +51,30 @@ assert.ok( false, 'Unexpected request: ' + request.requestBody ); } } ); - } ); - QUnit.test( 'saveOptions with Unit Separator', function ( assert ) { - QUnit.expect( 14 ); + return QUnit.whenPromisesComplete( + api.saveOptions( {} ).then( function () { + assert.ok( true, 'Request completed: empty case' ); + } ), + api.saveOptions( { foo: 'bar' } ).then( function () { + assert.ok( true, 'Request completed: simple' ); + } ), + api.saveOptions( { foo: 'bar', baz: 'quux' } ).then( function () { + assert.ok( true, 'Request completed: two options' ); + } ), + api.saveOptions( { foo: 'bar|quux', bar: 'a|b|c', baz: 'quux' } ).then( function () { + assert.ok( true, 'Request completed: not bundleable' ); + } ), + api.saveOptions( { foo: null } ).then( function () { + assert.ok( true, 'Request completed: reset an option' ); + } ), + api.saveOptions( { 'foo|bar=quux': null } ).then( function () { + assert.ok( true, 'Request completed: reset an option, not bundleable' ); + } ) + ); + } ); + QUnit.test( 'saveOptions with Unit Separator', 14, function ( assert ) { var api = new mw.Api( { useUS: true } ); // We need to respond to the request for token first, otherwise the other requests won't be sent @@ -90,28 +86,6 @@ '{ "query": { "tokens": { "csrftoken": "+\\\\" } } }' ] ); - api.saveOptions( {} ).done( function () { - assert.ok( true, 'Request completed: empty case' ); - } ); - api.saveOptions( { foo: 'bar' } ).done( function () { - assert.ok( true, 'Request completed: simple' ); - } ); - api.saveOptions( { foo: 'bar', baz: 'quux' } ).done( function () { - assert.ok( true, 'Request completed: two options' ); - } ); - api.saveOptions( { foo: 'bar|quux', bar: 'a|b|c', baz: 'quux' } ).done( function () { - assert.ok( true, 'Request completed: bundleable with unit separator' ); - } ); - api.saveOptions( { foo: 'bar|quux', bar: 'a|b|c', 'baz=baz': 'quux' } ).done( function () { - assert.ok( true, 'Request completed: not bundleable with unit separator' ); - } ); - api.saveOptions( { foo: null } ).done( function () { - assert.ok( true, 'Request completed: reset an option' ); - } ); - api.saveOptions( { 'foo|bar=quux': null } ).done( function () { - assert.ok( true, 'Request completed: reset an option, not bundleable' ); - } ); - // Requests are POST, match requestBody instead of url this.server.respond( function ( request ) { switch ( request.requestBody ) { @@ -136,5 +110,29 @@ assert.ok( false, 'Unexpected request: ' + request.requestBody ); } } ); + + return QUnit.whenPromisesComplete( + api.saveOptions( {} ).done( function () { + assert.ok( true, 'Request completed: empty case' ); + } ), + api.saveOptions( { foo: 'bar' } ).done( function () { + assert.ok( true, 'Request completed: simple' ); + } ), + api.saveOptions( { foo: 'bar', baz: 'quux' } ).done( function () { + assert.ok( true, 'Request completed: two options' ); + } ), + api.saveOptions( { foo: 'bar|quux', bar: 'a|b|c', baz: 'quux' } ).done( function () { + assert.ok( true, 'Request completed: bundleable with unit separator' ); + } ), + api.saveOptions( { foo: 'bar|quux', bar: 'a|b|c', 'baz=baz': 'quux' } ).done( function () { + assert.ok( true, 'Request completed: not bundleable with unit separator' ); + } ), + api.saveOptions( { foo: null } ).done( function () { + assert.ok( true, 'Request completed: reset an option' ); + } ), + api.saveOptions( { 'foo|bar=quux': null } ).done( function () { + assert.ok( true, 'Request completed: reset an option, not bundleable' ); + } ) + ); } ); }( mediaWiki ) ); diff --git a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.parse.test.js b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.parse.test.js index dc0cff40e6..7d27352200 100644 --- a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.parse.test.js +++ b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.parse.test.js @@ -2,42 +2,44 @@ QUnit.module( 'mediawiki.api.parse', QUnit.newMwEnvironment( { setup: function () { this.server = this.sandbox.useFakeServer(); + this.server.respondImmediately = true; } } ) ); - QUnit.test( 'Hello world', function ( assert ) { - QUnit.expect( 3 ); + QUnit.test( '.parse( string )', function ( assert ) { + this.server.respondWith( /action=parse.*&text='''Hello(\+|%20)world'''/, [ 200, + { 'Content-Type': 'application/json' }, + '{ "parse": { "text": "

    Hello world

    " } }' + ] ); - var api = new mw.Api(); - - api.parse( '\'\'\'Hello world\'\'\'' ).done( function ( html ) { + return new mw.Api().parse( '\'\'\'Hello world\'\'\'' ).done( function ( html ) { assert.equal( html, '

    Hello world

    ', 'Parse wikitext by string' ); } ); + } ); - api.parse( { + QUnit.test( '.parse( Object.toString )', function ( assert ) { + this.server.respondWith( /action=parse.*&text='''Hello(\+|%20)world'''/, [ 200, + { 'Content-Type': 'application/json' }, + '{ "parse": { "text": "

    Hello world

    " } }' + ] ); + + return new mw.Api().parse( { toString: function () { return '\'\'\'Hello world\'\'\''; } } ).done( function ( html ) { assert.equal( html, '

    Hello world

    ', 'Parse wikitext by toString object' ); } ); + } ); - this.server.respondWith( /action=parse.*&text='''Hello\+world'''/, function ( request ) { - request.respond( 200, { 'Content-Type': 'application/json' }, - '{ "parse": { "text": "

    Hello world

    " } }' - ); - } ); + QUnit.test( '.parse( mw.Title )', function ( assert ) { + this.server.respondWith( /action=parse.*&page=Earth/, [ 200, + { 'Content-Type': 'application/json' }, + '{ "parse": { "text": "

    Earth is a planet.

    " } }' + ] ); - api.parse( new mw.Title( 'Earth' ) ).done( function ( html ) { + return new mw.Api().parse( new mw.Title( 'Earth' ) ).done( function ( html ) { assert.equal( html, '

    Earth is a planet.

    ', 'Parse page by Title object' ); } ); - - this.server.respondWith( /action=parse.*&page=Earth/, function ( request ) { - request.respond( 200, { 'Content-Type': 'application/json' }, - '{ "parse": { "text": "

    Earth is a planet.

    " } }' - ); - } ); - - this.server.respond(); } ); }( mediaWiki ) ); diff --git a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.upload.test.js b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.upload.test.js index 10fcd5da68..b1bd12ba17 100644 --- a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.upload.test.js +++ b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.upload.test.js @@ -1,8 +1,7 @@ ( function ( mw, $ ) { QUnit.module( 'mediawiki.api.upload', QUnit.newMwEnvironment( {} ) ); - QUnit.test( 'Basic functionality', function ( assert ) { - QUnit.expect( 2 ); + QUnit.test( 'Basic functionality', 2, function ( assert ) { var api = new mw.Api(); assert.ok( api.upload ); assert.throws( function () { @@ -10,8 +9,7 @@ } ); } ); - QUnit.test( 'Set up iframe upload', function ( assert ) { - QUnit.expect( 5 ); + QUnit.test( 'Set up iframe upload', 5, function ( assert ) { var $iframe, $form, $input, api = new mw.Api(); diff --git a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.watch.test.js b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.watch.test.js index 64a5184711..86414691d8 100644 --- a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.watch.test.js +++ b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.watch.test.js @@ -2,37 +2,45 @@ QUnit.module( 'mediawiki.api.watch', QUnit.newMwEnvironment( { setup: function () { this.server = this.sandbox.useFakeServer(); + this.server.respondImmediately = true; } } ) ); - QUnit.test( '.watch()', function ( assert ) { - QUnit.expect( 4 ); - - var api = new mw.Api(); - - // Ensure we don't mistake a single item array for a single item and vice versa. - // The query parameter in request is the same either way (separated by pipe). - api.watch( 'Foo' ).done( function ( item ) { - assert.equal( item.title, 'Foo' ); - } ); - - api.watch( [ 'Foo' ] ).done( function ( items ) { - assert.equal( items[ 0 ].title, 'Foo' ); + QUnit.test( '.watch( string )', function ( assert ) { + this.server.respond( function ( req ) { + // Match POST requestBody + if ( /action=watch.*&titles=Foo(&|$)/.test( req.requestBody ) ) { + req.respond( 200, { 'Content-Type': 'application/json' }, + '{ "watch": [ { "title": "Foo", "watched": true, "message": "Added" } ] }' + ); + } } ); - api.watch( [ 'Foo', 'Bar' ] ).done( function ( items ) { - assert.equal( items[ 0 ].title, 'Foo' ); - assert.equal( items[ 1 ].title, 'Bar' ); + return new mw.Api().watch( 'Foo' ).done( function ( item ) { + assert.equal( item.title, 'Foo' ); } ); + } ); - // Requests are POST, match requestBody instead of url + // Ensure we don't mistake a single item array for a single item and vice versa. + // The query parameter in request is the same either way (separated by pipe). + QUnit.test( '.watch( Array ) - single', function ( assert ) { this.server.respond( function ( req ) { + // Match POST requestBody if ( /action=watch.*&titles=Foo(&|$)/.test( req.requestBody ) ) { req.respond( 200, { 'Content-Type': 'application/json' }, '{ "watch": [ { "title": "Foo", "watched": true, "message": "Added" } ] }' ); } + } ); + + return new mw.Api().watch( [ 'Foo' ] ).done( function ( items ) { + assert.equal( items[ 0 ].title, 'Foo' ); + } ); + } ); + QUnit.test( '.watch( Array ) - multi', function ( assert ) { + this.server.respond( function ( req ) { + // Match POST requestBody if ( /action=watch.*&titles=Foo%7CBar/.test( req.requestBody ) ) { req.respond( 200, { 'Content-Type': 'application/json' }, '{ "watch": [ ' + @@ -42,5 +50,11 @@ ); } } ); + + return new mw.Api().watch( [ 'Foo', 'Bar' ] ).done( function ( items ) { + assert.equal( items[ 0 ].title, 'Foo' ); + assert.equal( items[ 1 ].title, 'Bar' ); + } ); } ); + }( mediaWiki ) ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js index 12181379f0..f848f3edc8 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js @@ -367,8 +367,8 @@ QUnit.test( 'Match PHP parser', mw.libs.phpParserData.tests.length, function ( assert ) { mw.messages.set( mw.libs.phpParserData.messages ); var tasks = $.map( mw.libs.phpParserData.tests, function ( test ) { + var done = assert.async(); return function ( next, abort ) { - var done = assert.async(); getMwLanguage( test.lang ) .then( function ( langClass ) { mw.config.set( 'wgUserLanguage', test.lang ); @@ -884,7 +884,7 @@ }, { lang: 'hi', - number: '१२३४५६,७८९', + number: '१,२३,४५६', result: '123456', integer: true, description: 'formatnum test for Hindi, Devanagari digits passed to get integer value' @@ -895,8 +895,8 @@ mw.messages.set( 'formatnum-msg', '{{formatnum:$1}}' ); mw.messages.set( 'formatnum-msg-int', '{{formatnum:$1|R}}' ); var queue = $.map( formatnumTests, function ( test ) { + var done = assert.async(); return function ( next, abort ) { - var done = assert.async(); getMwLanguage( test.lang ) .then( function ( langClass ) { mw.config.set( 'wgUserLanguage', test.lang ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js index e4c3851593..23720a891c 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js @@ -43,6 +43,16 @@ assert.equal( mw.language.commafy( 123456789.567, '###,###,#0.00' ), '1,234,567,89.56', 'Decimal part as group of 3 and last one 2' ); } ); + QUnit.test( 'mw.language.convertNumber', 2, function ( assert ) { + mw.language.setData( 'en', 'digitGroupingPattern', null ); + mw.language.setData( 'en', 'digitTransformTable', null ); + mw.language.setData( 'en', 'separatorTransformTable', { ',': '.', '.': ',' } ); + mw.config.set( 'wgUserLanguage', 'en' ); + + assert.equal( mw.language.convertNumber( 1800 ), '1.800', 'formatting' ); + assert.equal( mw.language.convertNumber( "1.800", true ), '1800', 'unformatting' ); + } ); + function grammarTest( langCode, test ) { // The test works only if the content language is opt.language // because it requires [lang].js to be loaded. diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js index 92d13260e9..92ee7dd360 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js @@ -127,7 +127,6 @@ .done( function () { assert.strictEqual( isAwesomeDone, true, 'test.promise module should\'ve caused isAwesomeDone to be true' ); delete mw.loader.testCallback; - } ) .fail( function () { assert.ok( false, 'Error callback fired while loader.using "test.promise" module' ); @@ -135,6 +134,8 @@ } ); QUnit.test( '.using() Error: Circular dependency', function ( assert ) { + var done = assert.async(); + mw.loader.register( [ [ 'test.circle1', '0', [ 'test.circle2' ] ], [ 'test.circle2', '0', [ 'test.circle3' ] ], @@ -147,7 +148,8 @@ function fail( e ) { assert.ok( /Circular/.test( String( e ) ), 'Detect circular dependency' ); } - ); + ) + .always( done ); } ); QUnit.test( '.load() - Error: Circular dependency', function ( assert ) { @@ -162,6 +164,8 @@ } ); QUnit.test( '.using() - Error: Unregistered', function ( assert ) { + var done = assert.async(); + mw.loader.using( 'test.using.unreg' ).then( function done() { assert.ok( false, 'Unexpected resolution, expected error.' ); @@ -169,7 +173,7 @@ function fail( e ) { assert.ok( /Unknown/.test( String( e ) ), 'Detect unknown dependency' ); } - ); + ).always( done ); } ); QUnit.test( '.load() - Error: Unregistered (ignored)', 0, function ( assert ) { diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.storage.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.storage.test.js index 6cef4a7c81..436cb2ed75 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.storage.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.storage.test.js @@ -1,36 +1,56 @@ ( function ( mw ) { QUnit.module( 'mediawiki.storage' ); - QUnit.test( 'set/get with localStorage', 3, function ( assert ) { - this.sandbox.stub( mw.storage, 'localStorage', { + QUnit.test( 'set/get with storage support', function ( assert ) { + var stub = { setItem: this.sandbox.spy(), getItem: this.sandbox.stub() - } ); + }; + stub.getItem.withArgs( 'foo' ).returns( 'test' ); + stub.getItem.returns( null ); + this.sandbox.stub( mw.storage, 'store', stub ); mw.storage.set( 'foo', 'test' ); - assert.ok( mw.storage.localStorage.setItem.calledOnce ); + assert.ok( stub.setItem.calledOnce ); - mw.storage.localStorage.getItem.withArgs( 'foo' ).returns( 'test' ); - mw.storage.localStorage.getItem.returns( null ); assert.strictEqual( mw.storage.get( 'foo' ), 'test', 'Check value gets stored.' ); assert.strictEqual( mw.storage.get( 'bar' ), null, 'Unset values are null.' ); } ); - QUnit.test( 'set/get without localStorage', 3, function ( assert ) { - this.sandbox.stub( mw.storage, 'localStorage', { + QUnit.test( 'set/get with storage methods disabled', function ( assert ) { + // This covers browsers where storage is disabled + // (quota full, or security/privacy settings). + // On most browsers, these interface will be accessible with + // their methods throwing. + var stub = { getItem: this.sandbox.stub(), removeItem: this.sandbox.stub(), setItem: this.sandbox.stub() - } ); + }; + stub.getItem.throws(); + stub.setItem.throws(); + stub.removeItem.throws(); + this.sandbox.stub( mw.storage, 'store', stub ); - mw.storage.localStorage.getItem.throws(); assert.strictEqual( mw.storage.get( 'foo' ), false ); - - mw.storage.localStorage.setItem.throws(); assert.strictEqual( mw.storage.set( 'foo', 'test' ), false ); + assert.strictEqual( mw.storage.remove( 'foo', 'test' ), false ); + } ); + + QUnit.test( 'set/get with storage object disabled', function ( assert ) { + // On other browsers, these entire object is disabled. + // `'localStorage' in window` would be true (and pass feature test) + // but trying to read the object as window.localStorage would throw + // an exception. Such case would instantiate SafeStorage with + // undefined after the internal try/catch. + var old = mw.storage.store; + mw.storage.store = undefined; - mw.storage.localStorage.removeItem.throws(); + assert.strictEqual( mw.storage.get( 'foo' ), false ); + assert.strictEqual( mw.storage.set( 'foo', 'test' ), false ); assert.strictEqual( mw.storage.remove( 'foo', 'test' ), false ); + + mw.storage.store = old; } ); }( mediaWiki ) );