]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / resources / src / mediawiki / mediawiki.ForeignStructuredUpload.BookletLayout.js
1 /* global moment, Uint8Array */
2 ( function ( $, mw ) {
3
4         /**
5          * mw.ForeignStructuredUpload.BookletLayout encapsulates the process
6          * of uploading a file to MediaWiki using the mw.ForeignStructuredUpload model.
7          *
8          *     var uploadDialog = new mw.Upload.Dialog( {
9          *         bookletClass: mw.ForeignStructuredUpload.BookletLayout,
10          *         booklet: {
11          *             target: 'local'
12          *         }
13          *     } );
14          *     var windowManager = new OO.ui.WindowManager();
15          *     $( 'body' ).append( windowManager.$element );
16          *     windowManager.addWindows( [ uploadDialog ] );
17          *
18          * @class mw.ForeignStructuredUpload.BookletLayout
19          * @uses mw.ForeignStructuredUpload
20          * @extends mw.Upload.BookletLayout
21          *
22          * @constructor
23          * @param {Object} config Configuration options
24          * @cfg {string} [target] Used to choose the target repository.
25          *     If nothing is passed, the {@link mw.ForeignUpload#property-target default} is used.
26          */
27         mw.ForeignStructuredUpload.BookletLayout = function ( config ) {
28                 config = config || {};
29                 // Parent constructor
30                 mw.ForeignStructuredUpload.BookletLayout.parent.call( this, config );
31
32                 this.target = config.target;
33         };
34
35         /* Setup */
36
37         OO.inheritClass( mw.ForeignStructuredUpload.BookletLayout, mw.Upload.BookletLayout );
38
39         /* Uploading */
40
41         /**
42          * @inheritdoc
43          */
44         mw.ForeignStructuredUpload.BookletLayout.prototype.initialize = function () {
45                 var booklet = this;
46                 return mw.ForeignStructuredUpload.BookletLayout.parent.prototype.initialize.call( this ).then(
47                         function () {
48                                 return $.when(
49                                         // Point the CategoryMultiselectWidget to the right wiki
50                                         booklet.upload.getApi().then( function ( api ) {
51                                                 // If this is a ForeignApi, it will have a apiUrl, otherwise we don't need to do anything
52                                                 if ( api.apiUrl ) {
53                                                         // Can't reuse the same object, CategoryMultiselectWidget calls #abort on its mw.Api instance
54                                                         booklet.categoriesWidget.api = new mw.ForeignApi( api.apiUrl );
55                                                 }
56                                                 return $.Deferred().resolve();
57                                         } ),
58                                         // Set up booklet fields and license messages to match configuration
59                                         booklet.upload.loadConfig().then( function ( config ) {
60                                                 var
61                                                         msgPromise,
62                                                         isLocal = booklet.upload.target === 'local',
63                                                         fields = config.fields,
64                                                         msgs = config.licensemessages[ isLocal ? 'local' : 'foreign' ];
65
66                                                 // Hide disabled fields
67                                                 booklet.descriptionField.toggle( !!fields.description );
68                                                 booklet.categoriesField.toggle( !!fields.categories );
69                                                 booklet.dateField.toggle( !!fields.date );
70                                                 // Update form validity
71                                                 booklet.onInfoFormChange();
72
73                                                 // Load license messages from the remote wiki if we don't have these messages locally
74                                                 // (this means that we only load messages from the foreign wiki for custom config)
75                                                 if ( mw.message( 'upload-form-label-own-work-message-' + msgs ).exists() ) {
76                                                         msgPromise = $.Deferred().resolve();
77                                                 } else {
78                                                         msgPromise = booklet.upload.apiPromise.then( function ( api ) {
79                                                                 return api.loadMessages( [
80                                                                         'upload-form-label-own-work-message-' + msgs,
81                                                                         'upload-form-label-not-own-work-message-' + msgs,
82                                                                         'upload-form-label-not-own-work-local-' + msgs
83                                                                 ] );
84                                                         } );
85                                                 }
86
87                                                 // Update license messages
88                                                 return msgPromise.then( function () {
89                                                         var $labels;
90                                                         booklet.$ownWorkMessage.msg( 'upload-form-label-own-work-message-' + msgs );
91                                                         booklet.$notOwnWorkMessage.msg( 'upload-form-label-not-own-work-message-' + msgs );
92                                                         booklet.$notOwnWorkLocal.msg( 'upload-form-label-not-own-work-local-' + msgs );
93
94                                                         $labels = $( [
95                                                                 booklet.$ownWorkMessage[ 0 ],
96                                                                 booklet.$notOwnWorkMessage[ 0 ],
97                                                                 booklet.$notOwnWorkLocal[ 0 ]
98                                                         ] );
99
100                                                         // Improve the behavior of links inside these labels, which may point to important
101                                                         // things like licensing requirements or terms of use
102                                                         $labels.find( 'a' )
103                                                                 .attr( 'target', '_blank' )
104                                                                 .on( 'click', function ( e ) {
105                                                                         // OO.ui.FieldLayout#onLabelClick is trying to prevent default on all clicks,
106                                                                         // which causes the links to not be openable. Don't let it do that.
107                                                                         e.stopPropagation();
108                                                                 } );
109                                                 } );
110                                         }, function ( errorMsg ) {
111                                                 booklet.getPage( 'upload' ).$element.msg( errorMsg );
112                                                 return $.Deferred().resolve();
113                                         } )
114                                 );
115                         }
116                 ).then(
117                         null,
118                         // Always resolve, never reject
119                         function () { return $.Deferred().resolve(); }
120                 );
121         };
122
123         /**
124          * Returns a {@link mw.ForeignStructuredUpload mw.ForeignStructuredUpload}
125          * with the {@link #cfg-target target} specified in config.
126          *
127          * @protected
128          * @return {mw.Upload}
129          */
130         mw.ForeignStructuredUpload.BookletLayout.prototype.createUpload = function () {
131                 return new mw.ForeignStructuredUpload( this.target, {
132                         parameters: {
133                                 errorformat: 'html',
134                                 errorlang: mw.config.get( 'wgUserLanguage' ),
135                                 errorsuselocal: 1,
136                                 formatversion: 2
137                         }
138                 } );
139         };
140
141         /* Form renderers */
142
143         /**
144          * @inheritdoc
145          */
146         mw.ForeignStructuredUpload.BookletLayout.prototype.renderUploadForm = function () {
147                 var fieldset,
148                         layout = this;
149
150                 // These elements are filled with text in #initialize
151                 // TODO Refactor this to be in one place
152                 this.$ownWorkMessage = $( '<p>' )
153                         .addClass( 'mw-foreignStructuredUpload-bookletLayout-license' );
154                 this.$notOwnWorkMessage = $( '<p>' );
155                 this.$notOwnWorkLocal = $( '<p>' );
156
157                 this.selectFileWidget = new OO.ui.SelectFileWidget( {
158                         showDropTarget: true
159                 } );
160                 this.messageLabel = new OO.ui.LabelWidget( {
161                         label: $( '<div>' ).append(
162                                 this.$notOwnWorkMessage,
163                                 this.$notOwnWorkLocal
164                         )
165                 } );
166                 this.ownWorkCheckbox = new OO.ui.CheckboxInputWidget().on( 'change', function ( on ) {
167                         layout.messageLabel.toggle( !on );
168                 } );
169
170                 fieldset = new OO.ui.FieldsetLayout();
171                 fieldset.addItems( [
172                         new OO.ui.FieldLayout( this.selectFileWidget, {
173                                 align: 'top'
174                         } ),
175                         new OO.ui.FieldLayout( this.ownWorkCheckbox, {
176                                 align: 'inline',
177                                 label: $( '<div>' ).append(
178                                         $( '<p>' ).text( mw.msg( 'upload-form-label-own-work' ) ),
179                                         this.$ownWorkMessage
180                                 )
181                         } ),
182                         new OO.ui.FieldLayout( this.messageLabel, {
183                                 align: 'top'
184                         } )
185                 ] );
186                 this.uploadForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
187
188                 // Validation
189                 this.selectFileWidget.on( 'change', this.onUploadFormChange.bind( this ) );
190                 this.ownWorkCheckbox.on( 'change', this.onUploadFormChange.bind( this ) );
191
192                 this.selectFileWidget.on( 'change', function () {
193                         var file = layout.getFile();
194
195                         // Set the date to lastModified once we have the file
196                         if ( layout.getDateFromLastModified( file ) !== undefined ) {
197                                 layout.dateWidget.setValue( layout.getDateFromLastModified( file ) );
198                         }
199
200                         // Check if we have EXIF data and set to that where available
201                         layout.getDateFromExif( file ).done( function ( date ) {
202                                 layout.dateWidget.setValue( date );
203                         } );
204
205                         layout.updateFilePreview();
206                 } );
207
208                 return this.uploadForm;
209         };
210
211         /**
212          * @inheritdoc
213          */
214         mw.ForeignStructuredUpload.BookletLayout.prototype.onUploadFormChange = function () {
215                 var file = this.selectFileWidget.getValue(),
216                         ownWork = this.ownWorkCheckbox.isSelected(),
217                         valid = !!file && ownWork;
218                 this.emit( 'uploadValid', valid );
219         };
220
221         /**
222          * @inheritdoc
223          */
224         mw.ForeignStructuredUpload.BookletLayout.prototype.renderInfoForm = function () {
225                 var fieldset;
226
227                 this.filePreview = new OO.ui.Widget( {
228                         classes: [ 'mw-upload-bookletLayout-filePreview' ]
229                 } );
230                 this.progressBarWidget = new OO.ui.ProgressBarWidget( {
231                         progress: 0
232                 } );
233                 this.filePreview.$element.append( this.progressBarWidget.$element );
234
235                 this.filenameWidget = new OO.ui.TextInputWidget( {
236                         required: true,
237                         validate: /.+/
238                 } );
239                 this.descriptionWidget = new OO.ui.MultilineTextInputWidget( {
240                         required: true,
241                         validate: /\S+/,
242                         autosize: true
243                 } );
244                 this.categoriesWidget = new mw.widgets.CategoryMultiselectWidget( {
245                         // Can't be done here because we don't know the target wiki yet... done in #initialize.
246                         // api: new mw.ForeignApi( ... ),
247                         $overlay: this.$overlay
248                 } );
249                 this.dateWidget = new mw.widgets.DateInputWidget( {
250                         $overlay: this.$overlay,
251                         required: true,
252                         mustBeBefore: moment().add( 1, 'day' ).locale( 'en' ).format( 'YYYY-MM-DD' ) // Tomorrow
253                 } );
254
255                 this.filenameField = new OO.ui.FieldLayout( this.filenameWidget, {
256                         label: mw.msg( 'upload-form-label-infoform-name' ),
257                         align: 'top',
258                         classes: [ 'mw-foreignStructuredUploa-bookletLayout-small-notice' ],
259                         notices: [ mw.msg( 'upload-form-label-infoform-name-tooltip' ) ]
260                 } );
261                 this.descriptionField = new OO.ui.FieldLayout( this.descriptionWidget, {
262                         label: mw.msg( 'upload-form-label-infoform-description' ),
263                         align: 'top',
264                         classes: [ 'mw-foreignStructuredUploa-bookletLayout-small-notice' ],
265                         notices: [ mw.msg( 'upload-form-label-infoform-description-tooltip' ) ]
266                 } );
267                 this.categoriesField = new OO.ui.FieldLayout( this.categoriesWidget, {
268                         label: mw.msg( 'upload-form-label-infoform-categories' ),
269                         align: 'top'
270                 } );
271                 this.dateField = new OO.ui.FieldLayout( this.dateWidget, {
272                         label: mw.msg( 'upload-form-label-infoform-date' ),
273                         align: 'top'
274                 } );
275
276                 fieldset = new OO.ui.FieldsetLayout( {
277                         label: mw.msg( 'upload-form-label-infoform-title' )
278                 } );
279                 fieldset.addItems( [
280                         this.filenameField,
281                         this.descriptionField,
282                         this.categoriesField,
283                         this.dateField
284                 ] );
285                 this.infoForm = new OO.ui.FormLayout( {
286                         classes: [ 'mw-upload-bookletLayout-infoForm' ],
287                         items: [ this.filePreview, fieldset ]
288                 } );
289
290                 // Validation
291                 this.filenameWidget.on( 'change', this.onInfoFormChange.bind( this ) );
292                 this.descriptionWidget.on( 'change', this.onInfoFormChange.bind( this ) );
293                 this.dateWidget.on( 'change', this.onInfoFormChange.bind( this ) );
294
295                 this.on( 'fileUploadProgress', function ( progress ) {
296                         this.progressBarWidget.setProgress( progress * 100 );
297                 }.bind( this ) );
298
299                 return this.infoForm;
300         };
301
302         /**
303          * @inheritdoc
304          */
305         mw.ForeignStructuredUpload.BookletLayout.prototype.onInfoFormChange = function () {
306                 var layout = this,
307                         validityPromises = [];
308
309                 validityPromises.push( this.filenameWidget.getValidity() );
310                 if ( this.descriptionField.isVisible() ) {
311                         validityPromises.push( this.descriptionWidget.getValidity() );
312                 }
313                 if ( this.dateField.isVisible() ) {
314                         validityPromises.push( this.dateWidget.getValidity() );
315                 }
316
317                 $.when.apply( $, validityPromises ).done( function () {
318                         layout.emit( 'infoValid', true );
319                 } ).fail( function () {
320                         layout.emit( 'infoValid', false );
321                 } );
322         };
323
324         /**
325          * @param {mw.Title} filename
326          * @return {jQuery.Promise} Resolves (on success) or rejects with OO.ui.Error
327          */
328         mw.ForeignStructuredUpload.BookletLayout.prototype.validateFilename = function ( filename ) {
329                 return ( new mw.Api() ).get( {
330                         action: 'query',
331                         prop: 'info',
332                         titles: filename.getPrefixedDb(),
333                         formatversion: 2
334                 } ).then(
335                         function ( result ) {
336                                 // if the file already exists, reject right away, before
337                                 // ever firing finishStashUpload()
338                                 if ( !result.query.pages[ 0 ].missing ) {
339                                         return $.Deferred().reject( new OO.ui.Error(
340                                                 $( '<p>' ).msg( 'fileexists', filename.getPrefixedDb() ),
341                                                 { recoverable: false }
342                                         ) );
343                                 }
344                         },
345                         function () {
346                                 // API call failed - this could be a connection hiccup...
347                                 // Let's just ignore this validation step and turn this
348                                 // failure into a successful resolve ;)
349                                 return $.Deferred().resolve();
350                         }
351                 );
352         };
353
354         /**
355          * @inheritdoc
356          */
357         mw.ForeignStructuredUpload.BookletLayout.prototype.saveFile = function () {
358                 var title = mw.Title.newFromText(
359                         this.getFilename(),
360                         mw.config.get( 'wgNamespaceIds' ).file
361                 );
362
363                 return this.uploadPromise
364                         .then( this.validateFilename.bind( this, title ) )
365                         .then( mw.ForeignStructuredUpload.BookletLayout.parent.prototype.saveFile.bind( this ) );
366         };
367
368         /* Getters */
369
370         /**
371          * @inheritdoc
372          */
373         mw.ForeignStructuredUpload.BookletLayout.prototype.getText = function () {
374                 var language = mw.config.get( 'wgContentLanguage' );
375                 this.upload.clearDescriptions();
376                 this.upload.addDescription( language, this.descriptionWidget.getValue() );
377                 this.upload.setDate( this.dateWidget.getValue() );
378                 this.upload.clearCategories();
379                 this.upload.addCategories( this.categoriesWidget.getItemsData() );
380                 return this.upload.getText();
381         };
382
383         /**
384          * Get original date from EXIF data
385          *
386          * @param {Object} file
387          * @return {jQuery.Promise} Promise resolved with the EXIF date
388          */
389         mw.ForeignStructuredUpload.BookletLayout.prototype.getDateFromExif = function ( file ) {
390                 var fileReader,
391                         deferred = $.Deferred();
392
393                 if ( file && file.type === 'image/jpeg' ) {
394                         fileReader = new FileReader();
395                         fileReader.onload = function () {
396                                 var fileStr, arr, i, metadata;
397
398                                 if ( typeof fileReader.result === 'string' ) {
399                                         fileStr = fileReader.result;
400                                 } else {
401                                         // Array buffer; convert to binary string for the library.
402                                         arr = new Uint8Array( fileReader.result );
403                                         fileStr = '';
404                                         for ( i = 0; i < arr.byteLength; i++ ) {
405                                                 fileStr += String.fromCharCode( arr[ i ] );
406                                         }
407                                 }
408
409                                 try {
410                                         metadata = mw.libs.jpegmeta( fileStr, file.name );
411                                 } catch ( e ) {
412                                         metadata = null;
413                                 }
414
415                                 if ( metadata !== null && metadata.exif !== undefined && metadata.exif.DateTimeOriginal ) {
416                                         deferred.resolve( moment( metadata.exif.DateTimeOriginal, 'YYYY:MM:DD' ).format( 'YYYY-MM-DD' ) );
417                                 } else {
418                                         deferred.reject();
419                                 }
420                         };
421
422                         if ( 'readAsBinaryString' in fileReader ) {
423                                 fileReader.readAsBinaryString( file );
424                         } else if ( 'readAsArrayBuffer' in fileReader ) {
425                                 fileReader.readAsArrayBuffer( file );
426                         } else {
427                                 // We should never get here
428                                 deferred.reject();
429                                 throw new Error( 'Cannot read thumbnail as binary string or array buffer.' );
430                         }
431                 }
432
433                 return deferred.promise();
434         };
435
436         /**
437          * Get last modified date from file
438          *
439          * @param {Object} file
440          * @return {Object} Last modified date from file
441          */
442         mw.ForeignStructuredUpload.BookletLayout.prototype.getDateFromLastModified = function ( file ) {
443                 if ( file && file.lastModified ) {
444                         return moment( file.lastModified ).format( 'YYYY-MM-DD' );
445                 }
446         };
447
448         /* Setters */
449
450         /**
451          * @inheritdoc
452          */
453         mw.ForeignStructuredUpload.BookletLayout.prototype.clear = function () {
454                 mw.ForeignStructuredUpload.BookletLayout.parent.prototype.clear.call( this );
455
456                 this.ownWorkCheckbox.setSelected( false );
457                 this.categoriesWidget.setItemsFromData( [] );
458                 this.dateWidget.setValue( '' ).setValidityFlag( true );
459         };
460
461 }( jQuery, mediaWiki ) );