=== 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 ==
'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',
'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',
"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",
'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.
$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 = '<span class="redirect-in-category">' . $link . '</span>';
*/
$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.
*/
/** 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.
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.
*
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 ) );
}
}
+
+ /**
+ * 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 );
+ }
}
/**
*/
use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
use MediaWiki\Session\SessionManager;
use WrappedString\WrappedString;
use WrappedString\WrappedStringList;
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 ) );
}
/**
'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;
}
$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 ) );
}
}
}
* @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( "<p id=\"mw-returnto\">{$link}</p>\n" );
}
*/
use MediaWiki\Auth\AuthManager;
use MediaWiki\Auth\PasswordAuthenticationRequest;
+use MediaWiki\MediaWikiServices;
/**
* We're now using the HTMLForm object with some customisation to generate the
'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'] = [
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'] = [
$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() ] );
$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'] = [
$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();
# 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' )
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 );
// 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 ) {
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 );
}
/**
}
// Backwards compatibility warning
-if ( !$wgSessionsInObjectCache && !$wgSessionsInMemcached ) {
+if ( !$wgSessionsInObjectCache ) {
wfDeprecated( '$wgSessionsInObjectCache = false', '1.27' );
if ( $wgSessionHandler ) {
wfDeprecated( '$wgSessionsHandler', '1.27' );
$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.
* @author <evan@wikitravel.org>
*/
+use MediaWiki\MediaWikiServices;
+
/**
* @ingroup Actions
*/
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 );
}
/**
* @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' ]
);
* @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
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() ]
);
$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',
*/
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 = "<span class=\"history-deleted\">$link</span>";
if ( $latest || !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
return $cur;
} else {
- return Linker::linkKnown(
+ return MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink(
$this->getTitle(),
$cur,
[],
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,
[],
return $last;
}
- return Linker::linkKnown(
+ return $linkRenderer->makeKnownLink(
$this->getTitle(),
$last,
[],
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()
) {
// 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
// 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,
foreach ( $sources as $sourceTitle ) {
$cascadingFrom .= Html::rawElement(
- 'li', [], Linker::linkKnown( $sourceTitle ) );
+ 'li', [], $linkRenderer->makeKnownLink( $sourceTitle ) );
}
$cascadingFrom = Html::rawElement( 'ul', [], $cascadingFrom );
// 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() ]
)
// 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() ]
)
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 ]
);
$real_names = [];
$user_names = [];
$anon_ips = [];
+ $linkRenderer = MediaWikiServices::getLinkRenderer();
# Sift for real versus user names
/** @var $user User */
$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() );
}
}
* @ingroup Actions
*/
+use MediaWiki\MediaWikiServices;
+
/**
* Mark a revision as patrolled on a page
*
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' );
'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(),
* @file
*/
+use MediaWiki\Session\Token;
+
/**
* @since 1.25
* @ingroup API
$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 ) ) {
$res['result'] = 'invalid';
}
- $ts = MediaWiki\Session\Token::getTimestamp( $token );
+ $ts = Token::getTimestamp( $token );
if ( $ts !== null ) {
$mwts = new MWTimestamp();
$mwts->timestamp->setTimestamp( $ts );
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' ) ],
],
],
];
$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 ) {
$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 ) {
function setExpiry( $cacheExpiry );
}
+use MediaWiki\MediaWikiServices;
+
/**
* Helper class for caching various elements in a single cache entry.
*
$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
);
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',
$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 );
}
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
* @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
$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() ]
);
--- /dev/null
+<?php
+
+namespace MediaWiki\Logger\Monolog;
+
+/**
+ * LogstashFormatter squashes the base message array and the context and extras subarrays into one.
+ * This can result in unfortunately named context fields overwriting other data (T145133).
+ * This class modifies the standard LogstashFormatter to rename such fields and flag the message.
+ *
+ * Compatible with Monolog 1.x only.
+ *
+ * @since 1.29
+ */
+class LogstashFormatter extends \Monolog\Formatter\LogstashFormatter {
+ /** @var array Keys which should not be used in log context */
+ protected $reservedKeys = [
+ // from LogstashFormatter
+ 'message', 'channel', 'level', 'type',
+ // from WebProcessor
+ 'url', 'ip', 'http_method', 'server', 'referrer',
+ // from WikiProcessor
+ 'host', 'wiki', 'reqId', 'mwversion',
+ // from config magic
+ 'normalized_message',
+ ];
+
+ /**
+ * Prevent key conflicts
+ * @param array $record
+ * @return array
+ */
+ protected function formatV0( array $record ) {
+ if ( $this->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;
+ }
+}
* @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
*/
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'],
[
);
return $record;
}
+
}
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' ) );
}
}
}
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;
$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() ) {
$this->parseHeader();
$this->setStatus();
- return $this->status;
+ return Status::wrap( $this->status ); // TODO B/C; move this to callers
}
/**
* - 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
*/
} 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;
}
protected $reqHeaders = [];
protected $url;
protected $parsedUrl;
+ /** @var callable */
protected $callback;
protected $maxRedirects = 5;
protected $followRedirects = false;
+ protected $connectTimeout;
/**
* @var CookieJar
protected $respStatus = "200 Ok";
protected $respHeaders = [];
- public $status;
+ /** @var StatusValue */
+ protected $status;
/**
* @var Profiler
}
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' ) {
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" ];
* @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
*/
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"' );
}
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.'
}
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.' );
}
}
*
* @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;
* Get an array of the headers
* @return array
*/
- public function getHeaderList() {
+ protected function getHeaderList() {
$list = [];
if ( $this->cookieJar ) {
* 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;
}
* @param resource $fh
* @param string $content
* @return int
+ * @internal
*/
public function read( $fh, $content ) {
$this->content .= $content;
/**
* 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" ) {
$this->proxySetup(); // set up any proxy as needed
if ( !$this->callback ) {
- $this->setCallback( [ $this, 'read' ] );
+ $this->setCallback( null );
}
if ( !isset( $this->reqHeaders['User-Agent'] ) ) {
/**
* 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 ) {
* 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 );
}
* 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;
}
public function execute() {
-
- parent::execute();
+ $this->prepare();
if ( is_array( $this->postData ) ) {
$this->postData = wfArrayToCgi( $this->postData );
. ': 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
}
fclose( $fh );
- return $this->status;
+ return Status::wrap( $this->status ); // TODO B/C; move this to callers
}
}
* @since 1.17
*/
abstract class DatabaseUpdater {
- protected static $updateCounter = 0;
-
/**
* Array of updates to perform on the database
*
* @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 );
$this->checkStats();
}
- $this->setAppliedUpdates( $wgVersion, $this->updates );
-
if ( $this->fileHandle ) {
$this->skipSchema = false;
$this->writeSchemaUpdateFile();
- $this->setAppliedUpdates( "$wgVersion-schema", $this->updatesSkipped );
}
}
$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
"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": "<strong>MediaWiki has been installed.</strong>",
- "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]"
}
* @ingroup HTTP
*/
+/**
+ * Cookie jar to use with MWHttpRequest. Does not handle cookie unsetting.
+ */
class CookieJar {
+ /** @var Cookie[] */
private $cookie = [];
/**
--- /dev/null
+<?php
+
+namespace Wikimedia\Rdbms;
+
+use Database;
+use DBConnRef;
+use IDatabase;
+use InvalidArgumentException;
+use LoadBalancer;
+
+/**
+ * Database connection manager.
+ *
+ * This manages access to master and replica databases.
+ *
+ * @since 1.29
+ *
+ * @license GPL-2.0+
+ * @author Addshore
+ */
+class ConnectionManager {
+
+ /**
+ * @var LoadBalancer
+ */
+ private $loadBalancer;
+
+ /**
+ * The symbolic name of the target database, or false for the local wiki's database.
+ *
+ * @var string|false
+ */
+ private $domain;
+
+ /**
+ * @var string[]
+ */
+ private $groups = [];
+
+ /**
+ * @param LoadBalancer $loadBalancer
+ * @param string|bool $domain Optional logical DB name, defaults to current wiki.
+ * This follows the convention for database names used by $loadBalancer.
+ * @param string[] $groups see LoadBalancer::getConnection
+ *
+ * @throws InvalidArgumentException
+ */
+ public function __construct( LoadBalancer $loadBalancer, $domain = false, array $groups = [] ) {
+ if ( !is_string( $domain ) && $domain !== false ) {
+ throw new InvalidArgumentException( '$dbName must be a string, or false.' );
+ }
+
+ $this->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 );
+ }
+
+}
--- /dev/null
+<?php
+
+namespace Wikimedia\Rdbms;
+
+use Database;
+use DBConnRef;
+
+/**
+ * Database connection manager.
+ *
+ * This manages access to master and replica databases. It also manages state that indicates whether
+ * the replica databases are possibly outdated after a write operation, and thus the master database
+ * should be used for subsequent read operations.
+ *
+ * @note: Services that access overlapping sets of database tables, or interact with logically
+ * related sets of data in the database, should share a SessionConsistentConnectionManager.
+ * Services accessing unrelated sets of information may prefer to not share a
+ * SessionConsistentConnectionManager, so they can still perform read operations against replica
+ * databases after a (unrelated, per the assumption) write operation to the master database.
+ * Generally, sharing a SessionConsistentConnectionManager improves consistency (by avoiding race
+ * conditions due to replication lag), but can reduce performance (by directing more read
+ * operations to the master database server).
+ *
+ * @since 1.29
+ *
+ * @license GPL-2.0+
+ * @author Daniel Kinzler
+ * @author Addshore
+ */
+class SessionConsistentConnectionManager extends ConnectionManager {
+
+ /**
+ * @var bool
+ */
+ private $forceWriteConnection = false;
+
+ /**
+ * Forces all future calls to getReadConnection() to return a write connection.
+ * Use this before performing read operations that are critical for a future update.
+ *
+ * @since 1.29
+ */
+ public function prepareForUpdates() {
+ $this->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();
+ }
+
+}
/**
* 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
* @since 1.25
*/
+use MediaWiki\MediaWikiServices;
+
/**
* This class formats block log entries.
*
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' )
// 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()
)
];
<?php
+use MediaWiki\MediaWikiServices;
+
class ContentModelLogFormatter extends LogFormatter {
protected function getMessageParameters() {
$lang = $this->context->getLanguage();
}
$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(),
* @since 1.22
*/
+use MediaWiki\MediaWikiServices;
+
/**
* This class formats delete log entries.
*
public function getActionLinks() {
$user = $this->context->getUser();
+ $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
if ( !$user->isAllowed( 'deletedhistory' )
|| $this->entry->isDeleted( LogPage::DELETED_ACTION )
) {
} 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() ]
);
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] ),
);
// 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(),
}
// 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(),
$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(),
* @file
*/
+use MediaWiki\MediaWikiServices;
+
class LogEventsList extends ContextSource {
const NO_ACTION_LINK = 1;
const NO_EXTRA_USER_LINKS = 2;
*/
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.
$hideVal = 1 - intval( $val );
$query[$queryKey] = $hideVal;
- $link = Linker::linkKnown(
+ $link = $linkRenderer->makeKnownLink(
$this->getTitle(),
$messages[$hideVal],
[],
$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
);
* @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;
'thumbnail_error',
$width,
$height,
- wfMessage( 'thumbnail_dest_directory' )->text()
+ wfMessage( 'thumbnail_dest_directory' )
);
}
return new MediaTransformError( 'thumbnail_error',
$params['width'], $params['height'],
- wfMessage( 'filemissing' )->text()
+ wfMessage( 'filemissing' )
);
}
* @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;
* @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;
function toHtml( $options = [] ) {
return "<div class=\"MediaTransformError\" style=\"" .
"width: {$this->width}px; height: {$this->height}px; display:inline-block;\">" .
- $this->htmlMsg .
+ $this->getHtmlMsg() .
"</div>";
}
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() {
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() {
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() {
$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();
return new MediaTransformError( 'thumbnail_error',
$params['width'], $params['height'],
- wfMessage( 'filemissing' )->text()
+ wfMessage( 'filemissing' )
);
}
wfHostname(), $lnPath, $srcPath ) );
return new MediaTransformError( 'thumbnail_error',
$params['width'], $params['height'],
- wfMessage( 'thumbnail-temp-create' )->text()
+ wfMessage( 'thumbnail-temp-create' )
);
}
return new MediaTransformError( 'thumbnail_error',
$scalerParams['clientWidth'], $scalerParams['clientHeight'],
- wfMessage( 'filemissing' )->text()
+ wfMessage( 'filemissing' )
);
}
# 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;
* @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,
$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 );
}
/**
$str .= $this->getDelimiter();
}
- return $str . $this->hash;
+ $res = $str . $this->hash;
+ $this->assertIsSafeSize( $res );
+ return $res;
}
/**
*/
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
*
* 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" );
+ }
}
/**
* 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,
* 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;
}
$groups = User::getAllGroups();
foreach ( $groups as $group ) {
- $msg = User::getGroupName( $group );
+ $msg = htmlspecialchars( User::getGroupName( $group ) );
$options[$msg] = $group;
}
]
);
+ $linkRenderer = $this->getLinkRenderer();
if ( $res->numRows() > 0 ) {
$out = Html::openElement( 'ul', [ 'class' => 'mw-allpages-chunk' ] );
$out .= '<li' .
( $s->page_is_redirect ? ' class="allpagesredirect"' : '' ) .
'>' .
- Linker::link( $t ) .
+ $linkRenderer->makeLink( $t ) .
"</li>\n";
} else {
$out .= '<li>[[' . htmlspecialchars( $s->page_title ) . "]]</li>\n";
$navLinks = [];
$self = $this->getPageTitle();
+ $linkRenderer = $this->getLinkRenderer();
// Generate a "previous page" link if needed
if ( $prevTitle ) {
$query = [ 'from' => $prevTitle->getText() ];
$query['hideredirects'] = $hideredirects;
}
- $navLinks[] = Linker::linkKnown(
+ $navLinks[] = $linkRenderer->makeKnownLink(
$self,
- $this->msg( 'prevpage', $prevTitle->getText() )->escaped(),
+ $this->msg( 'prevpage', $prevTitle->getText() )->text(),
[],
$query
);
$query['hideredirects'] = $hideredirects;
}
- $navLinks[] = Linker::linkKnown(
+ $navLinks[] = $linkRenderer->makeKnownLink(
$self,
- $this->msg( 'nextpage', $t->getText() )->escaped(),
+ $this->msg( 'nextpage', $t->getText() )->text(),
[],
$query
);
$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 ) );
$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()
);
}
$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' ]
);
}
}
+ $linkRenderer = $this->getLinkRenderer();
// $toObj may very easily be false if the $result list is cached
if ( !is_object( $toObj ) ) {
- return '<del>' . Linker::link( $fromObj ) . '</del>';
+ return '<del>' . $linkRenderer->makeLink( $fromObj ) . '</del>';
}
- $from = Linker::linkKnown(
+ $from = $linkRenderer->makeKnownLink(
$fromObj,
null,
[],
// 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' ]
);
$result = $dbr->fetchObject( $res );
}
}
+ $linkRenderer = $this->getLinkRenderer();
if ( !$result ) {
- return '<del>' . Linker::link( $titleA, null, [], [ 'redirect' => 'no' ] ) . '</del>';
+ return '<del>' . $linkRenderer->makeLink( $titleA, null, [], [ 'redirect' => 'no' ] ) . '</del>';
}
$titleB = Title::makeTitle( $result->nsb, $result->tb );
$titleC = Title::makeTitle( $result->nsc, $result->tc, '', $result->iwc );
- $linkA = Linker::linkKnown(
+ $linkA = $linkRenderer->makeKnownLink(
$titleA,
null,
[],
// 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();
// 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(),
);
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 ) );
}
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',
'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',
'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' ]
]
];
)
);
}
+ $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,
[],
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' );
* @ingroup SpecialPage
*/
+use MediaWiki\MediaWikiServices;
+
/**
* MediaWiki page data importer
*
}
$this->mPageCount++;
-
+ $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
if ( $successCount > 0 ) {
// <bdi> prevents jumbling of the versions count
// in RTL wikis in case the page title is LTR
$this->getOutput()->addHTML(
- "<li>" . Linker::linkKnown( $title ) . " " .
+ "<li>" . $linkRenderer->makeLink( $title ) . " " .
"<bdi>" .
$this->msg( 'import-revision-count' )->numParams( $successCount )->escaped() .
"</bdi>" .
);
}
} else {
- $this->getOutput()->addHTML( "<li>" . Linker::linkKnown( $title ) . " " .
+ $this->getOutput()->addHTML( "<li>" . $linkRenderer->makeKnownLink( $title ) . " " .
$this->msg( 'import-nonewrevisions' )->escaped() . "</li>\n" );
}
}
$id = \Sanitizer::escapeId( $grant );
$out->addHTML( \Html::rawElement( 'tr', [ 'id' => $id ],
- "<td>" . $this->msg( "grant-$grant" )->escaped() . "</td>" .
- "<td>" . $grantCellHtml . '</td>'
+ "<td>" .
+ $this->msg(
+ "listgrants-grant-display",
+ \User::getGrantName( $grant ),
+ "<span class='mw-listgrants-grant-name'>" . $id . "</span>"
+ )->parse() .
+ "</td>" .
+ "<td>" . $grantCellHtml . "</td>"
) );
}
) );
asort( $allGroups );
+ $linkRenderer = $this->getLinkRenderer();
+
foreach ( $allGroups as $group ) {
$permissions = isset( $groupPermissions[$group] )
? $groupPermissions[$group]
// 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 = '<br />' . Linker::linkKnown(
+ $grouplink = '<br />' . $linkRenderer->makeKnownLink(
SpecialPage::getTitleFor( 'Listusers' ),
- $this->msg( 'listgrouprights-members' )->escaped()
+ $this->msg( 'listgrouprights-members' )->text()
);
} elseif ( !in_array( $group, $config->get( 'ImplicitGroups' ) ) ) {
- $grouplink = '<br />' . Linker::linkKnown(
+ $grouplink = '<br />' . $linkRenderer->makeKnownLink(
SpecialPage::getTitleFor( 'Listusers' ),
- $this->msg( 'listgrouprights-members' )->escaped(),
+ $this->msg( 'listgrouprights-members' )->text(),
[],
[ 'group' => $group ]
);
$this->msg( 'listgrouprights-namespaceprotection-restrictedto' )->text()
)
);
-
+ $linkRenderer = $this->getLinkRenderer();
ksort( $namespaceProtection );
foreach ( $namespaceProtection as $namespace => $rights ) {
if ( !in_array( $namespace, MWNamespace::getValidNamespaces() ) ) {
Html::rawElement(
'td',
[],
- Linker::link(
+ $linkRenderer->makeLink(
SpecialPage::getTitleFor( 'Allpages' ),
- htmlspecialchars( $namespaceText ),
+ $namespaceText,
[],
[ 'namespace' => $namespace ]
)
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() );
$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() );
*/
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',
function formatRevisionRow( $row ) {
$rev = new Revision( $row );
+ $linkRenderer = $this->getLinkRenderer();
+
$stxt = '';
$last = $this->msg( 'last' )->escaped();
$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() ]
);
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,
return false;
}
- $targetLink = Linker::link(
+ $linkRenderer = $this->getLinkRenderer();
+
+ $targetLink = $linkRenderer->makeLink(
$targetTitle,
null,
[],
);
}
+ $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();
);
}
+ $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();
function makeWlhLink( $title, $caption ) {
$wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedDBkey() );
- return Linker::linkKnown( $wlh, $caption );
+ $linkRenderer = $this->getLinkRenderer();
+ return $linkRenderer->makeKnownLink( $wlh, $caption );
}
/**
);
}
- $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 );
$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() ) {
$out->addHTML( "<div class='searchresults'>" );
+ $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 ) {
}
$titleMatches->free();
}
- if ( $textMatches && !$textStatus ) {
+
+ if ( $textMatches ) {
// output appropriate heading
if ( $numTextMatches > 0 && $numTitleMatches > 0 ) {
$out->addHTML( '<div class="mw-search-visualclear"></div>' );
$hasOtherResults = $textMatches &&
$textMatches->hasInterwikiResults( SearchResultSet::INLINE_RESULTS );
- if ( $num === 0 ) {
- if ( $textStatus ) {
- $out->addHTML( '<div class="error">' .
- $textStatus->getMessage( 'search-error' ) . '</div>' );
- } 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( "<p class=\"mw-search-nonefound\">\n$1</p>",
- [ $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( "<p class=\"mw-search-nonefound\">\n$1</p>", [
+ $hasOtherResults ? 'search-nonefound-thiswiki' : 'search-nonefound',
+ wfEscapeWikiText( $term )
+ ] );
}
if ( $hasOtherResults ) {
* @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 );
return [
'tables' => [ 'category' ],
'fields' => [ 'cat_title', 'cat_pages' ],
- 'conds' => [ 'cat_pages > 0' ],
'options' => [ 'USE INDEX' => 'cat_title' ],
];
}
* 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(
}
if ( $result === false ) {
- $status->merge( $upp->checkUserPassword( $this, $password, $purpose ) );
+ $status->merge( $upp->checkUserPassword( $this, $password ) );
return $status;
} elseif ( $result === true ) {
return $status;
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.
*
/**
* 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();
}
/**
"whatlinkshere-hidelinks": "$1 باغلانتیلاری",
"whatlinkshere-hideimages": "فایل باغلانتیلارینی $1",
"whatlinkshere-filters": "سۆزگَجلر",
+ "whatlinkshere-submit": "گئت",
"autoblockid": "اوتوماتیک باغلانما #$1",
"block": "ایستیفادچینی باغلاما",
"unblock": "ایستیفادهچینین باغلانماسین گؤتور",
"pageinfo-category-pages": "Колькасьць старонак",
"pageinfo-category-subcats": "Колькасьць падкатэгорыяў",
"pageinfo-category-files": "Колькасьць файлаў",
+ "pageinfo-user-id": "Ідэнтыфікатар удзельніка",
"markaspatrolleddiff": "Пазначыць як «патруляваную»",
"markaspatrolledtext": "Пазначыць гэтую старонку як «патруляваную»",
"markaspatrolledtext-file": "Пазначыць гэтую вэрсію файлу як патруляваную",
"tuesday": "Sêşeme",
"wednesday": "Çarşeme",
"thursday": "Pancşeme",
- "friday": "Yene",
+ "friday": "Êne",
"saturday": "Şeme",
"sun": "Krê",
"mon": "Dış",
"morenotlisted": "Na lista qay kemi ya.",
"mypage": "Pele",
"mytalk": "Mesac",
- "anontalk": "Werênayış",
+ "anontalk": "Vaten",
"navigation": "Pusula",
"and": " u",
"qbfind": "Bıvêne",
"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",
"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",
"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ı",
"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",
"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.",
"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": "<strong>İqaz:</strong> Şıma be hesabê xo nêkewtê cı. \nAdresê şımayê IP tarixê vırnayışê na pele de do qeyd bo. Eke şıma <strong>[$1 cıkewê]</strong> ya zi <strong>[$2 hesab vırazê]</strong>, 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.\"",
"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 →",
"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",
"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": "<strong>Na wiki de pela \"[[:$1]]\" vıraze!</strong> {{PLURAL:$2|0=|Sewbina pela ke şıma geyrayê cı aye bıvênê.|Yew zi neticanê cıgeyrayışê xo bıvênê.}}",
- "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}})",
"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",
"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",
"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",
"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",
"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ê",
"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",
"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:",
"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",
"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",
"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",
"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/* <table class=\"highlighthovertable\"> */\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. */",
"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",
"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* <span class=\"mw-specialpagerestricted\">Pelê xasê nımıtey.</span>",
"specialpages-group-maintenance": "Raporê pawıtışi",
"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": "<form method=\"get\" action=\"//www.google.com/search\" id=\"googlesearch\">\n\t<input type=\"hidden\" name=\"domains\" value=\"{{SERVER}}\" />\n\t<input type=\"hidden\" name=\"num\" value=\"50\" />\n\t<input type=\"hidden\" name=\"ie\" value=\"$2\" />\n\t<input type=\"hidden\" name=\"oe\" value=\"$2\" />\n\n\t<input type=\"text\" name=\"q\" size=\"31\" maxlength=\"255\" value=\"$1\" />\n\t<input type=\"submit\" name=\"btnG\" value=\"$3\" />\n <div>\n\t<input type=\"radio\" name=\"sitesearch\" id=\"gwiki\" value=\"{{SERVER}}\" checked=\"checked\" /><label for=\"gwiki\">{{SITENAME}}</label>\n\t<input type=\"radio\" name=\"sitesearch\" id=\"gWWW\" value=\"\" /><label for=\"gWWW\">WWW</label>\n </div>\n</form>",
"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": "",
"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 <code>($2)</code>",
"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",
"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",
"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",
"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.",
"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ä",
"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:"
}
"cannotdelete": "Չհաջողվեց ջնջել «$1» էջը կամ ֆայլը։\nՀավանաբար այն արդեն ջնջվել է մեկ այլ մասնակցի կողմից։",
"cannotdelete-title": "Հնարավոր չէ ջնջել $1 էջը",
"delete-hook-aborted": "Խմբագրել չեղյալ է.\nԼրացուցիչ պարզաբանումներ չի դրվել.",
- "no-null-revision": "Չի հաջողվել ստեղծել նոր զրոյական правку համար էջը \"$1\"",
+ "no-null-revision": "Չի հաջողվել ստեղծել նոր զրոյական խմբագրում էջի համար \"$1\"",
"badtitle": "Անընդունելի անվանում",
"badtitletext": "Հարցված էջի անվանումը անընդունելի է, դատարկ է կամ սխալ միջ-լեզվական կամ ինտերվիքի անվանում է։ Հնարավոր է, որ այն պարունակում է անթույլատրելի սիմվոլներ։",
"title-invalid-empty": "Էջի հայցվող վերնագիրը դատարկ է կամ պարունակում է միայն անվանատարածքի անունը։",
"namespaces": "नामविश्वे",
"variants": "चले(व्हेरियंट्स)",
"navigation-heading": "दिक्चालन यादी",
- "errorpagetitle": "à¤\9aà¥\82à¤\95",
+ "errorpagetitle": "तà¥\8dरà¥\81à¤\9fà¥\80",
"returnto": "$1 कडे परत चला.",
"tagline": "{{SITENAME}} कडून",
"help": "साहाय्य",
"listgrants-rights": "अधिकार",
"trackingcategories": "मागोवा घेणारे वर्ग",
"trackingcategories-summary": "या पानात ते रेखापथनातील वर्ग(tracking categories) आहेत, जे, मिडियाविकि संचेतनाद्वारे स्वयंचलितरित्या वसविण्यात (तयार करण्यात) आले आहेत. त्यांची नावे, {{ns:8}} नामविश्वातील संबंधित प्रणाली संदेशात फेरफार करुन, बदलविता येतात.",
+ "trackingcategories-msg": "मागोवा घेणारा वर्ग",
"trackingcategories-name": "संदेश नाम",
"trackingcategories-desc": "वर्ग अंतर्भूत करण्याचे निकष",
"trackingcategories-nodesc": "वर्णन उपलब्ध नाही.",
"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}}",
"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}}",
"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è <em>$2</em>.",
- "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",
"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è.",
"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",
"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.",
"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",
"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",
"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": "Муҥутуур болдьох <kbd>муҥутуурдук</kbd> туттулларын туоруор.",
+ "apisandbox-multivalue-all-namespaces": "$1 (Аат даллара барыта)",
+ "apisandbox-multivalue-all-values": "$1 (Бары суолталара)",
"booksources": "Кинигэлэр источниктара",
"booksources-search-legend": "Кинигэ туһунан көрдөө",
"booksources-search": "Бул",
"databaseerror-query": "Курон: $1",
"databaseerror-function": "Функция: $1",
"databaseerror-error": "Янгыш: $1",
+ "badtitle": "Умойтэм ним",
"badtitletext": "Курем бам ним луэ мыдлань, буш либо кылъёс куспын яке викиос куспын нимыз умойтэм герӟамын.\nНимын, вылды, ярантэм символъёс вань.",
"viewsource": "Кодзэ учкыны",
"viewsource-title": "Кодзэ учкыны бам $1",
"createacct-another-username-ph": "Учётной книга нимъёс пыртэмын",
"yourpassword": "Лушкемкыл:",
"userlogin-yourpassword": "Лушкемкыл",
+ "userlogin-yourpassword-ph": "Гожтэ асьтэлэсь парольдэс",
"createacct-yourpassword-ph": "Гожтэ паролез",
"createacct-yourpasswordagain": "Пароль юнматэ",
"createacct-yourpasswordagain-ph": "Гожтэ паролез эшшо одӥг пол",
"logout": "Кошкыны",
"userlogout": "Потыны",
"notloggedin": "Тон эн тусбуяськыны сӧзнэтэз",
+ "userlogin-noaccount": "Ас учётной записьты ӧвӧл?",
"nologin": "Учётной книга ӧвӧл-а? $1.",
"nologinlink": "Выль вики-авторлэн регистрациез",
"createaccount": "выль вики-авторлэн регистрациез",
"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",
"editing": "Тупатон: $1",
"creating": "«$1» бамез кылдытон",
"editingsection": "Тупатон: $1 (люкет)",
+ "templatesused": "Та бам пушкы пыртэм {{PLURAL:$1|шаблон|шаблонъёс}}:",
"template-protected": "(утемын)",
"template-semiprotected": "(полуутемын)",
+ "hiddencategories": "Та бам пыре {{PLURAL:$1|$1 ватэм категорие}}:",
"nocreatetext": "Та сайтлэн бамаз выль сюбегатэм луонлыкъёсын кылдытон.\nТон улыса, берлань вуэ быгатэ бам отредактировать, [[Special:UserLogin|тусбуяськыны книгае яке выль система кылдыто учётной]].",
"nocreate-loggedin": "Тон доразы юаськыны кылдӥз выль бам ӧвӧл.",
"permissionserrors": "Янгышъёс юаське",
"cantcreateaccount-text": "Та книгаез кылдытонлы учётной IP-адрес (<strong>$1</strong>) заблокировать луизы [[User:$3|$3]].\n\nМугез, вайиз $3 возьматэ <em>$2</em>",
"cantcreateaccount-range-text": "Учётной кылдытон - гожъян IP-адрес диапазонын <strong>$1</strong>, Тон пыриське со IP-адрес (<strong>$4</strong>), заблокировать луизы [[User:$3|$3]].\n\nМугез, вайиз $3 возьматэ <em>$2</em>",
"viewpagelogs": "Та бамлы журналъёсыз возьматыны",
+ "currentrev-asof": "Алиез версия $1",
"revisionasof": "Версия $1",
+ "revision-info": "Версия $1; {{GENDER:$6|$2}}$7",
"previousrevision": "← Вужгем",
+ "nextrevision": "Выльгем →",
+ "currentrevisionlink": "Алиез версия",
"cur": "али",
"last": "азьв.",
"history-show-deleted": "Ӵушылэмъёссэ гинэ",
"revdelete-radio-unset": "Адӟымон",
"revdelete-reason-dropdown": "*Вӧлскем палэнскон мугъёсты\n** Авторской правоосты тӥян\n** Яке кулэтэм информациез личной комментарий\n** Логин несоответствовать\n** Курла информациез Потенциально",
"history-title": "$1 — воштонъёслэн историзы",
+ "difference-title": "$1 — версиосыз куспын пӧртэмлык",
"lineno": "$1-тӥ чур:",
"compareselectedversions": "Быръем версиосыз ӵошатыны",
"showhideselectedversions": "Возьматыны/ватыны быръем версиосыз",
"search-result-size": "$1 ({{PLURAL:$2|$2 кыл}})",
"search-redirect": "($1 бамысь ыстон)",
"search-section": "(«$1» люкет)",
+ "search-suggest": "Тӥ, вылды, утчаллямды «$1».",
"search-interwiki-more": "(эшшо)",
"searchall": "Ваньзэ",
"search-showingresults": "{{PLURAL:$4|<strong>$3</strong> пӧлысь <strong>$1-тӥ</strong> шедьтэм|<strong>$3</strong> пӧлысь <strong>$1—$2</strong> шедьтэмъёс}}",
"rcshowhideminor-hide": "Ватыны",
"rcshowhidebots": "$1 ботъёсыз",
"rcshowhidebots-show": "Возьматыны",
+ "rcshowhidebots-hide": "Ватыны",
"rcshowhideliu": "$1 пырем викиавторъёсыз",
"rcshowhideliu-show": "Возьматыны",
"rcshowhideliu-hide": "Ватыны",
"upload-dialog-button-cancel": "Берытсконо",
"license-header": "Лицензия",
"nolicense": "Ӧвӧл",
+ "imgfile": "файл",
"file-anchor-link": "Файл",
"filehist": "Файллэн историез",
"filehist-help": "Зӥбе дата/дыр шоры, кызьы файл со дырын адӟиськемез учкыны вылысь.",
"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": "Возьматыны/ватыны быръем журналъёсысь гожъямъёсыз",
"checkbox-all": "Ваньзэ",
"checkbox-none": "Номыре",
"checkbox-invert": "Воштыны интыен",
+ "allarticles": "Ваньмыз бамъёс",
"allpagessubmit": "Быдэстоно",
+ "categories": "Категориос",
"categories-submit": "Возьматыны",
"sp-deletedcontributions-contribs": "тупатонъёсыз",
"listusers-submit": "Возьматыны",
"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": "Тупатон",
"mycontris": "Гожтэмъёс",
"anoncontribs": "Гожтэмъёс",
"nocontribs": "Критерии нокыӵе воштӥськонъёс та соответствующий шедьтыны уг луы.",
+ "month": "Толэзьысен (вазен но):",
+ "year": "Арысен (вазен но):",
"sp-contributions-blocklog": "блокировка",
"sp-contributions-deleted": "шонертон палэнтыны {{GENDER:$1|участник|куакеч}}",
"sp-contributions-blocked-notice": "Пользователь заблокирован сётӥз та учырлы. Справка понна радъяськылӥсь журнал блокировка лапег берпуметӥ гожтэт:",
"block-log-flags-nousertalk": "тупатъяны ачиз уггес быгаты бамлэн обсуждениосаз",
"range_block_disabled": "Администратор диапазонэз блокировать али.",
"move-watch": "Чаклан списоке пыртоно инъет но валтӥсь бамъёсыз",
+ "movelogpage": "Нимъёсты воштонъёсын журнал",
"export": "Бамъёсты поттон",
"allmessagesname": "Ивортон",
"allmessages-filter-all": "Ваньзэ",
"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": "Категорилэн бамез",
"pageinfo-header-edits": "Воштонъёслэн историзы",
"pageinfo-toolboxlink": "Бам сярысь тодэтъёс",
"previousdiff": "← Вужгес тупатон",
+ "nextdiff": "Выльгес тупатон →",
"file-info-size": "$1 × $2 пиксель, файллэн быдӟалаез: $3, MIME-тип: $4",
"file-nohires": "Бадӟымгес быдӟалаен суред ӧвӧл.",
"svg-long-desc": "SVG файл, номинально $1 × $2 пиксель, файллэн быдӟалаез: $3",
"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-адрес заблокировать эрказ луи, малы ке шуоно со заблокировать пользователь кутыны луоз.",
* @author Numulunj pilgae
*/
-$fallbak = 'ru';
+$fallback = 'ru';
$namespaceNames = [
NS_MEDIA => 'Медиа',
"mw.notification",
"mw.Notification_",
"mw.storage",
+ "mw.storage.session",
"mw.user",
"mw.util",
"mw.plugin.*",
* @ingroup Maintenance
*/
class MergeMessageFileList extends Maintenance {
- /**
- * @var bool
- */
- protected $hasError;
-
function __construct() {
parent::__construct();
$this->addOption(
}
if ( !$found ) {
- $this->hasError = true;
$this->error( "Extension {$extname} in {$extdir} lacks expected entry point: " .
"extension.json, skin.json, or {$extname}.php." );
}
$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' );
}
}
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' );
"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": "Σύρετε το αρχείο εδώ"
/*!
- * 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 ) {
/*!
- * 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;
/*!
- * 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;
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,
}
.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;
}
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;
.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;
}
.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 {
}
.oo-ui-fieldLayout {
display: block;
- margin-bottom: 1em;
+ margin-top: 1.640625em;
}
.oo-ui-fieldLayout:before,
.oo-ui-fieldLayout:after {
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;
.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;
}
.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;
}
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;
}
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;
}
.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;
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;
/*!
- * 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 ) {
}
return message;
};
-} )();
+}() );
/**
* Package a message and arguments for deferred resolution.
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 );
}
this.popup = new OO.ui.PopupWidget( $.extend(
{ autoClose: true },
config.popup,
- { $autoCloseIgnore: this.$element }
+ { $autoCloseIgnore: this.$element.add( config.popup && config.popup.$autoCloseIgnore ) }
) );
};
* @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 ) {
icon: 'info'
} );
- div = $( '<div>' );
+ $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 {
* @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 || {};
icon: 'info'
} );
+ $div = $( '<div>' );
+ if ( config.help instanceof OO.ui.HtmlSnippet ) {
+ $div.html( config.help.toString() );
+ } else {
+ $div.text( config.help );
+ }
this.popupButtonWidget.getPopup().$body.append(
- $( '<div>' )
- .text( config.help )
- .addClass( 'oo-ui-fieldsetLayout-help-content' )
+ $div.addClass( 'oo-ui-fieldsetLayout-help-content' )
);
this.$help = this.popupButtonWidget.$element;
} else {
/*!
- * 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 ) {
/*!
- * 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 {
/*!
- * 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;
/*!
- * 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 ) {
/*!
- * 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 {
/*!
- * 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 {
.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;
/*!
- * 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 ) {
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 );
}
/*!
- * 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);
/*!
- * 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;
/*!
- * 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 ) {
*
* @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
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 );
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #c33 }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #d33 }</style>
<path d="M12 4c-4.4 0-8 3.6-8 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm5 9H7v-2h10v2z"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #c33 }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #d33 }</style>
<g id="cancel">
<path id="circle-with-strike" d="M12 5.022a6.98 6.98 0 0 0-.003 13.956 6.98 6.98 0 0 0-.002-13.956zM6.885 12c0-1.092.572-3.25.93-2.93l7.113 7.114c.487.525-1.838.93-2.93.93A5.113 5.113 0 0 1 6.884 12zm9.298 2.93L9.07 7.815c-.445-.483 1.837-.93 2.93-.93a5.112 5.112 0 0 1 5.114 5.113c0 1.092-.364 3.542-.93 2.93z"/>
</g>
<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #c33 }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #d33 }</style>
<path d="M17 7.5L9.5 15 6 11.5 4.5 13l5 5L20 7.5c-.706-.706-2.294-.706-3 0z" id="check"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #c33 }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #d33 }</style>
<path d="M15 8s0-3-2.5-3S10 8 10 8v1h5zm2 0v1h2v10H9c-1.7 0-3-1.3-3-3V9h2V8s0-5 4.5-5S17 8 17 8z"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #c33 }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #d33 }</style>
<path d="M10 8s0-3 2.5-3S15 8 15 8v1h-5zM8 8v1H6v10h10c1.7 0 3-1.3 3-3V9h-2V8s0-5-4.5-5S8 8 8 8z"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #c33 }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #d33 }</style>
<path d="M18.748 11.717a1 1 0 0 1 0 1.414l-4.95 4.95a1 1 0 0 1-1.413 0l-6.01-6.01c-.39-.382-.707-1.15-.707-1.7V6c0-.55.45-1 1-1h4.363c.55 0 1.32.318 1.71.707l6.01 6.01zM8.104 7.457a1.477 1.477 0 0 0 0 2.092 1.49 1.49 0 0 0 2.094 0 1.49 1.49 0 0 0 0-2.1 1.484 1.484 0 0 0-2.094 0z" id="tag"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #c33 }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #d33 }</style>
<path d="M6 8c0-1.1.9-2 2-2h2l1-1h2l1 1h2c1.1 0 2 .9 2 2H6zm1 1h10l-1 11H8z"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #c33 }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #d33 }</style>
<path d="M12 9V7s0-5-4.5-5S3 7 3 7h2s0-3 2.5-3S10 7 10 7v2H7v7c0 1.7 1.3 3 3 3h10V9z"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #c33 }</style>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><style>* { fill: #d33 }</style>
<path d="M11 9V7s0-5 4.5-5S20 7 20 7h-2s0-3-2.5-3S13 7 13 7v2h3v7c0 1.7-1.3 3-3 3H3V9z"/>
</svg>
"color": "#36c"
},
"destructive": {
- "color": "#c33"
+ "color": "#d33"
},
"warning": {
"color": "#ff5d00"
}
#pagehistory li.selected {
- background-color: #f9f9f9;
- border: 1px dashed #aaa;
+ background-color: #f8f9fa;
+ border: 1px dashed #a2a9b1;
}
.mw-history-revisionactions {
*/
#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;
.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 {
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, {
/**
* @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;
},
/**
*/
table.wikitable {
margin: 1em 0;
- background-color: #f9f9f9;
- border: 1px solid #aaa;
+ background-color: #f8f9fa;
+ border: 1px solid #a2a9b1;
border-collapse: collapse;
color: #000;
}
table.wikitable > tr > th,
table.wikitable > * > tr > th {
- background-color: #f2f2f2;
+ background-color: #eaecf0;
text-align: center;
}
.toc,
.mw-warning,
.toccolours {
- border: 1px solid #aaa;
- background-color: #f9f9f9;
+ border: 1px solid #a2a9b1;
+ background-color: #f8f9fa;
padding: 5px;
font-size: 95%;
}
}
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,
}
html .thumbimage {
- border: 1px solid #ccc;
+ border: 1px solid #c8ccd1;
}
html .thumbcaption {
}
img.thumbborder {
- border: 1px solid #ddd;
+ border: 1px solid #eaecf0;
}
/* Directionality-specific styles for thumbnails - their positioning depends on content language */
code {
color: #000;
- background-color: #f9f9f9;
- border: 1px solid #ddd;
+ background-color: #f8f9fa;
+ border: 1px solid #eaecf0;
border-radius: 2px;
padding: 1px 4px;
}
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;
/* Categories */
.catlinks {
- border: 1px solid #aaa;
- background-color: #f9f9f9;
+ border: 1px solid #a2a9b1;
+ background-color: #f8f9fa;
padding: 5px;
margin-top: 1em;
clear: both;
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: [
{
};
/**
- * @class mw.special.ApiSandbox.Utils
+ * @class mw.special.ApiSandbox.Util
* @private
*/
Util = {
},
/**
- * 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;
}
};
$content
.empty()
- .append( $( '<p>' ).append( mw.message( 'apisandbox-intro' ).parse() ) )
+ .append( $( '<p>' ).append( Util.parseMsg( 'apisandbox-intro' ) ) )
.append(
$( '<div>', { id: 'mw-apisandbox-ui' } )
.append( $toolbar )
$.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',
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
if ( data.status && data.status !== 200 ) {
$( '<div>' )
.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 ) );
framed: false,
icon: 'info',
popup: {
- $content: $( '<div>' ).append( mw.message( 'apisandbox-continue-help' ).parse() ),
+ $content: $( '<div>' ).append( Util.parseMsg( 'apisandbox-continue-help' ) ),
padded: true
}
} ).$element
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',
dl.append( $( '<dd>', {
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( $( '<dd>', {
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' )
]
} ) );
}
if ( tmp !== '' ) {
dl.append( $( '<dd>', {
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;
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' )
}
) );
}
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;
padding: 0.5em;
}
.search-types .current a {
- color: #333;
+ color: #222;
cursor: default;
}
.search-types .current a:hover {
float: right;
padding: 0.5em;
padding-right: 0.75em;
- color: #666;
+ color: #54595d;
font-size: 95%;
}
#mw-search-top-table div.oo-ui-actionFieldLayout {
#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 {
}
#mw-searchoptions .divider {
clear: both;
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid #eaecf0;
padding-top: 0.5em;
margin-bottom: 0.5em;
}
#mw-search-interwiki {
float: right;
width: 18em;
- border: 1px solid #aaa;
+ border: 1px solid #a2a9b1;
margin-top: 2ex;
}
.searchalttitle,
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;
}
* @inheritdoc mw.widgets.TitleWidget
*/
mw.widgets.SearchInputWidget.prototype.getSuggestionsPromise = function () {
- var api = new mw.Api(),
+ var api = this.getApi(),
promise,
self = this;
*/
( 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
*
* @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
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' );
OO.initClass( mw.widgets.TitleWidget );
+ /* Static properties */
+
+ mw.widgets.TitleWidget.static.interwikiPrefixesPromiseCache = {};
+
/* Methods */
/**
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.
*/
mw.widgets.TitleWidget.prototype.getSuggestionsPromise = function () {
var req,
+ api = this.getApi(),
query = this.getQueryValue(),
widget = this,
promiseAbortObject = { abort: function () {
} };
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 (
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;
}
};
+ /**
+ * 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
*
} );
}
- // 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 () {
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',
// 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;
}
( 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 ) );
li.gallerybox div.thumb {
text-align: center;
- border: 1px solid #ccc;
- background-color: #f9f9f9;
+ border: 1px solid #c8ccd1;
+ background-color: #f8f9fa;
margin: 2px;
}
# 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",
--- /dev/null
+<?php
+
+class CurlHttpRequestTest extends MWHttpRequestTestCase {
+ protected static $httpEngine = 'curl';
+}
--- /dev/null
+<?php
+
+class MWHttpRequestTestCase extends PHPUnit_Framework_TestCase {
+ protected static $httpEngine;
+ protected $oldHttpEngine;
+
+ public function setUp() {
+ parent::setUp();
+ $this->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 ) );
+ }
+}
+
--- /dev/null
+<?php
+
+class PhpHttpRequestTest extends MWHttpRequestTestCase {
+ protected static $httpEngine = 'php';
+}
+++ /dev/null
-<?php
-
-/**
- * @group Http
- */
-class HttpTest extends MediaWikiTestCase {
- /**
- * @dataProvider cookieDomains
- * @covers Cookie::validateCookieDomain
- */
- public function testValidateCookieDomain( $expected, $domain, $origin = null ) {
- if ( $origin ) {
- $ok = Cookie::validateCookieDomain( $domain, $origin );
- $msg = "$domain against origin $origin";
- } else {
- $ok = Cookie::validateCookieDomain( $domain );
- $msg = "$domain";
- }
- $this->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;
- }
-}
);
}
+ 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',
+ '<i>link</i>; 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; <i>link</i>; [[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; <i>link</i>; [[foo]]; <i>link</i>; 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
*/
'results' => [
'Special:ActiveUsers',
'Special:AllMessages',
- 'Special:AllMyFiles',
+ 'Special:AllMyUploads',
],
// Third result when testing offset
'offsetresult' => [
- 'Special:AllMyUploads',
+ 'Special:AllPages',
],
] ],
[ [
],
// Third result when testing offset
'offsetresult' => [
- 'Special:UncategorizedImages',
+ 'Special:UncategorizedPages',
],
] ],
[ [
);
}
- /**
- * @covers Xml::escapeJsString
- */
- public function testEscapeJsStringSpecialChars() {
- $this->assertEquals(
- '\\\\\r\n',
- Xml::escapeJsString( "\\\r\n" ),
- 'escapeJsString() with special characters'
- );
- }
-
/**
* @covers Xml::encodeJsVar
*/
$changeReq->password = $newpass;
$provider->providerChangeAuthenticationData( $changeReq );
- if ( $loginOnly ) {
+ if ( $loginOnly && $changed ) {
$old = 'fail';
$new = 'fail';
$expectExpiry = null;
--- /dev/null
+<?php
+
+namespace MediaWiki\Logger\Monolog;
+
+class LogstashFormatterTest extends \PHPUnit_Framework_TestCase {
+ /**
+ * @dataProvider provideV1
+ * @param array $record The input record.
+ * @param array $expected Associative array of expected keys and their values.
+ * @param array $notExpected List of keys that should not exist.
+ */
+ public function testV1( array $record, array $expected, array $notExpected ) {
+ $formatter = new LogstashFormatter( 'app', 'system', null, null, LogstashFormatter::V1 );
+ $formatted = json_decode( $formatter->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 );
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * @group Http
+ */
+class HttpTest extends MediaWikiTestCase {
+ /**
+ * @dataProvider cookieDomains
+ * @covers Cookie::validateCookieDomain
+ */
+ public function testValidateCookieDomain( $expected, $domain, $origin = null ) {
+ if ( $origin ) {
+ $ok = Cookie::validateCookieDomain( $domain, $origin );
+ $msg = "$domain against origin $origin";
+ } else {
+ $ok = Cookie::validateCookieDomain( $domain );
+ $msg = "$domain";
+ }
+ $this->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;
+ }
+}
+++ /dev/null
-<?php
-
-class DatabaseUpdaterTest extends MediaWikiTestCase {
-
- public function testSetAppliedUpdates() {
- $db = new FakeDatabase();
- $dbu = new FakeDatabaseUpdater( $db );
- $dbu->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 );
- }
-}
--- /dev/null
+<?php
+
+namespace Wikimedia\Tests\Rdbms;
+
+use IDatabase;
+use LoadBalancer;
+use PHPUnit_Framework_MockObject_MockObject;
+use Wikimedia\Rdbms\ConnectionManager;
+
+/**
+ * @covers Wikimedia\Rdbms\ConnectionManager
+ *
+ * @license GPL-2.0+
+ * @author Daniel Kinzler
+ */
+class ConnectionManagerTest extends \PHPUnit_Framework_TestCase {
+
+ /**
+ * @return IDatabase|PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getIDatabaseMock() {
+ return $this->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 );
+ }
+
+}
--- /dev/null
+<?php
+
+namespace Wikimedia\Tests\Rdbms;
+
+use IDatabase;
+use LoadBalancer;
+use PHPUnit_Framework_MockObject_MockObject;
+use Wikimedia\Rdbms\SessionConsistentConnectionManager;
+
+/**
+ * @covers Wikimedia\Rdbms\SessionConsistentConnectionManager
+ *
+ * @license GPL-2.0+
+ * @author Daniel Kinzler
+ */
+class SessionConsistentConnectionManagerTest extends \PHPUnit_Framework_TestCase {
+
+ /**
+ * @return IDatabase|PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getIDatabaseMock() {
+ return $this->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 );
+ }
+}
'JsonZeroConfig' => [
'namespace' => 480,
'nsName' => 'Zero',
- 'isLocal' => false,
+ 'isLocal' => true,
],
],
],
$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() );
+ }
}
'results' => [
'Special:ActiveUsers',
'Special:AllMessages',
- 'Special:AllMyFiles',
+ 'Special:AllMyUploads',
],
// Third result when testing offset
'offsetresult' => [
- 'Special:AllMyUploads',
+ 'Special:AllPages',
],
] ],
[ [
],
// Third result when testing offset
'offsetresult' => [
- 'Special:UncategorizedImages',
+ 'Special:UncategorizedPages',
],
] ],
[ [
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 ) {
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 ) {
request.respond( 200, { 'Content-Type': 'application/json' }, '[]' );
} );
- api.post( {} );
+ return api.post( {} );
} );
}( mediaWiki ) );
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 ) );
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,
{
}
);
} );
-
- this.server.respond( /ammessages=foo%7Cbaz/, [
- 200,
- { 'Content-Type': 'application/json' },
- '{ "query": { "allmessages": [' +
- '{ "name": "foo", "content": "Foo bar" },' +
- '{ "name": "baz", "content": "Baz Quux" }' +
- '] } }'
- ] );
} );
}( mediaWiki ) );
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' );
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
'{ "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 ) {
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
'{ "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 ) {
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 ) );
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": "<p><b>Hello world</b></p>" } }'
+ ] );
- 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, '<p><b>Hello world</b></p>', '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": "<p><b>Hello world</b></p>" } }'
+ ] );
+
+ return new mw.Api().parse( {
toString: function () {
return '\'\'\'Hello world\'\'\'';
}
} ).done( function ( html ) {
assert.equal( html, '<p><b>Hello world</b></p>', 'Parse wikitext by toString object' );
} );
+ } );
- this.server.respondWith( /action=parse.*&text='''Hello\+world'''/, function ( request ) {
- request.respond( 200, { 'Content-Type': 'application/json' },
- '{ "parse": { "text": "<p><b>Hello world</b></p>" } }'
- );
- } );
+ QUnit.test( '.parse( mw.Title )', function ( assert ) {
+ this.server.respondWith( /action=parse.*&page=Earth/, [ 200,
+ { 'Content-Type': 'application/json' },
+ '{ "parse": { "text": "<p><b>Earth</b> is a planet.</p>" } }'
+ ] );
- api.parse( new mw.Title( 'Earth' ) ).done( function ( html ) {
+ return new mw.Api().parse( new mw.Title( 'Earth' ) ).done( function ( html ) {
assert.equal( html, '<p><b>Earth</b> is a planet.</p>', 'Parse page by Title object' );
} );
-
- this.server.respondWith( /action=parse.*&page=Earth/, function ( request ) {
- request.respond( 200, { 'Content-Type': 'application/json' },
- '{ "parse": { "text": "<p><b>Earth</b> is a planet.</p>" } }'
- );
- } );
-
- this.server.respond();
} );
}( mediaWiki ) );
( 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 () {
} );
} );
- 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();
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": "<b>Added</b>" } ] }'
+ );
+ }
} );
- 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": "<b>Added</b>" } ] }'
);
}
+ } );
+
+ 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": [ ' +
);
}
} );
+
+ return new mw.Api().watch( [ 'Foo', 'Bar' ] ).done( function ( items ) {
+ assert.equal( items[ 0 ].title, 'Foo' );
+ assert.equal( items[ 1 ].title, 'Bar' );
+ } );
} );
+
}( mediaWiki ) );
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 );
},
{
lang: 'hi',
- number: '१२३४५६,७८९',
+ number: '१,२३,४५६',
result: '123456',
integer: true,
description: 'formatnum test for Hindi, Devanagari digits passed to get integer value'
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 );
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.
.done( function () {
assert.strictEqual( isAwesomeDone, true, 'test.promise module should\'ve caused isAwesomeDone to be true' );
delete mw.loader.testCallback;
-
} )
.fail( function () {
assert.ok( false, 'Error callback fired while loader.using "test.promise" module' );
} );
QUnit.test( '.using() Error: Circular dependency', function ( assert ) {
+ var done = assert.async();
+
mw.loader.register( [
[ 'test.circle1', '0', [ 'test.circle2' ] ],
[ 'test.circle2', '0', [ 'test.circle3' ] ],
function fail( e ) {
assert.ok( /Circular/.test( String( e ) ), 'Detect circular dependency' );
}
- );
+ )
+ .always( done );
} );
QUnit.test( '.load() - Error: Circular dependency', function ( assert ) {
} );
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.' );
function fail( e ) {
assert.ok( /Unknown/.test( String( e ) ), 'Detect unknown dependency' );
}
- );
+ ).always( done );
} );
QUnit.test( '.load() - Error: Unregistered (ignored)', 0, function ( assert ) {
( 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 ) );