==== Removed and replaced external libraries ====
=== Bug fixes in 1.29 ===
+* (T62604) Core parser functions returning a number now format the number according
+ to the page content language, not wiki content language.
=== Action API changes in 1.29 ===
* Submitting sensitive authentication request parameters to action=clientlogin,
'stasherrors' rather than a 'stashfailed' text string.
* action=watch reports 'errors' and 'warnings' instead of a single 'error', and
no longer returns a 'message' on success.
+* Added action=validatepassword to validate passwords for the account creation
+ and password change forms.
=== Action API internal changes in 1.29 ===
* New methods were added to ApiBase to handle errors and warnings using i18n
'ApiUpload' => __DIR__ . '/includes/api/ApiUpload.php',
'ApiUsageException' => __DIR__ . '/includes/api/ApiUsageException.php',
'ApiUserrights' => __DIR__ . '/includes/api/ApiUserrights.php',
+ 'ApiValidatePassword' => __DIR__ . '/includes/api/ApiValidatePassword.php',
'ApiWatch' => __DIR__ . '/includes/api/ApiWatch.php',
'ArchivedFile' => __DIR__ . '/includes/filerepo/file/ArchivedFile.php',
'ArrayDiffFormatter' => __DIR__ . '/includes/diff/ArrayDiffFormatter.php',
&$tokenTypes: supported token types in format 'type' => callback function
used to retrieve this type of tokens.
+'ApiValidatePassword': Called from ApiValidatePassword.
+$module: ApiValidatePassword instance.
+&$r: Result array.
+
'Article::MissingArticleConditions': Before fetching deletion & move log entries
to display a message of a non-existing page being deleted/moved, give extensions
a chance to hide their (unrelated) log entries.
$revision: New Revision of the article
$status: Status object about to be returned by doEditContent()
$baseRevId: the rev ID (or false) this edit was based on
+$undidRevId: the rev ID (or 0) this edit undid
'ArticleUndelete': When one or more revisions of an article are restored.
&$title: Title corresponding to the article restored
nothing)
$status: Status object about to be returned by doEditContent()
$baseRevId: the rev ID (or false) this edit was based on
+$undidRevId: the rev ID (or 0) this edit undid
'PageHistoryBeforeList': When a history page list is about to be constructed.
&$article: the article that the history is loading for
false,
$wgUser,
$content->getDefaultFormat(),
- $this->changeTags
+ $this->changeTags,
+ $this->undidRev
);
if ( !$doEditStatus->isOK() ) {
* @param Exception|Throwable $exception
* @param array $options
* - wrap: (string|array|MessageSpecifier) Used to wrap the exception's
- * message. The exception's message will be added as the final parameter.
+ * message if it's not an ILocalizedException. The exception's message
+ * will be added as the final parameter.
* - code: (string) Default code
- * - data: (array) Extra data
- * @return ApiMessage
+ * - data: (array) Default extra data
+ * @return IApiMessage
*/
public function getMessageFromException( $exception, array $options = [] ) {
$options += [ 'code' => null, 'data' => [] ];
// Extract code and data from the exception, if applicable
if ( $exception instanceof UsageException ) {
$data = $exception->getMessageArray();
- if ( !isset( $options['code'] ) ) {
+ if ( !$options['code'] ) {
$options['code'] = $data['code'];
}
unset( $data['code'], $data['info'] );
- $options['data'] = array_merge( $data['code'], $options['data'] );
+ $options['data'] = array_merge( $data, $options['data'] );
}
if ( isset( $options['wrap'] ) ) {
'tokens' => 'ApiTokens',
'checktoken' => 'ApiCheckToken',
'cspreport' => 'ApiCSPReport',
+ 'validatepassword' => 'ApiValidatePassword',
// Write modules
'purge' => 'ApiPurge',
if ( !$toTitle || $toTitle->isExternal() ) {
$this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['to'] ) ] );
}
- $toTalk = $toTitle->getTalkPage();
+ $toTalk = $toTitle->canTalk() ? $toTitle->getTalkPage() : null;
if ( $toTitle->getNamespace() == NS_FILE
&& !RepoGroup::singleton()->getLocalRepo()->findFile( $toTitle )
$r['moveoverredirect'] = $toTitleExists;
// Move the talk page
- if ( $params['movetalk'] && $fromTalk->exists() && !$fromTitle->isTalkPage() ) {
+ if ( $params['movetalk'] && $toTalk && $fromTalk->exists() && !$fromTitle->isTalkPage() ) {
$toTalkExists = $toTalk->exists();
$status = $this->movePage( $fromTalk, $toTalk, $params['reason'], !$params['noredirect'] );
if ( $status->isOK() ) {
if ( !is_null( $params['type'] ) ) {
try {
- $options['rcTypes'] = RecentChange::parseToRCType( $params['type'] );
+ $rcTypes = RecentChange::parseToRCType( $params['type'] );
+ if ( $rcTypes ) {
+ $options['rcTypes'] = $rcTypes;
+ }
} catch ( Exception $e ) {
ApiBase::dieDebug( __METHOD__, $e->getMessage() );
}
} else {
$idResult['status'] = 'success';
if ( is_null( $status->value->logId ) ) {
- $idResult['noop'] = '';
+ $idResult['noop'] = true;
} else {
$idResult['actionlogid'] = $status->value->logId;
$idResult['added'] = $status->value->addedTags;
--- /dev/null
+<?php
+
+use MediaWiki\Auth\AuthManager;
+
+/**
+ * @ingroup API
+ */
+class ApiValidatePassword extends ApiBase {
+
+ public function execute() {
+ $params = $this->extractRequestParams();
+
+ // For sanity
+ $this->requirePostedParameters( [ 'password' ] );
+
+ if ( $params['user'] !== null ) {
+ $user = User::newFromName( $params['user'], 'creatable' );
+ if ( !$user ) {
+ $encParamName = $this->encodeParamName( 'user' );
+ $this->dieWithError(
+ [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $params['user'] ) ],
+ "baduser_{$encParamName}"
+ );
+ }
+
+ if ( !$user->isAnon() || AuthManager::singleton()->userExists( $user->getName() ) ) {
+ $this->dieWithError( 'userexists' );
+ }
+
+ $user->setEmail( (string)$params['email'] );
+ $user->setRealName( (string)$params['realname'] );
+ } else {
+ $user = $this->getUser();
+ }
+
+ $validity = $user->checkPasswordValidity( $params['password'] );
+ $r['validity'] = $validity->isGood() ? 'Good' : ( $validity->isOK() ? 'Change' : 'Invalid' );
+ $messages = array_merge(
+ $this->getErrorFormatter()->arrayFromStatus( $validity, 'error' ),
+ $this->getErrorFormatter()->arrayFromStatus( $validity, 'warning' )
+ );
+ if ( $messages ) {
+ $r['validitymessages'] = $messages;
+ }
+
+ Hooks::run( 'ApiValidatePassword', [ $this, &$r ] );
+
+ $this->getResult()->addValue( null, $this->getModuleName(), $r );
+ }
+
+ public function mustBePosted() {
+ return true;
+ }
+
+ public function getAllowedParams() {
+ return [
+ 'password' => [
+ ApiBase::PARAM_TYPE => 'password',
+ ApiBase::PARAM_REQUIRED => true
+ ],
+ 'user' => [
+ ApiBase::PARAM_TYPE => 'user',
+ ],
+ 'email' => null,
+ 'realname' => null,
+ ];
+ }
+
+ protected function getExamplesMessages() {
+ return [
+ 'action=validatepassword&password=foobar'
+ => 'apihelp-validatepassword-example-1',
+ 'action=validatepassword&password=querty&user=Example'
+ => 'apihelp-validatepassword-example-2',
+ ];
+ }
+
+ public function getHelpUrls() {
+ return 'https://www.mediawiki.org/wiki/API:Validatepassword';
+ }
+}
foreach ( $pageSet->getMissingTitles() as $title ) {
$r = $this->watchTitle( $title, $user, $params );
- $r['missing'] = 1;
+ $r['missing'] = true;
$res[] = $r;
}
"apihelp-userrights-example-user": "Add user <kbd>FooBot</kbd> to group <kbd>bot</kbd>, and remove from groups <kbd>sysop</kbd> and <kbd>bureaucrat</kbd>.",
"apihelp-userrights-example-userid": "Add the user with ID <kbd>123</kbd> to group <kbd>bot</kbd>, and remove from groups <kbd>sysop</kbd> and <kbd>bureaucrat</kbd>.",
+ "apihelp-validatepassword-description": "Validate a password against the wiki's password policies.\n\nValidity is reported as <samp>Good</samp> if the password is acceptable, <samp>Change</samp> if the password may be used for login but must be changed, or <samp>Invalid</samp> if the password is not usable.",
+ "apihelp-validatepassword-param-password": "Password to validate.",
+ "apihelp-validatepassword-param-user": "User name, for use when testing account creation. The named user must not exist.",
+ "apihelp-validatepassword-param-email": "Email address, for use when testing account creation.",
+ "apihelp-validatepassword-param-realname": "Real name, for use when testing account creation.",
+ "apihelp-validatepassword-example-1": "Validate the password <kbd>foobar</kbd> for the current user.",
+ "apihelp-validatepassword-example-2": "Validate the password <kbd>qwerty</kbd> for creating user <kbd>Example</kbd>.",
+
"apihelp-watch-description": "Add or remove pages from the current user's watchlist.",
"apihelp-watch-param-title": "The page to (un)watch. Use <var>$1titles</var> instead.",
"apihelp-watch-param-unwatch": "If set the page will be unwatched rather than watched.",
"apihelp-userrights-param-reason": "{{doc-apihelp-param|userrights|reason}}",
"apihelp-userrights-example-user": "{{doc-apihelp-example|userrights}}",
"apihelp-userrights-example-userid": "{{doc-apihelp-example|userrights}}",
+ "apihelp-validatepassword-description": "{{doc-apihelp-description|validatepassword}}",
+ "apihelp-validatepassword-param-email": "{{doc-apihelp-param|validatepassword|email}}",
+ "apihelp-validatepassword-param-password": "{{doc-apihelp-param|validatepassword|password}}",
+ "apihelp-validatepassword-param-realname": "{{doc-apihelp-param|validatepassword|realname}}",
+ "apihelp-validatepassword-param-user": "{{doc-apihelp-param|validatepassword|user}}",
+ "apihelp-validatepassword-example-1": "{{doc-apihelp-example|validatepassword}}",
+ "apihelp-validatepassword-example-2": "{{doc-apihelp-example|validatepassword}}",
"apihelp-watch-description": "{{doc-apihelp-description|watch}}",
"apihelp-watch-param-title": "{{doc-apihelp-param|watch|title}}",
"apihelp-watch-param-unwatch": "{{doc-apihelp-param|watch|unwatch}}",
( $newPHP || preg_match( "/\xf4[\x90-\xbf]|[\xf5-\xff]/S", $value ) === 0 );
}
+ /**
+ * Explode a string, but ignore any instances of the separator inside
+ * the given start and end delimiters, which may optionally nest.
+ * The delimiters are literal strings, not regular expressions.
+ * @param string $startDelim Start delimiter
+ * @param string $endDelim End delimiter
+ * @param string $separator Separator string for the explode.
+ * @param string $subject Subject string to explode.
+ * @param bool $nested True iff the delimiters are allowed to nest.
+ * @return ArrayIterator
+ */
+ static function delimiterExplode( $startDelim, $endDelim, $separator,
+ $subject, $nested = false ) {
+ $inputPos = 0;
+ $lastPos = 0;
+ $depth = 0;
+ $encStart = preg_quote( $startDelim, '!' );
+ $encEnd = preg_quote( $endDelim, '!' );
+ $encSep = preg_quote( $separator, '!' );
+ $len = strlen( $subject );
+ $m = [];
+ $exploded = [];
+ while (
+ $inputPos < $len &&
+ preg_match(
+ "!$encStart|$encEnd|$encSep!S", $subject, $m,
+ PREG_OFFSET_CAPTURE, $inputPos
+ )
+ ) {
+ $match = $m[0][0];
+ $matchPos = $m[0][1];
+ $inputPos = $matchPos + strlen( $match );
+ if ( $match === $separator ) {
+ if ( $depth === 0 ) {
+ $exploded[] = substr(
+ $subject, $lastPos, $matchPos - $lastPos
+ );
+ $lastPos = $inputPos;
+ }
+ } elseif ( $match === $startDelim ) {
+ if ( $depth === 0 || $nested ) {
+ $depth++;
+ }
+ } else {
+ $depth--;
+ }
+ }
+ $exploded[] = substr( $subject, $lastPos );
+ // This method could be rewritten in the future to avoid creating an
+ // intermediate array, since the return type is just an iterator.
+ return new ArrayIterator( $exploded );
+ }
+
/**
* Perform an operation equivalent to `preg_replace()`
*
* @param array|null $tags Change tags to apply to this edit
* Callers are responsible for permission checks
* (with ChangeTags::canAddTagsAccompanyingChange)
+ * @param Int $undidRevId Id of revision that was undone or 0
*
* @throws MWException
* @return Status Possible errors:
*/
public function doEditContent(
Content $content, $summary, $flags = 0, $baseRevId = false,
- User $user = null, $serialFormat = null, $tags = []
+ User $user = null, $serialFormat = null, $tags = [], $undidRevId = 0
) {
global $wgUser, $wgUseAutomaticEditSummaries;
'oldId' => $this->getLatest(),
'oldIsRedirect' => $this->isRedirect(),
'oldCountable' => $this->isCountable(),
- 'tags' => ( $tags !== null ) ? (array)$tags : []
+ 'tags' => ( $tags !== null ) ? (array)$tags : [],
+ 'undidRevId' => $undidRevId
];
// Actually create the revision and create/update the page
);
// Trigger post-save hook
$params = [ &$this, &$user, $content, $summary, $flags & EDIT_MINOR,
- null, null, &$flags, $revision, &$status, $meta['baseRevId'] ];
+ null, null, &$flags, $revision, &$status, $meta['baseRevId'],
+ $meta['undidRevId'] ];
ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $params );
Hooks::run( 'PageContentSaveComplete', $params );
}
return $mwObject->matchStartToEnd( $value );
}
- public static function formatRaw( $num, $raw ) {
+ /**
+ * Formats a number according to a language.
+ *
+ * @param int|float $num
+ * @param string $raw
+ * @param Language|StubUserLang $language
+ * @return string
+ */
+ public static function formatRaw( $num, $raw, $language ) {
if ( self::matchAgainstMagicword( 'rawsuffix', $raw ) ) {
return $num;
} else {
- global $wgContLang;
- return $wgContLang->formatNum( $num );
+ return $language->formatNum( $num );
}
}
+
public static function numberofpages( $parser, $raw = null ) {
- return self::formatRaw( SiteStats::pages(), $raw );
+ return self::formatRaw( SiteStats::pages(), $raw, $parser->getFunctionLang() );
}
+
public static function numberofusers( $parser, $raw = null ) {
- return self::formatRaw( SiteStats::users(), $raw );
+ return self::formatRaw( SiteStats::users(), $raw, $parser->getFunctionLang() );
}
public static function numberofactiveusers( $parser, $raw = null ) {
- return self::formatRaw( SiteStats::activeUsers(), $raw );
+ return self::formatRaw( SiteStats::activeUsers(), $raw, $parser->getFunctionLang() );
}
+
public static function numberofarticles( $parser, $raw = null ) {
- return self::formatRaw( SiteStats::articles(), $raw );
+ return self::formatRaw( SiteStats::articles(), $raw, $parser->getFunctionLang() );
}
+
public static function numberoffiles( $parser, $raw = null ) {
- return self::formatRaw( SiteStats::images(), $raw );
+ return self::formatRaw( SiteStats::images(), $raw, $parser->getFunctionLang() );
}
+
public static function numberofadmins( $parser, $raw = null ) {
- return self::formatRaw( SiteStats::numberingroup( 'sysop' ), $raw );
+ return self::formatRaw(
+ SiteStats::numberingroup( 'sysop' ),
+ $raw,
+ $parser->getFunctionLang()
+ );
}
+
public static function numberofedits( $parser, $raw = null ) {
- return self::formatRaw( SiteStats::edits(), $raw );
+ return self::formatRaw( SiteStats::edits(), $raw, $parser->getFunctionLang() );
}
+
public static function pagesinnamespace( $parser, $namespace = 0, $raw = null ) {
- return self::formatRaw( SiteStats::pagesInNs( intval( $namespace ) ), $raw );
+ return self::formatRaw(
+ SiteStats::pagesInNs( intval( $namespace ) ),
+ $raw,
+ $parser->getFunctionLang()
+ );
}
public static function numberingroup( $parser, $name = '', $raw = null ) {
- return self::formatRaw( SiteStats::numberingroup( strtolower( $name ) ), $raw );
+ return self::formatRaw(
+ SiteStats::numberingroup( strtolower( $name ) ),
+ $raw,
+ $parser->getFunctionLang()
+ );
}
/**
$title = Title::makeTitleSafe( NS_CATEGORY, $name );
if ( !$title ) { # invalid title
- return self::formatRaw( 0, $raw );
+ return self::formatRaw( 0, $raw, $parser->getFunctionLang() );
}
$wgContLang->findVariantLink( $name, $title, true );
}
$count = $cache[$name][$type];
- return self::formatRaw( $count, $raw );
+ return self::formatRaw( $count, $raw, $parser->getFunctionLang() );
}
/**
$title = Title::newFromText( $page );
if ( !is_object( $title ) ) {
- return self::formatRaw( 0, $raw );
+ return self::formatRaw( 0, $raw, $parser->getFunctionLang() );
}
// fetch revision from cache/database and return the value
// We've had bugs where rev_len was not being recorded for empty pages, see T135414
$length = 0;
}
- return self::formatRaw( $length, $raw );
+ return self::formatRaw( $length, $raw, $parser->getFunctionLang() );
}
/**
// FIXME: Doing recursiveTagParse at this stage, and the trim before
// splitting on '|' is a bit odd, and different from makeImage.
$matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
- $parameterMatches = StringUtils::explode( '|', $matches[3] );
+ // Protect LanguageConverter markup
+ $parameterMatches = StringUtils::delimiterExplode(
+ '-{', '}-', '|', $matches[3], true /* nested */
+ );
foreach ( $parameterMatches as $parameterMatch ) {
list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
$addr = self::EXT_LINK_ADDR;
$prots = $this->mUrlProtocols;
// check to see if link matches an absolute url, if not then it must be a wiki link.
+ if ( preg_match( '/^-{R|(.*)}-$/', $linkValue ) ) {
+ // Result of LanguageConverter::markNoConversion
+ // invoked on an external link.
+ $linkValue = substr( $linkValue, 4, -2 );
+ }
if ( preg_match( "/^($prots)$addr$chars*$/u", $linkValue ) ) {
$link = $linkValue;
} else {
# * bottom
# * text-bottom
- $parts = StringUtils::explode( "|", $options );
+ # Protect LanguageConverter markup when splitting into parts
+ $parts = StringUtils::delimiterExplode(
+ '-{', '}-', '|', $options, true /* allow nesting */
+ );
# Give extensions a chance to select the file revision for us
$options = [];
$this->processUpload();
} else {
# Backwards compatibility hook
- if ( !Hooks::run( 'UploadForm:initial', [ &$this ] ) ) {
+ // Avoid PHP 7.1 warning of passing $this by reference
+ $upload = $this;
+ if ( !Hooks::run( 'UploadForm:initial', [ &$upload ] ) ) {
wfDebug( "Hook 'UploadForm:initial' broke output of the upload form\n" );
return;
return;
}
-
- if ( !Hooks::run( 'UploadForm:BeforeProcessing', [ &$this ] ) ) {
+ // Avoid PHP 7.1 warning of passing $this by reference
+ $upload = $this;
+ if ( !Hooks::run( 'UploadForm:BeforeProcessing', [ &$upload ] ) ) {
wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file.\n" );
// This code path is deprecated. If you want to break upload processing
// do so by hooking into the appropriate hooks in UploadBase::verifyUpload
// Success, redirect to description page
$this->mUploadSuccessful = true;
- Hooks::run( 'SpecialUploadComplete', [ &$this ] );
+ // Avoid PHP 7.1 warning of passing $this by reference
+ $upload = $this;
+ Hooks::run( 'SpecialUploadComplete', [ &$upload ] );
$this->getOutput()->redirect( $this->mLocalFile->getTitle()->getFullURL() );
}
User::IGNORE_USER_RIGHTS
);
}
- Hooks::run( 'UploadComplete', [ &$this ] );
+ // Avoid PHP 7.1 warning of passing $this by reference
+ $uploadBase = $this;
+ Hooks::run( 'UploadComplete', [ &$uploadBase ] );
$this->postProcessUpload();
}
}
}
+ /**
+ * {@inheritdoc}
+ */
+ public function tryStashFile( User $user, $isPartial = false ) {
+ try {
+ $this->verifyChunk();
+ } catch ( UploadChunkVerificationException $e ) {
+ return Status::newFatal( $e->msg );
+ }
+
+ return parent::tryStashFile( $user, $isPartial );
+ }
+
+ /**
+ * {@inheritdoc}
+ * @throws UploadChunkVerificationException
+ * @deprecated since 1.28 Use tryStashFile() instead
+ */
+ public function stashFile( User $user = null ) {
+ wfDeprecated( __METHOD__, '1.28' );
+ $this->verifyChunk();
+ return parent::stashFile( $user );
+ }
+
+ /**
+ * {@inheritdoc}
+ * @throws UploadChunkVerificationException
+ * @deprecated since 1.28
+ */
+ public function stashFileGetKey() {
+ wfDeprecated( __METHOD__, '1.28' );
+ $this->verifyChunk();
+ return parent::stashFileGetKey();
+ }
+
+ /**
+ * {@inheritdoc}
+ * @throws UploadChunkVerificationException
+ * @deprecated since 1.28
+ */
+ public function stashSession() {
+ wfDeprecated( __METHOD__, '1.28' );
+ $this->verifyChunk();
+ return parent::stashSession();
+ }
+
/**
* Calls the parent doStashFile and updates the uploadsession table to handle "chunks"
*
$this->mChunkIndex = 0;
$this->mOffset = 0;
- $this->verifyChunk();
// Create a local stash target
$this->mStashFile = parent::doStashFile( $user );
// Update the initial file offset (based on file size)
"postdoc": "grunt copy:jsduck"
},
"devDependencies": {
+ "eslint": "3.12.2",
"eslint-config-wikimedia": "0.3.0",
"grunt": "1.0.1",
"grunt-banana-checker": "0.5.0",
{
formatversion: 2,
action: 'watch',
- titles: $.isArray( pages ) ? pages.join( '|' ) : String( pages ),
- uselang: mw.config.get( 'wgUserLanguage' )
+ titles: $.isArray( pages ) ? pages.join( '|' ) : String( pages )
},
addParams
)
</p>
!! end
-# FIXME: This test is currently broken in the PHP parser (bug 52661)
!! test
-Don't break image parsing if language converter markup is in the caption.
+T146305: Don't break image parsing if language converter markup is in the caption.
!! options
language=sr
!! wikitext
-[[File:Foobar.jpg|-{R|caption}-]]
+[[File:Foobar.jpg|thumb|-{R|caption:}-]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/%D0%94%D0%B0%D1%82%D0%BE%D1%82%D0%B5%D0%BA%D0%B0:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/%D0%94%D0%B0%D1%82%D0%BE%D1%82%D0%B5%D0%BA%D0%B0:Foobar.jpg" class="internal" title="Повећај"></a></div>caption:</div></div></div>
+
!! html/parsoid
-<p><a href="/wiki/File:Foobar.jpg" class="image" title="caption"><img alt="caption" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./Датотека:Foobar.jpg"><img resource="./Датотека:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><span typeof="mw:LanguageVariant" data-mw='{"disabled":true,"show":true,"text":"caption:"}'></span></figcaption></figure>
+!! end
+
+!! test
+T146305: Don't break image parsing if nested language converter markup is in the caption.
+!! options
+language=zh variant=zh-cn
+!! wikitext
+[[File:Foobar.jpg|thumb|-{zh-cn:blog (hk: -{zh-hans|WEBJOURNAL}-, tw: -{zh-hans|WEBLOG}-)}-]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="放大"></a></div>blog (hk: WEBJOURNAL, tw: WEBLOG)</div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><span typeof="mw:LanguageVariant" data-mw='{"bidir":[{"l":"zh-cn","t":"blog (hk: <span typeof=\"mw:LanguageVariant\" data-parsoid='{\"fl\":[\"zh-hans\"],\"dsr\":[42,64,null,2]}' data-mw='{\"filter\":[\"zh-hans\"],\"text\":\"WEBJOURNAL\"}'></span>, tw: <span typeof=\"mw:LanguageVariant\" data-parsoid='{\"fl\":[\"zh-hans\"],\"dsr\":[70,88,null,2]}' data-mw='{\"filter\":[\"zh-hans\"],\"text\":\"WEBLOG\"}'></span>)"}],"show":true}'></span></figcaption></figure>
+!! end
+
+!! test
+Don't break gallery if language converter markup is inside.
+!! options
+language=zh
+!! wikitext
+<gallery>
+File:foobar.jpg|[[File:foobar.jpg|20px|desc|alt=-{R|foo}-|-{R|bar}-]]|alt=-{R|bat}-
+File:foobar.jpg|{{Test|unamedParam|alt=-{R|param}-}}|alt=galleryalt
+</gallery>
+!! html
+<ul class="gallery mw-gallery-traditional">
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="bat" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div>
+ <div class="gallerytext">
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="bar"><img alt="foo" src="http://example.com/images/thumb/3/3a/Foobar.jpg/20px-Foobar.jpg" width="20" height="2" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/30px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/40px-Foobar.jpg 2x" /></a>
+</p>
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="galleryalt" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div>
+ <div class="gallerytext">
+<p>This is a test template
</p>
+ </div>
+ </div></li>
+</ul>
+
!! end
# FIXME: This test is currently broken in the PHP parser (bug 52661)
!! end
+!!test
+Gallery override link with WikiLink (bug 34852)
+!! wikitext
+<gallery>
+File:foobar.jpg|caption|alt=galleryalt|link=InterWikiLink
+</gallery>
+!! html
+<ul class="gallery mw-gallery-traditional">
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/InterWikiLink"><img alt="galleryalt" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div>
+ <div class="gallerytext">
+<p>caption
+</p>
+ </div>
+ </div></li>
+</ul>
+
+!! end
+
+!!test
+Gallery override link with absolute external link (bug 34852)
+!! wikitext
+<gallery>
+File:foobar.jpg|caption|alt=galleryalt|link=http://www.example.org
+</gallery>
+!! html
+<ul class="gallery mw-gallery-traditional">
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="http://www.example.org"><img alt="galleryalt" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div>
+ <div class="gallerytext">
+<p>caption
+</p>
+ </div>
+ </div></li>
+</ul>
+
+!! end
+
!! test
+Gallery override link with absolute external link with LanguageConverter
+!! options
+language=zh
+!! input
+<gallery>
+File:foobar.jpg|caption|alt=galleryalt|link=http://www.example.org
+</gallery>
+!! result
+<ul class="gallery mw-gallery-traditional">
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="http://www.example.org"><img alt="galleryalt" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div>
+ <div class="gallerytext">
+<p>caption
+</p>
+ </div>
+ </div></li>
+</ul>
+
+!! end
+
+!!test
+Gallery override link with malicious javascript (bug 34852)
+!! wikitext
+<gallery>
+File:foobar.jpg|caption|alt=galleryalt|link=" onclick="alert('malicious javascript code!');
+</gallery>
+!! html
+<ul class="gallery mw-gallery-traditional">
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/%22_onclick%3D%22alert(%27malicious_javascript_code!%27);"><img alt="galleryalt" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div>
+ <div class="gallerytext">
+<p>caption
+</p>
+ </div>
+ </div></li>
+</ul>
+
+!! end
+
+!!test
+Gallery with invalid title as link (bug 43964)
+!! wikitext
+<gallery>
+File:foobar.jpg|link=<
+</gallery>
+!! html
+<ul class="gallery mw-gallery-traditional">
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div>
+ <div class="gallerytext">
+ </div>
+ </div></li>
+</ul>
+
+!! end
+
+!!test
Language parser function
!! wikitext
{{#language:ar}}
);
}
+ /**
+ * @dataProvider provideGetMessageFromException
+ * @covers ApiErrorFormatter::getMessageFromException
+ * @covers ApiErrorFormatter::formatException
+ * @param Exception $exception
+ * @param array $options
+ * @param array $expect
+ */
+ public function testGetMessageFromException( $exception, $options, $expect ) {
+ $result = new ApiResult( 8388608 );
+ $formatter = new ApiErrorFormatter( $result, Language::factory( 'en' ), 'html', false );
+
+ $msg = $formatter->getMessageFromException( $exception, $options );
+ $this->assertInstanceOf( Message::class, $msg );
+ $this->assertInstanceOf( IApiMessage::class, $msg );
+ $this->assertSame( $expect, [
+ 'text' => $msg->parse(),
+ 'code' => $msg->getApiCode(),
+ 'data' => $msg->getApiData(),
+ ] );
+
+ $expectFormatted = $formatter->formatMessage( $msg );
+ $formatted = $formatter->formatException( $exception, $options );
+ $this->assertSame( $expectFormatted, $formatted );
+ }
+
+ /**
+ * @dataProvider provideGetMessageFromException
+ * @covers ApiErrorFormatter_BackCompat::formatException
+ * @param Exception $exception
+ * @param array $options
+ * @param array $expect
+ */
+ public function testGetMessageFromException_BC( $exception, $options, $expect ) {
+ $result = new ApiResult( 8388608 );
+ $formatter = new ApiErrorFormatter_BackCompat( $result );
+
+ $msg = $formatter->getMessageFromException( $exception, $options );
+ $this->assertInstanceOf( Message::class, $msg );
+ $this->assertInstanceOf( IApiMessage::class, $msg );
+ $this->assertSame( $expect, [
+ 'text' => $msg->parse(),
+ 'code' => $msg->getApiCode(),
+ 'data' => $msg->getApiData(),
+ ] );
+
+ $expectFormatted = $formatter->formatMessage( $msg );
+ $formatted = $formatter->formatException( $exception, $options );
+ $this->assertSame( $expectFormatted, $formatted );
+ $formatted = $formatter->formatException( $exception, $options + [ 'bc' => true ] );
+ $this->assertSame( $expectFormatted['info'], $formatted );
+ }
+
+ public static function provideGetMessageFromException() {
+ return [
+ 'Normal exception' => [
+ new RuntimeException( '<b>Something broke!</b>' ),
+ [],
+ [
+ 'text' => '<b>Something broke!</b>',
+ 'code' => 'internal_api_error_RuntimeException',
+ 'data' => [],
+ ]
+ ],
+ 'Normal exception, wrapped' => [
+ new RuntimeException( '<b>Something broke!</b>' ),
+ [ 'wrap' => 'parentheses', 'code' => 'some-code', 'data' => [ 'foo' => 'bar', 'baz' => 42 ] ],
+ [
+ 'text' => '(<b>Something broke!</b>)',
+ 'code' => 'some-code',
+ 'data' => [ 'foo' => 'bar', 'baz' => 42 ],
+ ]
+ ],
+ 'UsageException' => [
+ new UsageException( '<b>Something broke!</b>', 'ue-code', 0, [ 'xxx' => 'yyy', 'baz' => 23 ] ),
+ [],
+ [
+ 'text' => '<b>Something broke!</b>',
+ 'code' => 'ue-code',
+ 'data' => [ 'xxx' => 'yyy', 'baz' => 23 ],
+ ]
+ ],
+ 'UsageException, wrapped' => [
+ new UsageException( '<b>Something broke!</b>', 'ue-code', 0, [ 'xxx' => 'yyy', 'baz' => 23 ] ),
+ [ 'wrap' => 'parentheses', 'code' => 'some-code', 'data' => [ 'foo' => 'bar', 'baz' => 42 ] ],
+ [
+ 'text' => '(<b>Something broke!</b>)',
+ 'code' => 'some-code',
+ 'data' => [ 'xxx' => 'yyy', 'baz' => 42, 'foo' => 'bar' ],
+ ]
+ ],
+ 'LocalizedException' => [
+ new LocalizedException( [ 'returnto', '<b>FooBar</b>' ] ),
+ [],
+ [
+ 'text' => 'Return to <b>FooBar</b>.',
+ 'code' => 'returnto',
+ 'data' => [],
+ ]
+ ],
+ 'LocalizedException, wrapped' => [
+ new LocalizedException( [ 'returnto', '<b>FooBar</b>' ] ),
+ [ 'wrap' => 'parentheses', 'code' => 'some-code', 'data' => [ 'foo' => 'bar', 'baz' => 42 ] ],
+ [
+ 'text' => 'Return to <b>FooBar</b>.',
+ 'code' => 'some-code',
+ 'data' => [ 'foo' => 'bar', 'baz' => 42 ],
+ ]
+ ],
+ ];
+ }
+
}
$cookies = $request1->response()->getCookies();
// Calculate the expected cookie expiry date.
$this->assertArrayHasKey( 'wm_infinite_blockBlockID', $cookies );
- $this->assertEquals( time() + $cookieExpiration, $cookies['wm_infinite_blockBlockID']['expire'] );
+ // Check for expiry dates in a 10-second window, to account for slow testing.
+ $this->assertGreaterThan(
+ time() + $cookieExpiration - 5,
+ $cookies['wm_infinite_blockBlockID']['expire']
+ );
+ $this->assertLessThan(
+ time() + $cookieExpiration + 5,
+ $cookies['wm_infinite_blockBlockID']['expire']
+ );
// 3. Change the block's expiry (to 2 days), and the cookie's should be changed also.
$newExpiry = time() + 2 * 24 * 60 * 60;