Create fallback for undefined content models.

This causes RevisionStore to use FallbackContent instances to represent
content for which no content handler is defined.

This may happen when loading revisions using a model that was defined
by an extension that has since been uninstalled.

Bug: T220594
Bug: T220793
Bug: T228921
Change-Id: I5cc9e61223ab22406091479617b077512aa6ae2d
This commit is contained in:
daniel 2020-07-19 21:50:37 +02:00
parent 39fb017285
commit a67cad6d0f
12 changed files with 127 additions and 65 deletions

View file

@ -497,6 +497,8 @@ $wgAutoloadLocalClasses = [
'FakeConverter' => __DIR__ . '/includes/language/TrivialLanguageConverter.php',
'FakeMaintenance' => __DIR__ . '/maintenance/includes/FakeMaintenance.php',
'FakeResultWrapper' => __DIR__ . '/includes/libs/rdbms/database/resultwrapper/FakeResultWrapper.php',
'FallbackContent' => __DIR__ . '/includes/content/FallbackContent.php',
'FallbackContentHandler' => __DIR__ . '/includes/content/FallbackContentHandler.php',
'FatalError' => __DIR__ . '/includes/exception/FatalError.php',
'FauxRequest' => __DIR__ . '/includes/FauxRequest.php',
'FauxResponse' => __DIR__ . '/includes/FauxResponse.php',
@ -1697,8 +1699,8 @@ $wgAutoloadLocalClasses = [
'UcdXmlReader' => __DIR__ . '/maintenance/language/generateCollationData.php',
'Undelete' => __DIR__ . '/maintenance/undelete.php',
'UnifiedDiffFormatter' => __DIR__ . '/includes/diff/UnifiedDiffFormatter.php',
'UnknownContent' => __DIR__ . '/includes/content/UnknownContent.php',
'UnknownContentHandler' => __DIR__ . '/includes/content/UnknownContentHandler.php',
'UnknownContent' => __DIR__ . '/includes/content/FallbackContent.php',
'UnknownContentHandler' => __DIR__ . '/includes/content/FallbackContentHandler.php',
'UnlistedSpecialPage' => __DIR__ . '/includes/specialpage/UnlistedSpecialPage.php',
'UnprotectAction' => __DIR__ . '/includes/actions/UnprotectAction.php',
'UnregisteredLocalFile' => __DIR__ . '/includes/filerepo/file/UnregisteredLocalFile.php',

View file

@ -1156,6 +1156,8 @@ $wgContentHandlers = [
CONTENT_MODEL_CSS => CssContentHandler::class,
// plain text, for use by extensions, etc.
CONTENT_MODEL_TEXT => TextContentHandler::class,
// fallback for unknown models, from imports or extensions that were removed
CONTENT_MODEL_UNKNOWN => FallbackContentHandler::class,
];
/**

View file

@ -226,6 +226,7 @@ define( 'CONTENT_MODEL_JAVASCRIPT', 'javascript' );
define( 'CONTENT_MODEL_CSS', 'css' );
define( 'CONTENT_MODEL_TEXT', 'text' );
define( 'CONTENT_MODEL_JSON', 'json' );
define( 'CONTENT_MODEL_UNKNOWN', 'unknown' );
/** @} */
/** @{

View file

@ -36,8 +36,7 @@ use MediaWiki\Linker\LinkTarget;
class FallbackSlotRoleHandler extends SlotRoleHandler {
public function __construct( $role ) {
// treat unknown content as plain text
parent::__construct( $role, CONTENT_MODEL_TEXT );
parent::__construct( $role, CONTENT_MODEL_UNKNOWN );
}
/**
@ -61,11 +60,11 @@ class FallbackSlotRoleHandler extends SlotRoleHandler {
}
public function getOutputLayoutHints() {
// TODO: should be return [ 'display' => 'none'] here, causing undefined slots
// TODO: should we return [ 'display' => 'none'] here, causing undefined slots
// to be hidden? We'd still need some place to surface the content of such
// slots, see T209923.
return parent::getOutputLayoutHints(); // TODO: Change the autogenerated stub
return parent::getOutputLayoutHints();
}
}

View file

@ -32,6 +32,7 @@ use CommentStoreComment;
use Content;
use ContentHandler;
use DBAccessObjectUtils;
use FallbackContent;
use IDBAccessObject;
use InvalidArgumentException;
use MediaWiki\Content\IContentHandlerFactory;
@ -1036,8 +1037,26 @@ class RevisionStore
}
}
$model = $slot->getModel();
// If the content model is not known, don't fail here (T220594, T220793, T228921)
if ( !$this->contentHandlerFactory->isDefinedModel( $model ) ) {
$this->logger->warning(
"Undefined content model '$model', falling back to UnknownContent",
[
'content_address' => $slot->getAddress(),
'rev_id' => $slot->getRevision(),
'role_name' => $slot->getRole(),
'model_name' => $model,
'trace' => wfBacktrace()
]
);
return new FallbackContent( $data, $model );
}
return $this->contentHandlerFactory
->getContentHandler( $slot->getModel() )
->getContentHandler( $model )
->unserializeContent( $data, $blobFormat );
}

View file

@ -35,7 +35,7 @@
*
* @ingroup Content
*/
class UnknownContent extends AbstractContent {
class FallbackContent extends AbstractContent {
/** @var string */
private $data;
@ -104,6 +104,15 @@ class UnknownContent extends AbstractContent {
return $this->data;
}
/**
* @param string|null $format
*
* @return string data of unknown format and meaning
*/
public function serialize( $format = null ) {
return $this->getData();
}
/**
* Returns an empty string.
*
@ -146,7 +155,7 @@ class UnknownContent extends AbstractContent {
}
protected function equalsInternal( Content $that ) {
if ( !$that instanceof UnknownContent ) {
if ( !$that instanceof FallbackContent ) {
return false;
}
@ -154,3 +163,5 @@ class UnknownContent extends AbstractContent {
}
}
class_alias( FallbackContent::class, 'UnknownContent' );

View file

@ -31,7 +31,7 @@
*
* @ingroup Content
*/
class UnknownContentHandler extends ContentHandler {
class FallbackContentHandler extends ContentHandler {
/**
* Constructs an UnknownContentHandler. Since UnknownContentHandler can be registered
@ -67,8 +67,8 @@ class UnknownContentHandler extends ContentHandler {
* @return mixed
*/
public function serializeContent( Content $content, $format = null ) {
/** @var UnknownContent $content */
'@phan-var UnknownContent $content';
/** @var FallbackContent $content */
'@phan-var FallbackContent $content';
return $content->getData();
}
@ -83,7 +83,7 @@ class UnknownContentHandler extends ContentHandler {
* @return Content The UnknownContent object wrapping $data
*/
public function unserializeContent( $blob, $format = null ) {
return new UnknownContent( $blob, $this->getModelID() );
return new FallbackContent( $blob, $this->getModelID() );
}
/**
@ -113,3 +113,5 @@ class UnknownContentHandler extends ContentHandler {
return new UnsupportedSlotDiffRenderer( $context );
}
}
class_alias( FallbackContentHandler::class, 'UnknownContentHandler' );

View file

@ -6,10 +6,12 @@ use CommentStoreComment;
use Content;
use ContentHandler;
use Exception;
use FallbackContent;
use HashBagOStuff;
use IDBAccessObject;
use InvalidArgumentException;
use JavaScriptContent;
use MediaWiki\Content\IContentHandlerFactory;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\MediaWikiServices;
use MediaWiki\Revision\IncompleteRevisionException;
@ -751,6 +753,34 @@ abstract class RevisionStoreDbTestBase extends MediaWikiIntegrationTestCase {
$this->assertSame( __METHOD__, $storeRecord->getComment()->text );
}
/**
* @covers \MediaWiki\Revision\RevisionStore::getRevisionById
*/
public function testGetRevisionById_undefinedContentModel() {
$page = $this->getTestPage();
$content = new WikitextContent( __METHOD__ );
$status = $page->doEditContent( $content, __METHOD__ );
/** @var RevisionRecord $revRecord */
$revRecord = $status->value['revision-record'];
$mockContentHandlerFactory =
$this->createNoOpMock( IContentHandlerFactory::class, [ 'isDefinedModel' ] );
$mockContentHandlerFactory->method( 'isDefinedModel' )
->willReturn( false );
$this->setService( 'ContentHandlerFactory', $mockContentHandlerFactory );
$store = MediaWikiServices::getInstance()->getRevisionStore();
$storeRecord = $store->getRevisionById( $revRecord->getId() );
$this->assertSame( $revRecord->getId(), $storeRecord->getId() );
$actualContent = $storeRecord->getSlot( SlotRecord::MAIN )->getContent();
$this->assertInstanceOf( FallbackContent::class, $actualContent );
$this->assertSame( __METHOD__, $actualContent->serialize() );
}
/**
* @covers \MediaWiki\Revision\RevisionStore::getRevisionByTitle
*/

View file

@ -6,21 +6,21 @@ use MediaWiki\Revision\SlotRenderingProvider;
/**
* @group ContentHandler
*/
class UnknownContentHandlerTest extends MediaWikiLangTestCase {
class FallbackContentHandlerTest extends MediaWikiLangTestCase {
/**
* @covers UnknownContentHandler::supportsDirectEditing
* @covers FallbackContentHandler::supportsDirectEditing
*/
public function testSupportsDirectEditing() {
$handler = new UnknownContentHandler( 'horkyporky' );
$handler = new FallbackContentHandler( 'horkyporky' );
$this->assertFalse( $handler->supportsDirectEditing(), 'direct editing supported' );
}
/**
* @covers UnknownContentHandler::serializeContent
* @covers FallbackContentHandler::serializeContent
*/
public function testSerializeContent() {
$handler = new UnknownContentHandler( 'horkyporky' );
$content = new UnknownContent( 'hello world', 'horkyporky' );
$handler = new FallbackContentHandler( 'horkyporky' );
$content = new FallbackContent( 'hello world', 'horkyporky' );
$this->assertEquals( 'hello world', $handler->serializeContent( $content ) );
$this->assertEquals(
@ -30,10 +30,10 @@ class UnknownContentHandlerTest extends MediaWikiLangTestCase {
}
/**
* @covers UnknownContentHandler::unserializeContent
* @covers FallbackContentHandler::unserializeContent
*/
public function testUnserializeContent() {
$handler = new UnknownContentHandler( 'horkyporky' );
$handler = new FallbackContentHandler( 'horkyporky' );
$content = $handler->unserializeContent( 'hello world' );
$this->assertEquals( 'hello world', $content->getData() );
@ -42,10 +42,10 @@ class UnknownContentHandlerTest extends MediaWikiLangTestCase {
}
/**
* @covers UnknownContentHandler::makeEmptyContent
* @covers FallbackContentHandler::makeEmptyContent
*/
public function testMakeEmptyContent() {
$handler = new UnknownContentHandler( 'horkyporky' );
$handler = new FallbackContentHandler( 'horkyporky' );
$content = $handler->makeEmptyContent();
$this->assertTrue( $content->isEmpty() );
@ -64,10 +64,10 @@ class UnknownContentHandlerTest extends MediaWikiLangTestCase {
/**
* @dataProvider dataIsSupportedFormat
* @covers UnknownContentHandler::isSupportedFormat
* @covers FallbackContentHandler::isSupportedFormat
*/
public function testIsSupportedFormat( $format, $supported ) {
$handler = new UnknownContentHandler( 'horkyporky' );
$handler = new FallbackContentHandler( 'horkyporky' );
$this->assertEquals( $supported, $handler->isSupportedFormat( $format ) );
}
@ -76,12 +76,12 @@ class UnknownContentHandlerTest extends MediaWikiLangTestCase {
*/
public function testGetSecondaryDataUpdates() {
$title = Title::newFromText( 'Somefile.jpg', NS_FILE );
$content = new UnknownContent( '', 'horkyporky' );
$content = new FallbackContent( '', 'horkyporky' );
/** @var SlotRenderingProvider $srp */
$srp = $this->createMock( SlotRenderingProvider::class );
$handler = new UnknownContentHandler( 'horkyporky' );
$handler = new FallbackContentHandler( 'horkyporky' );
$updates = $handler->getSecondaryDataUpdates( $title, $content, SlotRecord::MAIN, $srp );
$this->assertEquals( [], $updates );
@ -93,7 +93,7 @@ class UnknownContentHandlerTest extends MediaWikiLangTestCase {
public function testGetDeletionUpdates() {
$title = Title::newFromText( 'Somefile.jpg', NS_FILE );
$handler = new UnknownContentHandler( 'horkyporky' );
$handler = new FallbackContentHandler( 'horkyporky' );
$updates = $handler->getDeletionUpdates( $title, SlotRecord::MAIN );
$this->assertEquals( [], $updates );
@ -106,7 +106,7 @@ class UnknownContentHandlerTest extends MediaWikiLangTestCase {
$context = new RequestContext();
$context->setRequest( new FauxRequest() );
$handler = new UnknownContentHandler( 'horkyporky' );
$handler = new FallbackContentHandler( 'horkyporky' );
$slotDiffRenderer = $handler->getSlotDiffRenderer( $context );
$oldContent = $handler->unserializeContent( 'Foo' );

View file

@ -3,18 +3,19 @@
/**
* @group ContentHandler
*/
class UnknownContentTest extends MediaWikiLangTestCase {
class FallbackContentTest extends MediaWikiLangTestCase {
/**
* @param string $data
* @return UnknownContent
*
* @return FallbackContent
*/
public function newContent( $data, $type = 'xyzzy' ) {
return new UnknownContent( $data, $type );
return new FallbackContent( $data, $type );
}
/**
* @covers UnknownContent::getParserOutput
* @covers FallbackContent::getParserOutput
*/
public function testGetParserOutput() {
$this->setUserLang( 'en' );
@ -32,7 +33,7 @@ class UnknownContentTest extends MediaWikiLangTestCase {
}
/**
* @covers UnknownContent::preSaveTransform
* @covers FallbackContent::preSaveTransform
*/
public function testPreSaveTransform() {
$title = Title::newFromText( 'Test' );
@ -45,7 +46,7 @@ class UnknownContentTest extends MediaWikiLangTestCase {
}
/**
* @covers UnknownContent::preloadTransform
* @covers FallbackContent::preloadTransform
*/
public function testPreloadTransform() {
$title = Title::newFromText( 'Test' );
@ -57,7 +58,7 @@ class UnknownContentTest extends MediaWikiLangTestCase {
}
/**
* @covers UnknownContent::getRedirectTarget
* @covers FallbackContent::getRedirectTarget
*/
public function testGetRedirectTarget() {
$content = $this->newContent( '#REDIRECT [[Horkyporky]]' );
@ -65,7 +66,7 @@ class UnknownContentTest extends MediaWikiLangTestCase {
}
/**
* @covers UnknownContent::isRedirect
* @covers FallbackContent::isRedirect
*/
public function testIsRedirect() {
$content = $this->newContent( '#REDIRECT [[Horkyporky]]' );
@ -73,7 +74,7 @@ class UnknownContentTest extends MediaWikiLangTestCase {
}
/**
* @covers UnknownContent::isCountable
* @covers FallbackContent::isCountable
*/
public function testIsCountable() {
$content = $this->newContent( '[[Horkyporky]]' );
@ -81,7 +82,7 @@ class UnknownContentTest extends MediaWikiLangTestCase {
}
/**
* @covers UnknownContent::getTextForSummary
* @covers FallbackContent::getTextForSummary
*/
public function testGetTextForSummary() {
$content = $this->newContent( 'Horkyporky' );
@ -89,7 +90,7 @@ class UnknownContentTest extends MediaWikiLangTestCase {
}
/**
* @covers UnknownContent::getTextForSearchIndex
* @covers FallbackContent::getTextForSearchIndex
*/
public function testGetTextForSearchIndex() {
$content = $this->newContent( 'Horkyporky' );
@ -97,7 +98,7 @@ class UnknownContentTest extends MediaWikiLangTestCase {
}
/**
* @covers UnknownContent::copy
* @covers FallbackContent::copy
*/
public function testCopy() {
$content = $this->newContent( 'hello world.' );
@ -107,7 +108,7 @@ class UnknownContentTest extends MediaWikiLangTestCase {
}
/**
* @covers UnknownContent::getSize
* @covers FallbackContent::getSize
*/
public function testGetSize() {
$content = $this->newContent( 'hello world.' );
@ -116,7 +117,7 @@ class UnknownContentTest extends MediaWikiLangTestCase {
}
/**
* @covers UnknownContent::getData
* @covers FallbackContent::getData
*/
public function testGetData() {
$content = $this->newContent( 'hello world.' );
@ -125,7 +126,7 @@ class UnknownContentTest extends MediaWikiLangTestCase {
}
/**
* @covers UnknownContent::getNativeData
* @covers FallbackContent::getNativeData
*/
public function testGetNativeData() {
$content = $this->newContent( 'hello world.' );
@ -134,7 +135,7 @@ class UnknownContentTest extends MediaWikiLangTestCase {
}
/**
* @covers UnknownContent::getWikitextForTransclusion
* @covers FallbackContent::getWikitextForTransclusion
*/
public function testGetWikitextForTransclusion() {
$content = $this->newContent( 'hello world.' );
@ -143,7 +144,7 @@ class UnknownContentTest extends MediaWikiLangTestCase {
}
/**
* @covers UnknownContent::getModel
* @covers FallbackContent::getModel
*/
public function testGetModel() {
$content = $this->newContent( "hello world.", 'horkyporky' );
@ -152,7 +153,7 @@ class UnknownContentTest extends MediaWikiLangTestCase {
}
/**
* @covers UnknownContent::getContentHandler
* @covers FallbackContent::getContentHandler
*/
public function testGetContentHandler() {
$this->mergeMwGlobalArrayValue(
@ -162,7 +163,7 @@ class UnknownContentTest extends MediaWikiLangTestCase {
$content = $this->newContent( "hello world.", 'horkyporky' );
$this->assertInstanceOf( UnknownContentHandler::class, $content->getContentHandler() );
$this->assertInstanceOf( FallbackContentHandler::class, $content->getContentHandler() );
$this->assertEquals( 'horkyporky', $content->getContentHandler()->getModelID() );
}
@ -177,7 +178,7 @@ class UnknownContentTest extends MediaWikiLangTestCase {
/**
* @dataProvider dataIsEmpty
* @covers UnknownContent::isEmpty
* @covers FallbackContent::isEmpty
*/
public function testIsEmpty( $text, $empty ) {
$content = $this->newContent( $text );
@ -187,17 +188,17 @@ class UnknownContentTest extends MediaWikiLangTestCase {
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 ],
[ new FallbackContent( "hallo", 'horky' ), null, false ],
[ new FallbackContent( "hallo", 'horky' ), new FallbackContent( "hallo", 'horky' ), true ],
[ new FallbackContent( "hallo", 'horky' ), new FallbackContent( "hallo", 'xyzzy' ), false ],
[ new FallbackContent( "hallo", 'horky' ), new JavaScriptContent( "hallo" ), false ],
[ new FallbackContent( "hallo", 'horky' ), new WikitextContent( "hallo" ), false ],
];
}
/**
* @dataProvider provideEquals
* @covers UnknownContent::equals
* @covers FallbackContent::equals
*/
public function testEquals( Content $a, Content $b = null, $equal = false ) {
$this->assertEquals( $equal, $a->equals( $b ) );
@ -233,7 +234,7 @@ class UnknownContentTest extends MediaWikiLangTestCase {
}
/**
* @covers UnknownContent::convert
* @covers FallbackContent::convert
*/
public function testConvert() {
$content = $this->newContent( 'More horkyporky?' );
@ -242,15 +243,10 @@ class UnknownContentTest extends MediaWikiLangTestCase {
}
/**
* @covers UnknownContent::__construct
* @covers UnknownContentHandler::serializeContent
* @covers FallbackContent::__construct
* @covers FallbackContentHandler::serializeContent
*/
public function testSerialize() {
$this->mergeMwGlobalArrayValue(
'wgContentHandlers',
[ 'horkyporky' => 'UnknownContentHandler' ]
);
$content = $this->newContent( 'Hörkypörky', 'horkyporky' );
$this->assertSame( 'Hörkypörky', $content->serialize() );

View file

@ -8,7 +8,7 @@ class UnsupportedSlotDiffRendererTest extends MediaWikiIntegrationTestCase {
public function provideDiff() {
$oldContent = new TextContent( 'Kittens' );
$newContent = new TextContent( 'Goats' );
$badContent = new UnknownContent( 'Dragons', 'xyzzy' );
$badContent = new FallbackContent( 'Dragons', 'xyzzy' );
yield [ '(unsupported-content-diff)', $oldContent, null ];
yield [ '(unsupported-content-diff)', null, $newContent ];

View file

@ -30,7 +30,7 @@ class FallbackSlotRoleHandlerTest extends \MediaWikiUnitTestCase {
$this->assertSame( 'slot-name-foo', $handler->getNameMessageKey() );
$title = $this->makeBlankTitleObject();
$this->assertSame( CONTENT_MODEL_TEXT, $handler->getDefaultModel( $title ) );
$this->assertSame( CONTENT_MODEL_UNKNOWN, $handler->getDefaultModel( $title ) );
$hints = $handler->getOutputLayoutHints();
$this->assertArrayHasKey( 'display', $hints );