2020-08-19 20:03:41 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace MediaWiki\Extension\DiscussionTools;
|
|
|
|
|
|
|
|
use ApiBase;
|
|
|
|
use ApiMain;
|
|
|
|
use ApiParsoidTrait;
|
2021-04-29 00:06:56 +00:00
|
|
|
use DerivativeContext;
|
2020-08-19 20:03:41 +00:00
|
|
|
use DerivativeRequest;
|
2020-08-20 16:12:10 +00:00
|
|
|
use MediaWiki\Logger\LoggerFactory;
|
2020-08-19 20:03:41 +00:00
|
|
|
use Title;
|
|
|
|
use Wikimedia\ParamValidator\ParamValidator;
|
|
|
|
use Wikimedia\Parsoid\Utils\DOMCompat;
|
|
|
|
use Wikimedia\Parsoid\Utils\DOMUtils;
|
|
|
|
|
|
|
|
class ApiDiscussionToolsEdit extends ApiBase {
|
|
|
|
|
|
|
|
use ApiParsoidTrait;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function __construct( ApiMain $main, string $name ) {
|
|
|
|
parent::__construct( $main, $name );
|
2020-08-20 16:12:10 +00:00
|
|
|
$this->setLogger( LoggerFactory::getInstance( 'DiscussionTools' ) );
|
2020-08-19 20:03:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function execute() {
|
|
|
|
$params = $this->extractRequestParams();
|
|
|
|
$title = Title::newFromText( $params['page'] );
|
|
|
|
$result = null;
|
|
|
|
|
|
|
|
if ( !$title ) {
|
|
|
|
$this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['page'] ) ] );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-05-21 16:03:39 +00:00
|
|
|
$this->getErrorFormatter()->setContextTitle( $title );
|
|
|
|
|
2020-08-19 20:03:41 +00:00
|
|
|
switch ( $params['paction'] ) {
|
2020-08-06 14:22:47 +00:00
|
|
|
case 'addtopic':
|
|
|
|
$this->requireAtLeastOneParameter( $params, 'sectiontitle' );
|
|
|
|
$this->requireOnlyOneParameter( $params, 'wikitext', 'html' );
|
|
|
|
|
|
|
|
$wikitext = $params['wikitext'];
|
|
|
|
$html = $params['html'];
|
|
|
|
|
|
|
|
if ( $wikitext !== null ) {
|
2021-03-24 16:48:10 +00:00
|
|
|
$wikitext = CommentUtils::htmlTrim( $wikitext );
|
2020-08-06 14:22:47 +00:00
|
|
|
if ( !CommentModifier::isWikitextSigned( $wikitext ) ) {
|
|
|
|
$wikitext .=
|
|
|
|
$this->msg( 'discussiontools-signature-prefix' )->inContentLanguage()->text() . '~~~~';
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$doc = DOMUtils::parseHTML( $html );
|
2021-08-02 13:45:39 +00:00
|
|
|
$container = DOMCompat::getBody( $doc );
|
2020-08-06 14:22:47 +00:00
|
|
|
if ( !CommentModifier::isHtmlSigned( $container ) ) {
|
|
|
|
CommentModifier::appendSignature( $container );
|
|
|
|
}
|
|
|
|
$html = DOMCompat::getInnerHTML( $container );
|
|
|
|
$wikitext = $this->transformHTML( $title, $html )[ 'body' ];
|
|
|
|
}
|
|
|
|
|
|
|
|
// As section=new this is append only so we don't need to
|
|
|
|
// worry about edit-conflict params such as oldid/baserevid/etag.
|
|
|
|
// Edit summary is also automatically generated when section=new
|
2021-04-29 00:06:56 +00:00
|
|
|
$context = new DerivativeContext( $this->getContext() );
|
|
|
|
$context->setRequest(
|
2020-08-06 14:22:47 +00:00
|
|
|
new DerivativeRequest(
|
2021-04-29 00:06:56 +00:00
|
|
|
$context->getRequest(),
|
2020-08-06 14:22:47 +00:00
|
|
|
[
|
|
|
|
'action' => 'visualeditoredit',
|
|
|
|
'paction' => 'save',
|
|
|
|
'page' => $params['page'],
|
|
|
|
'token' => $params['token'],
|
|
|
|
'wikitext' => $wikitext,
|
2020-08-25 12:31:54 +00:00
|
|
|
// A default is provided automatically by the Edit API
|
|
|
|
// for new sections when the summary is empty.
|
|
|
|
'summary' => $params['summary'],
|
2020-08-06 14:22:47 +00:00
|
|
|
'section' => 'new',
|
|
|
|
'sectiontitle' => $params['sectiontitle'],
|
|
|
|
'starttimestamp' => wfTimestampNow(),
|
|
|
|
'watchlist' => $params['watchlist'],
|
|
|
|
'captchaid' => $params['captchaid'],
|
|
|
|
'captchaword' => $params['captchaword']
|
|
|
|
],
|
|
|
|
/* was posted? */ true
|
2021-04-29 00:06:56 +00:00
|
|
|
)
|
|
|
|
);
|
|
|
|
$api = new ApiMain(
|
|
|
|
$context,
|
2020-08-06 14:22:47 +00:00
|
|
|
/* enable write? */ true
|
|
|
|
);
|
|
|
|
|
|
|
|
$api->execute();
|
|
|
|
|
|
|
|
$data = $api->getResult()->getResultData();
|
|
|
|
$result = $data['visualeditoredit'];
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
2020-08-19 20:03:41 +00:00
|
|
|
case 'addcomment':
|
2021-02-12 19:16:13 +00:00
|
|
|
$this->requireAtLeastOneParameter( $params, 'commentid', 'commentname' );
|
2021-01-27 15:40:57 +00:00
|
|
|
|
2021-02-12 19:16:13 +00:00
|
|
|
$commentId = $params['commentid'] ?? null;
|
|
|
|
$commentName = $params['commentname'] ?? null;
|
2021-01-27 15:40:57 +00:00
|
|
|
|
|
|
|
if ( !$title->exists() ) {
|
|
|
|
// The page does not exist, so the comment we're trying to reply to can't exist either.
|
2021-02-12 19:16:13 +00:00
|
|
|
if ( $commentId ) {
|
|
|
|
$this->dieWithError( [ 'apierror-discussiontools-commentid-notfound', $commentId ] );
|
|
|
|
} else {
|
|
|
|
$this->dieWithError( [ 'apierror-discussiontools-commentname-notfound', $commentName ] );
|
|
|
|
}
|
2021-01-27 15:40:57 +00:00
|
|
|
}
|
|
|
|
|
2020-08-19 20:03:41 +00:00
|
|
|
// Fetch the latest revision
|
2020-08-20 16:12:10 +00:00
|
|
|
$requestedRevision = $this->getLatestRevision( $title );
|
|
|
|
$response = $this->requestRestbasePageHtml( $requestedRevision );
|
2020-08-19 20:03:41 +00:00
|
|
|
|
2020-08-20 16:12:10 +00:00
|
|
|
$headers = $response['headers'];
|
2020-08-19 20:03:41 +00:00
|
|
|
$doc = DOMUtils::parseHTML( $response['body'] );
|
2020-08-20 16:12:10 +00:00
|
|
|
|
|
|
|
// Don't trust RESTBase to always give us the revision we requested,
|
|
|
|
// instead get the revision ID from the document and use that.
|
|
|
|
// Ported from ve.init.mw.ArticleTarget.prototype.parseMetadata
|
|
|
|
$docRevId = null;
|
|
|
|
$aboutDoc = $doc->documentElement->getAttribute( 'about' );
|
|
|
|
|
|
|
|
if ( $aboutDoc ) {
|
|
|
|
preg_match( '/revision\\/([0-9]+)$/', $aboutDoc, $docRevIdMatches );
|
|
|
|
if ( $docRevIdMatches ) {
|
|
|
|
$docRevId = (int)$docRevIdMatches[ 1 ];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !$docRevId ) {
|
|
|
|
$this->dieWithError( 'apierror-visualeditor-docserver', 'docserver' );
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( $docRevId !== $requestedRevision->getId() ) {
|
|
|
|
// TODO: If this never triggers, consider removing the check.
|
|
|
|
$this->getLogger()->warning(
|
|
|
|
"Requested revision {$requestedRevision->getId()} " .
|
|
|
|
"but received {$docRevId}."
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-08-02 13:45:39 +00:00
|
|
|
$container = DOMCompat::getBody( $doc );
|
2020-08-19 20:03:41 +00:00
|
|
|
|
|
|
|
$parser = CommentParser::newFromGlobalState( $container );
|
2020-09-01 23:00:08 +00:00
|
|
|
|
2021-02-12 19:16:13 +00:00
|
|
|
if ( $commentId ) {
|
|
|
|
$comment = $parser->findCommentById( $commentId );
|
|
|
|
|
|
|
|
if ( !$comment || !( $comment instanceof CommentItem ) ) {
|
|
|
|
$this->dieWithError( [ 'apierror-discussiontools-commentid-notfound', $commentId ] );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
$comments = $parser->findCommentsByName( $commentName );
|
|
|
|
$comment = $comments[ 0 ] ?? null;
|
|
|
|
|
|
|
|
if ( count( $comments ) > 1 ) {
|
|
|
|
$this->dieWithError( [ 'apierror-discussiontools-commentname-ambiguous', $commentName ] );
|
|
|
|
return;
|
|
|
|
} elseif ( !$comment || !( $comment instanceof CommentItem ) ) {
|
|
|
|
$this->dieWithError( [ 'apierror-discussiontools-commentname-notfound', $commentName ] );
|
|
|
|
return;
|
|
|
|
}
|
2020-08-19 20:03:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
$this->requireOnlyOneParameter( $params, 'wikitext', 'html' );
|
|
|
|
|
|
|
|
if ( $params['wikitext'] !== null ) {
|
|
|
|
CommentModifier::addWikitextReply( $comment, $params['wikitext'] );
|
|
|
|
} else {
|
|
|
|
CommentModifier::addHtmlReply( $comment, $params['html'] );
|
|
|
|
}
|
|
|
|
|
2020-08-25 12:31:54 +00:00
|
|
|
if ( isset( $params['summary'] ) ) {
|
|
|
|
$summary = $params['summary'];
|
2020-08-19 20:03:41 +00:00
|
|
|
} else {
|
2020-11-05 16:07:56 +00:00
|
|
|
$title = $comment->getHeading()->getLinkableTitle();
|
|
|
|
$summary = ( $title ? '/* ' . $title . ' */ ' : '' ) .
|
|
|
|
$this->msg( 'discussiontools-defaultsummary-reply' )->inContentLanguage()->text();
|
2020-08-19 20:03:41 +00:00
|
|
|
}
|
|
|
|
|
2021-04-29 00:06:56 +00:00
|
|
|
$context = new DerivativeContext( $this->getContext() );
|
|
|
|
$context->setRequest(
|
2020-08-19 20:03:41 +00:00
|
|
|
new DerivativeRequest(
|
2021-04-29 00:06:56 +00:00
|
|
|
$context->getRequest(),
|
2020-08-19 20:03:41 +00:00
|
|
|
[
|
|
|
|
'action' => 'visualeditoredit',
|
|
|
|
'paction' => 'save',
|
|
|
|
'page' => $params['page'],
|
|
|
|
'token' => $params['token'],
|
2020-08-20 16:12:10 +00:00
|
|
|
'oldid' => $docRevId,
|
2020-08-19 20:03:41 +00:00
|
|
|
'html' => DOMCompat::getOuterHTML( $doc->documentElement ),
|
|
|
|
'summary' => $summary,
|
2020-08-20 16:12:10 +00:00
|
|
|
'baserevid' => $docRevId,
|
2020-08-19 20:03:41 +00:00
|
|
|
'starttimestamp' => wfTimestampNow(),
|
|
|
|
'etag' => $headers['etag'],
|
|
|
|
'watchlist' => $params['watchlist'],
|
|
|
|
'captchaid' => $params['captchaid'],
|
|
|
|
'captchaword' => $params['captchaword']
|
|
|
|
],
|
|
|
|
/* was posted? */ true
|
2021-04-29 00:06:56 +00:00
|
|
|
)
|
|
|
|
);
|
|
|
|
$api = new ApiMain(
|
|
|
|
$context,
|
2020-08-19 20:03:41 +00:00
|
|
|
/* enable write? */ true
|
|
|
|
);
|
|
|
|
|
|
|
|
$api->execute();
|
|
|
|
|
|
|
|
// TODO: Tags are only added by 'dttags' existing on the original request
|
|
|
|
// context (see Hook::onRecentChangeSave). What tags (if any) should be
|
|
|
|
// added in this API?
|
|
|
|
|
|
|
|
$data = $api->getResult()->getResultData();
|
|
|
|
$result = $data['visualeditoredit'];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2021-01-25 23:02:15 +00:00
|
|
|
if ( !isset( $result['newrevid'] ) && isset( $result['result'] ) && $result['result'] === 'success' ) {
|
2020-11-18 19:40:05 +00:00
|
|
|
// No new revision, so no changes were made to the page (null edit).
|
|
|
|
// Comment was not actually saved, so for this API, that's an error.
|
|
|
|
// This is probably because changes were inside a transclusion's HTML?
|
|
|
|
$this->dieWithError( 'discussiontools-error-comment-not-saved', 'comment-comment-not-saved' );
|
|
|
|
}
|
|
|
|
|
2020-08-19 20:03:41 +00:00
|
|
|
$this->getResult()->addValue( null, $this->getModuleName(), $result );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function getAllowedParams() {
|
|
|
|
return [
|
|
|
|
'paction' => [
|
|
|
|
ParamValidator::PARAM_REQUIRED => true,
|
|
|
|
ParamValidator::PARAM_TYPE => [
|
|
|
|
'addcomment',
|
2020-08-06 14:22:47 +00:00
|
|
|
'addtopic',
|
2020-08-19 20:03:41 +00:00
|
|
|
],
|
|
|
|
ApiBase::PARAM_HELP_MSG => 'apihelp-visualeditoredit-param-paction',
|
2021-02-10 23:05:38 +00:00
|
|
|
ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
|
2020-08-19 20:03:41 +00:00
|
|
|
],
|
|
|
|
'page' => [
|
|
|
|
ParamValidator::PARAM_REQUIRED => true,
|
|
|
|
ApiBase::PARAM_HELP_MSG => 'apihelp-visualeditoredit-param-page',
|
|
|
|
],
|
|
|
|
'token' => [
|
|
|
|
ParamValidator::PARAM_REQUIRED => true,
|
|
|
|
],
|
2021-02-12 19:16:13 +00:00
|
|
|
'commentname' => null,
|
2020-08-19 20:03:41 +00:00
|
|
|
'commentid' => null,
|
|
|
|
'wikitext' => [
|
|
|
|
ParamValidator::PARAM_TYPE => 'text',
|
|
|
|
ParamValidator::PARAM_DEFAULT => null,
|
|
|
|
],
|
|
|
|
'html' => [
|
|
|
|
ParamValidator::PARAM_TYPE => 'text',
|
|
|
|
ParamValidator::PARAM_DEFAULT => null,
|
|
|
|
],
|
2020-08-25 12:31:54 +00:00
|
|
|
'summary' => [
|
2020-09-14 20:44:03 +00:00
|
|
|
ParamValidator::PARAM_TYPE => 'string',
|
2020-08-25 12:31:54 +00:00
|
|
|
ParamValidator::PARAM_DEFAULT => null,
|
|
|
|
ApiBase::PARAM_HELP_MSG => 'apihelp-visualeditoredit-param-summary',
|
|
|
|
],
|
2020-08-06 14:22:47 +00:00
|
|
|
'sectiontitle' => [
|
2020-09-14 20:44:03 +00:00
|
|
|
ParamValidator::PARAM_TYPE => 'string',
|
2020-08-06 14:22:47 +00:00
|
|
|
],
|
2020-08-19 20:03:41 +00:00
|
|
|
'watchlist' => [
|
|
|
|
ApiBase::PARAM_HELP_MSG => 'apihelp-edit-param-watchlist',
|
|
|
|
],
|
|
|
|
'captchaid' => [
|
2020-08-25 12:24:31 +00:00
|
|
|
ApiBase::PARAM_HELP_MSG => 'apihelp-visualeditoredit-param-captchaid',
|
2020-08-19 20:03:41 +00:00
|
|
|
],
|
|
|
|
'captchaword' => [
|
|
|
|
ApiBase::PARAM_HELP_MSG => 'apihelp-visualeditoredit-param-captchaword',
|
|
|
|
],
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function needsToken() {
|
|
|
|
return 'csrf';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function isInternal() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function isWriteMode() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|