* ChangeTags::tagDescription() will return false if the interface message
for the tag is disabled.
* Added PageHistoryPager::doBatchLookups hook.
+* supportsDirectEditing and supportsDirectApiEditing methods added to
+ContentHandler, to provide a way for ApiEditPage and EditPage to check
+if direct editing of content is allowed. These methods return false,
+by default for the ContentHandler base class and true for TextContentHandler
+and it's derivative classes (everything in core). For Content types that
+do not support direct editing, an alternative mechanism should be provided
+for editing, such as action overrides or specific api modules.
== Compatibility ==
public $suppressIntro = false;
- /** @var bool Set to true to allow editing of non-text content types. */
- public $allowNonTextContent = false;
-
/** @var bool */
protected $edit;
+ /**
+ * @var bool Set in ApiEditPage, based on ContentHandler::allowsDirectApiEditing
+ */
+ private $enableApiEditOverride = false;
+
/**
* @param Article $article
*/
* @throws MWException If $modelId has no known handler
*/
public function isSupportedContentModel( $modelId ) {
- return $this->allowNonTextContent ||
- ContentHandler::getForModelID( $modelId ) instanceof TextContentHandler;
+ return $this->enableApiEditOverride === true ||
+ ContentHandler::getForModelID( $modelId )->supportsDirectEditing();
+ }
+
+ /**
+ * Allow editing of content that supports API direct editing, but not general
+ * direct editing. Set to false by default.
+ *
+ * @param bool $enableOverride
+ */
+ public function setApiEditOverride( $enableOverride ) {
+ $this->enableApiEditOverride = $enableOverride;
}
function submit() {
$contentHandler = ContentHandler::getForModelID( $params['contentmodel'] );
}
- // @todo Ask handler whether direct editing is supported at all! make
- // allowFlatEdit() method or some such
+ if ( $contentHandler->supportsDirectApiEditing() === false ) {
+ $this->dieUsage(
+ 'Direct editing via API is not supported for this content type.',
+ 'no-direct-editing'
+ );
+ }
if ( !isset( $params['contentformat'] ) || $params['contentformat'] == '' ) {
$params['contentformat'] = $contentHandler->getDefaultFormat();
$ep = new EditPage( $articleObject );
- // allow editing of non-textual content.
- $ep->allowNonTextContent = true;
-
+ $ep->setApiEditOverride( true );
$ep->setContextTitle( $titleObj );
$ep->importFormData( $req );
$content = $ep->textbox1;
return false;
}
+ /**
+ * Return true if this content model supports direct editing, such as via EditPage.
+ *
+ * @return bool Default is false, and true for TextContent and it's derivatives.
+ */
+ public function supportsDirectEditing() {
+ return false;
+ }
+
+ /**
+ * Whether or not this content model supports direct editing via ApiEditPage
+ *
+ * @return bool Default is false, and true for TextContent and derivatives.
+ */
+ public function supportsDirectApiEditing() {
+ return $this->supportsDirectEditing();
+ }
+
/**
* Logs a deprecation warning, visible if $wgDevelopmentWarnings, but only if
* self::$enableDeprecationWarnings is set to true.
return new $class( '' );
}
+ /**
+ * @see ContentHandler::supportsDirectEditing
+ *
+ * @return bool Default is true for TextContent and derivatives.
+ */
+ public function supportsDirectEditing() {
+ return true;
+ }
+
}
'TestRecentChangesHelper' => "$testDir/phpunit/includes/changes/TestRecentChangesHelper.php",
# tests/phpunit/includes/content
- 'DummyContentHandlerForTesting' => "$testDir/phpunit/includes/content/ContentHandlerTest.php",
- 'DummyContentForTesting' => "$testDir/phpunit/includes/content/ContentHandlerTest.php",
+ 'DummyContentHandlerForTesting' => "$testDir/phpunit/mocks/content/DummyContentHandlerForTesting.php",
+ 'DummyContentForTesting' => "$testDir/phpunit/mocks/content/DummyContentForTesting.php",
+ 'DummyNonTextContentHandler' => "$testDir/phpunit/mocks/content/DummyNonTextContentHandler.php",
+ 'DummyNonTextContent' => "$testDir/phpunit/mocks/content/DummyNonTextContent.php",
'ContentHandlerTest' => "$testDir/phpunit/includes/content/ContentHandlerTest.php",
'JavaScriptContentTest' => "$testDir/phpunit/includes/content/JavaScriptContentTest.php",
'TextContentTest' => "$testDir/phpunit/includes/content/TextContentTest.php",
*/
class EditPageTest extends MediaWikiLangTestCase {
+ protected function setUp() {
+ global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
+
+ parent::setUp();
+
+ $this->setMwGlobals( array(
+ 'wgExtraNamespaces' => $wgExtraNamespaces,
+ 'wgNamespaceContentModels' => $wgNamespaceContentModels,
+ 'wgContentHandlers' => $wgContentHandlers,
+ 'wgContLang' => $wgContLang,
+ ) );
+
+ $wgExtraNamespaces[12312] = 'Dummy';
+ $wgExtraNamespaces[12313] = 'Dummy_talk';
+
+ $wgNamespaceContentModels[12312] = "testing";
+ $wgContentHandlers["testing"] = 'DummyContentHandlerForTesting';
+
+ MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+ $wgContLang->resetNamespaces(); # reset namespace cache
+ }
+
/**
* @dataProvider provideExtractSectionTitle
* @covers EditPage::extractSectionTitle
$this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Berta', $bertasEdit,
$expectedCode, $expectedText, $message );
}
+
+ /**
+ * @depends testAutoMerge
+ */
+ public function testCheckDirectEditingDisallowed_forNonTextContent() {
+ $title = Title::newFromText( 'Dummy:NonTextPageForEditPage' );
+ $page = WikiPage::factory( $title );
+
+ $article = new Article( $title );
+ $article->getContext()->setTitle( $title );
+ $ep = new EditPage( $article );
+ $ep->setContextTitle( $title );
+
+ $user = $GLOBALS['wgUser'];
+
+ $edit = array(
+ 'wpTextbox1' => serialize( 'non-text content' ),
+ 'wpEditToken' => $user->getEditToken(),
+ 'wpEdittime' => '',
+ 'wpStarttime' => wfTimestampNow()
+ );
+
+ $req = new FauxRequest( $edit, true );
+ $ep->importFormData( $req );
+
+ $this->setExpectedException(
+ 'MWException',
+ 'This content model is not supported: testing'
+ );
+
+ $ep->internalAttemptSave( $result, false );
+ }
+
}
$wgExtraNamespaces[12312] = 'Dummy';
$wgExtraNamespaces[12313] = 'Dummy_talk';
+ $wgExtraNamespaces[12314] = 'DummyNonText';
+ $wgExtraNamespaces[12315] = 'DummyNonText_talk';
$wgNamespaceContentModels[12312] = "testing";
+ $wgNamespaceContentModels[12314] = "testing-nontext";
+
$wgContentHandlers["testing"] = 'DummyContentHandlerForTesting';
+ $wgContentHandlers["testing-nontext"] = 'DummyNonTextContentHandler';
MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
$wgContLang->resetNamespaces(); # reset namespace cache
);
}
- public function testNonTextEdit() {
- $name = 'Dummy:ApiEditPageTest_testNonTextEdit';
- $data = serialize( 'some bla bla text' );
-
- // -- test new page --------------------------------------------
- $apiResult = $this->doApiRequestWithToken( array(
- 'action' => 'edit',
- 'title' => $name,
- 'text' => $data, ) );
- $apiResult = $apiResult[0];
-
- // Validate API result data
- $this->assertArrayHasKey( 'edit', $apiResult );
- $this->assertArrayHasKey( 'result', $apiResult['edit'] );
- $this->assertEquals( 'Success', $apiResult['edit']['result'] );
-
- $this->assertArrayHasKey( 'new', $apiResult['edit'] );
- $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] );
-
- $this->assertArrayHasKey( 'pageid', $apiResult['edit'] );
-
- // validate resulting revision
- $page = WikiPage::factory( Title::newFromText( $name ) );
- $this->assertEquals( "testing", $page->getContentModel() );
- $this->assertEquals( $data, $page->getContent()->serialize() );
- }
-
/**
* @return array
*/
$page->clear();
}
+
+ public function testCheckDirectApiEditingDisallowed_forNonTextContent() {
+ $this->setExpectedException(
+ 'UsageException',
+ 'Direct editing via API is not supported for this content type.'
+ );
+
+ $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => 'Dummy:ApiEditPageTest_nonTextPageEdit',
+ 'text' => '{"animals":["kittens!"]}'
+ ) );
+ }
+
+ public function testSupportsDirectApiEditing_withContentHandlerOverride() {
+ $name = 'DummyNonText:ApiEditPageTest_testNonTextEdit';
+ $data = serialize( 'some bla bla text' );
+
+ $result = $this->doApiRequestWithToken( array(
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => $data,
+ ) );
+
+ $apiResult = $result[0];
+
+ // Validate API result data
+ $this->assertArrayHasKey( 'edit', $apiResult );
+ $this->assertArrayHasKey( 'result', $apiResult['edit'] );
+ $this->assertEquals( 'Success', $apiResult['edit']['result'] );
+
+ $this->assertArrayHasKey( 'new', $apiResult['edit'] );
+ $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] );
+
+ $this->assertArrayHasKey( 'pageid', $apiResult['edit'] );
+
+ // validate resulting revision
+ $page = WikiPage::factory( Title::newFromText( $name ) );
+ $this->assertEquals( "testing-nontext", $page->getContentModel() );
+ $this->assertEquals( $data, $page->getContent()->serialize() );
+ }
}
}
*/
+ public function testSupportsDirectEditing() {
+ $handler = new DummyContentHandlerForTesting( CONTENT_MODEL_JSON );
+ $this->assertFalse( $handler->supportsDirectEditing(), 'direct editing is not supported' );
+ }
+
/**
* @covers ContentHandler::runLegacyHooks
*/
return true;
}
}
-
-class DummyContentHandlerForTesting extends ContentHandler {
-
- public function __construct( $dataModel ) {
- parent::__construct( $dataModel, array( "testing" ) );
- }
-
- /**
- * @see ContentHandler::serializeContent
- *
- * @param Content $content
- * @param string $format
- *
- * @return string
- */
- public function serializeContent( Content $content, $format = null ) {
- return $content->serialize();
- }
-
- /**
- * @see ContentHandler::unserializeContent
- *
- * @param string $blob
- * @param string $format Unused.
- *
- * @return Content
- */
- public function unserializeContent( $blob, $format = null ) {
- $d = unserialize( $blob );
-
- return new DummyContentForTesting( $d );
- }
-
- /**
- * Creates an empty Content object of the type supported by this ContentHandler.
- *
- */
- public function makeEmptyContent() {
- return new DummyContentForTesting( '' );
- }
-}
-
-class DummyContentForTesting extends AbstractContent {
-
- public function __construct( $data ) {
- parent::__construct( "testing" );
-
- $this->data = $data;
- }
-
- public function serialize( $format = null ) {
- return serialize( $this->data );
- }
-
- /**
- * @return string A string representing the content in a way useful for
- * building a full text search index. If no useful representation exists,
- * this method returns an empty string.
- */
- public function getTextForSearchIndex() {
- return '';
- }
-
- /**
- * @return string|bool The wikitext to include when another page includes this content,
- * or false if the content is not includable in a wikitext page.
- */
- public function getWikitextForTransclusion() {
- return false;
- }
-
- /**
- * Returns a textual representation of the content suitable for use in edit
- * summaries and log messages.
- *
- * @param int $maxlength Maximum length of the summary text.
- * @return string The summary text.
- */
- public function getTextForSummary( $maxlength = 250 ) {
- return '';
- }
-
- /**
- * Returns native represenation of the data. Interpretation depends on the data model used,
- * as given by getDataModel().
- *
- * @return mixed The native representation of the content. Could be a string, a nested array
- * structure, an object, a binary blob... anything, really.
- */
- public function getNativeData() {
- return $this->data;
- }
-
- /**
- * returns the content's nominal size in bogo-bytes.
- *
- * @return int
- */
- public function getSize() {
- return strlen( $this->data );
- }
-
- /**
- * Return a copy of this Content object. The following must be true for the object returned
- * if $copy = $original->copy()
- *
- * * get_class($original) === get_class($copy)
- * * $original->getModel() === $copy->getModel()
- * * $original->equals( $copy )
- *
- * If and only if the Content object is imutable, the copy() method can and should
- * return $this. That is, $copy === $original may be true, but only for imutable content
- * objects.
- *
- * @return Content A copy of this object
- */
- public function copy() {
- return $this;
- }
-
- /**
- * Returns true if this content is countable as a "real" wiki page, provided
- * that it's also in a countable location (e.g. a current revision in the main namespace).
- *
- * @param bool $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;
- }
-
- /**
- * @param Title $title
- * @param int $revId Unused.
- * @param null|ParserOptions $options
- * @param bool $generateHtml Whether to generate Html (default: true). If false, the result
- * of calling getText() on the ParserOutput object returned by this method is undefined.
- *
- * @return ParserOutput
- */
- public function getParserOutput( Title $title, $revId = null,
- ParserOptions $options = null, $generateHtml = true
- ) {
- return new ParserOutput( $this->getNativeData() );
- }
-
- /**
- * @see AbstractContent::fillParserOutput()
- *
- * @param Title $title Context title for parsing
- * @param int|null $revId Revision ID (for {{REVISIONID}})
- * @param ParserOptions $options Parser options
- * @param bool $generateHtml Whether or not to generate HTML
- * @param ParserOutput &$output The output object to fill (reference).
- */
- protected function fillParserOutput( Title $title, $revId,
- ParserOptions $options, $generateHtml, ParserOutput &$output ) {
- $output = new ParserOutput( $this->getNativeData() );
- }
-}
--- /dev/null
+<?php
+
+/**
+ * @group ContentHandler
+ */
+class TextContentHandlerTest extends MediaWikiLangTestCase {
+
+ public function testSupportsDirectEditing() {
+ $handler = new TextContentHandler();
+ $this->assertTrue( $handler->supportsDirectEditing(), 'direct editing is supported' );
+ }
+
+}
$this->assertEquals( $supported, $this->handler->isSupportedFormat( $format ) );
}
+ public function testSupportsDirectEditing() {
+ $handler = new WikiTextContentHandler();
+ $this->assertTrue( $handler->supportsDirectEditing(), 'direct editing is supported' );
+ }
+
public static function dataMerge3() {
return array(
array(
--- /dev/null
+<?php
+
+class DummyContentForTesting extends AbstractContent {
+
+ public function __construct( $data ) {
+ parent::__construct( "testing" );
+
+ $this->data = $data;
+ }
+
+ public function serialize( $format = null ) {
+ return serialize( $this->data );
+ }
+
+ /**
+ * @return string A string representing the content in a way useful for
+ * building a full text search index. If no useful representation exists,
+ * this method returns an empty string.
+ */
+ public function getTextForSearchIndex() {
+ return '';
+ }
+
+ /**
+ * @return string|bool The wikitext to include when another page includes this content,
+ * or false if the content is not includable in a wikitext page.
+ */
+ public function getWikitextForTransclusion() {
+ return false;
+ }
+
+ /**
+ * Returns a textual representation of the content suitable for use in edit
+ * summaries and log messages.
+ *
+ * @param int $maxlength Maximum length of the summary text.
+ * @return string The summary text.
+ */
+ public function getTextForSummary( $maxlength = 250 ) {
+ return '';
+ }
+
+ /**
+ * Returns native represenation of the data. Interpretation depends on the data model used,
+ * as given by getDataModel().
+ *
+ * @return mixed The native representation of the content. Could be a string, a nested array
+ * structure, an object, a binary blob... anything, really.
+ */
+ public function getNativeData() {
+ return $this->data;
+ }
+
+ /**
+ * returns the content's nominal size in bogo-bytes.
+ *
+ * @return int
+ */
+ public function getSize() {
+ return strlen( $this->data );
+ }
+
+ /**
+ * Return a copy of this Content object. The following must be true for the object returned
+ * if $copy = $original->copy()
+ *
+ * * get_class($original) === get_class($copy)
+ * * $original->getModel() === $copy->getModel()
+ * * $original->equals( $copy )
+ *
+ * If and only if the Content object is imutable, the copy() method can and should
+ * return $this. That is, $copy === $original may be true, but only for imutable content
+ * objects.
+ *
+ * @return Content A copy of this object
+ */
+ public function copy() {
+ return $this;
+ }
+
+ /**
+ * Returns true if this content is countable as a "real" wiki page, provided
+ * that it's also in a countable location (e.g. a current revision in the main namespace).
+ *
+ * @param bool $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;
+ }
+
+ /**
+ * @param Title $title
+ * @param int $revId Unused.
+ * @param null|ParserOptions $options
+ * @param bool $generateHtml Whether to generate Html (default: true). If false, the result
+ * of calling getText() on the ParserOutput object returned by this method is undefined.
+ *
+ * @return ParserOutput
+ */
+ public function getParserOutput( Title $title, $revId = null,
+ ParserOptions $options = null, $generateHtml = true
+ ) {
+ return new ParserOutput( $this->getNativeData() );
+ }
+
+ /**
+ * @see AbstractContent::fillParserOutput()
+ *
+ * @param Title $title Context title for parsing
+ * @param int|null $revId Revision ID (for {{REVISIONID}})
+ * @param ParserOptions $options Parser options
+ * @param bool $generateHtml Whether or not to generate HTML
+ * @param ParserOutput &$output The output object to fill (reference).
+ */
+ protected function fillParserOutput( Title $title, $revId,
+ ParserOptions $options, $generateHtml, ParserOutput &$output ) {
+ $output = new ParserOutput( $this->getNativeData() );
+ }
+}
--- /dev/null
+<?php
+
+class DummyContentHandlerForTesting extends ContentHandler {
+
+ public function __construct( $dataModel ) {
+ parent::__construct( $dataModel, array( "testing" ) );
+ }
+
+ /**
+ * @see ContentHandler::serializeContent
+ *
+ * @param Content $content
+ * @param string $format
+ *
+ * @return string
+ */
+ public function serializeContent( Content $content, $format = null ) {
+ return $content->serialize();
+ }
+
+ /**
+ * @see ContentHandler::unserializeContent
+ *
+ * @param string $blob
+ * @param string $format Unused.
+ *
+ * @return Content
+ */
+ public function unserializeContent( $blob, $format = null ) {
+ $d = unserialize( $blob );
+
+ return new DummyContentForTesting( $d );
+ }
+
+ /**
+ * Creates an empty Content object of the type supported by this ContentHandler.
+ *
+ */
+ public function makeEmptyContent() {
+ return new DummyContentForTesting( '' );
+ }
+}
--- /dev/null
+<?php
+
+class DummyNonTextContent extends AbstractContent {
+
+ public function __construct( $data ) {
+ parent::__construct( "testing-nontext" );
+
+ $this->data = $data;
+ }
+
+ public function serialize( $format = null ) {
+ return serialize( $this->data );
+ }
+
+ /**
+ * @return string A string representing the content in a way useful for
+ * building a full text search index. If no useful representation exists,
+ * this method returns an empty string.
+ */
+ public function getTextForSearchIndex() {
+ return '';
+ }
+
+ /**
+ * @return string|bool The wikitext to include when another page includes this content,
+ * or false if the content is not includable in a wikitext page.
+ */
+ public function getWikitextForTransclusion() {
+ return false;
+ }
+
+ /**
+ * Returns a textual representation of the content suitable for use in edit
+ * summaries and log messages.
+ *
+ * @param int $maxlength Maximum length of the summary text.
+ * @return string The summary text.
+ */
+ public function getTextForSummary( $maxlength = 250 ) {
+ return '';
+ }
+
+ /**
+ * Returns native represenation of the data. Interpretation depends on the data model used,
+ * as given by getDataModel().
+ *
+ * @return mixed The native representation of the content. Could be a string, a nested array
+ * structure, an object, a binary blob... anything, really.
+ */
+ public function getNativeData() {
+ return $this->data;
+ }
+
+ /**
+ * returns the content's nominal size in bogo-bytes.
+ *
+ * @return int
+ */
+ public function getSize() {
+ return strlen( $this->data );
+ }
+
+ /**
+ * Return a copy of this Content object. The following must be true for the object returned
+ * if $copy = $original->copy()
+ *
+ * * get_class($original) === get_class($copy)
+ * * $original->getModel() === $copy->getModel()
+ * * $original->equals( $copy )
+ *
+ * If and only if the Content object is imutable, the copy() method can and should
+ * return $this. That is, $copy === $original may be true, but only for imutable content
+ * objects.
+ *
+ * @return Content A copy of this object
+ */
+ public function copy() {
+ return $this;
+ }
+
+ /**
+ * Returns true if this content is countable as a "real" wiki page, provided
+ * that it's also in a countable location (e.g. a current revision in the main namespace).
+ *
+ * @param bool $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;
+ }
+
+ /**
+ * @param Title $title
+ * @param int $revId Unused.
+ * @param null|ParserOptions $options
+ * @param bool $generateHtml Whether to generate Html (default: true). If false, the result
+ * of calling getText() on the ParserOutput object returned by this method is undefined.
+ *
+ * @return ParserOutput
+ */
+ public function getParserOutput( Title $title, $revId = null,
+ ParserOptions $options = null, $generateHtml = true
+ ) {
+ return new ParserOutput( $this->getNativeData() );
+ }
+
+ /**
+ * @see AbstractContent::fillParserOutput()
+ *
+ * @param Title $title Context title for parsing
+ * @param int|null $revId Revision ID (for {{REVISIONID}})
+ * @param ParserOptions $options Parser options
+ * @param bool $generateHtml Whether or not to generate HTML
+ * @param ParserOutput &$output The output object to fill (reference).
+ */
+ protected function fillParserOutput( Title $title, $revId,
+ ParserOptions $options, $generateHtml, ParserOutput &$output ) {
+ $output = new ParserOutput( $this->getNativeData() );
+ }
+}
--- /dev/null
+<?php
+
+class DummyNonTextContentHandler extends DummyContentHandlerForTesting {
+
+ public function __construct( $dataModel ) {
+ parent::__construct( $dataModel, array( "testing-nontext" ) );
+ }
+
+ /**
+ * @see ContentHandler::serializeContent
+ *
+ * @param Content $content
+ * @param string $format
+ *
+ * @return string
+ */
+ public function serializeContent( Content $content, $format = null ) {
+ return $content->serialize();
+ }
+
+ /**
+ * @see ContentHandler::unserializeContent
+ *
+ * @param string $blob
+ * @param string $format Unused.
+ *
+ * @return Content
+ */
+ public function unserializeContent( $blob, $format = null ) {
+ $d = unserialize( $blob );
+
+ return new DummyNonTextContent( $d );
+ }
+
+ /**
+ * Creates an empty Content object of the type supported by this ContentHandler.
+ */
+ public function makeEmptyContent() {
+ return new DummyNonTextContent( '' );
+ }
+
+ public function supportsDirectApiEditing() {
+ return true;
+ }
+
+}