2016-06-16 20:25:58 +00:00
|
|
|
<?php
|
|
|
|
|
|
2019-04-13 04:38:55 +00:00
|
|
|
use MediaWiki\MediaWikiServices;
|
|
|
|
|
use MediaWiki\Storage\PageEditStash;
|
|
|
|
|
use Psr\Log\NullLogger;
|
2020-01-10 00:00:51 +00:00
|
|
|
use Wikimedia\TestingAccessWrapper;
|
2018-04-10 13:45:53 +00:00
|
|
|
|
2016-06-16 20:25:58 +00:00
|
|
|
/**
|
|
|
|
|
* @covers ApiStashEdit
|
2019-04-13 04:38:55 +00:00
|
|
|
* @covers \MediaWiki\Storage\PageEditStash
|
2016-06-16 20:25:58 +00:00
|
|
|
* @group API
|
|
|
|
|
* @group medium
|
|
|
|
|
* @group Database
|
|
|
|
|
*/
|
|
|
|
|
class ApiStashEditTest extends ApiTestCase {
|
2020-06-14 10:51:39 +00:00
|
|
|
protected function setUp() : void {
|
2018-04-10 13:45:53 +00:00
|
|
|
parent::setUp();
|
2019-04-13 04:38:55 +00:00
|
|
|
$this->setService( 'PageEditStash', new PageEditStash(
|
|
|
|
|
new HashBagOStuff( [] ),
|
|
|
|
|
MediaWikiServices::getInstance()->getDBLoadBalancer(),
|
|
|
|
|
new NullLogger(),
|
|
|
|
|
new NullStatsdDataFactory(),
|
Hooks::run() call site migration
Migrate all callers of Hooks::run() to use the new
HookContainer/HookRunner system.
General principles:
* Use DI if it is already used. We're not changing the way state is
managed in this patch.
* HookContainer is always injected, not HookRunner. HookContainer
is a service, it's a more generic interface, it is the only
thing that provides isRegistered() which is needed in some cases,
and a HookRunner can be efficiently constructed from it
(confirmed by benchmark). Because HookContainer is needed
for object construction, it is also needed by all factories.
* "Ask your friendly local base class". Big hierarchies like
SpecialPage and ApiBase have getHookContainer() and getHookRunner()
methods in the base class, and classes that extend that base class
are not expected to know or care where the base class gets its
HookContainer from.
* ProtectedHookAccessorTrait provides protected getHookContainer() and
getHookRunner() methods, getting them from the global service
container. The point of this is to ease migration to DI by ensuring
that call sites ask their local friendly base class rather than
getting a HookRunner from the service container directly.
* Private $this->hookRunner. In some smaller classes where accessor
methods did not seem warranted, there is a private HookRunner property
which is accessed directly. Very rarely (two cases), there is a
protected property, for consistency with code that conventionally
assumes protected=private, but in cases where the class might actually
be overridden, a protected accessor is preferred over a protected
property.
* The last resort: Hooks::runner(). Mostly for static, file-scope and
global code. In a few cases it was used for objects with broken
construction schemes, out of horror or laziness.
Constructors with new required arguments:
* AuthManager
* BadFileLookup
* BlockManager
* ClassicInterwikiLookup
* ContentHandlerFactory
* ContentSecurityPolicy
* DefaultOptionsManager
* DerivedPageDataUpdater
* FullSearchResultWidget
* HtmlCacheUpdater
* LanguageFactory
* LanguageNameUtils
* LinkRenderer
* LinkRendererFactory
* LocalisationCache
* MagicWordFactory
* MessageCache
* NamespaceInfo
* PageEditStash
* PageHandlerFactory
* PageUpdater
* ParserFactory
* PermissionManager
* RevisionStore
* RevisionStoreFactory
* SearchEngineConfig
* SearchEngineFactory
* SearchFormWidget
* SearchNearMatcher
* SessionBackend
* SpecialPageFactory
* UserNameUtils
* UserOptionsManager
* WatchedItemQueryService
* WatchedItemStore
Constructors with new optional arguments:
* DefaultPreferencesFactory
* Language
* LinkHolderArray
* MovePage
* Parser
* ParserCache
* PasswordReset
* Router
setHookContainer() now required after construction:
* AuthenticationProvider
* ResourceLoaderModule
* SearchEngine
Change-Id: Id442b0dbe43aba84bd5cf801d86dedc768b082c7
2020-03-19 02:42:09 +00:00
|
|
|
MediaWikiServices::getInstance()->getHookContainer(),
|
2019-04-13 04:38:55 +00:00
|
|
|
PageEditStash::INITIATOR_USER
|
|
|
|
|
) );
|
|
|
|
|
// Clear rate-limiting cache between tests
|
2018-04-10 13:45:53 +00:00
|
|
|
$this->setMwGlobals( 'wgMainCacheType', 'hash' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Make a stashedit API call with suitable default parameters
|
|
|
|
|
*
|
|
|
|
|
* @param array $params Query parameters for API request. All are optional and will have
|
|
|
|
|
* sensible defaults filled in. To make a parameter actually not passed, set to null.
|
|
|
|
|
* @param User $user User to do the request
|
|
|
|
|
* @param string $expectedResult 'stashed', 'editconflict'
|
2019-04-13 04:38:55 +00:00
|
|
|
* @return array
|
2018-04-10 13:45:53 +00:00
|
|
|
*/
|
|
|
|
|
protected function doStash(
|
|
|
|
|
array $params = [], User $user = null, $expectedResult = 'stashed'
|
|
|
|
|
) {
|
|
|
|
|
$params = array_merge( [
|
|
|
|
|
'action' => 'stashedit',
|
|
|
|
|
'title' => __CLASS__,
|
|
|
|
|
'contentmodel' => 'wikitext',
|
|
|
|
|
'contentformat' => 'text/x-wiki',
|
|
|
|
|
'baserevid' => 0,
|
|
|
|
|
], $params );
|
|
|
|
|
if ( !array_key_exists( 'text', $params ) &&
|
|
|
|
|
!array_key_exists( 'stashedtexthash', $params )
|
|
|
|
|
) {
|
|
|
|
|
$params['text'] = 'Content';
|
|
|
|
|
}
|
|
|
|
|
foreach ( $params as $key => $val ) {
|
|
|
|
|
if ( $val === null ) {
|
|
|
|
|
unset( $params[$key] );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( isset( $params['text'] ) ) {
|
|
|
|
|
$expectedText = $params['text'];
|
|
|
|
|
} elseif ( isset( $params['stashedtexthash'] ) ) {
|
|
|
|
|
$expectedText = $this->getStashedText( $params['stashedtexthash'] );
|
|
|
|
|
}
|
|
|
|
|
if ( isset( $expectedText ) ) {
|
|
|
|
|
$expectedText = rtrim( str_replace( "\r\n", "\n", $expectedText ) );
|
|
|
|
|
$expectedHash = sha1( $expectedText );
|
|
|
|
|
$origText = $this->getStashedText( $expectedHash );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$res = $this->doApiRequestWithToken( $params, null, $user );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( $expectedResult, $res[0]['stashedit']['status'] );
|
|
|
|
|
$this->assertCount( $expectedResult === 'stashed' ? 2 : 1, $res[0]['stashedit'] );
|
|
|
|
|
|
|
|
|
|
if ( $expectedResult === 'stashed' ) {
|
|
|
|
|
$hash = $res[0]['stashedit']['texthash'];
|
|
|
|
|
|
|
|
|
|
$this->assertSame( $expectedText, $this->getStashedText( $hash ) );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( $expectedHash, $hash );
|
|
|
|
|
|
|
|
|
|
if ( isset( $params['stashedtexthash'] ) ) {
|
|
|
|
|
$this->assertSame( $params['stashedtexthash'], $expectedHash, 'Sanity' );
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$this->assertSame( $origText, $this->getStashedText( $expectedHash ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->assertArrayNotHasKey( 'warnings', $res[0] );
|
|
|
|
|
|
|
|
|
|
return $res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return the text stashed for $hash.
|
|
|
|
|
*
|
|
|
|
|
* @param string $hash
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
protected function getStashedText( $hash ) {
|
2019-04-13 04:38:55 +00:00
|
|
|
return MediaWikiServices::getInstance()->getPageEditStash()->fetchInputText( $hash );
|
2018-04-10 13:45:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2019-04-13 04:38:55 +00:00
|
|
|
* Return a key that can be passed to the cache to obtain a stashed edit object.
|
2018-04-10 13:45:53 +00:00
|
|
|
*
|
|
|
|
|
* @param string $title Title of page
|
|
|
|
|
* @param string Content $text Content of edit
|
|
|
|
|
* @param User $user User who made edit
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
protected function getStashKey( $title = __CLASS__, $text = 'Content', User $user = null ) {
|
|
|
|
|
$titleObj = Title::newFromText( $title );
|
|
|
|
|
$content = new WikitextContent( $text );
|
|
|
|
|
if ( !$user ) {
|
|
|
|
|
$user = $this->getTestSysop()->getUser();
|
|
|
|
|
}
|
2019-04-13 04:38:55 +00:00
|
|
|
$editStash = TestingAccessWrapper::newFromObject(
|
|
|
|
|
MediaWikiServices::getInstance()->getPageEditStash() );
|
|
|
|
|
|
|
|
|
|
return $editStash->getStashKey( $titleObj, $editStash->getContentHash( $content ), $user );
|
2018-04-10 13:45:53 +00:00
|
|
|
}
|
2016-06-16 20:25:58 +00:00
|
|
|
|
|
|
|
|
public function testBasicEdit() {
|
2018-04-10 13:45:53 +00:00
|
|
|
$this->doStash();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testBot() {
|
|
|
|
|
// @todo This restriction seems arbitrary, is there any good reason to keep it?
|
|
|
|
|
$this->setExpectedApiException( 'apierror-botsnotsupported' );
|
|
|
|
|
|
|
|
|
|
$this->doStash( [], $this->getTestUser( [ 'bot' ] )->getUser() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testUnrecognizedFormat() {
|
|
|
|
|
$this->setExpectedApiException(
|
|
|
|
|
[ 'apierror-badformat-generic', 'application/json', 'wikitext' ] );
|
|
|
|
|
|
|
|
|
|
$this->doStash( [ 'contentformat' => 'application/json' ] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testMissingTextAndStashedTextHash() {
|
|
|
|
|
$this->setExpectedApiException( [
|
|
|
|
|
'apierror-missingparam-one-of',
|
|
|
|
|
Message::listParam( [ '<var>stashedtexthash</var>', '<var>text</var>' ] ),
|
|
|
|
|
2
|
|
|
|
|
] );
|
|
|
|
|
$this->doStash( [ 'text' => null ] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testStashedTextHash() {
|
|
|
|
|
$res = $this->doStash();
|
|
|
|
|
|
|
|
|
|
$this->doStash( [ 'stashedtexthash' => $res[0]['stashedit']['texthash'] ] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testMalformedStashedTextHash() {
|
|
|
|
|
$this->setExpectedApiException( 'apierror-stashedit-missingtext' );
|
|
|
|
|
$this->doStash( [ 'stashedtexthash' => 'abc' ] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testMissingStashedTextHash() {
|
|
|
|
|
$this->setExpectedApiException( 'apierror-stashedit-missingtext' );
|
|
|
|
|
$this->doStash( [ 'stashedtexthash' => str_repeat( '0', 40 ) ] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testHashNormalization() {
|
|
|
|
|
$res1 = $this->doStash( [ 'text' => "a\r\nb\rc\nd \t\n\r" ] );
|
|
|
|
|
$res2 = $this->doStash( [ 'text' => "a\nb\rc\nd" ] );
|
|
|
|
|
|
|
|
|
|
$this->assertSame( $res1[0]['stashedit']['texthash'], $res2[0]['stashedit']['texthash'] );
|
|
|
|
|
$this->assertSame( "a\nb\rc\nd",
|
|
|
|
|
$this->getStashedText( $res1[0]['stashedit']['texthash'] ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testNonexistentBaseRevId() {
|
|
|
|
|
$this->setExpectedApiException( [ 'apierror-nosuchrevid', pow( 2, 31 ) - 1 ] );
|
|
|
|
|
|
|
|
|
|
$name = ucfirst( __FUNCTION__ );
|
|
|
|
|
$this->editPage( $name, '' );
|
|
|
|
|
$this->doStash( [ 'title' => $name, 'baserevid' => pow( 2, 31 ) - 1 ] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testPageWithNoRevisions() {
|
|
|
|
|
$name = ucfirst( __FUNCTION__ );
|
2020-06-10 00:37:04 +00:00
|
|
|
$revRecord = $this->editPage( $name, '' )->value['revision-record'];
|
2018-04-10 13:45:53 +00:00
|
|
|
|
2020-06-10 00:37:04 +00:00
|
|
|
$this->setExpectedApiException( [ 'apierror-missingrev-pageid', $revRecord->getPageId() ] );
|
2018-04-10 13:45:53 +00:00
|
|
|
|
|
|
|
|
// Corrupt the database. @todo Does the API really need to fail gracefully for this case?
|
|
|
|
|
$dbw = wfGetDB( DB_MASTER );
|
|
|
|
|
$dbw->update(
|
|
|
|
|
'page',
|
|
|
|
|
[ 'page_latest' => 0 ],
|
2020-06-10 00:37:04 +00:00
|
|
|
[ 'page_id' => $revRecord->getPageId() ],
|
2018-04-10 13:45:53 +00:00
|
|
|
__METHOD__
|
2016-06-16 20:25:58 +00:00
|
|
|
);
|
2018-04-10 13:45:53 +00:00
|
|
|
|
2020-06-10 00:37:04 +00:00
|
|
|
$this->doStash( [ 'title' => $name, 'baserevid' => $revRecord->getId() ] );
|
2018-04-10 13:45:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testExistingPage() {
|
|
|
|
|
$name = ucfirst( __FUNCTION__ );
|
2020-06-10 00:37:04 +00:00
|
|
|
$revRecord = $this->editPage( $name, '' )->value['revision-record'];
|
2018-04-10 13:45:53 +00:00
|
|
|
|
2020-06-10 00:37:04 +00:00
|
|
|
$this->doStash( [ 'title' => $name, 'baserevid' => $revRecord->getId() ] );
|
2018-04-10 13:45:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testInterveningEdit() {
|
|
|
|
|
$name = ucfirst( __FUNCTION__ );
|
2020-06-10 00:37:04 +00:00
|
|
|
$oldRevRecord = $this->editPage( $name, "A\n\nB" )->value['revision-record'];
|
2018-04-10 13:45:53 +00:00
|
|
|
$this->editPage( $name, "A\n\nC" );
|
|
|
|
|
|
|
|
|
|
$this->doStash( [
|
|
|
|
|
'title' => $name,
|
2020-06-10 00:37:04 +00:00
|
|
|
'baserevid' => $oldRevRecord->getId(),
|
2018-04-10 13:45:53 +00:00
|
|
|
'text' => "D\n\nB",
|
|
|
|
|
] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditConflict() {
|
|
|
|
|
$name = ucfirst( __FUNCTION__ );
|
2020-06-10 00:37:04 +00:00
|
|
|
$oldRevRecord = $this->editPage( $name, 'A' )->value['revision-record'];
|
2018-04-10 13:45:53 +00:00
|
|
|
$this->editPage( $name, 'B' );
|
|
|
|
|
|
|
|
|
|
$this->doStash( [
|
|
|
|
|
'title' => $name,
|
2020-06-10 00:37:04 +00:00
|
|
|
'baserevid' => $oldRevRecord->getId(),
|
2018-04-10 13:45:53 +00:00
|
|
|
'text' => 'C',
|
|
|
|
|
], null, 'editconflict' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testDeletedRevision() {
|
|
|
|
|
$name = ucfirst( __FUNCTION__ );
|
2020-06-10 00:37:04 +00:00
|
|
|
$oldRevRecord = $this->editPage( $name, 'A' )->value['revision-record'];
|
2018-04-10 13:45:53 +00:00
|
|
|
$this->editPage( $name, 'B' );
|
|
|
|
|
|
2020-06-10 00:37:04 +00:00
|
|
|
$this->setExpectedApiException(
|
|
|
|
|
[ 'apierror-missingcontent-pageid', $oldRevRecord->getPageId() ]
|
|
|
|
|
);
|
2018-04-10 13:45:53 +00:00
|
|
|
|
2020-06-10 00:37:04 +00:00
|
|
|
$this->revisionDelete( $oldRevRecord );
|
2018-04-10 13:45:53 +00:00
|
|
|
|
|
|
|
|
$this->doStash( [
|
|
|
|
|
'title' => $name,
|
2020-06-10 00:37:04 +00:00
|
|
|
'baserevid' => $oldRevRecord->getId(),
|
2018-04-10 13:45:53 +00:00
|
|
|
'text' => 'C',
|
|
|
|
|
] );
|
2016-06-16 20:25:58 +00:00
|
|
|
}
|
|
|
|
|
|
2018-04-10 13:45:53 +00:00
|
|
|
public function testDeletedRevisionSection() {
|
|
|
|
|
$name = ucfirst( __FUNCTION__ );
|
2020-06-10 00:37:04 +00:00
|
|
|
$oldRevRecord = $this->editPage( $name, 'A' )->value['revision-record'];
|
2018-04-10 13:45:53 +00:00
|
|
|
$this->editPage( $name, 'B' );
|
|
|
|
|
|
|
|
|
|
$this->setExpectedApiException( 'apierror-sectionreplacefailed' );
|
|
|
|
|
|
2020-06-10 00:37:04 +00:00
|
|
|
$this->revisionDelete( $oldRevRecord );
|
2018-04-10 13:45:53 +00:00
|
|
|
|
|
|
|
|
$this->doStash( [
|
|
|
|
|
'title' => $name,
|
2020-06-10 00:37:04 +00:00
|
|
|
'baserevid' => $oldRevRecord->getId(),
|
2018-04-10 13:45:53 +00:00
|
|
|
'text' => 'C',
|
|
|
|
|
'section' => '1',
|
|
|
|
|
] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testPingLimiter() {
|
2018-10-07 12:26:18 +00:00
|
|
|
$this->mergeMwGlobalArrayValue( 'wgRateLimits',
|
|
|
|
|
[ 'stashedit' => [ '&can-bypass' => false, 'user' => [ 1, 60 ] ] ] );
|
2018-04-10 13:45:53 +00:00
|
|
|
|
|
|
|
|
$this->doStash( [ 'text' => 'A' ] );
|
|
|
|
|
|
|
|
|
|
$this->doStash( [ 'text' => 'B' ], null, 'ratelimited' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2019-04-13 04:38:55 +00:00
|
|
|
* Shortcut for calling PageStashEdit::checkCache() without
|
|
|
|
|
* having to create Titles and Contents in every test.
|
2018-04-10 13:45:53 +00:00
|
|
|
*
|
|
|
|
|
* @param User $user
|
|
|
|
|
* @param string $text The text of the article
|
2019-04-13 04:38:55 +00:00
|
|
|
* @return stdClass|bool Return value of PageStashEdit::checkCache(), false if not in cache
|
2018-04-10 13:45:53 +00:00
|
|
|
*/
|
|
|
|
|
protected function doCheckCache( User $user, $text = 'Content' ) {
|
2019-04-13 04:38:55 +00:00
|
|
|
return MediaWikiServices::getInstance()->getPageEditStash()->checkCache(
|
2018-04-10 13:45:53 +00:00
|
|
|
Title::newFromText( __CLASS__ ),
|
|
|
|
|
new WikitextContent( $text ),
|
|
|
|
|
$user
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCheckCache() {
|
|
|
|
|
$user = $this->getMutableTestUser()->getUser();
|
|
|
|
|
|
|
|
|
|
$this->doStash( [], $user );
|
|
|
|
|
|
|
|
|
|
$this->assertInstanceOf( stdClass::class, $this->doCheckCache( $user ) );
|
|
|
|
|
|
|
|
|
|
// Another user doesn't see the cache
|
|
|
|
|
$this->assertFalse(
|
|
|
|
|
$this->doCheckCache( $this->getTestUser()->getUser() ),
|
|
|
|
|
'Cache is user-specific'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Nor does the original one if they become a bot
|
|
|
|
|
$user->addGroup( 'bot' );
|
2019-04-09 06:58:04 +00:00
|
|
|
MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache();
|
2018-04-10 13:45:53 +00:00
|
|
|
$this->assertFalse(
|
|
|
|
|
$this->doCheckCache( $user ),
|
|
|
|
|
"We assume bots don't have cache entries"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// But other groups are okay
|
|
|
|
|
$user->removeGroup( 'bot' );
|
|
|
|
|
$user->addGroup( 'sysop' );
|
2019-04-09 06:58:04 +00:00
|
|
|
MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache();
|
2018-04-10 13:45:53 +00:00
|
|
|
$this->assertInstanceOf( stdClass::class, $this->doCheckCache( $user ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCheckCacheAnon() {
|
2019-04-13 04:38:55 +00:00
|
|
|
$user = User::newFromName( '174.5.4.6', false );
|
2018-04-10 13:45:53 +00:00
|
|
|
|
|
|
|
|
$this->doStash( [], $user );
|
|
|
|
|
|
2019-04-13 04:38:55 +00:00
|
|
|
$this->assertInstanceOf( stdClass::class, $this->doCheckCache( $user ) );
|
2018-04-10 13:45:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Stash an edit some time in the past, for testing expiry and freshness logic.
|
|
|
|
|
*
|
|
|
|
|
* @param User $user Who's doing the editing
|
|
|
|
|
* @param string $text What text should be cached
|
|
|
|
|
* @param int $howOld How many seconds is "old" (we actually set it one second before this)
|
|
|
|
|
*/
|
|
|
|
|
protected function doStashOld(
|
2019-04-13 04:38:55 +00:00
|
|
|
User $user, $text = 'Content', $howOld = PageEditStash::PRESUME_FRESH_TTL_SEC
|
2018-04-10 13:45:53 +00:00
|
|
|
) {
|
|
|
|
|
$this->doStash( [ 'text' => $text ], $user );
|
|
|
|
|
|
|
|
|
|
// Monkey with the cache to make the edit look old. @todo Is there a less fragile way to
|
|
|
|
|
// fake the time?
|
|
|
|
|
$key = $this->getStashKey( __CLASS__, $text, $user );
|
|
|
|
|
|
2019-04-13 04:38:55 +00:00
|
|
|
$editStash = TestingAccessWrapper::newFromObject(
|
|
|
|
|
MediaWikiServices::getInstance()->getPageEditStash() );
|
|
|
|
|
$cache = $editStash->cache;
|
2018-04-10 13:45:53 +00:00
|
|
|
|
|
|
|
|
$editInfo = $cache->get( $key );
|
|
|
|
|
$editInfo->output->setCacheTime( wfTimestamp( TS_MW,
|
|
|
|
|
wfTimestamp( TS_UNIX, $editInfo->output->getCacheTime() ) - $howOld - 1 ) );
|
|
|
|
|
|
|
|
|
|
$cache->set( $key, $editInfo );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCheckCacheOldNoEdits() {
|
|
|
|
|
$user = $this->getTestSysop()->getUser();
|
|
|
|
|
|
|
|
|
|
$this->doStashOld( $user );
|
|
|
|
|
|
|
|
|
|
// Should still be good, because no intervening edits
|
|
|
|
|
$this->assertInstanceOf( stdClass::class, $this->doCheckCache( $user ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCheckCacheOldNoEditsAnon() {
|
|
|
|
|
// Specify a made-up IP address to make sure no edits are lying around
|
2019-04-13 04:38:55 +00:00
|
|
|
$user = User::newFromName( '172.0.2.77', false );
|
2018-04-10 13:45:53 +00:00
|
|
|
|
|
|
|
|
$this->doStashOld( $user );
|
|
|
|
|
|
|
|
|
|
// Should still be good, because no intervening edits
|
|
|
|
|
$this->assertInstanceOf( stdClass::class, $this->doCheckCache( $user ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCheckCacheInterveningEdits() {
|
|
|
|
|
$user = $this->getTestSysop()->getUser();
|
|
|
|
|
|
|
|
|
|
$this->doStashOld( $user );
|
|
|
|
|
|
|
|
|
|
// Now let's also increment our editcount
|
|
|
|
|
$this->editPage( ucfirst( __FUNCTION__ ), '' );
|
|
|
|
|
|
2018-10-22 22:58:02 +00:00
|
|
|
$user->clearInstanceCache();
|
2018-04-10 13:45:53 +00:00
|
|
|
$this->assertFalse( $this->doCheckCache( $user ),
|
|
|
|
|
"Cache should be invalidated when it's old and the user has an intervening edit" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @dataProvider signatureProvider
|
|
|
|
|
* @param string $text Which signature to test (~~~, ~~~~, or ~~~~~)
|
|
|
|
|
* @param int $ttl Expected TTL in seconds
|
|
|
|
|
*/
|
|
|
|
|
public function testSignatureTtl( $text, $ttl ) {
|
|
|
|
|
$this->doStash( [ 'text' => $text ] );
|
|
|
|
|
|
2019-04-13 04:38:55 +00:00
|
|
|
$editStash = TestingAccessWrapper::newFromObject(
|
|
|
|
|
MediaWikiServices::getInstance()->getPageEditStash() );
|
|
|
|
|
$cache = $editStash->cache;
|
2018-04-10 13:45:53 +00:00
|
|
|
$key = $this->getStashKey( __CLASS__, $text );
|
|
|
|
|
|
|
|
|
|
$wrapper = TestingAccessWrapper::newFromObject( $cache );
|
|
|
|
|
|
2019-12-14 12:45:35 +00:00
|
|
|
$this->assertEqualsWithDelta( $ttl, $wrapper->bag[$key][HashBagOStuff::KEY_EXP] - time(), 1 );
|
2018-04-10 13:45:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function signatureProvider() {
|
|
|
|
|
return [
|
2019-04-13 04:38:55 +00:00
|
|
|
'~~~' => [ '~~~', PageEditStash::MAX_SIGNATURE_TTL ],
|
|
|
|
|
'~~~~' => [ '~~~~', PageEditStash::MAX_SIGNATURE_TTL ],
|
|
|
|
|
'~~~~~' => [ '~~~~~', PageEditStash::MAX_SIGNATURE_TTL ],
|
2018-04-10 13:45:53 +00:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testIsInternal() {
|
|
|
|
|
$res = $this->doApiRequest( [
|
|
|
|
|
'action' => 'paraminfo',
|
|
|
|
|
'modules' => 'stashedit',
|
|
|
|
|
] );
|
|
|
|
|
|
|
|
|
|
$this->assertCount( 1, $res[0]['paraminfo']['modules'] );
|
|
|
|
|
$this->assertSame( true, $res[0]['paraminfo']['modules'][0]['internal'] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testBusy() {
|
|
|
|
|
// @todo This doesn't work because both lock acquisitions are in the same MySQL session, so
|
|
|
|
|
// they don't conflict. How do I open a different session?
|
|
|
|
|
$this->markTestSkipped();
|
|
|
|
|
|
|
|
|
|
$key = $this->getStashKey();
|
|
|
|
|
$this->db->lock( $key, __METHOD__, 0 );
|
|
|
|
|
try {
|
|
|
|
|
$this->doStash( [], null, 'busy' );
|
|
|
|
|
} finally {
|
|
|
|
|
$this->db->unlock( $key, __METHOD__ );
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-06-16 20:25:58 +00:00
|
|
|
}
|