Merge "Improve sorting on SpecialWanted*-Pages"
[mediawiki.git] / resources / src / mediawiki.special / mediawiki.special.upload.js
blob0ddf7fe3f1d897a8f0c0d6389cbdfd1bf0e5ad69
1 /**
2  * JavaScript for Special:Upload
3  *
4  * @private
5  * @class mw.special.upload
6  * @singleton
7  */
9 /* eslint-disable no-use-before-define */
10 /* global Uint8Array */
12 ( function ( mw, $ ) {
13         var uploadWarning, uploadLicense,
14                 ajaxUploadDestCheck = mw.config.get( 'wgAjaxUploadDestCheck' ),
15                 $license = $( '#wpLicense' );
17         window.wgUploadWarningObj = uploadWarning = {
18                 responseCache: { '': ' ' },
19                 nameToCheck: '',
20                 typing: false,
21                 delay: 500, // ms
22                 timeoutID: false,
24                 keypress: function () {
25                         if ( !ajaxUploadDestCheck ) {
26                                 return;
27                         }
29                         // Find file to upload
30                         if ( !$( '#wpDestFile' ).length || !$( '#wpDestFile-warning' ).length ) {
31                                 return;
32                         }
34                         this.nameToCheck = $( '#wpDestFile' ).val();
36                         // Clear timer
37                         if ( this.timeoutID ) {
38                                 clearTimeout( this.timeoutID );
39                         }
40                         // Check response cache
41                         if ( this.responseCache.hasOwnProperty( this.nameToCheck ) ) {
42                                 this.setWarning( this.responseCache[ this.nameToCheck ] );
43                                 return;
44                         }
46                         this.timeoutID = setTimeout( function () {
47                                 uploadWarning.timeout();
48                         }, this.delay );
49                 },
51                 checkNow: function ( fname ) {
52                         if ( !ajaxUploadDestCheck ) {
53                                 return;
54                         }
55                         if ( this.timeoutID ) {
56                                 clearTimeout( this.timeoutID );
57                         }
58                         this.nameToCheck = fname;
59                         this.timeout();
60                 },
62                 timeout: function () {
63                         var $spinnerDestCheck, title;
64                         if ( !ajaxUploadDestCheck || this.nameToCheck === '' ) {
65                                 return;
66                         }
67                         $spinnerDestCheck = $.createSpinner().insertAfter( '#wpDestFile' );
68                         title = mw.Title.newFromText( this.nameToCheck, mw.config.get( 'wgNamespaceIds' ).file );
70                         ( new mw.Api() ).get( {
71                                 formatversion: 2,
72                                 action: 'query',
73                                 // If title is empty, user input is invalid, the API call will produce details about why
74                                 titles: title ? title.getPrefixedText() : this.nameToCheck,
75                                 prop: 'imageinfo',
76                                 iiprop: 'uploadwarning'
77                         } ).done( function ( result ) {
78                                 var
79                                         resultOut = '',
80                                         page = result.query.pages[ 0 ];
81                                 if ( page.imageinfo ) {
82                                         resultOut = page.imageinfo[ 0 ].html;
83                                 } else if ( page.invalidreason ) {
84                                         resultOut = mw.html.escape( page.invalidreason );
85                                 }
86                                 uploadWarning.processResult( resultOut, uploadWarning.nameToCheck );
87                         } ).always( function () {
88                                 $spinnerDestCheck.remove();
89                         } );
90                 },
92                 processResult: function ( result, fileName ) {
93                         this.setWarning( result );
94                         this.responseCache[ fileName ] = result;
95                 },
97                 setWarning: function ( warning ) {
98                         var $warningBox = $( '#wpDestFile-warning' ),
99                                 $warning = $( $.parseHTML( warning ) );
100                         mw.hook( 'wikipage.content' ).fire( $warning );
101                         $warningBox.empty().append( $warning );
103                         // Set a value in the form indicating that the warning is acknowledged and
104                         // doesn't need to be redisplayed post-upload
105                         if ( !warning ) {
106                                 $( '#wpDestFileWarningAck' ).val( '' );
107                                 $warningBox.removeAttr( 'class' );
108                         } else {
109                                 $( '#wpDestFileWarningAck' ).val( '1' );
110                                 $warningBox.attr( 'class', 'mw-destfile-warning' );
111                         }
113                 }
114         };
116         uploadLicense = {
118                 responseCache: { '': '' },
120                 fetchPreview: function ( license ) {
121                         var $spinnerLicense;
122                         if ( !mw.config.get( 'wgAjaxLicensePreview' ) ) {
123                                 return;
124                         }
125                         if ( this.responseCache.hasOwnProperty( license ) ) {
126                                 this.showPreview( this.responseCache[ license ] );
127                                 return;
128                         }
130                         $spinnerLicense = $.createSpinner().insertAfter( '#wpLicense' );
132                         ( new mw.Api() ).get( {
133                                 formatversion: 2,
134                                 action: 'parse',
135                                 text: '{{' + license + '}}',
136                                 title: $( '#wpDestFile' ).val() || 'File:Sample.jpg',
137                                 prop: 'text',
138                                 pst: true
139                         } ).done( function ( result ) {
140                                 uploadLicense.processResult( result, license );
141                         } ).always( function () {
142                                 $spinnerLicense.remove();
143                         } );
144                 },
146                 processResult: function ( result, license ) {
147                         this.responseCache[ license ] = result.parse.text;
148                         this.showPreview( this.responseCache[ license ] );
149                 },
151                 showPreview: function ( preview ) {
152                         $( '#mw-license-preview' ).html( preview );
153                 }
155         };
157         $( function () {
158                 // AJAX wpDestFile warnings
159                 if ( ajaxUploadDestCheck ) {
160                         // Insert an event handler that fetches upload warnings when wpDestFile
161                         // has been changed
162                         $( '#wpDestFile' ).change( function () {
163                                 uploadWarning.checkNow( $( this ).val() );
164                         } );
165                         // Insert a row where the warnings will be displayed just below the
166                         // wpDestFile row
167                         $( '#mw-htmlform-description tbody' ).append(
168                                 $( '<tr>' ).append(
169                                         $( '<td>' )
170                                                 .attr( 'id', 'wpDestFile-warning' )
171                                                 .attr( 'colspan', 2 )
172                                 )
173                         );
174                 }
176                 if ( mw.config.get( 'wgAjaxLicensePreview' ) && $license.length ) {
177                         // License selector check
178                         $license.change( function () {
179                                 // We might show a preview
180                                 uploadLicense.fetchPreview( $license.val() );
181                         } );
183                         // License selector table row
184                         $license.closest( 'tr' ).after(
185                                 $( '<tr>' ).append(
186                                         $( '<td>' ),
187                                         $( '<td>' ).attr( 'id', 'mw-license-preview' )
188                                 )
189                         );
190                 }
192                 // fillDestFile setup
193                 $.each( mw.config.get( 'wgUploadSourceIds' ), function ( index, sourceId ) {
194                         $( '#' + sourceId ).change( function () {
195                                 var path, slash, backslash, fname;
196                                 if ( !mw.config.get( 'wgUploadAutoFill' ) ) {
197                                         return;
198                                 }
199                                 // Remove any previously flagged errors
200                                 $( '#mw-upload-permitted' ).attr( 'class', '' );
201                                 $( '#mw-upload-prohibited' ).attr( 'class', '' );
203                                 path = $( this ).val();
204                                 // Find trailing part
205                                 slash = path.lastIndexOf( '/' );
206                                 backslash = path.lastIndexOf( '\\' );
207                                 if ( slash === -1 && backslash === -1 ) {
208                                         fname = path;
209                                 } else if ( slash > backslash ) {
210                                         fname = path.slice( slash + 1 );
211                                 } else {
212                                         fname = path.slice( backslash + 1 );
213                                 }
215                                 // Clear the filename if it does not have a valid extension.
216                                 // URLs are less likely to have a useful extension, so don't include them in the
217                                 // extension check.
218                                 if (
219                                         mw.config.get( 'wgCheckFileExtensions' ) &&
220                                         mw.config.get( 'wgStrictFileExtensions' ) &&
221                                         mw.config.get( 'wgFileExtensions' ) &&
222                                         $( this ).attr( 'id' ) !== 'wpUploadFileURL'
223                                 ) {
224                                         if (
225                                                 fname.lastIndexOf( '.' ) === -1 ||
226                                                 $.inArray(
227                                                         fname.slice( fname.lastIndexOf( '.' ) + 1 ).toLowerCase(),
228                                                         $.map( mw.config.get( 'wgFileExtensions' ), function ( element ) {
229                                                                 return element.toLowerCase();
230                                                         } )
231                                                 ) === -1
232                                         ) {
233                                                 // Not a valid extension
234                                                 // Clear the upload and set mw-upload-permitted to error
235                                                 $( this ).val( '' );
236                                                 $( '#mw-upload-permitted' ).attr( 'class', 'error' );
237                                                 $( '#mw-upload-prohibited' ).attr( 'class', 'error' );
238                                                 // Clear wpDestFile as well
239                                                 $( '#wpDestFile' ).val( '' );
241                                                 return false;
242                                         }
243                                 }
245                                 // Replace spaces by underscores
246                                 fname = fname.replace( / /g, '_' );
247                                 // Capitalise first letter if needed
248                                 if ( mw.config.get( 'wgCapitalizeUploads' ) ) {
249                                         fname = fname[ 0 ].toUpperCase() + fname.slice( 1 );
250                                 }
252                                 // Output result
253                                 if ( $( '#wpDestFile' ).length ) {
254                                         // Call decodeURIComponent function to remove possible URL-encoded characters
255                                         // from the file name (T32390). Especially likely with upload-form-url.
256                                         // decodeURIComponent can throw an exception if input is invalid utf-8
257                                         try {
258                                                 $( '#wpDestFile' ).val( decodeURIComponent( fname ) );
259                                         } catch ( err ) {
260                                                 $( '#wpDestFile' ).val( fname );
261                                         }
262                                         uploadWarning.checkNow( fname );
263                                 }
264                         } );
265                 } );
266         } );
268         // Add a preview to the upload form
269         $( function () {
270                 /**
271                  * Is the FileAPI available with sufficient functionality?
272                  *
273                  * @return {boolean}
274                  */
275                 function hasFileAPI() {
276                         return window.FileReader !== undefined;
277                 }
279                 /**
280                  * Check if this is a recognizable image type...
281                  * Also excludes files over 10M to avoid going insane on memory usage.
282                  *
283                  * TODO: Is there a way we can ask the browser what's supported in `<img>`s?
284                  *
285                  * TODO: Put SVG back after working around Firefox 7 bug <https://phabricator.wikimedia.org/T33643>
286                  *
287                  * @param {File} file
288                  * @return {boolean}
289                  */
290                 function fileIsPreviewable( file ) {
291                         var known = [ 'image/png', 'image/gif', 'image/jpeg', 'image/svg+xml' ],
292                                 tooHuge = 10 * 1024 * 1024;
293                         return ( $.inArray( file.type, known ) !== -1 ) && file.size > 0 && file.size < tooHuge;
294                 }
296                 /**
297                  * Format a file size attractively.
298                  *
299                  * TODO: Match numeric formatting
300                  *
301                  * @param {number} s
302                  * @return {string}
303                  */
304                 function prettySize( s ) {
305                         var sizeMsgs = [ 'size-bytes', 'size-kilobytes', 'size-megabytes', 'size-gigabytes' ];
306                         while ( s >= 1024 && sizeMsgs.length > 1 ) {
307                                 s /= 1024;
308                                 sizeMsgs = sizeMsgs.slice( 1 );
309                         }
310                         return mw.msg( sizeMsgs[ 0 ], Math.round( s ) );
311                 }
313                 /**
314                  * Show a thumbnail preview of PNG, JPEG, GIF, and SVG files prior to upload
315                  * in browsers supporting HTML5 FileAPI.
316                  *
317                  * As of this writing, known good:
318                  *
319                  * - Firefox 3.6+
320                  * - Chrome 7.something
321                  *
322                  * TODO: Check file size limits and warn of likely failures
323                  *
324                  * @param {File} file
325                  */
326                 function showPreview( file ) {
327                         var $canvas,
328                                 ctx,
329                                 meta,
330                                 previewSize = 180,
331                                 $spinner = $.createSpinner( { size: 'small', type: 'block' } )
332                                         .css( { width: previewSize, height: previewSize } ),
333                                 thumb = mw.template.get( 'mediawiki.special.upload', 'thumbnail.html' ).render();
335                         thumb
336                                 .find( '.filename' ).text( file.name ).end()
337                                 .find( '.fileinfo' ).text( prettySize( file.size ) ).end()
338                                 .find( '.thumbinner' ).prepend( $spinner ).end();
340                         $canvas = $( '<canvas>' ).attr( { width: previewSize, height: previewSize } );
341                         ctx = $canvas[ 0 ].getContext( '2d' );
342                         $( '#mw-htmlform-source' ).parent().prepend( thumb );
344                         fetchPreview( file, function ( dataURL ) {
345                                 var img = new Image(),
346                                         rotation = 0;
348                                 if ( meta && meta.tiff && meta.tiff.Orientation ) {
349                                         rotation = ( 360 - ( function () {
350                                                 // See includes/media/Bitmap.php
351                                                 switch ( meta.tiff.Orientation.value ) {
352                                                         case 8:
353                                                                 return 90;
354                                                         case 3:
355                                                                 return 180;
356                                                         case 6:
357                                                                 return 270;
358                                                         default:
359                                                                 return 0;
360                                                 }
361                                         }() ) ) % 360;
362                                 }
364                                 img.onload = function () {
365                                         var info, width, height, x, y, dx, dy, logicalWidth, logicalHeight;
367                                         // Fit the image within the previewSizexpreviewSize box
368                                         if ( img.width > img.height ) {
369                                                 width = previewSize;
370                                                 height = img.height / img.width * previewSize;
371                                         } else {
372                                                 height = previewSize;
373                                                 width = img.width / img.height * previewSize;
374                                         }
375                                         // Determine the offset required to center the image
376                                         dx = ( 180 - width ) / 2;
377                                         dy = ( 180 - height ) / 2;
378                                         switch ( rotation ) {
379                                                 // If a rotation is applied, the direction of the axis
380                                                 // changes as well. You can derive the values below by
381                                                 // drawing on paper an axis system, rotate it and see
382                                                 // where the positive axis direction is
383                                                 case 0:
384                                                         x = dx;
385                                                         y = dy;
386                                                         logicalWidth = img.width;
387                                                         logicalHeight = img.height;
388                                                         break;
389                                                 case 90:
391                                                         x = dx;
392                                                         y = dy - previewSize;
393                                                         logicalWidth = img.height;
394                                                         logicalHeight = img.width;
395                                                         break;
396                                                 case 180:
397                                                         x = dx - previewSize;
398                                                         y = dy - previewSize;
399                                                         logicalWidth = img.width;
400                                                         logicalHeight = img.height;
401                                                         break;
402                                                 case 270:
403                                                         x = dx - previewSize;
404                                                         y = dy;
405                                                         logicalWidth = img.height;
406                                                         logicalHeight = img.width;
407                                                         break;
408                                         }
410                                         ctx.clearRect( 0, 0, 180, 180 );
411                                         ctx.rotate( rotation / 180 * Math.PI );
412                                         ctx.drawImage( img, x, y, width, height );
413                                         $spinner.replaceWith( $canvas );
415                                         // Image size
416                                         info = mw.msg( 'widthheight', logicalWidth, logicalHeight ) +
417                                                 ', ' + prettySize( file.size );
419                                         $( '#mw-upload-thumbnail .fileinfo' ).text( info );
420                                 };
421                                 img.onerror = function () {
422                                         // Can happen for example for invalid SVG files
423                                         clearPreview();
424                                 };
425                                 img.src = dataURL;
426                         }, mw.config.get( 'wgFileCanRotate' ) ? function ( data ) {
427                                 try {
428                                         meta = mw.libs.jpegmeta( data, file.fileName );
429                                         // eslint-disable-next-line no-underscore-dangle, camelcase
430                                         meta._binary_data = null;
431                                 } catch ( e ) {
432                                         meta = null;
433                                 }
434                         } : null );
435                 }
437                 /**
438                  * Start loading a file into memory; when complete, pass it as a
439                  * data URL to the callback function. If the callbackBinary is set it will
440                  * first be read as binary and afterwards as data URL. Useful if you want
441                  * to do preprocessing on the binary data first.
442                  *
443                  * @param {File} file
444                  * @param {Function} callback
445                  * @param {Function} callbackBinary
446                  */
447                 function fetchPreview( file, callback, callbackBinary ) {
448                         var reader = new FileReader();
449                         if ( callbackBinary && 'readAsBinaryString' in reader ) {
450                                 // To fetch JPEG metadata we need a binary string; start there.
451                                 // TODO
452                                 reader.onload = function () {
453                                         callbackBinary( reader.result );
455                                         // Now run back through the regular code path.
456                                         fetchPreview( file, callback );
457                                 };
458                                 reader.readAsBinaryString( file );
459                         } else if ( callbackBinary && 'readAsArrayBuffer' in reader ) {
460                                 // readAsArrayBuffer replaces readAsBinaryString
461                                 // However, our JPEG metadata library wants a string.
462                                 // So, this is going to be an ugly conversion.
463                                 reader.onload = function () {
464                                         var i,
465                                                 buffer = new Uint8Array( reader.result ),
466                                                 string = '';
467                                         for ( i = 0; i < buffer.byteLength; i++ ) {
468                                                 string += String.fromCharCode( buffer[ i ] );
469                                         }
470                                         callbackBinary( string );
472                                         // Now run back through the regular code path.
473                                         fetchPreview( file, callback );
474                                 };
475                                 reader.readAsArrayBuffer( file );
476                         } else if ( 'URL' in window && 'createObjectURL' in window.URL ) {
477                                 // Supported in Firefox 4.0 and above <https://developer.mozilla.org/en/DOM/window.URL.createObjectURL>
478                                 // WebKit has it in a namespace for now but that's ok. ;)
479                                 //
480                                 // Lifetime of this URL is until document close, which is fine
481                                 // for Special:Upload -- if this code gets used on longer-running
482                                 // pages, add a revokeObjectURL() when it's no longer needed.
483                                 //
484                                 // Prefer this over readAsDataURL for Firefox 7 due to bug reading
485                                 // some SVG files from data URIs <https://bugzilla.mozilla.org/show_bug.cgi?id=694165>
486                                 callback( window.URL.createObjectURL( file ) );
487                         } else {
488                                 // This ends up decoding the file to base-64 and back again, which
489                                 // feels horribly inefficient.
490                                 reader.onload = function () {
491                                         callback( reader.result );
492                                 };
493                                 reader.readAsDataURL( file );
494                         }
495                 }
497                 /**
498                  * Clear the file upload preview area.
499                  */
500                 function clearPreview() {
501                         $( '#mw-upload-thumbnail' ).remove();
502                 }
504                 /**
505                  * Check if the file does not exceed the maximum size
506                  *
507                  * @param {File} file
508                  * @return {boolean}
509                  */
510                 function checkMaxUploadSize( file ) {
511                         var maxSize, $error;
513                         function getMaxUploadSize( type ) {
514                                 var sizes = mw.config.get( 'wgMaxUploadSize' );
516                                 if ( sizes[ type ] !== undefined ) {
517                                         return sizes[ type ];
518                                 }
519                                 return sizes[ '*' ];
520                         }
522                         $( '.mw-upload-source-error' ).remove();
524                         maxSize = getMaxUploadSize( 'file' );
525                         if ( file.size > maxSize ) {
526                                 $error = $( '<p class="error mw-upload-source-error" id="wpSourceTypeFile-error">' +
527                                         mw.message( 'largefileserver', file.size, maxSize ).escaped() + '</p>' );
529                                 $( '#wpUploadFile' ).after( $error );
531                                 return false;
532                         }
534                         return true;
535                 }
537                 /* Initialization */
538                 if ( hasFileAPI() ) {
539                         // Update thumbnail when the file selection control is updated.
540                         $( '#wpUploadFile' ).change( function () {
541                                 var file;
542                                 clearPreview();
543                                 if ( this.files && this.files.length ) {
544                                         // Note: would need to be updated to handle multiple files.
545                                         file = this.files[ 0 ];
547                                         if ( !checkMaxUploadSize( file ) ) {
548                                                 return;
549                                         }
551                                         if ( fileIsPreviewable( file ) ) {
552                                                 showPreview( file );
553                                         }
554                                 }
555                         } );
556                 }
557         } );
559         // Disable all upload source fields except the selected one
560         $( function () {
561                 var $rows = $( '.mw-htmlform-field-UploadSourceField' );
563                 $rows.on( 'change', 'input[type="radio"]', function ( e ) {
564                         var currentRow = e.delegateTarget;
566                         if ( !this.checked ) {
567                                 return;
568                         }
570                         $( '.mw-upload-source-error' ).remove();
572                         // Enable selected upload method
573                         $( currentRow ).find( 'input' ).prop( 'disabled', false );
575                         // Disable inputs of other upload methods
576                         // (except for the radio button to re-enable it)
577                         $rows
578                                 .not( currentRow )
579                                 .find( 'input[type!="radio"]' )
580                                 .prop( 'disabled', true );
581                 } );
583                 // Set initial state
584                 if ( !$( '#wpSourceTypeurl' ).prop( 'checked' ) ) {
585                         $( '#wpUploadFileURL' ).prop( 'disabled', true );
586                 }
587         } );
589         $( function () {
590                 // Prevent losing work
591                 var allowCloseWindow,
592                         $uploadForm = $( '#mw-upload-form' );
594                 if ( !mw.user.options.get( 'useeditwarning' ) ) {
595                         // If the user doesn't want edit warnings, don't set things up.
596                         return;
597                 }
599                 $uploadForm.data( 'origtext', $uploadForm.serialize() );
601                 allowCloseWindow = mw.confirmCloseWindow( {
602                         test: function () {
603                                 return $( '#wpUploadFile' ).get( 0 ).files.length !== 0 ||
604                                         $uploadForm.data( 'origtext' ) !== $uploadForm.serialize();
605                         },
607                         message: mw.msg( 'editwarning-warning' ),
608                         namespace: 'uploadwarning'
609                 } );
611                 $uploadForm.submit( function () {
612                         allowCloseWindow.release();
613                 } );
614         } );
615 }( mediaWiki, jQuery ) );