From: daniel Date: Wed, 21 Aug 2019 15:51:10 +0000 (+0200) Subject: Add UnknownContentHandler. X-Git-Tag: 1.34.0-rc.0~506^2 X-Git-Url: https://git.cyclocoop.org/%7B%24www_url%7Dadmin/compta/banques/?a=commitdiff_plain;h=6906a7728cf5447d30da6c7a51add936f469978a;p=lhc%2Fweb%2Fwiklou.git Add UnknownContentHandler. UnknownContentHandler can be configued to handle models that belong to extensions that have been undeployed: $wgContentHandlers['xyzzy'] = 'UnknownContentHandler'; This way, no errors will be thrown when trying to access pages with the unsupported model. Instead, an error message is shown, and editing is prevented. This patch also improves handling of non-editable content in EditPage and in DifferenceEngine. Bug: T220608 Change-Id: Ia94521b786c0a5225a674e4dc3cb6761a723d75b --- diff --git a/autoload.php b/autoload.php index 35c9b0a7d5..eb54f7c592 100644 --- a/autoload.php +++ b/autoload.php @@ -1521,9 +1521,12 @@ $wgAutoloadLocalClasses = [ 'UncategorizedTemplatesPage' => __DIR__ . '/includes/specials/SpecialUncategorizedtemplates.php', 'Undelete' => __DIR__ . '/maintenance/undelete.php', 'UnifiedDiffFormatter' => __DIR__ . '/includes/diff/UnifiedDiffFormatter.php', + 'UnknownContent' => __DIR__ . '/includes/content/UnknownContent.php', + 'UnknownContentHandler' => __DIR__ . '/includes/content/UnknownContentHandler.php', 'UnlistedSpecialPage' => __DIR__ . '/includes/specialpage/UnlistedSpecialPage.php', 'UnprotectAction' => __DIR__ . '/includes/actions/UnprotectAction.php', 'UnregisteredLocalFile' => __DIR__ . '/includes/filerepo/file/UnregisteredLocalFile.php', + 'UnsupportedSlotDiffRenderer' => __DIR__ . '/includes/diff/UnsupportedSlotDiffRenderer.php', 'UnusedCategoriesPage' => __DIR__ . '/includes/specials/SpecialUnusedcategories.php', 'UnusedimagesPage' => __DIR__ . '/includes/specials/SpecialUnusedimages.php', 'UnusedtemplatesPage' => __DIR__ . '/includes/specials/SpecialUnusedtemplates.php', diff --git a/includes/EditPage.php b/includes/EditPage.php index d0a5080d0b..e51fc52bd7 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -689,10 +689,6 @@ class EditPage { # checking, etc. if ( $this->formtype == 'initial' || $this->firsttime ) { if ( $this->initialiseForm() === false ) { - $out = $this->context->getOutput(); - if ( $out->getRedirect() === '' ) { // mcrundo hack redirects, don't override it - $this->noSuchSectionPage(); - } return; } @@ -1145,8 +1141,26 @@ class EditPage { $content = $this->getContentObject( false ); # TODO: track content object?! if ( $content === false ) { + $out = $this->context->getOutput(); + if ( $out->getRedirect() === '' ) { // mcrundo hack redirects, don't override it + $this->noSuchSectionPage(); + } + return false; + } + + if ( !$this->isSupportedContentModel( $content->getModel() ) ) { + $modelMsg = $this->getContext()->msg( 'content-model-' . $content->getModel() ); + $modelName = $modelMsg->exists() ? $modelMsg->text() : $content->getModel(); + + $out = $this->context->getOutput(); + $out->showErrorPage( + 'modeleditnotsupported-title', + 'modeleditnotsupported-text', + $modelName + ); return false; } + $this->textbox1 = $this->toEditText( $content ); $user = $this->context->getUser(); diff --git a/includes/content/UnknownContent.php b/includes/content/UnknownContent.php new file mode 100644 index 0000000000..27199a0038 --- /dev/null +++ b/includes/content/UnknownContent.php @@ -0,0 +1,149 @@ +data = $data; + } + + /** + * @return Content $this + */ + public function copy() { + // UnknownContent is immutable, so no need to copy. + return $this; + } + + /** + * Returns an empty string. + * + * @param int $maxlength + * + * @return string + */ + public function getTextForSummary( $maxlength = 250 ) { + return ''; + } + + /** + * Returns the data size in bytes. + * + * @return int + */ + public function getSize() { + return strlen( $this->data ); + } + + /** + * Returns false. + * + * @param bool|null $hasLinks If it is known whether this content contains links, + * provide this information here, to avoid redundant parsing to find out. + * + * @return bool + */ + public function isCountable( $hasLinks = null ) { + return false; + } + + /** + * @return string data of unknown format and meaning + */ + public function getNativeData() { + return $this->getData(); + } + + /** + * @return string data of unknown format and meaning + */ + public function getData() { + return $this->data; + } + + /** + * Returns an empty string. + * + * @return string The raw text. + */ + public function getTextForSearchIndex() { + return ''; + } + + /** + * Returns false. + */ + public function getWikitextForTransclusion() { + return false; + } + + /** + * Fills the ParserOutput with an error message. + */ + protected function fillParserOutput( Title $title, $revId, + ParserOptions $options, $generateHtml, ParserOutput &$output + ) { + $msg = wfMessage( 'unsupported-content-model', [ $this->getModel() ] ); + $html = Html::rawElement( 'div', [ 'class' => 'error' ], $msg->inContentLanguage()->parse() ); + $output->setText( $html ); + } + + /** + * Returns false. + */ + public function convert( $toModel, $lossy = '' ) { + return false; + } + + protected function equalsInternal( Content $that ) { + if ( !$that instanceof UnknownContent ) { + return false; + } + + return $this->getData() == $that->getData(); + } + +} diff --git a/includes/content/UnknownContentHandler.php b/includes/content/UnknownContentHandler.php new file mode 100644 index 0000000000..1427e2b3bd --- /dev/null +++ b/includes/content/UnknownContentHandler.php @@ -0,0 +1,114 @@ +getData(); + } + + /** + * Constructs an UnknownContent instance wrapping the given data. + * + * @since 1.21 + * + * @param string $blob serialized content in an unknown format + * @param string|null $format ignored + * + * @return Content The UnknownContent object wrapping $data + */ + public function unserializeContent( $blob, $format = null ) { + return new UnknownContent( $blob, $this->getModelID() ); + } + + /** + * Creates an empty UnknownContent object. + * + * @since 1.21 + * + * @return Content A new UnknownContent object with empty text. + */ + public function makeEmptyContent() { + return $this->unserializeContent( '' ); + } + + /** + * @return false + */ + public function supportsDirectEditing() { + return false; + } + + /** + * @param IContextSource $context + * + * @return SlotDiffRenderer + */ + protected function getSlotDiffRendererInternal( IContextSource $context ) { + return new UnsupportedSlotDiffRenderer( $context ); + } +} diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php index 1d3b402076..9723d5aa57 100644 --- a/includes/diff/DifferenceEngine.php +++ b/includes/diff/DifferenceEngine.php @@ -556,8 +556,8 @@ class DifferenceEngine extends ContextSource { } } - if ( !$this->mOldRev->isDeleted( RevisionRecord::DELETED_TEXT ) && - !$this->mNewRev->isDeleted( RevisionRecord::DELETED_TEXT ) + if ( $this->userCanEdit( $this->mOldRev ) && + $this->userCanEdit( $this->mNewRev ) ) { $undoLink = Html::element( 'a', [ 'href' => $this->mNewPage->getLocalURL( [ @@ -1500,6 +1500,24 @@ class DifferenceEngine extends ContextSource { return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse(); } + /** + * @param Revision $rev + * @return bool whether the user can see and edit the revision. + */ + private function userCanEdit( Revision $rev ) { + $user = $this->getUser(); + + if ( !$rev->getContentHandler()->supportsDirectEditing() ) { + return false; + } + + if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $user ) ) { + return false; + } + + return true; + } + /** * Get a header for a specified revision. * @@ -1533,7 +1551,7 @@ class DifferenceEngine extends ContextSource { $header = Linker::linkKnown( $title, $header, [], [ 'oldid' => $rev->getId() ] ); - if ( $rev->userCan( RevisionRecord::DELETED_TEXT, $user ) ) { + if ( $this->userCanEdit( $rev ) ) { $editQuery = [ 'action' => 'edit' ]; if ( !$rev->isCurrent() ) { $editQuery['oldid'] = $rev->getId(); diff --git a/includes/diff/SlotDiffRenderer.php b/includes/diff/SlotDiffRenderer.php index 969e0ba7a3..c58502b2b7 100644 --- a/includes/diff/SlotDiffRenderer.php +++ b/includes/diff/SlotDiffRenderer.php @@ -44,7 +44,7 @@ abstract class SlotDiffRenderer { * must have the same content model that was used to obtain this diff renderer. * @param Content|null $oldContent * @param Content|null $newContent - * @return string + * @return string HTML, one or more tags. */ abstract public function getDiff( Content $oldContent = null, Content $newContent = null ); diff --git a/includes/diff/TextSlotDiffRenderer.php b/includes/diff/TextSlotDiffRenderer.php index 510465bd96..935172a11a 100644 --- a/includes/diff/TextSlotDiffRenderer.php +++ b/includes/diff/TextSlotDiffRenderer.php @@ -112,7 +112,7 @@ class TextSlotDiffRenderer extends SlotDiffRenderer { * Diff the text representations of two content objects (or just two pieces of text in general). * @param string $oldText * @param string $newText - * @return string + * @return string HTML, one or more tags. */ public function getTextDiff( $oldText, $newText ) { Assert::parameterType( 'string', $oldText, '$oldText' ); diff --git a/includes/diff/UnsupportedSlotDiffRenderer.php b/includes/diff/UnsupportedSlotDiffRenderer.php new file mode 100644 index 0000000000..db1b868238 --- /dev/null +++ b/includes/diff/UnsupportedSlotDiffRenderer.php @@ -0,0 +1,71 @@ +localizer = $localizer; + } + + /** @inheritDoc */ + public function getDiff( Content $oldContent = null, Content $newContent = null ) { + $this->normalizeContents( $oldContent, $newContent ); + + $oldModel = $oldContent->getModel(); + $newModel = $newContent->getModel(); + + if ( $oldModel !== $newModel ) { + $msg = $this->localizer->msg( 'unsupported-content-diff2', $oldModel, $newModel ); + } else { + $msg = $this->localizer->msg( 'unsupported-content-diff', $oldModel ); + } + + return Html::rawElement( + 'tr', + [], + Html::rawElement( + 'td', + [ 'colspan' => 4, 'class' => 'error' ], + $msg->parse() + ) + ); + } + +} diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 106d6a7dc6..3cb9c66628 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -759,6 +759,8 @@ "nocreate-loggedin": "You do not have permission to create new pages.", "sectioneditnotsupported-title": "Section editing not supported", "sectioneditnotsupported-text": "Section editing is not supported in this page.", + "modeleditnotsupported-title": "Editing not supported", + "modeleditnotsupported-text": "Editing is not supported for content model $1.", "permissionserrors": "Permission error", "permissionserrorstext": "You do not have permission to do that, for the following {{PLURAL:$1|reason|reasons}}:", "permissionserrorstext-withaction": "You do not have permission to $2, for the following {{PLURAL:$1|reason|reasons}}:", @@ -798,6 +800,9 @@ "content-model-json": "JSON", "content-json-empty-object": "Empty object", "content-json-empty-array": "Empty array", + "unsupported-content-model": "Warning: Content model $1 is not supported on this wiki.", + "unsupported-content-diff": "Diffs are not supported for content model $1.", + "unsupported-content-diff2": "Diffs between the content models $1 and $2 are not supported on this wiki.", "deprecated-self-close-category": "Pages using invalid self-closed HTML tags", "deprecated-self-close-category-desc": "The page contains invalid self-closed HTML tags, such as <b/> or <span/>. The behavior of these will change soon to be consistent with the HTML5 specification, so their use in wikitext is deprecated.", "duplicate-args-warning": "Warning: [[:$1]] is calling [[:$2]] with more than one value for the \"$3\" parameter. Only the last value provided will be used.", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 5fdcb7dc37..03c1da458b 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -968,6 +968,8 @@ "nocreate-loggedin": "Used as error message.\n\nSee also:\n* {{msg-mw|Nocreatetext}}", "sectioneditnotsupported-title": "Page title of special page, which presumably appears when someone tries to edit a section, and section editing is disabled. Explanation of section editing on [[meta:Help:Section_editing#Section_editing|meta]].", "sectioneditnotsupported-text": "I think this is the text of an error message, which presumably appears when someone tries to edit a section, and section editing is disabled. Explanation of section editing on [[meta:Help:Section_editing#Section_editing|meta]].", + "modeleditnotsupported-title": "Page title used on the edit page when editing is not supported for the page's content model.", + "modeleditnotsupported-text": "Error message show on the edit page when editing is not supported for the page's content model..\n\nParameters:\n* $1 - the name of the content model.", "permissionserrors": "Used as title of error message.\n\nSee also:\n* {{msg-mw|loginreqtitle}}\n{{Identical|Permission error}}", "permissionserrorstext": "This message is \"without action\" version of {{msg-mw|Permissionserrorstext-withaction}}.\n\nParameters:\n* $1 - the number of reasons that were found why ''the action'' cannot be performed", "permissionserrorstext-withaction": "This message is \"with action\" version of {{msg-mw|Permissionserrorstext}}.\n\nParameters:\n* $1 - the number of reasons that were found why the action cannot be performed\n* $2 - one of the action-* messages (for example {{msg-mw|action-edit}}) or other such messages tagged with {{tl|doc-action}} in their documentation\n\nPlease report at [[Support]] if you are unable to properly translate this message. Also see [[phab:T16246]] (now closed) for background.", @@ -1007,6 +1009,9 @@ "content-model-json": "{{optional}}\nName for the JSON content model, used when decribing what type of content a page contains.\n\nThis message is substituted in:\n*{{msg-mw|Bad-target-model}}\n*{{msg-mw|Content-not-allowed-here}}\n{{identical|JSON}}", "content-json-empty-object": "Used to represent an object with no properties on a JSON content model page.", "content-json-empty-array": "Used to represent an array with no values on a JSON content model page.", + "unsupported-content-model": "Warning shown when trying to display content with an unknown model.\n\nParameters:\n* $1 - the technical name of the content model.", + "unsupported-content-diff": "Warning shown when trying to display a diff between content with a model that does not support diffing (perhaps because it's an unknown model).\n\nParameters:\n* $1 - the technical name of the model of the content", + "unsupported-content-diff2": "Warning shown when trying to display a diff between content that uses models that do not support diffing with each other.\n\nParameters:\n* $1 - the technical name of the model of the old content\n* $2 - the technical name of the model of the new content.", "deprecated-self-close-category": "This message is used as a category name for a [[mw:Special:MyLanguage/Help:Tracking categories|tracking category]] where pages are placed automatically if they contain invalid self-closed HTML tags, such as <b/> or <span/>. The behavior of these will change soon to be consistent with the HTML5 specification, so their use in wikitext is deprecated.", "deprecated-self-close-category-desc": "Invalid self-closed HTML tag category description. Shown on [[Special:TrackingCategories]].\n\nSee also:\n* {{msg-mw|deprecated-self-close-category}}", "duplicate-args-warning": "If a page calls a template and specifies the same argument more than once, such as {{foo|bar=1|bar=2}} or {{foo|bar|1=baz}}, this warning is displayed when previewing.\n\nParameters:\n* $1 - The calling page\n* $2 - The called template\n* $3 - The name of the duplicated argument", diff --git a/tests/phpunit/MediaWikiUnitTestCase.php b/tests/phpunit/MediaWikiUnitTestCase.php index ccf3357f63..edd8195991 100644 --- a/tests/phpunit/MediaWikiUnitTestCase.php +++ b/tests/phpunit/MediaWikiUnitTestCase.php @@ -72,4 +72,34 @@ abstract class MediaWikiUnitTestCase extends TestCase { global $wgHooks; $wgHooks[$hookName] = [ $handler ]; } + + protected function getMockMessage( $text, ...$params ) { + if ( isset( $params[0] ) && is_array( $params[0] ) ) { + $params = $params[0]; + } + + $msg = $this->getMockBuilder( Message::class ) + ->disableOriginalConstructor() + ->setMethods( [] ) + ->getMock(); + + $msg->method( 'toString' )->willReturn( $text ); + $msg->method( '__toString' )->willReturn( $text ); + $msg->method( 'text' )->willReturn( $text ); + $msg->method( 'parse' )->willReturn( $text ); + $msg->method( 'plain' )->willReturn( $text ); + $msg->method( 'parseAsBlock' )->willReturn( $text ); + $msg->method( 'escaped' )->willReturn( $text ); + + $msg->method( 'title' )->willReturn( $msg ); + $msg->method( 'inLanguage' )->willReturn( $msg ); + $msg->method( 'inContentLanguage' )->willReturn( $msg ); + $msg->method( 'useDatabase' )->willReturn( $msg ); + $msg->method( 'setContext' )->willReturn( $msg ); + + $msg->method( 'exists' )->willReturn( true ); + $msg->method( 'content' )->willReturn( new MessageContent( $msg ) ); + + return $msg; + } } diff --git a/tests/phpunit/includes/content/UnknownContentHandlerTest.php b/tests/phpunit/includes/content/UnknownContentHandlerTest.php new file mode 100644 index 0000000000..bc1d3c6405 --- /dev/null +++ b/tests/phpunit/includes/content/UnknownContentHandlerTest.php @@ -0,0 +1,119 @@ +assertFalse( $handler->supportsDirectEditing(), 'direct editing supported' ); + } + + /** + * @covers UnknownContentHandler::serializeContent + */ + public function testSerializeContent() { + $handler = new UnknownContentHandler( 'horkyporky' ); + $content = new UnknownContent( 'hello world', 'horkyporky' ); + + $this->assertEquals( 'hello world', $handler->serializeContent( $content ) ); + $this->assertEquals( + 'hello world', + $handler->serializeContent( $content, 'application/horkyporky' ) + ); + } + + /** + * @covers UnknownContentHandler::unserializeContent + */ + public function testUnserializeContent() { + $handler = new UnknownContentHandler( 'horkyporky' ); + $content = $handler->unserializeContent( 'hello world' ); + $this->assertEquals( 'hello world', $content->getData() ); + + $content = $handler->unserializeContent( 'hello world', 'application/horkyporky' ); + $this->assertEquals( 'hello world', $content->getData() ); + } + + /** + * @covers UnknownContentHandler::makeEmptyContent + */ + public function testMakeEmptyContent() { + $handler = new UnknownContentHandler( 'horkyporky' ); + $content = $handler->makeEmptyContent(); + + $this->assertTrue( $content->isEmpty() ); + $this->assertEquals( '', $content->getData() ); + } + + public static function dataIsSupportedFormat() { + return [ + [ null, true ], + [ 'application/octet-stream', true ], + [ 'unknown/unknown', true ], + [ 'text/plain', false ], + [ 99887766, false ], + ]; + } + + /** + * @dataProvider dataIsSupportedFormat + * @covers UnknownContentHandler::isSupportedFormat + */ + public function testIsSupportedFormat( $format, $supported ) { + $handler = new UnknownContentHandler( 'horkyporky' ); + $this->assertEquals( $supported, $handler->isSupportedFormat( $format ) ); + } + + /** + * @covers ContentHandler::getSecondaryDataUpdates + */ + public function testGetSecondaryDataUpdates() { + $title = Title::newFromText( 'Somefile.jpg', NS_FILE ); + $content = new UnknownContent( '', 'horkyporky' ); + + /** @var SlotRenderingProvider $srp */ + $srp = $this->getMock( SlotRenderingProvider::class ); + + $handler = new UnknownContentHandler( 'horkyporky' ); + $updates = $handler->getSecondaryDataUpdates( $title, $content, SlotRecord::MAIN, $srp ); + + $this->assertEquals( [], $updates ); + } + + /** + * @covers ContentHandler::getDeletionUpdates + */ + public function testGetDeletionUpdates() { + $title = Title::newFromText( 'Somefile.jpg', NS_FILE ); + + $handler = new UnknownContentHandler( 'horkyporky' ); + $updates = $handler->getDeletionUpdates( $title, SlotRecord::MAIN ); + + $this->assertEquals( [], $updates ); + } + + /** + * @covers ContentHandler::getDeletionUpdates + */ + public function testGetSlotDiffRenderer() { + $context = new RequestContext(); + $context->setRequest( new FauxRequest() ); + + $handler = new UnknownContentHandler( 'horkyporky' ); + $slotDiffRenderer = $handler->getSlotDiffRenderer( $context ); + + $oldContent = $handler->unserializeContent( 'Foo' ); + $newContent = $handler->unserializeContent( 'Foo bar' ); + + $diff = $slotDiffRenderer->getDiff( $oldContent, $newContent ); + $this->assertNotEmpty( $diff ); + } + +} diff --git a/tests/phpunit/includes/content/UnknownContentTest.php b/tests/phpunit/includes/content/UnknownContentTest.php new file mode 100644 index 0000000000..fd8e3ba585 --- /dev/null +++ b/tests/phpunit/includes/content/UnknownContentTest.php @@ -0,0 +1,259 @@ +setUserLang( 'en' ); + $this->setContentLang( 'qqx' ); + + $title = Title::newFromText( 'Test' ); + $content = $this->newContent( 'Horkyporky' ); + + $po = $content->getParserOutput( $title ); + $html = $po->getText(); + $html = preg_replace( '##sm', '', $html ); // strip comments + + $this->assertNotContains( 'Horkyporky', $html ); + $this->assertNotContains( '(unsupported-content-model)', $html ); + } + + /** + * @covers UnknownContent::preSaveTransform + */ + public function testPreSaveTransform() { + $title = Title::newFromText( 'Test' ); + $user = $this->getTestUser()->getUser(); + $content = $this->newContent( 'Horkyporky ~~~' ); + + $options = new ParserOptions(); + + $this->assertSame( $content, $content->preSaveTransform( $title, $user, $options ) ); + } + + /** + * @covers UnknownContent::preloadTransform + */ + public function testPreloadTransform() { + $title = Title::newFromText( 'Test' ); + $content = $this->newContent( 'Horkyporky ~~~' ); + + $options = new ParserOptions(); + + $this->assertSame( $content, $content->preloadTransform( $title, $options ) ); + } + + /** + * @covers UnknownContent::getRedirectTarget + */ + public function testGetRedirectTarget() { + $content = $this->newContent( '#REDIRECT [[Horkyporky]]' ); + $this->assertNull( $content->getRedirectTarget() ); + } + + /** + * @covers UnknownContent::isRedirect + */ + public function testIsRedirect() { + $content = $this->newContent( '#REDIRECT [[Horkyporky]]' ); + $this->assertFalse( $content->isRedirect() ); + } + + /** + * @covers UnknownContent::isCountable + */ + public function testIsCountable() { + $content = $this->newContent( '[[Horkyporky]]' ); + $this->assertFalse( $content->isCountable( true ) ); + } + + /** + * @covers UnknownContent::getTextForSummary + */ + public function testGetTextForSummary() { + $content = $this->newContent( 'Horkyporky' ); + $this->assertSame( '', $content->getTextForSummary() ); + } + + /** + * @covers UnknownContent::getTextForSearchIndex + */ + public function testGetTextForSearchIndex() { + $content = $this->newContent( 'Horkyporky' ); + $this->assertSame( '', $content->getTextForSearchIndex() ); + } + + /** + * @covers UnknownContent::copy + */ + public function testCopy() { + $content = $this->newContent( 'hello world.' ); + $copy = $content->copy(); + + $this->assertSame( $content, $copy ); + } + + /** + * @covers UnknownContent::getSize + */ + public function testGetSize() { + $content = $this->newContent( 'hello world.' ); + + $this->assertEquals( 12, $content->getSize() ); + } + + /** + * @covers UnknownContent::getData + */ + public function testGetData() { + $content = $this->newContent( 'hello world.' ); + + $this->assertEquals( 'hello world.', $content->getData() ); + } + + /** + * @covers UnknownContent::getNativeData + */ + public function testGetNativeData() { + $content = $this->newContent( 'hello world.' ); + + $this->assertEquals( 'hello world.', $content->getNativeData() ); + } + + /** + * @covers UnknownContent::getWikitextForTransclusion + */ + public function testGetWikitextForTransclusion() { + $content = $this->newContent( 'hello world.' ); + + $this->assertEquals( '', $content->getWikitextForTransclusion() ); + } + + /** + * @covers UnknownContent::getModel + */ + public function testGetModel() { + $content = $this->newContent( "hello world.", 'horkyporky' ); + + $this->assertEquals( 'horkyporky', $content->getModel() ); + } + + /** + * @covers UnknownContent::getContentHandler + */ + public function testGetContentHandler() { + $this->mergeMwGlobalArrayValue( + 'wgContentHandlers', + [ 'horkyporky' => 'UnknownContentHandler' ] + ); + + $content = $this->newContent( "hello world.", 'horkyporky' ); + + $this->assertInstanceOf( UnknownContentHandler::class, $content->getContentHandler() ); + $this->assertEquals( 'horkyporky', $content->getContentHandler()->getModelID() ); + } + + public static function dataIsEmpty() { + return [ + [ '', true ], + [ ' ', false ], + [ '0', false ], + [ 'hallo welt.', false ], + ]; + } + + /** + * @dataProvider dataIsEmpty + * @covers UnknownContent::isEmpty + */ + public function testIsEmpty( $text, $empty ) { + $content = $this->newContent( $text ); + + $this->assertEquals( $empty, $content->isEmpty() ); + } + + public function provideEquals() { + return [ + [ new UnknownContent( "hallo", 'horky' ), null, false ], + [ new UnknownContent( "hallo", 'horky' ), new UnknownContent( "hallo", 'horky' ), true ], + [ new UnknownContent( "hallo", 'horky' ), new UnknownContent( "hallo", 'xyzzy' ), false ], + [ new UnknownContent( "hallo", 'horky' ), new JavaScriptContent( "hallo" ), false ], + [ new UnknownContent( "hallo", 'horky' ), new WikitextContent( "hallo" ), false ], + ]; + } + + /** + * @dataProvider provideEquals + * @covers UnknownContent::equals + */ + public function testEquals( Content $a, Content $b = null, $equal = false ) { + $this->assertEquals( $equal, $a->equals( $b ) ); + } + + public static function provideConvert() { + return [ + [ // #0 + 'Hallo Welt', + CONTENT_MODEL_WIKITEXT, + 'lossless', + 'Hallo Welt' + ], + [ // #1 + 'Hallo Welt', + CONTENT_MODEL_WIKITEXT, + 'lossless', + 'Hallo Welt' + ], + [ // #1 + 'Hallo Welt', + CONTENT_MODEL_CSS, + 'lossless', + 'Hallo Welt' + ], + [ // #1 + 'Hallo Welt', + CONTENT_MODEL_JAVASCRIPT, + 'lossless', + 'Hallo Welt' + ], + ]; + } + + /** + * @covers UnknownContent::convert + */ + public function testConvert() { + $content = $this->newContent( 'More horkyporky?' ); + + $this->assertFalse( $content->convert( CONTENT_MODEL_TEXT ) ); + } + + /** + * @covers UnknownContent::__construct + * @covers UnknownContentHandler::serializeContent + */ + public function testSerialize() { + $this->mergeMwGlobalArrayValue( + 'wgContentHandlers', + [ 'horkyporky' => 'UnknownContentHandler' ] + ); + + $content = $this->newContent( 'Hörkypörky', 'horkyporky' ); + + $this->assertSame( 'Hörkypörky', $content->serialize() ); + } + +} diff --git a/tests/phpunit/includes/diff/UnsupportedSlotDiffRendererTest.php b/tests/phpunit/includes/diff/UnsupportedSlotDiffRendererTest.php new file mode 100644 index 0000000000..e8f0bb4ef1 --- /dev/null +++ b/tests/phpunit/includes/diff/UnsupportedSlotDiffRendererTest.php @@ -0,0 +1,42 @@ +mergeMwGlobalArrayValue( + 'wgContentHandlers', + [ 'xyzzy' => 'UnknownContentHandler' ] + ); + + $localizer = $this->getMock( MessageLocalizer::class ); + + $localizer->method( 'msg' ) + ->willReturnCallback( function ( $key, ...$params ) { + return new RawMessage( "($key)", $params ); + } ); + + $sdr = new UnsupportedSlotDiffRenderer( $localizer ); + $this->assertContains( $expected, $sdr->getDiff( $oldContent, $newContent ) ); + } + +}