mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/DiscussionTools
synced 2024-11-24 08:23:52 +00:00
Merge "Generate form tokens in the client to prevent double posting"
This commit is contained in:
commit
10111ea872
|
@ -5,6 +5,7 @@
|
|||
"apierror-discussiontools-commentid-notfound": "Comment with the ID '$1' not found.",
|
||||
"apierror-discussiontools-commentname-ambiguous": "Multiple comments with the name '$1' found, <var>commentid</var> is required.",
|
||||
"apierror-discussiontools-commentname-notfound": "Comment with the name '$1' not found.",
|
||||
"apierror-discussiontools-formtoken-used": "Comment already posted successfully. Reload the page to see it.",
|
||||
"apierror-discussiontools-subscription-failed-add": "Could not subscribe to this topic.",
|
||||
"apierror-discussiontools-subscription-failed-remove": "Could not unsubscribe from this topic.",
|
||||
"apihelp-discussiontools-param-oldid": "The revision number to use (defaults to latest revision).",
|
||||
|
@ -12,6 +13,7 @@
|
|||
"apihelp-discussiontools-summary": "Returns metadata required to initialize the discussion tools.",
|
||||
"apihelp-discussiontoolsedit-param-commentid": "ID of the comment to reply to. Only used when <var>paction</var> is <var>addcomment</var>. Overrides <var>commentname</var>.",
|
||||
"apihelp-discussiontoolsedit-param-commentname": "Name of the comment to reply to. Only used when <var>paction</var> is <var>addcomment</var>.",
|
||||
"apihelp-discussiontoolsedit-param-formtoken": "An optional unique ID generated in the client to prevent double-posting.",
|
||||
"apihelp-discussiontoolsedit-param-html": "Content to post, as HTML. Cannot be used together with <var>wikitext</var>.",
|
||||
"apihelp-discussiontoolsedit-param-sectiontitle": "{{int:apihelp-edit-param-sectiontitle}} Only used when <var>paction</var> is <var>addtopic</var>.",
|
||||
"apihelp-discussiontoolsedit-param-wikitext": "Content to post, as wikitext. Cannot be used together with <var>html</var>.",
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
"apierror-discussiontools-commentid-notfound": "{{doc-apierror}}\n\nParameters:\n* $1 - Comment ID",
|
||||
"apierror-discussiontools-commentname-ambiguous": "{{doc-apierror}}\n\nParameters:\n* $1 - Comment name",
|
||||
"apierror-discussiontools-commentname-notfound": "{{doc-apierror}}\n\nParameters:\n* $1 - Comment name",
|
||||
"apierror-discussiontools-formtoken-used": "{{doc-apierror}}",
|
||||
"apierror-discussiontools-subscription-failed-add": "{{doc-apierror}}",
|
||||
"apierror-discussiontools-subscription-failed-remove": "{{doc-apierror}}",
|
||||
"apihelp-discussiontools-param-oldid": "{{doc-apihelp-param|discussiontools|oldid}}",
|
||||
|
@ -14,6 +15,7 @@
|
|||
"apihelp-discussiontools-summary": "{{doc-apihelp-summary|discussiontools}}",
|
||||
"apihelp-discussiontoolsedit-param-commentid": "{{doc-apihelp-param|discussiontoolsedit|commentid}}",
|
||||
"apihelp-discussiontoolsedit-param-commentname": "{{doc-apihelp-param|discussiontoolsedit|commentname}}",
|
||||
"apihelp-discussiontoolsedit-param-formtoken": "{{doc-apihelp-param|discussiontoolsedit|formtoken}}",
|
||||
"apihelp-discussiontoolsedit-param-html": "{{doc-apihelp-param|discussiontoolsedit|html}}",
|
||||
"apihelp-discussiontoolsedit-param-sectiontitle": "{{doc-apihelp-param|discussiontoolsedit|sectiontitle}}",
|
||||
"apihelp-discussiontoolsedit-param-wikitext": "{{doc-apihelp-param|discussiontoolsedit|wikitext}}",
|
||||
|
|
|
@ -39,6 +39,17 @@ class ApiDiscussionToolsEdit extends ApiBase {
|
|||
|
||||
$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 ) ) {
|
||||
$this->dieWithError( [ 'apierror-discussiontools-formtoken-used' ] );
|
||||
}
|
||||
}
|
||||
|
||||
switch ( $params['paction'] ) {
|
||||
case 'addtopic':
|
||||
$this->requireAtLeastOneParameter( $params, 'sectiontitle' );
|
||||
|
@ -231,6 +242,21 @@ class ApiDiscussionToolsEdit extends ApiBase {
|
|||
$this->dieWithError( 'discussiontools-error-comment-not-saved', 'comment-comment-not-saved' );
|
||||
}
|
||||
|
||||
// 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 );
|
||||
}
|
||||
|
||||
|
@ -255,6 +281,10 @@ class ApiDiscussionToolsEdit extends ApiBase {
|
|||
'token' => [
|
||||
ParamValidator::PARAM_REQUIRED => true,
|
||||
],
|
||||
'formtoken' => [
|
||||
ApiBase::PARAM_TYPE => 'string',
|
||||
ApiBase::PARAM_MAX_CHARS => 16,
|
||||
],
|
||||
'commentname' => null,
|
||||
'commentid' => null,
|
||||
'wikitext' => [
|
||||
|
|
|
@ -250,6 +250,7 @@ CommentController.prototype.getApiQuery = function ( comment, pageName, checkbox
|
|||
// Only specify this if necessary to disambiguate, to avoid errors if the parent changes
|
||||
commentid: sameNameComments.length > 1 ? comment.id : undefined,
|
||||
summary: replyWidget.getEditSummary(),
|
||||
formtoken: replyWidget.getFormToken(),
|
||||
assert: mw.user.isAnon() ? 'anon' : 'user',
|
||||
assertuser: mw.user.getName() || undefined,
|
||||
uselang: mw.config.get( 'wgUserLanguage' ),
|
||||
|
|
|
@ -317,6 +317,7 @@ ReplyWidget.prototype.clearStorage = function () {
|
|||
this.storage.remove( this.storagePrefix + '/saveable' );
|
||||
this.storage.remove( this.storagePrefix + '/summary' );
|
||||
this.storage.remove( this.storagePrefix + '/showAdvanced' );
|
||||
this.storage.remove( this.storagePrefix + '/formToken' );
|
||||
|
||||
this.emit( 'clearStorage' );
|
||||
};
|
||||
|
@ -517,6 +518,20 @@ ReplyWidget.prototype.afterSetup = function () {
|
|||
this.storage.set( this.storagePrefix + '/mode', this.getMode() );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a random token that is unique to this reply instance
|
||||
*
|
||||
* @return {string} Form token
|
||||
*/
|
||||
ReplyWidget.prototype.getFormToken = function () {
|
||||
var formToken = this.storage.get( this.storagePrefix + '/formToken' );
|
||||
if ( !formToken ) {
|
||||
formToken = Math.random().toString( 36 ).slice( 2 );
|
||||
this.storage.set( this.storagePrefix + '/formToken', formToken );
|
||||
}
|
||||
return formToken;
|
||||
};
|
||||
|
||||
/**
|
||||
* Try to teardown the widget, prompting the user if unsaved changes will be lost.
|
||||
*
|
||||
|
|
Loading…
Reference in a new issue