* Wikimedia\Rdbms\IDatabase::doAtomicSection(), non-native ::insertSelect(),
and non-MySQL ::replace() and ::upsert() no longer roll back the whole
transaction on failure.
+* (T189785) Added a monthly heartbeat ping to the pingback feature.
=== External library changes in 1.31 ===
* StripState::merge()
* The "free" CSS class is now only applied to unbracketed URLs in wikitext. Links
written using square brackets will get the class "text" not "free".
+* SpecialPageFactory::getList(), deprecated in 1.24, has been removed. You can
+ use ::getNames() instead.
* OpenSearch::getOpenSearchTemplate(), deprecated in 1.25, has been removed. You
can use ApiOpenSearch::getOpenSearchTemplate() instead.
* The global function wfBaseConvert, deprecated in 1.27, has been removed. Use
* The global function wfOutputHandler() was removed, use the its replacement
MediaWiki\OutputHandler::handle() instead. The global function was only sometimes defined.
Its replacement is always available via the autoloader.
+* ChangeTags::listExtensionActivatedTags and ::listExtensionDefinedTags, deprecated
+ in 1.28, have been removed. Use ::listSoftwareActivatedTags() and
+ ::listSoftwareDefinedTags() instead.
+* Title::getTitleInvalidRegex(), deprecated in 1.25, has been removed. You
+ can use MediaWikiTitleCodec::getTitleInvalidRegex() instead.
+* HTMLForm & VFormHTMLForm::isVForm(), deprecated in 1.25, have been removed.
+* The ProfileSection class, deprecated in 1.25 and unused, has been removed.
== Compatibility ==
MediaWiki 1.31 requires PHP 5.5.9 or later. Although HHVM 3.18.5 or later is supported,
The supported versions are:
* MySQL 5.0.3 or later
-* PostgreSQL 8.3 or later
+* PostgreSQL 9.2 or later
* SQLite 3.3.7 or later
* Oracle 9.0.1 or later
* Microsoft SQL Server 2005 (9.00.1399)
'Preprocessor_Hash' => __DIR__ . '/includes/parser/Preprocessor_Hash.php',
'ProcessCacheLRU' => __DIR__ . '/includes/libs/ProcessCacheLRU.php',
'Processor' => __DIR__ . '/includes/registration/Processor.php',
- 'ProfileSection' => __DIR__ . '/includes/profiler/ProfileSection.php',
'Profiler' => __DIR__ . '/includes/profiler/Profiler.php',
'ProfilerOutput' => __DIR__ . '/includes/profiler/output/ProfilerOutput.php',
'ProfilerOutputDb' => __DIR__ . '/includes/profiler/output/ProfilerOutputDb.php',
}
/**
- * Has a pingback already been sent for this MediaWiki version?
+ * Has a pingback been sent in the last month for this MediaWiki version?
* @return bool
*/
private function checkIfSent() {
$dbr = wfGetDB( DB_REPLICA );
- $sent = $dbr->selectField(
- 'updatelog', '1', [ 'ul_key' => $this->key ], __METHOD__ );
- return $sent !== false;
+ $timestamp = $dbr->selectField(
+ 'updatelog',
+ 'ul_value',
+ [ 'ul_key' => $this->key ],
+ __METHOD__
+ );
+ if ( $timestamp === false ) {
+ return false;
+ }
+ // send heartbeat ping if last ping was over a month ago
+ if ( time() - (int)$timestamp > 60 * 60 * 24 * 30 ) {
+ return false;
+ }
+ return true;
}
/**
*/
private function markSent() {
$dbw = wfGetDB( DB_MASTER );
- return $dbw->insert(
- 'updatelog', [ 'ul_key' => $this->key ], __METHOD__, 'IGNORE' );
+ $timestamp = time();
+ return $dbw->upsert(
+ 'updatelog',
+ [ 'ul_key' => $this->key, 'ul_value' => $timestamp ],
+ [ 'ul_key' => $this->key ],
+ [ 'ul_value' => $timestamp ],
+ __METHOD__
+ );
}
/**
return $wgLegalTitleChars;
}
- /**
- * Returns a simple regex that will match on characters and sequences invalid in titles.
- * Note that this doesn't pick up many things that could be wrong with titles, but that
- * replacing this regex with something valid will make many titles valid.
- *
- * @deprecated since 1.25, use MediaWikiTitleCodec::getTitleInvalidRegex() instead
- *
- * @return string Regex string
- */
- static function getTitleInvalidRegex() {
- wfDeprecated( __METHOD__, '1.25' );
- return MediaWikiTitleCodec::getTitleInvalidRegex();
- }
-
/**
* Utility method for converting a character sequence from bytes to Unicode.
*
$res['id'] = $block->getId();
} else {
# should be unreachable
- $res['expiry'] = '';
- $res['id'] = '';
+ $res['expiry'] = ''; // @codeCoverageIgnore
+ $res['id'] = ''; // @codeCoverageIgnore
}
$res['reason'] = $params['reason'];
$hasHistory = false;
$reason = $page->getAutoDeleteReason( $hasHistory );
if ( $reason === false ) {
- return Status::newFatal( 'cannotdelete', $title->getPrefixedText() );
+ // Should be reachable only if the page has no revisions
+ return Status::newFatal( 'cannotdelete', $title->getPrefixedText() ); // @codeCoverageIgnore
}
}
);
}
- /**
- * @see listSoftwareActivatedTags
- * @deprecated since 1.28 call listSoftwareActivatedTags directly
- * @return array
- */
- public static function listExtensionActivatedTags() {
- wfDeprecated( __METHOD__, '1.28' );
- return self::listSoftwareActivatedTags();
- }
-
/**
* Basically lists defined tags which count even if they aren't applied to anything.
- * It returns a union of the results of listExplicitlyDefinedTags() and
- * listExtensionDefinedTags().
+ * It returns a union of the results of listExplicitlyDefinedTags()
*
* @return string[] Array of strings: tags
*/
);
}
- /**
- * Call listSoftwareDefinedTags directly
- *
- * @see listSoftwareDefinedTags
- * @deprecated since 1.28
- * @return array
- */
- public static function listExtensionDefinedTags() {
- wfDeprecated( __METHOD__, '1.28' );
- return self::listSoftwareDefinedTags();
- }
-
/**
* Invalidates the short-term cache of defined tags used by the
* list*DefinedTags functions, as well as the tag statistics cache.
return $this->displayFormat;
}
- /**
- * Test if displayFormat is 'vform'
- * @since 1.22
- * @deprecated since 1.25
- * @return bool
- */
- public function isVForm() {
- wfDeprecated( __METHOD__, '1.25' );
- return false;
- }
-
/**
* Get the HTMLFormField subclass for this descriptor.
*
*/
protected $displayFormat = 'vform';
- public function isVForm() {
- wfDeprecated( __METHOD__, '1.25' );
- return true;
- }
-
public static function loadInputFromParameters( $fieldname, $descriptor,
HTMLForm $parent = null
) {
'_InstallUser' => 'postgres',
];
- public static $minimumVersion = '8.3';
+ public static $minimumVersion = '9.2';
protected static $notMiniumumVerisonMessage = 'config-postgres-old';
public $maxRoleSearchDepth = 5;
[ 'log_timestamp', 'timestamptz_ops', 'btree', 0 ],
],
'CREATE INDEX "logging_times" ON "logging" USING "btree" ("log_timestamp")' ],
- [ 'dropIndex', 'oldimage', 'oi_name' ],
+ [ 'dropPgIndex', 'oldimage', 'oi_name' ],
[ 'checkIndex', 'oi_name_archive_name', [
[ 'oi_name', 'text_ops', 'btree', 0 ],
[ 'oi_archive_name', 'text_ops', 'btree', 0 ],
[ 'checkOiNameConstraint' ],
[ 'checkPageDeletedTrigger' ],
[ 'checkRevUserFkey' ],
- [ 'dropIndex', 'ipblocks', 'ipb_address' ],
+ [ 'dropPgIndex', 'ipblocks', 'ipb_address' ],
[ 'checkIndex', 'ipb_address_unique', [
[ 'ipb_address', 'text_ops', 'btree', 0 ],
[ 'ipb_user', 'int4_ops', 'btree', 0 ],
}
}
- protected function dropIndex( $table, $index, $patch = '', $fullpath = false ) {
+ protected function dropPgIndex( $table, $index ) {
if ( $this->db->indexExists( $table, $index ) ) {
$this->output( "Dropping obsolete index '$index'\n" );
$this->db->query( "DROP INDEX \"" . $index . "\"" );
use HashBagOStuff;
use LogicException;
use InvalidArgumentException;
+use UnexpectedValueException;
use Exception;
use RuntimeException;
$this->flags |= self::DBO_TRX;
}
}
+ // Disregard deprecated DBO_IGNORE flag (T189999)
+ $this->flags &= ~self::DBO_IGNORE;
$this->sessionVars = $params['variables'];
public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
if ( ( $flag & self::DBO_IGNORE ) ) {
- throw new \UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
+ throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
}
if ( $remember === self::REMEMBER_PRIOR ) {
public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
if ( ( $flag & self::DBO_IGNORE ) ) {
- throw new \UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
+ throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
}
if ( $remember === self::REMEMBER_PRIOR ) {
* @throws DBQueryError
*/
public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
- if ( $this->getFlag( self::DBO_IGNORE ) || $tempIgnore ) {
+ if ( $tempIgnore ) {
$this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
} else {
$sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
'section' => 'watchlist/advancedwatchlist',
'label-message' => 'tog-watchlisthideliu',
];
- $defaultPreferences['watchlistreloadautomatically'] = [
- 'type' => 'toggle',
- 'section' => 'watchlist/advancedwatchlist',
- 'label-message' => 'tog-watchlistreloadautomatically',
- ];
+
+ if ( !\SpecialWatchlist::checkStructuredFilterUiEnabled(
+ $this->config,
+ $user
+ ) ) {
+ $defaultPreferences['watchlistreloadautomatically'] = [
+ 'type' => 'toggle',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => 'tog-watchlistreloadautomatically',
+ ];
+ }
+
$defaultPreferences['watchlistunwatchlinks'] = [
'type' => 'toggle',
'section' => 'watchlist/advancedwatchlist',
+++ /dev/null
-<?php
-/**
- * Function scope profiling assistant
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Profiler
- */
-
-/**
- * Class for handling function-scope profiling
- *
- * @since 1.22
- * @deprecated since 1.25 No-op now
- */
-class ProfileSection {
- /**
- * Begin profiling of a function and return an object that ends profiling
- * of the function when that object leaves scope. As long as the object is
- * not specifically linked to other objects, it will fall out of scope at
- * the same moment that the function to be profiled terminates.
- *
- * This is typically called like:
- * @code $section = new ProfileSection( __METHOD__ ); @endcode
- *
- * @param string $name Name of the function to profile
- */
- public function __construct( $name ) {
- wfDeprecated( __CLASS__, '1.25' );
- }
-}
$this->request = $request;
$this->logger = $resourceLoader->getLogger();
- // Future developers: Avoid use of getVal() in this class, which performs
- // expensive UTF normalisation by default. Use getRawVal() instead.
- // Values here are either one of a finite number of internal IDs,
- // or previously-stored user input (e.g. titles, user names) that were passed
- // to this endpoint by ResourceLoader itself from the canonical value.
- // Values do not come directly from user input and need not match.
+ // Future developers: Use WebRequest::getRawVal() instead getVal().
+ // The getVal() method performs slow Language+UTF logic. (f303bb9360)
// List of modules
$modules = $request->getRawVal( 'modules' );
*/
public function getModuleRegistrations( ResourceLoaderContext $context ) {
$resourceLoader = $context->getResourceLoader();
- $target = $context->getRequest()->getVal( 'target', 'desktop' );
+ // Future developers: Use WebRequest::getRawVal() instead getVal().
+ // The getVal() method performs slow Language+UTF logic. (f303bb9360)
+ $target = $context->getRequest()->getRawVal( 'target', 'desktop' );
// Bypass target filter if this request is Special:JavaScriptTest.
// To prevent misuse in production, this is only allowed if testing is enabled server-side.
$byPassTargetFilter = $this->getConfig()->get( 'EnableJavaScriptTest' ) && $target === 'test';
return array_keys( self::getPageList() );
}
- /**
- * Get the special page list as an array
- *
- * @deprecated since 1.24, use getNames() instead.
- * @return array
- */
- public static function getList() {
- wfDeprecated( __FUNCTION__, '1.24' );
- return self::getPageList();
- }
-
/**
* Get the special page list as an array
*
"savechanges": "Save changes",
"publishpage": "Publish page",
"publishchanges": "Publish changes",
+ "savearticle-start": "Save page…",
+ "savechanges-start": "Save changes…",
+ "publishpage-start": "Publish page…",
+ "publishchanges-start": "Publish changes…",
"preview": "Preview",
"showpreview": "Show preview",
"showdiff": "Show changes",
"savechanges": "Text on the button to save the changes to an existing page. It should be an action which is short and makes clear that the effect is immediate and public.\n\nSee also {{msg-mw|showpreview}} and {{msg-mw|showdiff}} for the other buttons, and {{msg-mw|savearticle}} for the label for the button when the page is being modified.\n\nNote: This i18n is being introduced in advance of usage to provide extra time for translators.\n\nSee also:\n* {{msg-mw|Accesskey-publish}}\n* {{msg-mw|Tooltip-publish}}\n{{Identical|Save changes}}",
"publishpage": "Text on the button to create a new page on a public wiki. It should be an action which is short and makes clear that the effect is immediate and public.\n\nSee also {{msg-mw|showpreview}} and {{msg-mw|showdiff}} for the other buttons, and {{msg-mw|publishchanges}} for the label for the button when the page is being modified.\n\nNote: This i18n is being introduced in advance of usage to provide extra time for translators.\n\nSee also:\n* {{msg-mw|Accesskey-publish}}\n* {{msg-mw|Tooltip-publish}}\n{{Identical|Publish page}}",
"publishchanges": "Text on the button to save the changes to an existing page on a public wiki. It should be an action which is short and makes clear that the effect is immediate and public.\n\nSee also {{msg-mw|showpreview}} and {{msg-mw|showdiff}} for the other buttons, and {{msg-mw|publishchanges}} for the label for the button when the page is being created.\n\nNote: This i18n is being introduced in advance of usage to provide extra time for translators.\n\nSee also:\n* {{msg-mw|Accesskey-publish}}\n* {{msg-mw|Tooltip-publish}}\n{{Identical|Publish changes}}",
+ "savearticle-start": "Text on the button to start the process to create a new page. Usually just {{msg-mw|savearticle}} with an ellipsis (…).",
+ "savechanges-start": "Text on the button to start the process save the changes to an existing page. Usually just {{msg-mw|savechanges}} with an ellipsis (…).",
+ "publishpage-start": "Text on the button to start the process create a new page on a public wiki. Usually just {{msg-mw|publishpage}} with an ellipsis (…).",
+ "publishchanges-start": "Text on the button to start the process save the changes to an existing page on a public wiki. Usually just {{msg-mw|publishchanges}} with an ellipsis (…).",
"preview": "The title of the Preview page shown after clicking the \"Show preview\" button in the edit page. Since this is a heading, it should probably be translated as a noun and not as a verb.\n\n{{Identical|Preview}}",
"showpreview": "The text of the button to preview the page you are editing. See also {{msg-mw|showdiff}} and {{msg-mw|savearticle}} for the other buttons.\n\nSee also:\n* {{msg-mw|Showpreview}}\n* {{msg-mw|Accesskey-preview}}\n* {{msg-mw|Tooltip-preview}}\n{{Identical|Show preview}}",
"showdiff": "Button below the edit page. See also {{msg-mw|Showpreview}} and {{msg-mw|Savearticle}} for the other buttons.\n\nSee also:\n* {{msg-mw|Showdiff}}\n* {{msg-mw|Accesskey-diff}}\n* {{msg-mw|Tooltip-diff}}\n{{Identical|Show change}}",
// Set content language. This invalidates the magic word cache and title services
$lang = Language::factory( $langCode );
+ $lang->resetNamespaces();
$setup['wgContLang'] = $lang;
$reset = function () {
MagicWord::clearCache();
* @covers ApiBlock
*/
class ApiBlockTest extends ApiTestCase {
+ protected $mUser = null;
+
protected function setUp() {
parent::setUp();
$this->doLogin();
+
+ $this->mUser = $this->getMutableTestUser()->getUser();
}
protected function tearDown() {
- $block = Block::newFromTarget( 'UTApiBlockee' );
+ $block = Block::newFromTarget( $this->mUser->getName() );
if ( !is_null( $block ) ) {
$block->delete();
}
return $this->getTokenList( self::$users['sysop'] );
}
- function addDBDataOnce() {
- $user = User::newFromName( 'UTApiBlockee' );
-
- if ( $user->getId() == 0 ) {
- $user->addToDatabase();
- TestUser::setPasswordForUser( $user, 'UTApiBlockeePassword' );
-
- $user->saveSettings();
- }
- }
-
/**
- * This test has probably always been broken and use an invalid token
- * Bug tracking brokenness is https://phabricator.wikimedia.org/T37646
- *
- * Root cause is https://gerrit.wikimedia.org/r/3434
- * Which made the Block/Unblock API to actually verify the token
- * previously always considered valid (T36212).
+ * @param array $extraParams Extra API parameters to pass to doApiRequest
+ * @param User $blocker User to do the blocking, null to pick
+ * arbitrarily
*/
- public function testMakeNormalBlock() {
- $tokens = $this->getTokens();
+ private function doBlock( array $extraParams = [], User $blocker = null ) {
+ if ( $blocker === null ) {
+ $blocker = self::$users['sysop']->getUser();
+ }
- $user = User::newFromName( 'UTApiBlockee' );
+ $tokens = $this->getTokens();
- if ( !$user->getId() ) {
- $this->markTestIncomplete( "The user UTApiBlockee does not exist" );
- }
+ $this->assertNotNull( $this->mUser, 'Sanity check' );
+ $this->assertNotSame( 0, $this->mUser->getId(), 'Sanity check' );
- if ( !array_key_exists( 'blocktoken', $tokens ) ) {
- $this->markTestIncomplete( "No block token found" );
- }
+ $this->assertArrayHasKey( 'blocktoken', $tokens, 'Sanity check' );
- $this->doApiRequest( [
+ $params = [
'action' => 'block',
- 'user' => 'UTApiBlockee',
+ 'user' => $this->mUser->getName(),
'reason' => 'Some reason',
- 'token' => $tokens['blocktoken'] ], null, false, self::$users['sysop']->getUser() );
+ 'token' => $tokens['blocktoken'],
+ ];
+ if ( array_key_exists( 'userid', $extraParams ) ) {
+ // Make sure we don't have both user and userid
+ unset( $params['user'] );
+ }
+ $ret = $this->doApiRequest( array_merge( $params, $extraParams ), null,
+ false, $blocker );
- $block = Block::newFromTarget( 'UTApiBlockee' );
+ $block = Block::newFromTarget( $this->mUser->getName() );
$this->assertTrue( !is_null( $block ), 'Block is valid' );
- $this->assertEquals( 'UTApiBlockee', (string)$block->getTarget() );
- $this->assertEquals( 'Some reason', $block->mReason );
- $this->assertEquals( 'infinity', $block->mExpiry );
+ $this->assertSame( $this->mUser->getName(), (string)$block->getTarget() );
+ $this->assertSame( 'Some reason', $block->mReason );
+
+ return $ret;
+ }
+
+ /**
+ * Block by username
+ */
+ public function testNormalBlock() {
+ $this->doBlock();
}
/**
* Block by user ID
*/
- public function testMakeNormalBlockId() {
- $tokens = $this->getTokens();
- $user = User::newFromName( 'UTApiBlockee' );
+ public function testBlockById() {
+ $this->doBlock( [ 'userid' => $this->mUser->getId() ] );
+ }
- if ( !$user->getId() ) {
- $this->markTestIncomplete( "The user UTApiBlockee does not exist." );
- }
+ /**
+ * A blocked user can't block
+ */
+ public function testBlockByBlockedUser() {
+ $this->setExpectedException( ApiUsageException::class,
+ 'You cannot block or unblock other users because you are yourself blocked.' );
+
+ $blocked = $this->getMutableTestUser( [ 'sysop' ] )->getUser();
+ $block = new Block( [
+ 'address' => $blocked->getName(),
+ 'by' => self::$users['sysop']->getUser()->getId(),
+ 'reason' => 'Capriciousness',
+ 'timestamp' => '19370101000000',
+ 'expiry' => 'infinity',
+ ] );
+ $block->insert();
+
+ $this->doBlock( [], $blocked );
+ }
- if ( !array_key_exists( 'blocktoken', $tokens ) ) {
- $this->markTestIncomplete( "No block token found" );
- }
+ public function testBlockOfNonexistentUser() {
+ $this->setExpectedException( ApiUsageException::class,
+ 'There is no user by the name "Nonexistent". Check your spelling.' );
- $data = $this->doApiRequest( [
- 'action' => 'block',
- 'userid' => $user->getId(),
- 'reason' => 'Some reason',
- 'token' => $tokens['blocktoken'] ], null, false, self::$users['sysop']->getUser() );
+ $this->doBlock( [ 'user' => 'Nonexistent' ] );
+ }
+
+ public function testBlockOfNonexistentUserId() {
+ $id = 948206325;
+ $this->setExpectedException( ApiUsageException::class,
+ "There is no user with ID $id." );
+
+ $this->assertFalse( User::whoIs( $id ), 'Sanity check' );
+
+ $this->doBlock( [ 'userid' => $id ] );
+ }
+
+ public function testBlockWithTag() {
+ ChangeTags::defineTag( 'custom tag' );
+
+ $this->doBlock( [ 'tags' => 'custom tag' ] );
+
+ $dbw = wfGetDB( DB_MASTER );
+ $this->assertSame( 'custom tag', $dbw->selectField(
+ [ 'change_tag', 'logging' ],
+ 'ct_tag',
+ [ 'log_type' => 'block' ],
+ __METHOD__,
+ [],
+ [ 'change_tag' => [ 'INNER JOIN', 'ct_log_id = log_id' ] ]
+ ) );
+ }
+
+ public function testBlockWithProhibitedTag() {
+ $this->setExpectedException( ApiUsageException::class,
+ 'You do not have permission to apply change tags along with your changes.' );
+
+ ChangeTags::defineTag( 'custom tag' );
+
+ $this->setMwGlobals( 'wgRevokePermissions',
+ [ 'user' => [ 'applychangetags' => true ] ] );
+
+ $this->doBlock( [ 'tags' => 'custom tag' ] );
+ }
+
+ public function testBlockWithHide() {
+ global $wgGroupPermissions;
+ $newPermissions = $wgGroupPermissions['sysop'];
+ $newPermissions['hideuser'] = true;
+ $this->mergeMwGlobalArrayValue( 'wgGroupPermissions',
+ [ 'sysop' => $newPermissions ] );
+
+ $res = $this->doBlock( [ 'hidename' => '' ] );
+
+ $dbw = wfGetDB( DB_MASTER );
+ $this->assertSame( '1', $dbw->selectField(
+ 'ipblocks',
+ 'ipb_deleted',
+ [ 'ipb_id' => $res[0]['block']['id'] ],
+ __METHOD__
+ ) );
+ }
+
+ public function testBlockWithProhibitedHide() {
+ $this->setExpectedException( ApiUsageException::class,
+ "You don't have permission to hide user names from the block log." );
+
+ $this->doBlock( [ 'hidename' => '' ] );
+ }
+
+ public function testBlockWithEmailBlock() {
+ $res = $this->doBlock( [ 'noemail' => '' ] );
+
+ $dbw = wfGetDB( DB_MASTER );
+ $this->assertSame( '1', $dbw->selectField(
+ 'ipblocks',
+ 'ipb_block_email',
+ [ 'ipb_id' => $res[0]['block']['id'] ],
+ __METHOD__
+ ) );
+ }
+
+ public function testBlockWithProhibitedEmailBlock() {
+ $this->setExpectedException( ApiUsageException::class,
+ "You don't have permission to block users from sending email through the wiki." );
+
+ $this->setMwGlobals( 'wgRevokePermissions',
+ [ 'sysop' => [ 'blockemail' => true ] ] );
+
+ $this->doBlock( [ 'noemail' => '' ] );
+ }
+
+ public function testBlockWithExpiry() {
+ $res = $this->doBlock( [ 'expiry' => '1 day' ] );
+
+ $dbw = wfGetDB( DB_MASTER );
+ $expiry = $dbw->selectField(
+ 'ipblocks',
+ 'ipb_expiry',
+ [ 'ipb_id' => $res[0]['block']['id'] ],
+ __METHOD__
+ );
+
+ // Allow flakiness up to one second
+ $this->assertLessThanOrEqual( 1,
+ abs( wfTimestamp( TS_UNIX, $expiry ) - ( time() + 86400 ) ) );
+ }
- $block = Block::newFromTarget( 'UTApiBlockee' );
+ public function testBlockWithInvalidExpiry() {
+ $this->setExpectedException( ApiUsageException::class, "Expiry time invalid." );
- $this->assertTrue( !is_null( $block ), 'Block is valid.' );
- $this->assertEquals( 'UTApiBlockee', (string)$block->getTarget() );
- $this->assertEquals( 'Some reason', $block->mReason );
- $this->assertEquals( 'infinity', $block->mExpiry );
+ $this->doBlock( [ 'expiry' => '' ] );
}
/**
$this->doApiRequest(
[
'action' => 'block',
- 'user' => 'UTApiBlockee',
+ 'user' => $this->mUser->getName(),
'reason' => 'Some reason',
],
null,
}
public function testDelete() {
- $name = 'Help:ApiDeleteTest_testDelete';
-
- // test non-existing page
- try {
- $this->doApiRequestWithToken( [
- 'action' => 'delete',
- 'title' => $name,
- ] );
- $this->fail( "Should have raised an ApiUsageException" );
- } catch ( ApiUsageException $e ) {
- $this->assertTrue( self::apiExceptionHasCode( $e, 'missingtitle' ) );
- }
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
// create new page
$this->editPage( $name, 'Some text' );
$apiResult = $this->doApiRequestWithToken( [
'action' => 'delete',
'title' => $name,
- ] );
- $apiResult = $apiResult[0];
+ ] )[0];
$this->assertArrayHasKey( 'delete', $apiResult );
$this->assertArrayHasKey( 'title', $apiResult['delete'] );
- // Normalized $name is used
- $this->assertSame(
- 'Help:ApiDeleteTest testDelete',
- $apiResult['delete']['title']
- );
+ $this->assertSame( $name, $apiResult['delete']['title'] );
$this->assertArrayHasKey( 'logid', $apiResult['delete'] );
$this->assertFalse( Title::newFromText( $name )->exists() );
}
+ public function testDeleteNonexistent() {
+ $this->setExpectedException( ApiUsageException::class,
+ "The page you specified doesn't exist." );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'delete',
+ 'title' => 'This page deliberately left nonexistent',
+ ] );
+ }
+
public function testDeletionWithoutPermission() {
- $name = 'Help:ApiDeleteTest_testDeleteWithoutPermission';
+ $this->setExpectedException( ApiUsageException::class,
+ 'The action you have requested is limited to users in the group:' );
+
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
// create new page
$this->editPage( $name, 'Some text' );
'title' => $name,
'token' => $user->getEditToken(),
], null, null, $user );
- $this->fail( "Should have raised an ApiUsageException" );
- } catch ( ApiUsageException $e ) {
- $this->assertTrue( self::apiExceptionHasCode( $e, 'permissiondenied' ) );
+ } finally {
+ $this->assertTrue( Title::newFromText( $name )->exists() );
+ }
+ }
+
+ public function testDeleteWithTag() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ ChangeTags::defineTag( 'custom tag' );
+
+ $this->editPage( $name, 'Some text' );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'delete',
+ 'title' => $name,
+ 'tags' => 'custom tag',
+ ] );
+
+ $this->assertFalse( Title::newFromText( $name )->exists() );
+
+ $dbw = wfGetDB( DB_MASTER );
+ $this->assertSame( 'custom tag', $dbw->selectField(
+ [ 'change_tag', 'logging' ],
+ 'ct_tag',
+ [
+ 'log_namespace' => NS_HELP,
+ 'log_title' => ucfirst( __FUNCTION__ ),
+ ],
+ __METHOD__,
+ [],
+ [ 'change_tag' => [ 'INNER JOIN', 'ct_log_id = log_id' ] ]
+ ) );
+ }
+
+ public function testDeleteWithoutTagPermission() {
+ $this->setExpectedException( ApiUsageException::class,
+ 'You do not have permission to apply change tags along with your changes.' );
+
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ ChangeTags::defineTag( 'custom tag' );
+ $this->setMwGlobals( 'wgRevokePermissions',
+ [ 'user' => [ 'applychangetags' => true ] ] );
+
+ $this->editPage( $name, 'Some text' );
+
+ try {
+ $this->doApiRequestWithToken( [
+ 'action' => 'delete',
+ 'title' => $name,
+ 'tags' => 'custom tag',
+ ] );
+ } finally {
+ $this->assertTrue( Title::newFromText( $name )->exists() );
+ }
+ }
+
+ public function testDeleteAbortedByHook() {
+ $this->setExpectedException( ApiUsageException::class,
+ 'Deletion aborted by hook. It gave no explanation.' );
+
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->editPage( $name, 'Some text' );
+
+ $this->setTemporaryHook( 'ArticleDelete',
+ function () {
+ return false;
+ }
+ );
+
+ try {
+ $this->doApiRequestWithToken( [ 'action' => 'delete', 'title' => $name ] );
+ } finally {
+ $this->assertTrue( Title::newFromText( $name )->exists() );
}
+ }
+
+ public function testDeleteWatch() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+ $user = self::$users['sysop']->getUser();
+
+ $this->editPage( $name, 'Some text' );
+ $this->assertTrue( Title::newFromText( $name )->exists() );
+ $this->assertFalse( $user->isWatched( Title::newFromText( $name ) ) );
+
+ $this->doApiRequestWithToken( [ 'action' => 'delete', 'title' => $name, 'watch' => '' ] );
+ $this->assertFalse( Title::newFromText( $name )->exists() );
+ $this->assertTrue( $user->isWatched( Title::newFromText( $name ) ) );
+ }
+
+ public function testDeleteUnwatch() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+ $user = self::$users['sysop']->getUser();
+
+ $this->editPage( $name, 'Some text' );
$this->assertTrue( Title::newFromText( $name )->exists() );
+ $user->addWatch( Title::newFromText( $name ) );
+ $this->assertTrue( $user->isWatched( Title::newFromText( $name ) ) );
+
+ $this->doApiRequestWithToken( [ 'action' => 'delete', 'title' => $name, 'unwatch' => '' ] );
+
+ $this->assertFalse( Title::newFromText( $name )->exists() );
+ $this->assertFalse( $user->isWatched( Title::newFromText( $name ) ) );
}
}
api.uploadWithIframe( $( '<input>' )[ 0 ], { filename: 'Testing API upload.jpg' } );
- $iframe = $( 'iframe' );
+ $iframe = $( 'iframe:last-child' );
$form = $( 'form.mw-api-upload-form' );
$input = $form.find( 'input[name=filename]' );
- assert.ok( $form.length > 0 );
- assert.ok( $input.length > 0 );
- assert.ok( $iframe.length > 0 );
- assert.strictEqual( $form.prop( 'target' ), $iframe.prop( 'id' ) );
- assert.strictEqual( $input.val(), 'Testing API upload.jpg' );
+ assert.ok( $form.length > 0, 'form' );
+ assert.ok( $input.length > 0, 'input' );
+ assert.ok( $iframe.length > 0, 'frame' );
+ assert.strictEqual( $form.prop( 'target' ), $iframe.prop( 'id' ), 'form.target and frame.id ' );
+ assert.strictEqual( $input.val(), 'Testing API upload.jpg', 'input value' );
} );
}( mediaWiki, jQuery ) );