mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/DiscussionTools
synced 2024-11-28 10:11:45 +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-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>.",
|
||||||
|
|
|
@ -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}}",
|
||||||
|
|
|
@ -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' => [
|
||||||
|
|
|
@ -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' ),
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in a new issue