Improve namespace handling in tests
[lhc/web/wiklou.git] / tests / phpunit / includes / EditPageTest.php
1 <?php
2
3 /**
4 * @group Editing
5 *
6 * @group Database
7 * ^--- tell jenkins this test needs the database
8 *
9 * @group medium
10 * ^--- tell phpunit that these test cases may take longer than 2 seconds.
11 */
12 class EditPageTest extends MediaWikiLangTestCase {
13
14 protected function setUp() {
15 global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
16
17 parent::setUp();
18
19 $this->setMwGlobals( [
20 'wgExtraNamespaces' => $wgExtraNamespaces,
21 'wgNamespaceContentModels' => $wgNamespaceContentModels,
22 'wgContentHandlers' => $wgContentHandlers,
23 'wgContLang' => $wgContLang,
24 ] );
25
26 $wgExtraNamespaces[12312] = 'Dummy';
27 $wgExtraNamespaces[12313] = 'Dummy_talk';
28
29 $wgNamespaceContentModels[12312] = "testing";
30 $wgContentHandlers["testing"] = 'DummyContentHandlerForTesting';
31
32 MWNamespace::clearCaches();
33 $wgContLang->resetNamespaces(); # reset namespace cache
34 }
35
36 protected function tearDown() {
37 global $wgContLang;
38
39 MWNamespace::clearCaches();
40 $wgContLang->resetNamespaces(); # reset namespace cache
41 parent::tearDown();
42 }
43
44 /**
45 * @dataProvider provideExtractSectionTitle
46 * @covers EditPage::extractSectionTitle
47 */
48 public function testExtractSectionTitle( $section, $title ) {
49 $extracted = EditPage::extractSectionTitle( $section );
50 $this->assertEquals( $title, $extracted );
51 }
52
53 public static function provideExtractSectionTitle() {
54 return [
55 [
56 "== Test ==\n\nJust a test section.",
57 "Test"
58 ],
59 [
60 "An initial section, no header.",
61 false
62 ],
63 [
64 "An initial section with a fake heder (T34617)\n\n== Test == ??\nwtf",
65 false
66 ],
67 [
68 "== Section ==\nfollowed by a fake == Non-section == ??\nnoooo",
69 "Section"
70 ],
71 [
72 "== Section== \t\r\n followed by whitespace (T37051)",
73 'Section',
74 ],
75 ];
76 }
77
78 protected function forceRevisionDate( WikiPage $page, $timestamp ) {
79 $dbw = wfGetDB( DB_MASTER );
80
81 $dbw->update( 'revision',
82 [ 'rev_timestamp' => $dbw->timestamp( $timestamp ) ],
83 [ 'rev_id' => $page->getLatest() ] );
84
85 $page->clear();
86 }
87
88 /**
89 * User input text is passed to rtrim() by edit page. This is a simple
90 * wrapper around assertEquals() which calls rrtrim() to normalize the
91 * expected and actual texts.
92 * @param string $expected
93 * @param string $actual
94 * @param string $msg
95 */
96 protected function assertEditedTextEquals( $expected, $actual, $msg = '' ) {
97 $this->assertEquals( rtrim( $expected ), rtrim( $actual ), $msg );
98 }
99
100 /**
101 * Performs an edit and checks the result.
102 *
103 * @param string|Title $title The title of the page to edit
104 * @param string|null $baseText Some text to create the page with before attempting the edit.
105 * @param User|string|null $user The user to perform the edit as.
106 * @param array $edit An array of request parameters used to define the edit to perform.
107 * Some well known fields are:
108 * * wpTextbox1: the text to submit
109 * * wpSummary: the edit summary
110 * * wpEditToken: the edit token (will be inserted if not provided)
111 * * wpEdittime: timestamp of the edit's base revision (will be inserted
112 * if not provided)
113 * * wpStarttime: timestamp when the edit started (will be inserted if not provided)
114 * * wpSectionTitle: the section to edit
115 * * wpMinorEdit: mark as minor edit
116 * * wpWatchthis: whether to watch the page
117 * @param int|null $expectedCode The expected result code (EditPage::AS_XXX constants).
118 * Set to null to skip the check.
119 * @param string|null $expectedText The text expected to be on the page after the edit.
120 * Set to null to skip the check.
121 * @param string|null $message An optional message to show along with any error message.
122 *
123 * @return WikiPage The page that was just edited, useful for getting the edit's rev_id, etc.
124 */
125 protected function assertEdit( $title, $baseText, $user = null, array $edit,
126 $expectedCode = null, $expectedText = null, $message = null
127 ) {
128 if ( is_string( $title ) ) {
129 $ns = $this->getDefaultWikitextNS();
130 $title = Title::newFromText( $title, $ns );
131 }
132 $this->assertNotNull( $title );
133
134 if ( is_string( $user ) ) {
135 $user = User::newFromName( $user );
136
137 if ( $user->getId() === 0 ) {
138 $user->addToDatabase();
139 }
140 }
141
142 $page = WikiPage::factory( $title );
143
144 if ( $baseText !== null ) {
145 $content = ContentHandler::makeContent( $baseText, $title );
146 $page->doEditContent( $content, "base text for test" );
147 $this->forceRevisionDate( $page, '20120101000000' );
148
149 // sanity check
150 $page->clear();
151 $currentText = ContentHandler::getContentText( $page->getContent() );
152
153 # EditPage rtrim() the user input, so we alter our expected text
154 # to reflect that.
155 $this->assertEditedTextEquals( $baseText, $currentText );
156 }
157
158 if ( $user == null ) {
159 $user = $GLOBALS['wgUser'];
160 } else {
161 $this->setMwGlobals( 'wgUser', $user );
162 }
163
164 if ( !isset( $edit['wpEditToken'] ) ) {
165 $edit['wpEditToken'] = $user->getEditToken();
166 }
167
168 if ( !isset( $edit['wpEdittime'] ) ) {
169 $edit['wpEdittime'] = $page->exists() ? $page->getTimestamp() : '';
170 }
171
172 if ( !isset( $edit['wpStarttime'] ) ) {
173 $edit['wpStarttime'] = wfTimestampNow();
174 }
175
176 $req = new FauxRequest( $edit, true ); // session ??
177
178 $article = new Article( $title );
179 $article->getContext()->setTitle( $title );
180 $ep = new EditPage( $article );
181 $ep->setContextTitle( $title );
182 $ep->importFormData( $req );
183
184 $bot = isset( $edit['bot'] ) ? (bool)$edit['bot'] : false;
185
186 // this is where the edit happens!
187 // Note: don't want to use EditPage::AttemptSave, because it messes with $wgOut
188 // and throws exceptions like PermissionsError
189 $status = $ep->internalAttemptSave( $result, $bot );
190
191 if ( $expectedCode !== null ) {
192 // check edit code
193 $this->assertEquals( $expectedCode, $status->value,
194 "Expected result code mismatch. $message" );
195 }
196
197 $page = WikiPage::factory( $title );
198
199 if ( $expectedText !== null ) {
200 // check resulting page text
201 $content = $page->getContent();
202 $text = ContentHandler::getContentText( $content );
203
204 # EditPage rtrim() the user input, so we alter our expected text
205 # to reflect that.
206 $this->assertEditedTextEquals( $expectedText, $text,
207 "Expected article text mismatch. $message" );
208 }
209
210 return $page;
211 }
212
213 public static function provideCreatePages() {
214 return [
215 [ 'expected article being created',
216 'EditPageTest_testCreatePage',
217 null,
218 'Hello World!',
219 EditPage::AS_SUCCESS_NEW_ARTICLE,
220 'Hello World!'
221 ],
222 [ 'expected article not being created if empty',
223 'EditPageTest_testCreatePage',
224 null,
225 '',
226 EditPage::AS_BLANK_ARTICLE,
227 null
228 ],
229 [ 'expected MediaWiki: page being created',
230 'MediaWiki:January',
231 'UTSysop',
232 'Not January',
233 EditPage::AS_SUCCESS_NEW_ARTICLE,
234 'Not January'
235 ],
236 [ 'expected not-registered MediaWiki: page not being created if empty',
237 'MediaWiki:EditPageTest_testCreatePage',
238 'UTSysop',
239 '',
240 EditPage::AS_BLANK_ARTICLE,
241 null
242 ],
243 [ 'expected registered MediaWiki: page being created even if empty',
244 'MediaWiki:January',
245 'UTSysop',
246 '',
247 EditPage::AS_SUCCESS_NEW_ARTICLE,
248 ''
249 ],
250 [ 'expected registered MediaWiki: page whose default content is empty'
251 . ' not being created if empty',
252 'MediaWiki:Ipb-default-expiry',
253 'UTSysop',
254 '',
255 EditPage::AS_BLANK_ARTICLE,
256 ''
257 ],
258 [ 'expected MediaWiki: page not being created if text equals default message',
259 'MediaWiki:January',
260 'UTSysop',
261 'January',
262 EditPage::AS_BLANK_ARTICLE,
263 null
264 ],
265 [ 'expected empty article being created',
266 'EditPageTest_testCreatePage',
267 null,
268 '',
269 EditPage::AS_SUCCESS_NEW_ARTICLE,
270 '',
271 true
272 ],
273 ];
274 }
275
276 /**
277 * @dataProvider provideCreatePages
278 * @covers EditPage
279 */
280 public function testCreatePage(
281 $desc, $pageTitle, $user, $editText, $expectedCode, $expectedText, $ignoreBlank = false
282 ) {
283 $checkId = null;
284
285 $this->setMwGlobals( 'wgHooks', [
286 'PageContentInsertComplete' => [ function (
287 WikiPage &$page, User &$user, Content $content,
288 $summary, $minor, $u1, $u2, &$flags, Revision $revision
289 ) {
290 // types/refs checked
291 } ],
292 'PageContentSaveComplete' => [ function (
293 WikiPage &$page, User &$user, Content $content,
294 $summary, $minor, $u1, $u2, &$flags, Revision $revision,
295 Status &$status, $baseRevId
296 ) use ( &$checkId ) {
297 $checkId = $status->value['revision']->getId();
298 // types/refs checked
299 } ],
300 ] );
301
302 $edit = [ 'wpTextbox1' => $editText ];
303 if ( $ignoreBlank ) {
304 $edit['wpIgnoreBlankArticle'] = 1;
305 }
306
307 $page = $this->assertEdit( $pageTitle, null, $user, $edit, $expectedCode, $expectedText, $desc );
308
309 if ( $expectedCode != EditPage::AS_BLANK_ARTICLE ) {
310 $latest = $page->getLatest();
311 $page->doDeleteArticleReal( $pageTitle );
312
313 $this->assertGreaterThan( 0, $latest, "Page revision ID updated in object" );
314 $this->assertEquals( $latest, $checkId, "Revision in Status for hook" );
315 }
316 }
317
318 /**
319 * @dataProvider provideCreatePages
320 * @covers EditPage
321 */
322 public function testCreatePageTrx(
323 $desc, $pageTitle, $user, $editText, $expectedCode, $expectedText, $ignoreBlank = false
324 ) {
325 $checkIds = [];
326 $this->setMwGlobals( 'wgHooks', [
327 'PageContentInsertComplete' => [ function (
328 WikiPage &$page, User &$user, Content $content,
329 $summary, $minor, $u1, $u2, &$flags, Revision $revision
330 ) {
331 // types/refs checked
332 } ],
333 'PageContentSaveComplete' => [ function (
334 WikiPage &$page, User &$user, Content $content,
335 $summary, $minor, $u1, $u2, &$flags, Revision $revision,
336 Status &$status, $baseRevId
337 ) use ( &$checkIds ) {
338 $checkIds[] = $status->value['revision']->getId();
339 // types/refs checked
340 } ],
341 ] );
342
343 wfGetDB( DB_MASTER )->begin( __METHOD__ );
344
345 $edit = [ 'wpTextbox1' => $editText ];
346 if ( $ignoreBlank ) {
347 $edit['wpIgnoreBlankArticle'] = 1;
348 }
349
350 $page = $this->assertEdit(
351 $pageTitle, null, $user, $edit, $expectedCode, $expectedText, $desc );
352
353 $pageTitle2 = (string)$pageTitle . '/x';
354 $page2 = $this->assertEdit(
355 $pageTitle2, null, $user, $edit, $expectedCode, $expectedText, $desc );
356
357 wfGetDB( DB_MASTER )->commit( __METHOD__ );
358
359 $this->assertEquals( 0, DeferredUpdates::pendingUpdatesCount(), 'No deferred updates' );
360
361 if ( $expectedCode != EditPage::AS_BLANK_ARTICLE ) {
362 $latest = $page->getLatest();
363 $page->doDeleteArticleReal( $pageTitle );
364
365 $this->assertGreaterThan( 0, $latest, "Page #1 revision ID updated in object" );
366 $this->assertEquals( $latest, $checkIds[0], "Revision #1 in Status for hook" );
367
368 $latest2 = $page2->getLatest();
369 $page2->doDeleteArticleReal( $pageTitle2 );
370
371 $this->assertGreaterThan( 0, $latest2, "Page #2 revision ID updated in object" );
372 $this->assertEquals( $latest2, $checkIds[1], "Revision #2 in Status for hook" );
373 }
374 }
375
376 public function testUpdatePage() {
377 $checkIds = [];
378
379 $this->setMwGlobals( 'wgHooks', [
380 'PageContentInsertComplete' => [ function (
381 WikiPage &$page, User &$user, Content $content,
382 $summary, $minor, $u1, $u2, &$flags, Revision $revision
383 ) {
384 // types/refs checked
385 } ],
386 'PageContentSaveComplete' => [ function (
387 WikiPage &$page, User &$user, Content $content,
388 $summary, $minor, $u1, $u2, &$flags, Revision $revision,
389 Status &$status, $baseRevId
390 ) use ( &$checkIds ) {
391 $checkIds[] = $status->value['revision']->getId();
392 // types/refs checked
393 } ],
394 ] );
395
396 $text = "one";
397 $edit = [
398 'wpTextbox1' => $text,
399 'wpSummary' => 'first update',
400 ];
401
402 $page = $this->assertEdit( 'EditPageTest_testUpdatePage', "zero", null, $edit,
403 EditPage::AS_SUCCESS_UPDATE, $text,
404 "expected successfull update with given text" );
405 $this->assertGreaterThan( 0, $checkIds[0], "First event rev ID set" );
406
407 $this->forceRevisionDate( $page, '20120101000000' );
408
409 $text = "two";
410 $edit = [
411 'wpTextbox1' => $text,
412 'wpSummary' => 'second update',
413 ];
414
415 $this->assertEdit( 'EditPageTest_testUpdatePage', null, null, $edit,
416 EditPage::AS_SUCCESS_UPDATE, $text,
417 "expected successfull update with given text" );
418 $this->assertGreaterThan( 0, $checkIds[1], "Second edit hook rev ID set" );
419 $this->assertGreaterThan( $checkIds[0], $checkIds[1], "Second event rev ID is higher" );
420 }
421
422 public function testUpdatePageTrx() {
423 $text = "one";
424 $edit = [
425 'wpTextbox1' => $text,
426 'wpSummary' => 'first update',
427 ];
428
429 $page = $this->assertEdit( 'EditPageTest_testTrxUpdatePage', "zero", null, $edit,
430 EditPage::AS_SUCCESS_UPDATE, $text,
431 "expected successfull update with given text" );
432
433 $this->forceRevisionDate( $page, '20120101000000' );
434
435 $checkIds = [];
436 $this->setMwGlobals( 'wgHooks', [
437 'PageContentSaveComplete' => [ function (
438 WikiPage &$page, User &$user, Content $content,
439 $summary, $minor, $u1, $u2, &$flags, Revision $revision,
440 Status &$status, $baseRevId
441 ) use ( &$checkIds ) {
442 $checkIds[] = $status->value['revision']->getId();
443 // types/refs checked
444 } ],
445 ] );
446
447 wfGetDB( DB_MASTER )->begin( __METHOD__ );
448
449 $text = "two";
450 $edit = [
451 'wpTextbox1' => $text,
452 'wpSummary' => 'second update',
453 ];
454
455 $this->assertEdit( 'EditPageTest_testTrxUpdatePage', null, null, $edit,
456 EditPage::AS_SUCCESS_UPDATE, $text,
457 "expected successfull update with given text" );
458
459 $text = "three";
460 $edit = [
461 'wpTextbox1' => $text,
462 'wpSummary' => 'third update',
463 ];
464
465 $this->assertEdit( 'EditPageTest_testTrxUpdatePage', null, null, $edit,
466 EditPage::AS_SUCCESS_UPDATE, $text,
467 "expected successfull update with given text" );
468
469 wfGetDB( DB_MASTER )->commit( __METHOD__ );
470
471 $this->assertGreaterThan( 0, $checkIds[0], "First event rev ID set" );
472 $this->assertGreaterThan( 0, $checkIds[1], "Second edit hook rev ID set" );
473 $this->assertGreaterThan( $checkIds[0], $checkIds[1], "Second event rev ID is higher" );
474 }
475
476 public static function provideSectionEdit() {
477 $text = 'Intro
478
479 == one ==
480 first section.
481
482 == two ==
483 second section.
484 ';
485
486 $sectionOne = '== one ==
487 hello
488 ';
489
490 $newSection = '== new section ==
491
492 hello
493 ';
494
495 $textWithNewSectionOne = preg_replace(
496 '/== one ==.*== two ==/ms',
497 "$sectionOne\n== two ==", $text
498 );
499
500 $textWithNewSectionAdded = "$text\n$newSection";
501
502 return [
503 [ # 0
504 $text,
505 '',
506 'hello',
507 'replace all',
508 'hello'
509 ],
510
511 [ # 1
512 $text,
513 '1',
514 $sectionOne,
515 'replace first section',
516 $textWithNewSectionOne,
517 ],
518
519 [ # 2
520 $text,
521 'new',
522 'hello',
523 'new section',
524 $textWithNewSectionAdded,
525 ],
526 ];
527 }
528
529 /**
530 * @dataProvider provideSectionEdit
531 * @covers EditPage
532 */
533 public function testSectionEdit( $base, $section, $text, $summary, $expected ) {
534 $edit = [
535 'wpTextbox1' => $text,
536 'wpSummary' => $summary,
537 'wpSection' => $section,
538 ];
539
540 $this->assertEdit( 'EditPageTest_testSectionEdit', $base, null, $edit,
541 EditPage::AS_SUCCESS_UPDATE, $expected,
542 "expected successfull update of section" );
543 }
544
545 public static function provideAutoMerge() {
546 $tests = [];
547
548 $tests[] = [ # 0: plain conflict
549 "Elmo", # base edit user
550 "one\n\ntwo\n\nthree\n",
551 [ # adam's edit
552 'wpStarttime' => 1,
553 'wpTextbox1' => "ONE\n\ntwo\n\nthree\n",
554 ],
555 [ # berta's edit
556 'wpStarttime' => 2,
557 'wpTextbox1' => "(one)\n\ntwo\n\nthree\n",
558 ],
559 EditPage::AS_CONFLICT_DETECTED, # expected code
560 "ONE\n\ntwo\n\nthree\n", # expected text
561 'expected edit conflict', # message
562 ];
563
564 $tests[] = [ # 1: successful merge
565 "Elmo", # base edit user
566 "one\n\ntwo\n\nthree\n",
567 [ # adam's edit
568 'wpStarttime' => 1,
569 'wpTextbox1' => "ONE\n\ntwo\n\nthree\n",
570 ],
571 [ # berta's edit
572 'wpStarttime' => 2,
573 'wpTextbox1' => "one\n\ntwo\n\nTHREE\n",
574 ],
575 EditPage::AS_SUCCESS_UPDATE, # expected code
576 "ONE\n\ntwo\n\nTHREE\n", # expected text
577 'expected automatic merge', # message
578 ];
579
580 $text = "Intro\n\n";
581 $text .= "== first section ==\n\n";
582 $text .= "one\n\ntwo\n\nthree\n\n";
583 $text .= "== second section ==\n\n";
584 $text .= "four\n\nfive\n\nsix\n\n";
585
586 // extract the first section.
587 $section = preg_replace( '/.*(== first section ==.*)== second section ==.*/sm', '$1', $text );
588
589 // generate expected text after merge
590 $expected = str_replace( 'one', 'ONE', str_replace( 'three', 'THREE', $text ) );
591
592 $tests[] = [ # 2: merge in section
593 "Elmo", # base edit user
594 $text,
595 [ # adam's edit
596 'wpStarttime' => 1,
597 'wpTextbox1' => str_replace( 'one', 'ONE', $section ),
598 'wpSection' => '1'
599 ],
600 [ # berta's edit
601 'wpStarttime' => 2,
602 'wpTextbox1' => str_replace( 'three', 'THREE', $section ),
603 'wpSection' => '1'
604 ],
605 EditPage::AS_SUCCESS_UPDATE, # expected code
606 $expected, # expected text
607 'expected automatic section merge', # message
608 ];
609
610 // see whether it makes a difference who did the base edit
611 $testsWithAdam = array_map( function ( $test ) {
612 $test[0] = 'Adam'; // change base edit user
613 return $test;
614 }, $tests );
615
616 $testsWithBerta = array_map( function ( $test ) {
617 $test[0] = 'Berta'; // change base edit user
618 return $test;
619 }, $tests );
620
621 return array_merge( $tests, $testsWithAdam, $testsWithBerta );
622 }
623
624 /**
625 * @dataProvider provideAutoMerge
626 * @covers EditPage
627 */
628 public function testAutoMerge( $baseUser, $text, $adamsEdit, $bertasEdit,
629 $expectedCode, $expectedText, $message = null
630 ) {
631 $this->markTestSkippedIfNoDiff3();
632
633 // create page
634 $ns = $this->getDefaultWikitextNS();
635 $title = Title::newFromText( 'EditPageTest_testAutoMerge', $ns );
636 $page = WikiPage::factory( $title );
637
638 if ( $page->exists() ) {
639 $page->doDeleteArticle( "clean slate for testing" );
640 }
641
642 $baseEdit = [
643 'wpTextbox1' => $text,
644 ];
645
646 $page = $this->assertEdit( 'EditPageTest_testAutoMerge', null,
647 $baseUser, $baseEdit, null, null, __METHOD__ );
648
649 $this->forceRevisionDate( $page, '20120101000000' );
650
651 $edittime = $page->getTimestamp();
652
653 // start timestamps for conflict detection
654 if ( !isset( $adamsEdit['wpStarttime'] ) ) {
655 $adamsEdit['wpStarttime'] = 1;
656 }
657
658 if ( !isset( $bertasEdit['wpStarttime'] ) ) {
659 $bertasEdit['wpStarttime'] = 2;
660 }
661
662 $starttime = wfTimestampNow();
663 $adamsTime = wfTimestamp(
664 TS_MW,
665 (int)wfTimestamp( TS_UNIX, $starttime ) + (int)$adamsEdit['wpStarttime']
666 );
667 $bertasTime = wfTimestamp(
668 TS_MW,
669 (int)wfTimestamp( TS_UNIX, $starttime ) + (int)$bertasEdit['wpStarttime']
670 );
671
672 $adamsEdit['wpStarttime'] = $adamsTime;
673 $bertasEdit['wpStarttime'] = $bertasTime;
674
675 $adamsEdit['wpSummary'] = 'Adam\'s edit';
676 $bertasEdit['wpSummary'] = 'Bertas\'s edit';
677
678 $adamsEdit['wpEdittime'] = $edittime;
679 $bertasEdit['wpEdittime'] = $edittime;
680
681 // first edit
682 $this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Adam', $adamsEdit,
683 EditPage::AS_SUCCESS_UPDATE, null, "expected successfull update" );
684
685 // second edit
686 $this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Berta', $bertasEdit,
687 $expectedCode, $expectedText, $message );
688 }
689
690 /**
691 * @depends testAutoMerge
692 */
693 public function testCheckDirectEditingDisallowed_forNonTextContent() {
694 $title = Title::newFromText( 'Dummy:NonTextPageForEditPage' );
695 $page = WikiPage::factory( $title );
696
697 $article = new Article( $title );
698 $article->getContext()->setTitle( $title );
699 $ep = new EditPage( $article );
700 $ep->setContextTitle( $title );
701
702 $user = $GLOBALS['wgUser'];
703
704 $edit = [
705 'wpTextbox1' => serialize( 'non-text content' ),
706 'wpEditToken' => $user->getEditToken(),
707 'wpEdittime' => '',
708 'wpStarttime' => wfTimestampNow()
709 ];
710
711 $req = new FauxRequest( $edit, true );
712 $ep->importFormData( $req );
713
714 $this->setExpectedException(
715 'MWException',
716 'This content model is not supported: testing'
717 );
718
719 $ep->internalAttemptSave( $result, false );
720 }
721
722 }