Merge "Import: Handle uploads with sha1 starting with 0 properly"
[mediawiki.git] / resources / src / mediawiki / mediawiki.Upload.BookletLayout.js
blob26eabc7c53638cbf996c796c904db5c61ae5c130
1 ( function ( $, mw ) {
3         /**
4          * mw.Upload.BookletLayout encapsulates the process of uploading a file
5          * to MediaWiki using the {@link mw.Upload upload model}.
6          * The booklet emits events that can be used to get the stashed
7          * upload and the final file. It can be extended to accept
8          * additional fields from the user for specific scenarios like
9          * for Commons, or campaigns.
10          *
11          * ## Structure
12          *
13          * The {@link OO.ui.BookletLayout booklet layout} has three steps:
14          *
15          *  - **Upload**: Has a {@link OO.ui.SelectFileWidget field} to get the file object.
16          *
17          * - **Information**: Has a {@link OO.ui.FormLayout form} to collect metadata. This can be
18          *   extended.
19          *
20          * - **Insert**: Has details on how to use the file that was uploaded.
21          *
22          * Each step has a form associated with it defined in
23          * {@link #renderUploadForm renderUploadForm},
24          * {@link #renderInfoForm renderInfoForm}, and
25          * {@link #renderInsertForm renderInfoForm}. The
26          * {@link #getFile getFile},
27          * {@link #getFilename getFilename}, and
28          * {@link #getText getText} methods are used to get
29          * the information filled in these forms, required to call
30          * {@link mw.Upload mw.Upload}.
31          *
32          * ## Usage
33          *
34          * See the {@link mw.Upload.Dialog upload dialog}.
35          *
36          * The {@link #event-fileUploaded fileUploaded},
37          * and {@link #event-fileSaved fileSaved} events can
38          * be used to get details of the upload.
39          *
40          * ## Extending
41          *
42          * To extend using {@link mw.Upload mw.Upload}, override
43          * {@link #renderInfoForm renderInfoForm} to render
44          * the form required for the specific use-case. Update the
45          * {@link #getFilename getFilename}, and
46          * {@link #getText getText} methods to return data
47          * from your newly created form. If you added new fields you'll also have
48          * to update the {@link #clear} method.
49          *
50          * If you plan to use a different upload model, apart from what is mentioned
51          * above, you'll also have to override the
52          * {@link #createUpload createUpload} method to
53          * return the new model. The {@link #saveFile saveFile}, and
54          * the {@link #uploadFile uploadFile} methods need to be
55          * overridden to use the new model and data returned from the forms.
56          *
57          * @class
58          * @extends OO.ui.BookletLayout
59          *
60          * @constructor
61          * @param {Object} config Configuration options
62          * @cfg {jQuery} [$overlay] Overlay to use for widgets in the booklet
63          */
64         mw.Upload.BookletLayout = function ( config ) {
65                 // Parent constructor
66                 mw.Upload.BookletLayout.parent.call( this, config );
68                 this.$overlay = config.$overlay;
70                 this.renderUploadForm();
71                 this.renderInfoForm();
72                 this.renderInsertForm();
74                 this.addPages( [
75                         new OO.ui.PageLayout( 'upload', {
76                                 scrollable: true,
77                                 padded: true,
78                                 content: [ this.uploadForm ]
79                         } ),
80                         new OO.ui.PageLayout( 'info', {
81                                 scrollable: true,
82                                 padded: true,
83                                 content: [ this.infoForm ]
84                         } ),
85                         new OO.ui.PageLayout( 'insert', {
86                                 scrollable: true,
87                                 padded: true,
88                                 content: [ this.insertForm ]
89                         } )
90                 ] );
91         };
93         /* Setup */
95         OO.inheritClass( mw.Upload.BookletLayout, OO.ui.BookletLayout );
97         /* Events */
99         /**
100          * The file has finished uploading
101          *
102          * @event fileUploaded
103          */
105         /**
106          * The file has been saved to the database
107          *
108          * @event fileSaved
109          * @param {Object} imageInfo See mw.Upload#getImageInfo
110          */
112         /**
113          * The upload form has changed
114          *
115          * @event uploadValid
116          * @param {boolean} isValid The form is valid
117          */
119         /**
120          * The info form has changed
121          *
122          * @event infoValid
123          * @param {boolean} isValid The form is valid
124          */
126         /* Properties */
128         /**
129          * @property {OO.ui.FormLayout} uploadForm
130          * The form rendered in the first step to get the file object.
131          * Rendered in {@link #renderUploadForm renderUploadForm}.
132          */
134         /**
135          * @property {OO.ui.FormLayout} infoForm
136          * The form rendered in the second step to get metadata.
137          * Rendered in {@link #renderInfoForm renderInfoForm}
138          */
140         /**
141          * @property {OO.ui.FormLayout} insertForm
142          * The form rendered in the third step to show usage
143          * Rendered in {@link #renderInsertForm renderInsertForm}
144          */
146         /* Methods */
148         /**
149          * Initialize for a new upload
150          *
151          * @return {jQuery.Promise} Promise resolved when everything is initialized
152          */
153         mw.Upload.BookletLayout.prototype.initialize = function () {
154                 var
155                         apiPromise,
156                         booklet = this,
157                         deferred = $.Deferred();
159                 this.clear();
160                 this.upload = this.createUpload();
161                 this.setPage( 'upload' );
163                 apiPromise = this.upload.apiPromise || $.Deferred().resolve( this.upload.api );
164                 apiPromise.done( function ( api ) {
165                         // If the user can't upload anything, don't give them the option to.
166                         api.getUserInfo().done( function ( userInfo ) {
167                                 if ( userInfo.rights.indexOf( 'upload' ) === -1 ) {
168                                         // TODO Use a better error message when not all logged-in users can upload
169                                         booklet.getPage( 'upload' ).$element.msg( 'api-error-mustbeloggedin' );
170                                 }
171                         } ).always( function () {
172                                 deferred.resolve();
173                         } );
174                 } );
176                 return deferred.promise();
177         };
179         /**
180          * Create a new upload model
181          *
182          * @protected
183          * @return {mw.Upload} Upload model
184          */
185         mw.Upload.BookletLayout.prototype.createUpload = function () {
186                 return new mw.Upload();
187         };
189         /* Uploading */
191         /**
192          * Uploads the file that was added in the upload form. Uses
193          * {@link #getFile getFile} to get the HTML5
194          * file object.
195          *
196          * @protected
197          * @fires fileUploaded
198          * @return {jQuery.Promise}
199          */
200         mw.Upload.BookletLayout.prototype.uploadFile = function () {
201                 var deferred = $.Deferred(),
202                         layout = this,
203                         file = this.getFile();
205                 this.setFilename( file.name );
207                 this.setPage( 'info' );
209                 if ( this.shouldRecordBucket ) {
210                         this.upload.bucket = this.bucket;
211                 }
213                 this.upload.setFile( file );
214                 // The original file name might contain invalid characters, so use our sanitized one
215                 this.upload.setFilename( this.getFilename() );
217                 this.uploadPromise = this.upload.uploadToStash();
218                 this.uploadPromise.then( function () {
219                         deferred.resolve();
220                         layout.emit( 'fileUploaded' );
221                 }, function () {
222                         // These errors will be thrown while the user is on the info page.
223                         // Pretty sure it's impossible to get a warning other than 'stashfailed' here, which should
224                         // really be an error...
225                         var errorMessage = layout.getErrorMessageForStateDetails();
226                         deferred.reject( errorMessage );
227                 } );
229                 // If there is an error in uploading, come back to the upload page
230                 deferred.fail( function () {
231                         layout.setPage( 'upload' );
232                 } );
234                 return deferred;
235         };
237         /**
238          * Saves the stash finalizes upload. Uses
239          * {@link #getFilename getFilename}, and
240          * {@link #getText getText} to get details from
241          * the form.
242          *
243          * @protected
244          * @fires fileSaved
245          * @return {jQuery.Promise} Rejects the promise with an
246          * {@link OO.ui.Error error}, or resolves if the upload was successful.
247          */
248         mw.Upload.BookletLayout.prototype.saveFile = function () {
249                 var layout = this,
250                         deferred = $.Deferred();
252                 this.upload.setFilename( this.getFilename() );
253                 this.upload.setText( this.getText() );
255                 this.uploadPromise.then( function () {
256                         layout.upload.finishStashUpload().then( function () {
257                                 var name;
259                                 // Normalize page name and localise the 'File:' prefix
260                                 name = new mw.Title( 'File:' + layout.upload.getFilename() ).toString();
261                                 layout.filenameUsageWidget.setValue( '[[' + name + ']]' );
262                                 layout.setPage( 'insert' );
264                                 deferred.resolve();
265                                 layout.emit( 'fileSaved', layout.upload.getImageInfo() );
266                         }, function () {
267                                 var errorMessage = layout.getErrorMessageForStateDetails();
268                                 deferred.reject( errorMessage );
269                         } );
270                 } );
272                 return deferred.promise();
273         };
275         /**
276          * Get an error message (as OO.ui.Error object) that should be displayed to the user for current
277          * state and state details.
278          *
279          * @protected
280          * @return {OO.ui.Error} Error to display for given state and details.
281          */
282         mw.Upload.BookletLayout.prototype.getErrorMessageForStateDetails = function () {
283                 var message,
284                         state = this.upload.getState(),
285                         stateDetails = this.upload.getStateDetails(),
286                         error = stateDetails.error,
287                         warnings = stateDetails.upload && stateDetails.upload.warnings;
289                 if ( state === mw.Upload.State.ERROR ) {
290                         // HACK We should either have a hook here to allow TitleBlacklist to handle this, or just have
291                         // TitleBlacklist produce sane error messages that can be displayed without arcane knowledge
292                         if ( error.info === 'TitleBlacklist prevents this title from being created' ) {
293                                 // HACK Apparently the only reliable way to determine whether TitleBlacklist was involved
294                                 return new OO.ui.Error(
295                                         $( '<p>' ).html(
296                                                 // HACK TitleBlacklist doesn't have a sensible message, this one is from UploadWizard
297                                                 mw.message( 'api-error-blacklisted' ).parse()
298                                         ),
299                                         { recoverable: false }
300                                 );
301                         }
303                         message = mw.message( 'api-error-' + error.code );
304                         if ( !message.exists() ) {
305                                 message = mw.message( 'api-error-unknownerror', JSON.stringify( stateDetails ) );
306                         }
307                         return new OO.ui.Error(
308                                 $( '<p>' ).html(
309                                         message.parse()
310                                 ),
311                                 { recoverable: false }
312                         );
313                 }
315                 if ( state === mw.Upload.State.WARNING ) {
316                         // We could get more than one of these errors, these are in order
317                         // of importance. For example fixing the thumbnail like file name
318                         // won't help the fact that the file already exists.
319                         if ( warnings.stashfailed !== undefined ) {
320                                 return new OO.ui.Error(
321                                         $( '<p>' ).msg( 'api-error-stashfailed' ),
322                                         { recoverable: false }
323                                 );
324                         } else if ( warnings.exists !== undefined ) {
325                                 return new OO.ui.Error(
326                                         $( '<p>' ).msg( 'fileexists', 'File:' + warnings.exists ),
327                                         { recoverable: false }
328                                 );
329                         } else if ( warnings[ 'page-exists' ] !== undefined ) {
330                                 return new OO.ui.Error(
331                                         $( '<p>' ).msg( 'filepageexists', 'File:' + warnings[ 'page-exists' ] ),
332                                         { recoverable: false }
333                                 );
334                         } else if ( warnings.duplicate !== undefined ) {
335                                 return new OO.ui.Error(
336                                         $( '<p>' ).msg( 'api-error-duplicate', warnings.duplicate.length ),
337                                         { recoverable: false }
338                                 );
339                         } else if ( warnings[ 'thumb-name' ] !== undefined ) {
340                                 return new OO.ui.Error(
341                                         $( '<p>' ).msg( 'filename-thumb-name' ),
342                                         { recoverable: false }
343                                 );
344                         } else if ( warnings[ 'bad-prefix' ] !== undefined ) {
345                                 return new OO.ui.Error(
346                                         $( '<p>' ).msg( 'filename-bad-prefix', warnings[ 'bad-prefix' ] ),
347                                         { recoverable: false }
348                                 );
349                         } else if ( warnings[ 'duplicate-archive' ] !== undefined ) {
350                                 return new OO.ui.Error(
351                                         $( '<p>' ).msg( 'api-error-duplicate-archive', 1 ),
352                                         { recoverable: false }
353                                 );
354                         } else if ( warnings.badfilename !== undefined ) {
355                                 // Change the name if the current name isn't acceptable
356                                 // TODO This might not really be the best place to do this
357                                 this.setFilename( warnings.badfilename );
358                                 return new OO.ui.Error(
359                                         $( '<p>' ).msg( 'badfilename', warnings.badfilename )
360                                 );
361                         } else {
362                                 return new OO.ui.Error(
363                                         $( '<p>' ).html(
364                                                 // Let's get all the help we can if we can't pin point the error
365                                                 mw.message( 'api-error-unknown-warning', JSON.stringify( stateDetails ) ).parse()
366                                         ),
367                                         { recoverable: false }
368                                 );
369                         }
370                 }
371         };
373         /* Form renderers */
375         /**
376          * Renders and returns the upload form and sets the
377          * {@link #uploadForm uploadForm} property.
378          *
379          * @protected
380          * @fires selectFile
381          * @return {OO.ui.FormLayout}
382          */
383         mw.Upload.BookletLayout.prototype.renderUploadForm = function () {
384                 var fieldset;
386                 this.selectFileWidget = new OO.ui.SelectFileWidget();
387                 fieldset = new OO.ui.FieldsetLayout( { label: mw.msg( 'upload-form-label-select-file' ) } );
388                 fieldset.addItems( [ this.selectFileWidget ] );
389                 this.uploadForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
391                 // Validation
392                 this.selectFileWidget.on( 'change', this.onUploadFormChange.bind( this ) );
394                 return this.uploadForm;
395         };
397         /**
398          * Handle change events to the upload form
399          *
400          * @protected
401          * @fires uploadValid
402          */
403         mw.Upload.BookletLayout.prototype.onUploadFormChange = function () {
404                 this.emit( 'uploadValid', !!this.selectFileWidget.getValue() );
405         };
407         /**
408          * Renders and returns the information form for collecting
409          * metadata and sets the {@link #infoForm infoForm}
410          * property.
411          *
412          * @protected
413          * @return {OO.ui.FormLayout}
414          */
415         mw.Upload.BookletLayout.prototype.renderInfoForm = function () {
416                 var fieldset;
418                 this.filenameWidget = new OO.ui.TextInputWidget( {
419                         indicator: 'required',
420                         required: true,
421                         validate: /.+/
422                 } );
423                 this.descriptionWidget = new OO.ui.TextInputWidget( {
424                         indicator: 'required',
425                         required: true,
426                         validate: /\S+/,
427                         multiline: true,
428                         autosize: true
429                 } );
431                 fieldset = new OO.ui.FieldsetLayout( {
432                         label: mw.msg( 'upload-form-label-infoform-title' )
433                 } );
434                 fieldset.addItems( [
435                         new OO.ui.FieldLayout( this.filenameWidget, {
436                                 label: mw.msg( 'upload-form-label-infoform-name' ),
437                                 align: 'top',
438                                 help: mw.msg( 'upload-form-label-infoform-name-tooltip' )
439                         } ),
440                         new OO.ui.FieldLayout( this.descriptionWidget, {
441                                 label: mw.msg( 'upload-form-label-infoform-description' ),
442                                 align: 'top',
443                                 help: mw.msg( 'upload-form-label-infoform-description-tooltip' )
444                         } )
445                 ] );
446                 this.infoForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
448                 this.filenameWidget.on( 'change', this.onInfoFormChange.bind( this ) );
449                 this.descriptionWidget.on( 'change', this.onInfoFormChange.bind( this ) );
451                 return this.infoForm;
452         };
454         /**
455          * Handle change events to the info form
456          *
457          * @protected
458          * @fires infoValid
459          */
460         mw.Upload.BookletLayout.prototype.onInfoFormChange = function () {
461                 var layout = this;
462                 $.when(
463                         this.filenameWidget.getValidity(),
464                         this.descriptionWidget.getValidity()
465                 ).done( function () {
466                         layout.emit( 'infoValid', true );
467                 } ).fail( function () {
468                         layout.emit( 'infoValid', false );
469                 } );
470         };
472         /**
473          * Renders and returns the insert form to show file usage and
474          * sets the {@link #insertForm insertForm} property.
475          *
476          * @protected
477          * @return {OO.ui.FormLayout}
478          */
479         mw.Upload.BookletLayout.prototype.renderInsertForm = function () {
480                 var fieldset;
482                 this.filenameUsageWidget = new OO.ui.TextInputWidget();
483                 fieldset = new OO.ui.FieldsetLayout( {
484                         label: mw.msg( 'upload-form-label-usage-title' )
485                 } );
486                 fieldset.addItems( [
487                         new OO.ui.FieldLayout( this.filenameUsageWidget, {
488                                 label: mw.msg( 'upload-form-label-usage-filename' ),
489                                 align: 'top'
490                         } )
491                 ] );
492                 this.insertForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
494                 return this.insertForm;
495         };
497         /* Getters */
499         /**
500          * Gets the file object from the
501          * {@link #uploadForm upload form}.
502          *
503          * @protected
504          * @return {File|null}
505          */
506         mw.Upload.BookletLayout.prototype.getFile = function () {
507                 return this.selectFileWidget.getValue();
508         };
510         /**
511          * Gets the file name from the
512          * {@link #infoForm information form}.
513          *
514          * @protected
515          * @return {string}
516          */
517         mw.Upload.BookletLayout.prototype.getFilename = function () {
518                 var filename = this.filenameWidget.getValue();
519                 if ( this.filenameExtension ) {
520                         filename += '.' + this.filenameExtension;
521                 }
522                 return filename;
523         };
525         /**
526          * Prefills the {@link #infoForm information form} with the given filename.
527          *
528          * @protected
529          * @param {string} filename
530          */
531         mw.Upload.BookletLayout.prototype.setFilename = function ( filename ) {
532                 var title = mw.Title.newFromFileName( filename );
534                 if ( title ) {
535                         this.filenameWidget.setValue( title.getNameText() );
536                         this.filenameExtension = mw.Title.normalizeExtension( title.getExtension() );
537                 } else {
538                         // Seems to happen for files with no extension, which should fail some checks anyway...
539                         this.filenameWidget.setValue( filename );
540                         this.filenameExtension = null;
541                 }
542         };
544         /**
545          * Gets the page text from the
546          * {@link #infoForm information form}.
547          *
548          * @protected
549          * @return {string}
550          */
551         mw.Upload.BookletLayout.prototype.getText = function () {
552                 return this.descriptionWidget.getValue();
553         };
555         /* Setters */
557         /**
558          * Sets the file object
559          *
560          * @protected
561          * @param {File|null} file File to select
562          */
563         mw.Upload.BookletLayout.prototype.setFile = function ( file ) {
564                 this.selectFileWidget.setValue( file );
565         };
567         /**
568          * Clear the values of all fields
569          *
570          * @protected
571          */
572         mw.Upload.BookletLayout.prototype.clear = function () {
573                 this.selectFileWidget.setValue( null );
574                 this.filenameWidget.setValue( null ).setValidityFlag( true );
575                 this.descriptionWidget.setValue( null ).setValidityFlag( true );
576                 this.filenameUsageWidget.setValue( null );
577         };
579 }( jQuery, mediaWiki ) );