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.
== Compatibility ==
'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",
$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>';
/** 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 );
}
/**
* @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 ) {
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
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
* @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 );
+ }
+
+ /**
+ * Begins an atomic section and returns a database connection to the master DB, for updating.
+ *
+ * @since 1.29
+ *
+ * @param string $fname
+ *
+ * @return Database
+ */
+ public function beginAtomicSection( $fname ) {
+ $db = $this->getWriteConnection();
+ $db->startAtomic( $fname );
+
+ return $db;
+ }
+
+ /**
+ * @since 1.29
+ *
+ * @param IDatabase $db
+ * @param string $fname
+ */
+ public function commitAtomicSection( IDatabase $db, $fname ) {
+ $db->endAtomic( $fname );
+ $this->releaseConnection( $db );
+ }
+
+ /**
+ * @since 1.29
+ *
+ * @param IDatabase $db
+ * @param string $fname
+ */
+ public function rollbackAtomicSection( IDatabase $db, $fname ) {
+ // FIXME: there does not seem to be a clean way to roll back an atomic section?!
+ $db->rollback( $fname, 'flush' );
+ $this->releaseConnection( $db );
+ }
+
+}
--- /dev/null
+<?php
+
+namespace Wikimedia\Rdbms;
+
+use Database;
+use DBConnRef;
+
+/**
+ * Database connection manager.
+ *
+ * This manages access to master and slave databases. It also manages state that indicates whether
+ * the slave 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 slave
+ * 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.
+ * Calling beginAtomicSection() implies a call to prepareForUpdates().
+ *
+ * @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();
+ }
+
+ /**
+ * Begins an atomic section and returns a database connection to the master DB, for updating.
+ *
+ * @since 1.29
+ *
+ * @note: This causes all future calls to getReadConnection() to return a connection
+ * to the master DB, even after commitAtomicSection() or rollbackAtomicSection() have
+ * been called.
+ *
+ * @param string $fname
+ *
+ * @return Database
+ */
+ public function beginAtomicSection( $fname ) {
+ // Once we have written to master, do not read from replica.
+ $this->prepareForUpdates();
+
+ return parent::beginAtomicSection( $fname );
+ }
+
+}
* @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" );
+ }
}
/**
]
);
+ $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',
)
);
}
+ $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" );
}
}
) );
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' ],
];
}
"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": "",
"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}}",
"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.*",
"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;
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;
}
} );
}
- // 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',
],
] ],
[ [
$changeReq->password = $newpass;
$provider->providerChangeAuthenticationData( $changeReq );
- if ( $loginOnly ) {
+ if ( $loginOnly && $changed ) {
$old = 'fail';
$new = 'fail';
$expectExpiry = null;
--- /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 );
+ }
+
+ public function testBeginAtomicSection() {
+ $database = $this->getIDatabaseMock();
+ $lb = $this->getLoadBalancerMock();
+
+ $lb->expects( $this->exactly( 2 ) )
+ ->method( 'getConnection' )
+ ->with( DB_MASTER )
+ ->will( $this->returnValue( $database ) );
+
+ $database->expects( $this->once() )
+ ->method( 'startAtomic' )
+ ->will( $this->returnValue( null ) );
+
+ $manager = new SessionConsistentConnectionManager( $lb );
+ $manager->beginAtomicSection( 'TEST' );
+
+ // Should also ask for a DB_MASTER connection.
+ // This is asserted by the $lb mock.
+ $manager->getReadConnection();
+ }
+
+ public function testCommitAtomicSection() {
+ $database = $this->getIDatabaseMock();
+ $lb = $this->getLoadBalancerMock();
+
+ $lb->expects( $this->once() )
+ ->method( 'reuseConnection' )
+ ->with( $database )
+ ->will( $this->returnValue( null ) );
+
+ $database->expects( $this->once() )
+ ->method( 'endAtomic' )
+ ->will( $this->returnValue( null ) );
+
+ $manager = new SessionConsistentConnectionManager( $lb );
+ $manager->commitAtomicSection( $database, 'TEST' );
+ }
+
+ public function testRollbackAtomicSection() {
+ $database = $this->getIDatabaseMock();
+ $lb = $this->getLoadBalancerMock();
+
+ $lb->expects( $this->once() )
+ ->method( 'reuseConnection' )
+ ->with( $database )
+ ->will( $this->returnValue( null ) );
+
+ $database->expects( $this->once() )
+ ->method( 'rollback' )
+ ->will( $this->returnValue( null ) );
+
+ $manager = new SessionConsistentConnectionManager( $lb );
+ $manager->rollbackAtomicSection( $database, 'TEST' );
+ }
+
+}
'JsonZeroConfig' => [
'namespace' => 480,
'nsName' => 'Zero',
- 'isLocal' => false,
+ 'isLocal' => true,
],
],
],
'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 ) );