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