Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F1432622
ApiDiscussionToolsEdit.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
18 KB
Referenced Files
None
Subscribers
None
ApiDiscussionToolsEdit.php
View Options
<?php
namespace
MediaWiki\Extension\DiscussionTools
;
use
MediaWiki\Api\ApiBase
;
use
MediaWiki\Api\ApiMain
;
use
MediaWiki\Api\ApiUsageException
;
use
MediaWiki\Config\Config
;
use
MediaWiki\Config\ConfigFactory
;
use
MediaWiki\Context\DerivativeContext
;
use
MediaWiki\Extension\DiscussionTools\Hooks\HookUtils
;
use
MediaWiki\Extension\DiscussionTools\ThreadItem\ContentCommentItem
;
use
MediaWiki\Extension\VisualEditor\ApiParsoidTrait
;
use
MediaWiki\Extension\VisualEditor\VisualEditorParsoidClientFactory
;
use
MediaWiki\Logger\LoggerFactory
;
use
MediaWiki\Request\DerivativeRequest
;
use
MediaWiki\Revision\RevisionLookup
;
use
MediaWiki\Title\Title
;
use
MediaWiki\User\TempUser\TempUserCreator
;
use
MediaWiki\User\UserFactory
;
use
SkinFactory
;
use
Wikimedia\Assert\Assert
;
use
Wikimedia\ParamValidator\ParamValidator
;
use
Wikimedia\ParamValidator\TypeDef\StringDef
;
use
Wikimedia\Parsoid\Utils\DOMCompat
;
use
Wikimedia\Parsoid\Utils\DOMUtils
;
class
ApiDiscussionToolsEdit
extends
ApiBase
{
use
ApiDiscussionToolsTrait
;
use
ApiParsoidTrait
;
private
CommentParser
$commentParser
;
private
VisualEditorParsoidClientFactory
$parsoidClientFactory
;
private
SubscriptionStore
$subscriptionStore
;
private
TempUserCreator
$tempUserCreator
;
private
UserFactory
$userFactory
;
private
SkinFactory
$skinFactory
;
private
Config
$config
;
private
RevisionLookup
$revisionLookup
;
public
function
__construct
(
ApiMain
$main
,
string
$name
,
VisualEditorParsoidClientFactory
$parsoidClientFactory
,
CommentParser
$commentParser
,
SubscriptionStore
$subscriptionStore
,
TempUserCreator
$tempUserCreator
,
UserFactory
$userFactory
,
SkinFactory
$skinFactory
,
ConfigFactory
$configFactory
,
RevisionLookup
$revisionLookup
)
{
parent
::
__construct
(
$main
,
$name
);
$this
->
parsoidClientFactory
=
$parsoidClientFactory
;
$this
->
commentParser
=
$commentParser
;
$this
->
subscriptionStore
=
$subscriptionStore
;
$this
->
tempUserCreator
=
$tempUserCreator
;
$this
->
userFactory
=
$userFactory
;
$this
->
skinFactory
=
$skinFactory
;
$this
->
config
=
$configFactory
->
makeConfig
(
'discussiontools'
);
$this
->
revisionLookup
=
$revisionLookup
;
$this
->
setLogger
(
LoggerFactory
::
getInstance
(
'DiscussionTools'
)
);
}
/**
* @inheritDoc
* @throws ApiUsageException
*/
public
function
execute
()
{
$params
=
$this
->
extractRequestParams
();
$title
=
Title
::
newFromText
(
$params
[
'page'
]
);
$result
=
null
;
if
(
!
$title
)
{
$this
->
dieWithError
(
[
'apierror-invalidtitle'
,
wfEscapeWikiText
(
$params
[
'page'
]
)
]
);
}
$autoSubscribe
=
$params
[
'autosubscribe'
]
===
'yes'
||
(
$this
->
config
->
get
(
'DiscussionToolsAutoTopicSubEditor'
)
===
'discussiontoolsapi'
&&
HookUtils
::
shouldAddAutoSubscription
(
$this
->
getUser
(),
$title
)
&&
$params
[
'autosubscribe'
]
===
'default'
);
$subscribableHeadingName
=
null
;
$subscribableSectionTitle
=
''
;
$this
->
getErrorFormatter
()->
setContextTitle
(
$title
);
$session
=
null
;
$usedFormTokensKey
=
'DiscussionTools:usedFormTokens'
;
$formToken
=
$params
[
'formtoken'
];
if
(
$formToken
)
{
$session
=
$this
->
getContext
()->
getRequest
()->
getSession
();
$usedFormTokens
=
$session
->
get
(
$usedFormTokensKey
)
??
[];
if
(
in_array
(
$formToken
,
$usedFormTokens
,
true
)
)
{
$this
->
dieWithError
(
[
'apierror-discussiontools-formtoken-used'
]
);
}
}
$this
->
requireOnlyOneParameter
(
$params
,
'wikitext'
,
'html'
);
if
(
$params
[
'paction'
]
===
'addtopic'
)
{
$this
->
requireAtLeastOneParameter
(
$params
,
'sectiontitle'
);
}
// To determine if we need to add a signature,
// preview the comment without adding one and check if the result is signed properly.
$previewResult
=
$this
->
previewMessage
(
$params
[
'paction'
]
===
'addtopic'
?
'topic'
:
'reply'
,
$title
,
[
'wikitext'
=>
$params
[
'wikitext'
],
'html'
=>
$params
[
'html'
],
'sectiontitle'
=>
$params
[
'sectiontitle'
],
],
$params
);
$previewResultHtml
=
$previewResult
->
getResultData
(
[
'parse'
,
'text'
]
);
$previewContainer
=
DOMCompat
::
getBody
(
DOMUtils
::
parseHTML
(
$previewResultHtml
)
);
$previewThreadItemSet
=
$this
->
commentParser
->
parse
(
$previewContainer
,
$title
->
getTitleValue
()
);
if
(
CommentUtils
::
isSingleCommentSignedBy
(
$previewThreadItemSet
,
$this
->
getUserForPreview
()->
getName
(),
$previewContainer
)
)
{
$signature
=
null
;
}
else
{
$signature
=
$this
->
msg
(
'discussiontools-signature-prefix'
)->
inContentLanguage
()->
text
()
.
'~~~~'
;
}
switch
(
$params
[
'paction'
]
)
{
case
'addtopic'
:
$wikitext
=
$params
[
'wikitext'
];
$html
=
$params
[
'html'
];
$previewHeading
=
null
;
$previewHeadings
=
$previewThreadItemSet
->
getThreads
();
if
(
count
(
$previewHeadings
)
>
0
&&
!
$previewHeadings
[
0
]->
isPlaceholderHeading
()
)
{
$previewHeading
=
$previewHeadings
[
0
];
}
if
(
!
$params
[
'allownosectiontitle'
]
)
{
// Check if the preview HTML starts with a section title. Note that even if the provided
// 'sectiontitle' param is empty, a heading could been included in the message body, and
// that's acceptable (T338390). Heading levels other than the default level 2 are also
// acceptable (T267288).
if
(
!
$previewHeading
)
{
$this
->
dieWithError
(
[
'discussiontools-newtopic-missing-title'
]
);
}
}
if
(
isset
(
$params
[
'summary'
]
)
)
{
$summary
=
$params
[
'summary'
];
}
else
{
// Generate an edit summary from the heading in the preview HTML, rather than from the
// 'sectiontitle' param like the action=edit API would. This has two benefits:
// * Works when the heading is included in the message body instead of the param (T338390)
// * Works better for complicated markup in the heading, e.g. templates (T335200)
if
(
$previewHeading
)
{
$sectionTitle
=
$previewHeading
->
getLinkableTitle
();
$summary
=
$this
->
msg
(
'newsectionsummary'
)->
plaintextParams
(
$sectionTitle
)
->
inContentLanguage
()->
text
();
}
else
{
// TODO: Should we generate something here? (T275702)
$summary
=
''
;
}
}
if
(
$wikitext
!==
null
)
{
if
(
$signature
!==
null
)
{
$wikitext
=
CommentModifier
::
appendSignatureWikitext
(
$wikitext
,
$signature
);
}
}
else
{
$doc
=
DOMUtils
::
parseHTML
(
''
);
$container
=
DOMUtils
::
parseHTMLToFragment
(
$doc
,
$html
);
if
(
$signature
!==
null
)
{
CommentModifier
::
appendSignature
(
$container
,
$signature
);
}
$html
=
DOMUtils
::
getFragmentInnerHTML
(
$container
);
$wikitext
=
$this
->
transformHTML
(
$title
,
$html
)[
'body'
];
}
$mobileFormatParams
=
[];
// Boolean parameters must be omitted completely to be treated as false.
// Param is added by hook in MobileFrontend, so it may be unset.
if
(
isset
(
$params
[
'mobileformat'
]
)
&&
$params
[
'mobileformat'
]
)
{
$mobileFormatParams
[
'mobileformat'
]
=
'1'
;
}
// 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
$context
=
new
DerivativeContext
(
$this
->
getContext
()
);
$context
->
setRequest
(
new
DerivativeRequest
(
$context
->
getRequest
(),
[
'action'
=>
'visualeditoredit'
,
'paction'
=>
'save'
,
'page'
=>
$params
[
'page'
],
'token'
=>
$params
[
'token'
],
'wikitext'
=>
$wikitext
,
'summary'
=>
$summary
,
'section'
=>
'new'
,
'sectiontitle'
=>
$params
[
'sectiontitle'
],
'starttimestamp'
=>
wfTimestampNow
(),
'useskin'
=>
$params
[
'useskin'
],
'watchlist'
=>
$params
[
'watchlist'
],
'captchaid'
=>
$params
[
'captchaid'
],
'captchaword'
=>
$params
[
'captchaword'
],
// Always fetch content if auto-subscribing, it's needed below (T359751)
'nocontent'
=>
$autoSubscribe
?
null
:
$params
[
'nocontent'
],
// NOTE: Must use getText() to work; PHP array from $params['tags'] is not understood
// by the visualeditoredit API.
'tags'
=>
$this
->
getRequest
()->
getText
(
'tags'
),
'returnto'
=>
$params
[
'returnto'
],
'returntoquery'
=>
$params
[
'returntoquery'
],
'returntoanchor'
=>
$params
[
'returntoanchor'
],
]
+
$mobileFormatParams
,
/* was posted? */
true
)
);
$api
=
new
ApiMain
(
$context
,
/* enable write? */
true
);
$api
->
execute
();
$data
=
$api
->
getResult
()->
getResultData
();
$result
=
$data
[
'visualeditoredit'
];
if
(
$autoSubscribe
&&
isset
(
$result
[
'content'
]
)
)
{
// Determining the added topic's name directly is hard (we'd have to ensure we have the
// same timestamp, and replicate some CommentParser stuff). Just pull it out of the response.
$doc
=
DOMUtils
::
parseHTML
(
$result
[
'content'
]
);
$container
=
DOMCompat
::
getBody
(
$doc
);
$threadItemSet
=
$this
->
commentParser
->
parse
(
$container
,
$title
->
getTitleValue
()
);
$threads
=
$threadItemSet
->
getThreads
();
if
(
count
(
$threads
)
)
{
$lastHeading
=
end
(
$threads
);
$subscribableHeadingName
=
$lastHeading
->
getName
();
$subscribableSectionTitle
=
$lastHeading
->
getLinkableTitle
();
}
if
(
$params
[
'nocontent'
]
)
{
// We had to fetch content even if not requested by the caller (T359751), but pretend we didn't
unset
(
$result
[
'content'
]
);
$result
[
'nocontent'
]
=
true
;
}
}
break
;
case
'addcomment'
:
$this
->
requireAtLeastOneParameter
(
$params
,
'commentid'
,
'commentname'
);
$commentId
=
$params
[
'commentid'
]
??
null
;
$commentName
=
$params
[
'commentname'
]
??
null
;
if
(
!
$title
->
exists
()
)
{
// The page does not exist, so the comment we're trying to reply to can't exist either.
if
(
$commentId
)
{
$this
->
dieWithError
(
[
'apierror-discussiontools-commentid-notfound'
,
$commentId
]
);
}
else
{
$this
->
dieWithError
(
[
'apierror-discussiontools-commentname-notfound'
,
$commentName
]
);
}
}
// Fetch the latest revision
$requestedRevision
=
$this
->
revisionLookup
->
getRevisionByTitle
(
$title
);
if
(
!
$requestedRevision
)
{
$this
->
dieWithError
(
[
'apierror-missingrev-title'
,
wfEscapeWikiText
(
$title
->
getPrefixedText
()
)
],
'nosuchrevid'
);
}
$response
=
$this
->
requestRestbasePageHtml
(
$requestedRevision
);
$headers
=
$response
[
'headers'
];
$doc
=
DOMUtils
::
parseHTML
(
$response
[
'body'
]
);
// Validate that we got the revision we requested.
// 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
];
}
}
Assert
::
postcondition
(
$docRevId
!==
null
,
'Parsoid document had no revision information'
);
Assert
::
postcondition
(
$docRevId
===
$requestedRevision
->
getId
(),
'Parsoid revision did not match requested revision'
);
$container
=
DOMCompat
::
getBody
(
$doc
);
// Unwrap sections, so that transclusions overlapping section boundaries don't cause all
// comments in the sections to be treated as transcluded from another page.
CommentUtils
::
unwrapParsoidSections
(
$container
);
$threadItemSet
=
$this
->
commentParser
->
parse
(
$container
,
$title
->
getTitleValue
()
);
if
(
$commentId
)
{
$comment
=
$threadItemSet
->
findCommentById
(
$commentId
);
if
(
!
$comment
||
!(
$comment
instanceof
ContentCommentItem
)
)
{
$this
->
dieWithError
(
[
'apierror-discussiontools-commentid-notfound'
,
$commentId
]
);
}
}
else
{
$comments
=
$threadItemSet
->
findCommentsByName
(
$commentName
);
$comment
=
$comments
[
0
]
??
null
;
if
(
count
(
$comments
)
>
1
)
{
$this
->
dieWithError
(
[
'apierror-discussiontools-commentname-ambiguous'
,
$commentName
]
);
}
elseif
(
!
$comment
||
!(
$comment
instanceof
ContentCommentItem
)
)
{
$this
->
dieWithError
(
[
'apierror-discussiontools-commentname-notfound'
,
$commentName
]
);
}
}
if
(
$comment
->
getTranscludedFrom
()
)
{
// Replying to transcluded comments should not be possible. We check this client-side, but
// the comment might have become transcluded in the meantime (T268069), so check again. We
// didn't have this check before T313100, since usually the reply would just disappear in
// Parsoid, but now it would be placed after the transclusion, which would be wrong.
$this
->
dieWithError
(
'discussiontools-error-comment-not-saved'
,
'comment-became-transcluded'
);
}
if
(
$params
[
'wikitext'
]
!==
null
)
{
CommentModifier
::
addWikitextReply
(
$comment
,
$params
[
'wikitext'
],
$signature
);
}
else
{
CommentModifier
::
addHtmlReply
(
$comment
,
$params
[
'html'
],
$signature
);
}
if
(
isset
(
$params
[
'summary'
]
)
)
{
$summary
=
$params
[
'summary'
];
}
else
{
$sectionTitle
=
$comment
->
getHeading
()->
getLinkableTitle
();
$summary
=
(
$sectionTitle
?
'/* '
.
$sectionTitle
.
' */ '
:
''
)
.
$this
->
msg
(
'discussiontools-defaultsummary-reply'
)->
inContentLanguage
()->
text
();
}
if
(
$autoSubscribe
)
{
$heading
=
$comment
->
getSubscribableHeading
();
if
(
$heading
)
{
$subscribableHeadingName
=
$heading
->
getName
();
$subscribableSectionTitle
=
$heading
->
getLinkableTitle
();
}
}
$context
=
new
DerivativeContext
(
$this
->
getContext
()
);
$context
->
setRequest
(
new
DerivativeRequest
(
$context
->
getRequest
(),
[
'action'
=>
'visualeditoredit'
,
'paction'
=>
'save'
,
'page'
=>
$params
[
'page'
],
'token'
=>
$params
[
'token'
],
'oldid'
=>
$docRevId
,
'html'
=>
DOMCompat
::
getOuterHTML
(
$doc
->
documentElement
),
'summary'
=>
$summary
,
'baserevid'
=>
$docRevId
,
'starttimestamp'
=>
wfTimestampNow
(),
'etag'
=>
$headers
[
'etag'
]
??
null
,
'useskin'
=>
$params
[
'useskin'
],
'watchlist'
=>
$params
[
'watchlist'
],
'captchaid'
=>
$params
[
'captchaid'
],
'captchaword'
=>
$params
[
'captchaword'
],
'nocontent'
=>
$params
[
'nocontent'
],
// NOTE: Must use getText() to work; PHP array from $params['tags'] is not understood
// by the visualeditoredit API.
'tags'
=>
$this
->
getRequest
()->
getText
(
'tags'
),
'returnto'
=>
$params
[
'returnto'
],
'returntoquery'
=>
$params
[
'returntoquery'
],
'returntoanchor'
=>
$params
[
'returntoanchor'
],
],
/* was posted? */
true
)
);
$api
=
new
ApiMain
(
$context
,
/* 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
;
}
if
(
!
isset
(
$result
[
'newrevid'
]
)
&&
isset
(
$result
[
'result'
]
)
&&
$result
[
'result'
]
===
'success'
)
{
// 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 should not be possible after T313100.
$this
->
dieWithError
(
'discussiontools-error-comment-not-saved'
,
'comment-comment-not-saved'
);
}
if
(
$autoSubscribe
&&
$subscribableHeadingName
)
{
$subsTitle
=
$title
->
createFragmentTarget
(
$subscribableSectionTitle
);
$this
->
subscriptionStore
->
addAutoSubscriptionForUser
(
$this
->
getUser
(),
$subsTitle
,
$subscribableHeadingName
);
}
// Check the post was successful (could have been blocked by ConfirmEdit) before
// marking the form token as used.
if
(
$formToken
&&
isset
(
$result
[
'result'
]
)
&&
$result
[
'result'
]
===
'success'
)
{
$usedFormTokens
[]
=
$formToken
;
// Set an arbitrary limit of the number of form tokens to
// store to prevent session storage from becoming full.
// It is unlikely that form tokens other than the few most
// recently used will be needed.
while
(
count
(
$usedFormTokens
)
>
50
)
{
// Discard the oldest tokens first
array_shift
(
$usedFormTokens
);
}
$session
->
set
(
$usedFormTokensKey
,
$usedFormTokens
);
}
$this
->
getResult
()->
addValue
(
null
,
$this
->
getModuleName
(),
$result
);
}
/**
* @inheritDoc
*/
public
function
getAllowedParams
()
{
return
[
'paction'
=>
[
ParamValidator
::
PARAM_REQUIRED
=>
true
,
ParamValidator
::
PARAM_TYPE
=>
[
'addcomment'
,
'addtopic'
,
],
ApiBase
::
PARAM_HELP_MSG
=>
'apihelp-visualeditoredit-param-paction'
,
ApiBase
::
PARAM_HELP_MSG_PER_VALUE
=>
[],
],
'autosubscribe'
=>
[
ParamValidator
::
PARAM_TYPE
=>
[
'yes'
,
'no'
,
'default'
],
ParamValidator
::
PARAM_DEFAULT
=>
'default'
,
],
'page'
=>
[
ParamValidator
::
PARAM_REQUIRED
=>
true
,
ApiBase
::
PARAM_HELP_MSG
=>
'apihelp-visualeditoredit-param-page'
,
],
'token'
=>
[
ParamValidator
::
PARAM_REQUIRED
=>
true
,
],
'formtoken'
=>
[
ParamValidator
::
PARAM_TYPE
=>
'string'
,
StringDef
::
PARAM_MAX_CHARS
=>
16
,
],
'commentname'
=>
null
,
'commentid'
=>
null
,
'wikitext'
=>
[
ParamValidator
::
PARAM_TYPE
=>
'text'
,
ParamValidator
::
PARAM_DEFAULT
=>
null
,
],
'html'
=>
[
ParamValidator
::
PARAM_TYPE
=>
'text'
,
ParamValidator
::
PARAM_DEFAULT
=>
null
,
],
'summary'
=>
[
ParamValidator
::
PARAM_TYPE
=>
'string'
,
ParamValidator
::
PARAM_DEFAULT
=>
null
,
ApiBase
::
PARAM_HELP_MSG
=>
'apihelp-visualeditoredit-param-summary'
,
],
'sectiontitle'
=>
[
ParamValidator
::
PARAM_TYPE
=>
'string'
,
],
'allownosectiontitle'
=>
false
,
'useskin'
=>
[
ParamValidator
::
PARAM_TYPE
=>
array_keys
(
$this
->
skinFactory
->
getInstalledSkins
()
),
ApiBase
::
PARAM_HELP_MSG
=>
'apihelp-parse-param-useskin'
,
],
'watchlist'
=>
[
ApiBase
::
PARAM_HELP_MSG
=>
'apihelp-edit-param-watchlist'
,
],
'captchaid'
=>
[
ApiBase
::
PARAM_HELP_MSG
=>
'apihelp-visualeditoredit-param-captchaid'
,
],
'captchaword'
=>
[
ApiBase
::
PARAM_HELP_MSG
=>
'apihelp-visualeditoredit-param-captchaword'
,
],
'nocontent'
=>
[
ApiBase
::
PARAM_HELP_MSG
=>
'apihelp-visualeditoredit-param-nocontent'
,
],
'tags'
=>
[
ParamValidator
::
PARAM_ISMULTI
=>
true
,
ApiBase
::
PARAM_HELP_MSG
=>
'apihelp-visualeditoredit-param-tags'
,
],
'returnto'
=>
[
ParamValidator
::
PARAM_TYPE
=>
'title'
,
ApiBase
::
PARAM_HELP_MSG
=>
'apihelp-edit-param-returnto'
,
],
'returntoquery'
=>
[
ParamValidator
::
PARAM_TYPE
=>
'string'
,
ParamValidator
::
PARAM_DEFAULT
=>
''
,
ApiBase
::
PARAM_HELP_MSG
=>
'apihelp-edit-param-returntoquery'
,
],
'returntoanchor'
=>
[
ParamValidator
::
PARAM_TYPE
=>
'string'
,
ParamValidator
::
PARAM_DEFAULT
=>
''
,
ApiBase
::
PARAM_HELP_MSG
=>
'apihelp-edit-param-returntoanchor'
,
],
];
}
/**
* @inheritDoc
*/
public
function
needsToken
()
{
return
'csrf'
;
}
/**
* @inheritDoc
*/
public
function
isWriteMode
()
{
return
true
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, May 16, 22:05 (1 d, 2 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
12/dd/05581c0d8b9743af45c7b7e67674
Default Alt Text
ApiDiscussionToolsEdit.php (18 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment