MediaWiki  master
ApiStashEditTest.php
Go to the documentation of this file.
1 <?php
2 
7 
16  public function setUp() {
17  parent::setUp();
18  $this->setService( 'PageEditStash', new PageEditStash(
19  new HashBagOStuff( [] ),
20  MediaWikiServices::getInstance()->getDBLoadBalancer(),
21  new NullLogger(),
23  PageEditStash::INITIATOR_USER
24  ) );
25  // Clear rate-limiting cache between tests
26  $this->setMwGlobals( 'wgMainCacheType', 'hash' );
27  }
28 
29  public function tearDown() {
30  parent::tearDown();
31  }
32 
42  protected function doStash(
43  array $params = [], User $user = null, $expectedResult = 'stashed'
44  ) {
45  $params = array_merge( [
46  'action' => 'stashedit',
47  'title' => __CLASS__,
48  'contentmodel' => 'wikitext',
49  'contentformat' => 'text/x-wiki',
50  'baserevid' => 0,
51  ], $params );
52  if ( !array_key_exists( 'text', $params ) &&
53  !array_key_exists( 'stashedtexthash', $params )
54  ) {
55  $params['text'] = 'Content';
56  }
57  foreach ( $params as $key => $val ) {
58  if ( $val === null ) {
59  unset( $params[$key] );
60  }
61  }
62 
63  if ( isset( $params['text'] ) ) {
64  $expectedText = $params['text'];
65  } elseif ( isset( $params['stashedtexthash'] ) ) {
66  $expectedText = $this->getStashedText( $params['stashedtexthash'] );
67  }
68  if ( isset( $expectedText ) ) {
69  $expectedText = rtrim( str_replace( "\r\n", "\n", $expectedText ) );
70  $expectedHash = sha1( $expectedText );
71  $origText = $this->getStashedText( $expectedHash );
72  }
73 
75 
76  $this->assertSame( $expectedResult, $res[0]['stashedit']['status'] );
77  $this->assertCount( $expectedResult === 'stashed' ? 2 : 1, $res[0]['stashedit'] );
78 
79  if ( $expectedResult === 'stashed' ) {
80  $hash = $res[0]['stashedit']['texthash'];
81 
82  $this->assertSame( $expectedText, $this->getStashedText( $hash ) );
83 
84  $this->assertSame( $expectedHash, $hash );
85 
86  if ( isset( $params['stashedtexthash'] ) ) {
87  $this->assertSame( $params['stashedtexthash'], $expectedHash, 'Sanity' );
88  }
89  } else {
90  $this->assertSame( $origText, $this->getStashedText( $expectedHash ) );
91  }
92 
93  $this->assertArrayNotHasKey( 'warnings', $res[0] );
94 
95  return $res;
96  }
97 
104  protected function getStashedText( $hash ) {
105  return MediaWikiServices::getInstance()->getPageEditStash()->fetchInputText( $hash );
106  }
107 
116  protected function getStashKey( $title = __CLASS__, $text = 'Content', User $user = null ) {
117  $titleObj = Title::newFromText( $title );
118  $content = new WikitextContent( $text );
119  if ( !$user ) {
120  $user = $this->getTestSysop()->getUser();
121  }
122  $editStash = TestingAccessWrapper::newFromObject(
123  MediaWikiServices::getInstance()->getPageEditStash() );
124 
125  return $editStash->getStashKey( $titleObj, $editStash->getContentHash( $content ), $user );
126  }
127 
128  public function testBasicEdit() {
129  $this->doStash();
130  }
131 
132  public function testBot() {
133  // @todo This restriction seems arbitrary, is there any good reason to keep it?
134  $this->setExpectedApiException( 'apierror-botsnotsupported' );
135 
136  $this->doStash( [], $this->getTestUser( [ 'bot' ] )->getUser() );
137  }
138 
139  public function testUnrecognizedFormat() {
141  [ 'apierror-badformat-generic', 'application/json', 'wikitext' ] );
142 
143  $this->doStash( [ 'contentformat' => 'application/json' ] );
144  }
145 
147  $this->setExpectedApiException( [
148  'apierror-missingparam-one-of',
149  Message::listParam( [ '<var>stashedtexthash</var>', '<var>text</var>' ] ),
150  2
151  ] );
152  $this->doStash( [ 'text' => null ] );
153  }
154 
155  public function testStashedTextHash() {
156  $res = $this->doStash();
157 
158  $this->doStash( [ 'stashedtexthash' => $res[0]['stashedit']['texthash'] ] );
159  }
160 
161  public function testMalformedStashedTextHash() {
162  $this->setExpectedApiException( 'apierror-stashedit-missingtext' );
163  $this->doStash( [ 'stashedtexthash' => 'abc' ] );
164  }
165 
166  public function testMissingStashedTextHash() {
167  $this->setExpectedApiException( 'apierror-stashedit-missingtext' );
168  $this->doStash( [ 'stashedtexthash' => str_repeat( '0', 40 ) ] );
169  }
170 
171  public function testHashNormalization() {
172  $res1 = $this->doStash( [ 'text' => "a\r\nb\rc\nd \t\n\r" ] );
173  $res2 = $this->doStash( [ 'text' => "a\nb\rc\nd" ] );
174 
175  $this->assertSame( $res1[0]['stashedit']['texthash'], $res2[0]['stashedit']['texthash'] );
176  $this->assertSame( "a\nb\rc\nd",
177  $this->getStashedText( $res1[0]['stashedit']['texthash'] ) );
178  }
179 
180  public function testNonexistentBaseRevId() {
181  $this->setExpectedApiException( [ 'apierror-nosuchrevid', pow( 2, 31 ) - 1 ] );
182 
183  $name = ucfirst( __FUNCTION__ );
184  $this->editPage( $name, '' );
185  $this->doStash( [ 'title' => $name, 'baserevid' => pow( 2, 31 ) - 1 ] );
186  }
187 
188  public function testPageWithNoRevisions() {
189  $name = ucfirst( __FUNCTION__ );
190  $rev = $this->editPage( $name, '' )->value['revision'];
191 
192  $this->setExpectedApiException( [ 'apierror-missingrev-pageid', $rev->getPage() ] );
193 
194  // Corrupt the database. @todo Does the API really need to fail gracefully for this case?
195  $dbw = wfGetDB( DB_MASTER );
196  $dbw->update(
197  'page',
198  [ 'page_latest' => 0 ],
199  [ 'page_id' => $rev->getPage() ],
200  __METHOD__
201  );
202 
203  $this->doStash( [ 'title' => $name, 'baserevid' => $rev->getId() ] );
204  }
205 
206  public function testExistingPage() {
207  $name = ucfirst( __FUNCTION__ );
208  $rev = $this->editPage( $name, '' )->value['revision'];
209 
210  $this->doStash( [ 'title' => $name, 'baserevid' => $rev->getId() ] );
211  }
212 
213  public function testInterveningEdit() {
214  $name = ucfirst( __FUNCTION__ );
215  $oldRev = $this->editPage( $name, "A\n\nB" )->value['revision'];
216  $this->editPage( $name, "A\n\nC" );
217 
218  $this->doStash( [
219  'title' => $name,
220  'baserevid' => $oldRev->getId(),
221  'text' => "D\n\nB",
222  ] );
223  }
224 
225  public function testEditConflict() {
226  $name = ucfirst( __FUNCTION__ );
227  $oldRev = $this->editPage( $name, 'A' )->value['revision'];
228  $this->editPage( $name, 'B' );
229 
230  $this->doStash( [
231  'title' => $name,
232  'baserevid' => $oldRev->getId(),
233  'text' => 'C',
234  ], null, 'editconflict' );
235  }
236 
237  public function testDeletedRevision() {
238  $name = ucfirst( __FUNCTION__ );
239  $oldRev = $this->editPage( $name, 'A' )->value['revision'];
240  $this->editPage( $name, 'B' );
241 
242  $this->setExpectedApiException( [ 'apierror-missingcontent-pageid', $oldRev->getPage() ] );
243 
244  $this->revisionDelete( $oldRev );
245 
246  $this->doStash( [
247  'title' => $name,
248  'baserevid' => $oldRev->getId(),
249  'text' => 'C',
250  ] );
251  }
252 
253  public function testDeletedRevisionSection() {
254  $name = ucfirst( __FUNCTION__ );
255  $oldRev = $this->editPage( $name, 'A' )->value['revision'];
256  $this->editPage( $name, 'B' );
257 
258  $this->setExpectedApiException( 'apierror-sectionreplacefailed' );
259 
260  $this->revisionDelete( $oldRev );
261 
262  $this->doStash( [
263  'title' => $name,
264  'baserevid' => $oldRev->getId(),
265  'text' => 'C',
266  'section' => '1',
267  ] );
268  }
269 
270  public function testPingLimiter() {
271  $this->mergeMwGlobalArrayValue( 'wgRateLimits',
272  [ 'stashedit' => [ '&can-bypass' => false, 'user' => [ 1, 60 ] ] ] );
273 
274  $this->doStash( [ 'text' => 'A' ] );
275 
276  $this->doStash( [ 'text' => 'B' ], null, 'ratelimited' );
277  }
278 
287  protected function doCheckCache( User $user, $text = 'Content' ) {
288  return MediaWikiServices::getInstance()->getPageEditStash()->checkCache(
289  Title::newFromText( __CLASS__ ),
290  new WikitextContent( $text ),
291  $user
292  );
293  }
294 
295  public function testCheckCache() {
296  $user = $this->getMutableTestUser()->getUser();
297 
298  $this->doStash( [], $user );
299 
300  $this->assertInstanceOf( stdClass::class, $this->doCheckCache( $user ) );
301 
302  // Another user doesn't see the cache
303  $this->assertFalse(
304  $this->doCheckCache( $this->getTestUser()->getUser() ),
305  'Cache is user-specific'
306  );
307 
308  // Nor does the original one if they become a bot
309  $user->addGroup( 'bot' );
310  $this->assertFalse(
311  $this->doCheckCache( $user ),
312  "We assume bots don't have cache entries"
313  );
314 
315  // But other groups are okay
316  $user->removeGroup( 'bot' );
317  $user->addGroup( 'sysop' );
318  $this->assertInstanceOf( stdClass::class, $this->doCheckCache( $user ) );
319  }
320 
321  public function testCheckCacheAnon() {
322  $user = User::newFromName( '174.5.4.6', false );
323 
324  $this->doStash( [], $user );
325 
326  $this->assertInstanceOf( stdClass::class, $this->doCheckCache( $user ) );
327  }
328 
336  protected function doStashOld(
337  User $user, $text = 'Content', $howOld = PageEditStash::PRESUME_FRESH_TTL_SEC
338  ) {
339  $this->doStash( [ 'text' => $text ], $user );
340 
341  // Monkey with the cache to make the edit look old. @todo Is there a less fragile way to
342  // fake the time?
343  $key = $this->getStashKey( __CLASS__, $text, $user );
344 
345  $editStash = TestingAccessWrapper::newFromObject(
346  MediaWikiServices::getInstance()->getPageEditStash() );
347  $cache = $editStash->cache;
348 
349  $editInfo = $cache->get( $key );
350  $editInfo->output->setCacheTime( wfTimestamp( TS_MW,
351  wfTimestamp( TS_UNIX, $editInfo->output->getCacheTime() ) - $howOld - 1 ) );
352 
353  $cache->set( $key, $editInfo );
354  }
355 
356  public function testCheckCacheOldNoEdits() {
357  $user = $this->getTestSysop()->getUser();
358 
359  $this->doStashOld( $user );
360 
361  // Should still be good, because no intervening edits
362  $this->assertInstanceOf( stdClass::class, $this->doCheckCache( $user ) );
363  }
364 
365  public function testCheckCacheOldNoEditsAnon() {
366  // Specify a made-up IP address to make sure no edits are lying around
367  $user = User::newFromName( '172.0.2.77', false );
368 
369  $this->doStashOld( $user );
370 
371  // Should still be good, because no intervening edits
372  $this->assertInstanceOf( stdClass::class, $this->doCheckCache( $user ) );
373  }
374 
375  public function testCheckCacheInterveningEdits() {
376  $user = $this->getTestSysop()->getUser();
377 
378  $this->doStashOld( $user );
379 
380  // Now let's also increment our editcount
381  $this->editPage( ucfirst( __FUNCTION__ ), '' );
382 
383  $user->clearInstanceCache();
384  $this->assertFalse( $this->doCheckCache( $user ),
385  "Cache should be invalidated when it's old and the user has an intervening edit" );
386  }
387 
393  public function testSignatureTtl( $text, $ttl ) {
394  $this->doStash( [ 'text' => $text ] );
395 
396  $editStash = TestingAccessWrapper::newFromObject(
397  MediaWikiServices::getInstance()->getPageEditStash() );
398  $cache = $editStash->cache;
399  $key = $this->getStashKey( __CLASS__, $text );
400 
401  $wrapper = TestingAccessWrapper::newFromObject( $cache );
402 
403  $this->assertEquals( $ttl, $wrapper->bag[$key][HashBagOStuff::KEY_EXP] - time(), '', 1 );
404  }
405 
406  public function signatureProvider() {
407  return [
408  '~~~' => [ '~~~', PageEditStash::MAX_SIGNATURE_TTL ],
409  '~~~~' => [ '~~~~', PageEditStash::MAX_SIGNATURE_TTL ],
410  '~~~~~' => [ '~~~~~', PageEditStash::MAX_SIGNATURE_TTL ],
411  ];
412  }
413 
414  public function testIsInternal() {
415  $res = $this->doApiRequest( [
416  'action' => 'paraminfo',
417  'modules' => 'stashedit',
418  ] );
419 
420  $this->assertCount( 1, $res[0]['paraminfo']['modules'] );
421  $this->assertSame( true, $res[0]['paraminfo']['modules'][0]['internal'] );
422  }
423 
424  public function testBusy() {
425  // @todo This doesn't work because both lock acquisitions are in the same MySQL session, so
426  // they don't conflict. How do I open a different session?
427  $this->markTestSkipped();
428 
429  $key = $this->getStashKey();
430  $this->db->lock( $key, __METHOD__, 0 );
431  try {
432  $this->doStash( [], null, 'busy' );
433  } finally {
434  $this->db->unlock( $key, __METHOD__ );
435  }
436  }
437 }
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
editPage( $pageName, $text, $summary='', $defaultNs=NS_MAIN, User $user=null)
Edits or creates a page/revision.
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
Class for managing stashed edits used by the page updater classes.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
doApiRequest(array $params, array $session=null, $appendModule=false, User $user=null, $tokenType=null)
Does the API request and returns the result.
Definition: ApiTestCase.php:62
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
mergeMwGlobalArrayValue( $name, $values)
Merges the given values into a MW global array variable.
const DB_MASTER
Definition: defines.php:26
doApiRequestWithToken(array $params, array $session=null, User $user=null, $tokenType='auto')
Convenience function to access the token parameter of doApiRequest() more succinctly.
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
static getTestSysop()
Convenience method for getting an immutable admin test user.
revisionDelete( $rev, array $value=[Revision::DELETED_TEXT=> 1], $comment='')
Revision-deletes a revision.
static getMutableTestUser( $groups=[])
Convenience method for getting a mutable test user.
$res
Definition: database.txt:21
doCheckCache(User $user, $text='Content')
Shortcut for calling PageStashEdit::checkCache() without having to create Titles and Contents in ever...
testSignatureTtl( $text, $ttl)
signatureProvider
$cache
Definition: mcc.php:33
$params
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:780
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:925
ApiStashEdit \MediaWiki\Storage\PageEditStash API medium Database.
doStash(array $params=[], User $user=null, $expectedResult='stashed')
Make a stashedit API call with suitable default parameters.
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition: hooks.txt:1766
setMwGlobals( $pairs, $value=null)
Sets a global, maintaining a stashed version of the previous global to be restored in tearDown...
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
getStashKey( $title=__CLASS__, $text='Content', User $user=null)
Return a key that can be passed to the cache to obtain a stashed edit object.
setService( $name, $object)
Sets a service, maintaining a stashed version of the previous service to be restored in tearDown...
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
doStashOld(User $user, $text='Content', $howOld=PageEditStash::PRESUME_FRESH_TTL_SEC)
Stash an edit some time in the past, for testing expiry and freshness logic.
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:271
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:594
$content
Definition: pageupdater.txt:72
setExpectedApiException( $msg, $code=null, array $data=null, $httpCode=0)
Expect an ApiUsageException to be thrown with the given parameters, which are the same as ApiUsageExc...
static getTestUser( $groups=[])
Convenience method for getting an immutable test user.
getStashedText( $hash)
Return the text stashed for $hash.
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1473
static listParam(array $list, $type='text')
Definition: Message.php:1126
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:319