Merge "Generate form tokens in the client to prevent double posting"

This commit is contained in:
jenkins-bot 2021-10-29 22:32:47 +00:00 committed by Gerrit Code Review
commit 10111ea872
5 changed files with 50 additions and 0 deletions

View file

@ -5,6 +5,7 @@
"apierror-discussiontools-commentid-notfound": "Comment with the ID '$1' not found.", "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-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-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-add": "Could not subscribe to this topic.",
"apierror-discussiontools-subscription-failed-remove": "Could not unsubscribe from 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).", "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-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-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-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-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-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>.", "apihelp-discussiontoolsedit-param-wikitext": "Content to post, as wikitext. Cannot be used together with <var>html</var>.",

View file

@ -7,6 +7,7 @@
"apierror-discussiontools-commentid-notfound": "{{doc-apierror}}\n\nParameters:\n* $1 - Comment ID", "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-ambiguous": "{{doc-apierror}}\n\nParameters:\n* $1 - Comment name",
"apierror-discussiontools-commentname-notfound": "{{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-add": "{{doc-apierror}}",
"apierror-discussiontools-subscription-failed-remove": "{{doc-apierror}}", "apierror-discussiontools-subscription-failed-remove": "{{doc-apierror}}",
"apihelp-discussiontools-param-oldid": "{{doc-apihelp-param|discussiontools|oldid}}", "apihelp-discussiontools-param-oldid": "{{doc-apihelp-param|discussiontools|oldid}}",
@ -14,6 +15,7 @@
"apihelp-discussiontools-summary": "{{doc-apihelp-summary|discussiontools}}", "apihelp-discussiontools-summary": "{{doc-apihelp-summary|discussiontools}}",
"apihelp-discussiontoolsedit-param-commentid": "{{doc-apihelp-param|discussiontoolsedit|commentid}}", "apihelp-discussiontoolsedit-param-commentid": "{{doc-apihelp-param|discussiontoolsedit|commentid}}",
"apihelp-discussiontoolsedit-param-commentname": "{{doc-apihelp-param|discussiontoolsedit|commentname}}", "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-html": "{{doc-apihelp-param|discussiontoolsedit|html}}",
"apihelp-discussiontoolsedit-param-sectiontitle": "{{doc-apihelp-param|discussiontoolsedit|sectiontitle}}", "apihelp-discussiontoolsedit-param-sectiontitle": "{{doc-apihelp-param|discussiontoolsedit|sectiontitle}}",
"apihelp-discussiontoolsedit-param-wikitext": "{{doc-apihelp-param|discussiontoolsedit|wikitext}}", "apihelp-discussiontoolsedit-param-wikitext": "{{doc-apihelp-param|discussiontoolsedit|wikitext}}",

View file

@ -39,6 +39,17 @@ class ApiDiscussionToolsEdit extends ApiBase {
$this->getErrorFormatter()->setContextTitle( $title ); $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'] ) { switch ( $params['paction'] ) {
case 'addtopic': case 'addtopic':
$this->requireAtLeastOneParameter( $params, 'sectiontitle' ); $this->requireAtLeastOneParameter( $params, 'sectiontitle' );
@ -231,6 +242,21 @@ class ApiDiscussionToolsEdit extends ApiBase {
$this->dieWithError( 'discussiontools-error-comment-not-saved', 'comment-comment-not-saved' ); $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 ); $this->getResult()->addValue( null, $this->getModuleName(), $result );
} }
@ -255,6 +281,10 @@ class ApiDiscussionToolsEdit extends ApiBase {
'token' => [ 'token' => [
ParamValidator::PARAM_REQUIRED => true, ParamValidator::PARAM_REQUIRED => true,
], ],
'formtoken' => [
ApiBase::PARAM_TYPE => 'string',
ApiBase::PARAM_MAX_CHARS => 16,
],
'commentname' => null, 'commentname' => null,
'commentid' => null, 'commentid' => null,
'wikitext' => [ 'wikitext' => [

View file

@ -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 // Only specify this if necessary to disambiguate, to avoid errors if the parent changes
commentid: sameNameComments.length > 1 ? comment.id : undefined, commentid: sameNameComments.length > 1 ? comment.id : undefined,
summary: replyWidget.getEditSummary(), summary: replyWidget.getEditSummary(),
formtoken: replyWidget.getFormToken(),
assert: mw.user.isAnon() ? 'anon' : 'user', assert: mw.user.isAnon() ? 'anon' : 'user',
assertuser: mw.user.getName() || undefined, assertuser: mw.user.getName() || undefined,
uselang: mw.config.get( 'wgUserLanguage' ), uselang: mw.config.get( 'wgUserLanguage' ),

View file

@ -317,6 +317,7 @@ ReplyWidget.prototype.clearStorage = function () {
this.storage.remove( this.storagePrefix + '/saveable' ); this.storage.remove( this.storagePrefix + '/saveable' );
this.storage.remove( this.storagePrefix + '/summary' ); this.storage.remove( this.storagePrefix + '/summary' );
this.storage.remove( this.storagePrefix + '/showAdvanced' ); this.storage.remove( this.storagePrefix + '/showAdvanced' );
this.storage.remove( this.storagePrefix + '/formToken' );
this.emit( 'clearStorage' ); this.emit( 'clearStorage' );
}; };
@ -517,6 +518,20 @@ ReplyWidget.prototype.afterSetup = function () {
this.storage.set( this.storagePrefix + '/mode', this.getMode() ); 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. * Try to teardown the widget, prompting the user if unsaved changes will be lost.
* *