Import: Handle uploads with sha1 starting with 0 properly
[mediawiki.git] / resources / src / mediawiki / mediawiki.feedback.js
blob7afb9d30435292272cd3843e30f1f70d1885fa3f
1 /*!
2  * mediawiki.feedback
3  *
4  * @author Ryan Kaldari, 2010
5  * @author Neil Kandalgaonkar, 2010-11
6  * @author Moriel Schottlender, 2015
7  * @since 1.19
8  */
9 /*jshint es3:false */
10 /*global OO*/
11 ( function ( mw, $ ) {
12         /**
13          * This is a way of getting simple feedback from users. It's useful
14          * for testing new features -- users can give you feedback without
15          * the difficulty of opening a whole new talk page. For this reason,
16          * it also tends to collect a wider range of both positive and negative
17          * comments. However you do need to tend to the feedback page. It will
18          * get long relatively quickly, and you often get multiple messages
19          * reporting the same issue.
20          *
21          * It takes the form of thing on your page which, when clicked, opens a small
22          * dialog box. Submitting that dialog box appends its contents to a
23          * wiki page that you specify, as a new section.
24          *
25          * This feature works with any content model that defines a
26          * `mw.messagePoster.MessagePoster`.
27          *
28          * Minimal usage example:
29          *
30          *     var feedback = new mw.Feedback();
31          *     $( '#myButton' ).click( function () { feedback.launch(); } );
32          *
33          * You can also launch the feedback form with a prefilled subject and body.
34          * See the docs for the #launch() method.
35          *
36          * @class
37          * @constructor
38          * @param {Object} [config] Configuration object
39          * @cfg {mw.Title} [title="Feedback"] The title of the page where you collect
40          *  feedback.
41          * @cfg {string} [apiUrl] api.php URL if the feedback page is on another wiki
42          * @cfg {string} [dialogTitleMessageKey="feedback-dialog-title"] Message key for the
43          *  title of the dialog box
44          * @cfg {mw.Uri|string} [bugsLink="//phabricator.wikimedia.org/maniphest/task/create/"] URL where
45          *  bugs can be posted
46          * @cfg {mw.Uri|string} [bugsListLink="//phabricator.wikimedia.org/maniphest/query/advanced"] URL
47          *  where bugs can be listed
48          * @cfg {boolean} [showUseragentCheckbox=false] Show a Useragent agreement checkbox as part of the form.
49          * @cfg {boolean} [useragentCheckboxMandatory=false] Make the Useragent checkbox mandatory.
50          * @cfg {string|jQuery} [useragentCheckboxMessage] Supply a custom message for the useragent checkbox.
51          *  defaults to the message 'feedback-terms'.
52          */
53         mw.Feedback = function MwFeedback( config ) {
54                 config = config || {};
56                 this.dialogTitleMessageKey = config.dialogTitleMessageKey || 'feedback-dialog-title';
58                 // Feedback page title
59                 this.feedbackPageTitle = config.title || new mw.Title( 'Feedback' );
61                 this.messagePosterPromise = mw.messagePoster.factory.create( this.feedbackPageTitle, config.apiUrl );
63                 // Links
64                 this.bugsTaskSubmissionLink = config.bugsLink || '//phabricator.wikimedia.org/maniphest/task/create/';
65                 this.bugsTaskListLink = config.bugsListLink || '//phabricator.wikimedia.org/maniphest/query/advanced';
67                 // Terms of use
68                 this.useragentCheckboxShow = !!config.showUseragentCheckbox;
69                 this.useragentCheckboxMandatory = !!config.useragentCheckboxMandatory;
70                 this.useragentCheckboxMessage = config.useragentCheckboxMessage ||
71                         $( '<p>' ).append( mw.msg( 'feedback-terms' ) );
73                 // Message dialog
74                 this.thankYouDialog = new OO.ui.MessageDialog();
75         };
77         /* Initialize */
78         OO.initClass( mw.Feedback );
80         /* Static Properties */
81         mw.Feedback.static.windowManager = null;
82         mw.Feedback.static.dialog = null;
84         /* Methods */
86         /**
87          * Respond to dialog submit event. If the information was
88          * submitted, either successfully or with an error, open
89          * a MessageDialog to thank the user.
90          *
91          * @param {string} [status] A status of the end of operation
92          *  of the main feedback dialog. Empty if the dialog was
93          *  dismissed with no action or the user followed the button
94          *  to the external task reporting site.
95          */
96         mw.Feedback.prototype.onDialogSubmit = function ( status ) {
97                 var dialogConfig = {};
98                 switch ( status ) {
99                         case 'submitted':
100                                 dialogConfig = {
101                                         title: mw.msg( 'feedback-thanks-title' ),
102                                         message: $( '<span>' ).msg(
103                                                 'feedback-thanks',
104                                                 this.feedbackPageTitle.getNameText(),
105                                                 $( '<a>' ).attr( {
106                                                         target: '_blank',
107                                                         href: this.feedbackPageTitle.getUrl()
108                                                 } )
109                                         ),
110                                         actions: [
111                                                 {
112                                                         action: 'accept',
113                                                         label: mw.msg( 'feedback-close' ),
114                                                         flags: 'primary'
115                                                 }
116                                         ]
117                                 };
118                                 break;
119                         case 'error1':
120                         case 'error2':
121                         case 'error3':
122                         case 'error4':
123                                 dialogConfig = {
124                                         title: mw.msg( 'feedback-error-title' ),
125                                         message: mw.msg( 'feedback-' + status ),
126                                         actions: [
127                                                 {
128                                                         action: 'accept',
129                                                         label: mw.msg( 'feedback-close' ),
130                                                         flags: 'primary'
131                                                 }
132                                         ]
133                                 };
134                                 break;
135                 }
137                 // Show the message dialog
138                 if ( !$.isEmptyObject( dialogConfig ) ) {
139                         this.constructor.static.windowManager.openWindow(
140                                 this.thankYouDialog,
141                                 dialogConfig
142                         );
143                 }
144         };
146         /**
147          * Modify the display form, and then open it, focusing interface on the subject.
148          *
149          * @param {Object} [contents] Prefilled contents for the feedback form.
150          * @param {string} [contents.subject] The subject of the feedback, as plaintext
151          * @param {string} [contents.message] The content of the feedback, as wikitext
152          */
153         mw.Feedback.prototype.launch = function ( contents ) {
154                 // Dialog
155                 if ( !this.constructor.static.dialog ) {
156                         this.constructor.static.dialog = new mw.Feedback.Dialog();
157                         this.constructor.static.dialog.connect( this, { submit: 'onDialogSubmit' } );
158                 }
159                 if ( !this.constructor.static.windowManager ) {
160                         this.constructor.static.windowManager = new OO.ui.WindowManager();
161                         this.constructor.static.windowManager.addWindows( [
162                                 this.constructor.static.dialog,
163                                 this.thankYouDialog
164                         ] );
165                         $( 'body' )
166                                 .append( this.constructor.static.windowManager.$element );
167                 }
168                 // Open the dialog
169                 this.constructor.static.windowManager.openWindow(
170                         this.constructor.static.dialog,
171                         {
172                                 title: mw.msg( this.dialogTitleMessageKey ),
173                                 settings: {
174                                         messagePosterPromise: this.messagePosterPromise,
175                                         title: this.feedbackPageTitle,
176                                         dialogTitleMessageKey: this.dialogTitleMessageKey,
177                                         bugsTaskSubmissionLink: this.bugsTaskSubmissionLink,
178                                         bugsTaskListLink: this.bugsTaskListLink,
179                                         useragentCheckbox: {
180                                                 show: this.useragentCheckboxShow,
181                                                 mandatory: this.useragentCheckboxMandatory,
182                                                 message: this.useragentCheckboxMessage
183                                         }
184                                 },
185                                 contents: contents
186                         }
187                 );
188         };
190         /**
191          * mw.Feedback Dialog
192          *
193          * @class
194          * @extends OO.ui.ProcessDialog
195          *
196          * @constructor
197          * @param {Object} config Configuration object
198          */
199         mw.Feedback.Dialog = function mwFeedbackDialog( config ) {
200                 // Parent constructor
201                 mw.Feedback.Dialog.parent.call( this, config );
203                 this.status = '';
204                 this.feedbackPageTitle = null;
205                 // Initialize
206                 this.$element.addClass( 'mwFeedback-Dialog' );
207         };
209         OO.inheritClass( mw.Feedback.Dialog, OO.ui.ProcessDialog );
211         /* Static properties */
212         mw.Feedback.Dialog.static.name = 'mwFeedbackDialog';
213         mw.Feedback.Dialog.static.title = mw.msg( 'feedback-dialog-title' );
214         mw.Feedback.Dialog.static.size = 'medium';
215         mw.Feedback.Dialog.static.actions = [
216                 {
217                         action: 'submit',
218                         label: mw.msg( 'feedback-submit' ),
219                         flags: [ 'primary', 'constructive' ]
220                 },
221                 {
222                         action: 'external',
223                         label: mw.msg( 'feedback-external-bug-report-button' ),
224                         flags: 'constructive'
225                 },
226                 {
227                         action: 'cancel',
228                         label: mw.msg( 'feedback-cancel' ),
229                         flags: 'safe'
230                 }
231         ];
233         /**
234          * @inheritdoc
235          */
236         mw.Feedback.Dialog.prototype.initialize = function () {
237                 var feedbackSubjectFieldLayout, feedbackMessageFieldLayout,
238                         feedbackFieldsetLayout, termsOfUseLabel;
240                 // Parent method
241                 mw.Feedback.Dialog.parent.prototype.initialize.call( this );
243                 this.feedbackPanel = new OO.ui.PanelLayout( {
244                         scrollable: false,
245                         expanded: false,
246                         padded: true
247                 } );
249                 this.$spinner = $( '<div>' )
250                         .addClass( 'feedback-spinner' );
252                 // Feedback form
253                 this.feedbackMessageLabel = new OO.ui.LabelWidget( {
254                         classes: [ 'mw-feedbackDialog-welcome-message' ]
255                 } );
256                 this.feedbackSubjectInput = new OO.ui.TextInputWidget( {
257                         multiline: false
258                 } );
259                 this.feedbackMessageInput = new OO.ui.TextInputWidget( {
260                         autosize: true,
261                         multiline: true
262                 } );
263                 feedbackSubjectFieldLayout = new OO.ui.FieldLayout( this.feedbackSubjectInput, {
264                         label: mw.msg( 'feedback-subject' )
265                 } );
266                 feedbackMessageFieldLayout = new OO.ui.FieldLayout( this.feedbackMessageInput, {
267                         label: mw.msg( 'feedback-message' )
268                 } );
269                 feedbackFieldsetLayout = new OO.ui.FieldsetLayout( {
270                         items: [ feedbackSubjectFieldLayout, feedbackMessageFieldLayout ],
271                         classes: [ 'mw-feedbackDialog-feedback-form' ]
272                 } );
274                 // Useragent terms of use
275                 this.useragentCheckbox = new OO.ui.CheckboxInputWidget();
276                 this.useragentFieldLayout = new OO.ui.FieldLayout( this.useragentCheckbox, {
277                         classes: [ 'mw-feedbackDialog-feedback-terms' ],
278                         align: 'inline'
279                 } );
281                 termsOfUseLabel = new OO.ui.LabelWidget( {
282                         classes: [ 'mw-feedbackDialog-feedback-termsofuse' ],
283                         label: $( '<p>' ).append( mw.msg( 'feedback-termsofuse' ) )
284                 } );
286                 this.feedbackPanel.$element.append(
287                         this.feedbackMessageLabel.$element,
288                         feedbackFieldsetLayout.$element,
289                         this.useragentFieldLayout.$element,
290                         termsOfUseLabel.$element
291                 );
293                 // Events
294                 this.feedbackSubjectInput.connect( this, { change: 'validateFeedbackForm' } );
295                 this.feedbackMessageInput.connect( this, { change: 'validateFeedbackForm' } );
296                 this.feedbackMessageInput.connect( this, { change: 'updateSize' } );
297                 this.useragentCheckbox.connect( this, { change: 'validateFeedbackForm' } );
299                 this.$body.append( this.feedbackPanel.$element );
300         };
302         /**
303          * Validate the feedback form
304          */
305         mw.Feedback.Dialog.prototype.validateFeedbackForm = function () {
306                 var isValid = (
307                                 (
308                                         !this.useragentMandatory ||
309                                         this.useragentCheckbox.isSelected()
310                                 ) &&
311                                 (
312                                         !!this.feedbackMessageInput.getValue() ||
313                                         !!this.feedbackSubjectInput.getValue()
314                                 )
315                         );
317                 this.actions.setAbilities( { submit:  isValid } );
318         };
320         /**
321          * @inheritdoc
322          */
323         mw.Feedback.Dialog.prototype.getBodyHeight = function () {
324                 return this.feedbackPanel.$element.outerHeight( true );
325         };
327         /**
328          * @inheritdoc
329          */
330         mw.Feedback.Dialog.prototype.getSetupProcess = function ( data ) {
331                 return mw.Feedback.Dialog.parent.prototype.getSetupProcess.call( this, data )
332                         .next( function () {
333                                 var plainMsg, parsedMsg,
334                                         settings = data.settings;
335                                 data.contents = data.contents || {};
337                                 // Prefill subject/message
338                                 this.feedbackSubjectInput.setValue( data.contents.subject );
339                                 this.feedbackMessageInput.setValue( data.contents.message );
341                                 this.status = '';
342                                 this.messagePosterPromise = settings.messagePosterPromise;
343                                 this.setBugReportLink( settings.bugsTaskSubmissionLink );
344                                 this.feedbackPageTitle = settings.title;
345                                 this.feedbackPageName = settings.title.getNameText();
346                                 this.feedbackPageUrl = settings.title.getUrl();
348                                 // Useragent checkbox
349                                 if ( settings.useragentCheckbox.show ) {
350                                         this.useragentFieldLayout.setLabel( settings.useragentCheckbox.message );
351                                 }
353                                 this.useragentMandatory = settings.useragentCheckbox.mandatory;
354                                 this.useragentFieldLayout.toggle( settings.useragentCheckbox.show );
356                                 // HACK: Setting a link in the messages doesn't work. There is already a report
357                                 // about this, and the bug report offers a somewhat hacky work around that
358                                 // includes setting a separate message to be parsed.
359                                 // We want to make sure the user can configure both the title of the page and
360                                 // a separate url, so this must be allowed to parse correctly.
361                                 // See https://phabricator.wikimedia.org/T49395#490610
362                                 mw.messages.set( {
363                                         'feedback-dialog-temporary-message':
364                                                 '<a href="' + this.feedbackPageUrl + '" target="_blank">' + this.feedbackPageName + '</a>'
365                                 } );
366                                 plainMsg = mw.message( 'feedback-dialog-temporary-message' ).plain();
367                                 mw.messages.set( { 'feedback-dialog-temporary-message-parsed': plainMsg } );
368                                 parsedMsg = mw.message( 'feedback-dialog-temporary-message-parsed' );
369                                 this.feedbackMessageLabel.setLabel(
370                                         // Double-parse
371                                         $( '<span>' )
372                                                 .append( mw.message( 'feedback-dialog-intro', parsedMsg ).parse() )
373                                 );
375                                 this.validateFeedbackForm();
376                         }, this );
377         };
379         /**
380          * @inheritdoc
381          */
382         mw.Feedback.Dialog.prototype.getReadyProcess = function ( data ) {
383                 return mw.Feedback.Dialog.parent.prototype.getReadyProcess.call( this, data )
384                         .next( function () {
385                                 this.feedbackSubjectInput.focus();
386                         }, this );
387         };
389         /**
390          * @inheritdoc
391          */
392         mw.Feedback.Dialog.prototype.getActionProcess = function ( action ) {
393                 if ( action === 'cancel' ) {
394                         return new OO.ui.Process( function () {
395                                 this.close( { action: action } );
396                         }, this );
397                 } else if ( action === 'external' ) {
398                         return new OO.ui.Process( function () {
399                                 // Open in a new window
400                                 window.open( this.getBugReportLink(), '_blank' );
401                                 // Close the dialog
402                                 this.close();
403                         }, this );
404                 } else if ( action === 'submit' ) {
405                         return new OO.ui.Process( function () {
406                                 var fb = this,
407                                         userAgentMessage = ':' +
408                                                 '<small>' +
409                                                 mw.msg( 'feedback-useragent' ) +
410                                                 ' ' +
411                                                 mw.html.escape( navigator.userAgent ) +
412                                                 '</small>\n\n',
413                                         subject = this.feedbackSubjectInput.getValue(),
414                                         message = this.feedbackMessageInput.getValue();
416                                 // Add user agent if checkbox is selected
417                                 if ( this.useragentCheckbox.isSelected() ) {
418                                         message = userAgentMessage + message;
419                                 }
421                                 // Post the message
422                                 return this.messagePosterPromise.then( function ( poster ) {
423                                         return fb.postMessage( poster, subject, message );
424                                 }, function () {
425                                         fb.status = 'error4';
426                                         mw.log.warn( 'Feedback report failed because MessagePoster could not be fetched' );
427                                 } ).always( function () {
428                                         fb.close();
429                                 } );
430                         }, this );
431                 }
432                 // Fallback to parent handler
433                 return mw.Feedback.Dialog.parent.prototype.getActionProcess.call( this, action );
434         };
436         /**
437          * Posts the message
438          *
439          * @private
440          *
441          * @param {mw.messagePoster.MessagePoster} poster Poster implementation used to leave feedback
442          * @param {string} subject Subject of message
443          * @param {string} message Body of message
444          * @return {jQuery.Promise} Promise representing success of message posting action
445          */
446         mw.Feedback.Dialog.prototype.postMessage = function ( poster, subject, message ) {
447                 var fb = this;
449                 return poster.post(
450                         subject,
451                         message
452                 ).then( function () {
453                         fb.status = 'submitted';
454                 }, function ( mainCode, secondaryCode, details ) {
455                         if ( mainCode === 'api-fail' ) {
456                                 if ( secondaryCode === 'http' ) {
457                                         fb.status = 'error3';
458                                         // ajax request failed
459                                         mw.log.warn( 'Feedback report failed with HTTP error: ' +  details.textStatus );
460                                 } else {
461                                         fb.status = 'error2';
462                                         mw.log.warn( 'Feedback report failed with API error: ' +  secondaryCode );
463                                 }
464                         } else {
465                                 fb.status = 'error1';
466                         }
467                 } );
468         };
470         /**
471          * @inheritdoc
472          */
473         mw.Feedback.Dialog.prototype.getTeardownProcess = function ( data ) {
474                 return mw.Feedback.Dialog.parent.prototype.getTeardownProcess.call( this, data )
475                         .first( function () {
476                                 this.emit( 'submit', this.status, this.feedbackPageName, this.feedbackPageUrl );
477                                 // Cleanup
478                                 this.status = '';
479                                 this.feedbackPageTitle = null;
480                                 this.feedbackSubjectInput.setValue( '' );
481                                 this.feedbackMessageInput.setValue( '' );
482                                 this.useragentCheckbox.setSelected( false );
483                         }, this );
484         };
486         /**
487          * Set the bug report link
488          *
489          * @param {string} link Link to the external bug report form
490          */
491         mw.Feedback.Dialog.prototype.setBugReportLink = function ( link ) {
492                 this.bugReportLink = link;
493         };
495         /**
496          * Get the bug report link
497          *
498          * @return {string} Link to the external bug report form
499          */
500         mw.Feedback.Dialog.prototype.getBugReportLink = function () {
501                 return this.bugReportLink;
502         };
504 }( mediaWiki, jQuery ) );