r97840 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r97839‎ | r97840 | r97841 >
Date:19:16, 22 September 2011
Author:raindrift
Status:ok
Tags:
Comment:
split mw.UploadWizard and mw.UploadWizardUpload into their own files for clarity.
Modified paths:
  • /trunk/extensions/UploadWizard/UploadWizardHooks.php (modified) (history)
  • /trunk/extensions/UploadWizard/resources/mw.UploadWizard.js (modified) (history)
  • /trunk/extensions/UploadWizard/resources/mw.UploadWizardUpload.js (added) (history)

Diff [purge]

Index: trunk/extensions/UploadWizard/UploadWizardHooks.php
@@ -92,6 +92,7 @@
9393 'resources/mw.UploadWizard.js',
9494
9595 // main library components:
 96+ 'resources/mw.UploadWizardUpload.js',
9697 'resources/mw.UploadWizardDeed.js',
9798 'resources/mw.UploadWizardDescription.js',
9899 'resources/mw.UploadWizardDetails.js',
Index: trunk/extensions/UploadWizard/resources/mw.UploadWizard.js
@@ -1,925 +1,9 @@
22 /**
3 - * Represents the upload -- in its local and remote state. (Possibly those could be separate objects too...)
4 - * This is our 'model' object if we are thinking MVC. Needs to be better factored, lots of feature envy with the UploadWizard
5 - * states:
6 - * 'new' 'transporting' 'transported' 'metadata' 'stashed' 'details' 'submitting-details' 'complete' 'error'
7 - * should fork this into two -- local and remote, e.g. filename
8 - */
9 -( function( $j ) {
10 -
11 -mw.UploadWizardUpload = function( wizard, filesDiv ) {
12 -
13 - this.index = mw.UploadWizardUpload.prototype.count;
14 - mw.UploadWizardUpload.prototype.count++;
15 -
16 - this.wizard = wizard;
17 - this.api = wizard.api;
18 - this.state = 'new';
19 - this.thumbnails = {};
20 - this.thumbnailPublishers = {};
21 - this.imageinfo = {};
22 - this.title = undefined;
23 - this.mimetype = undefined;
24 - this.extension = undefined;
25 - this.filename = undefined;
26 -
27 - this.fileKey = undefined;
28 -
29 - // this should be moved to the interface, if we even keep this
30 - this.transportWeight = 1; // default all same
31 - this.detailsWeight = 1; // default all same
32 -
33 - // details
34 - this.ui = new mw.UploadWizardUploadInterface( this, filesDiv );
35 -
36 - // handler -- usually ApiUploadHandler
37 - // this.handler = new ( mw.UploadWizard.config[ 'uploadHandlerClass' ] )( this );
38 - // this.handler = new mw.MockUploadHandler( this );
39 - this.handler = this.getUploadHandler();
40 -
41 -
42 -};
43 -
44 -mw.UploadWizardUpload.prototype = {
45 - // Upload handler
46 - uploadHandler: null,
47 -
48 - // increments with each upload
49 - count: 0,
50 -
51 - acceptDeed: function( deed ) {
52 - var _this = this;
53 - _this.deed.applyDeed( _this );
54 - },
55 -
56 - /**
57 - * start
58 - */
59 - start: function() {
60 - var _this = this;
61 - _this.setTransportProgress(0.0);
62 - //_this.ui.start();
63 - _this.handler.start();
64 - },
65 -
66 - /**
67 - * remove this upload. n.b. we trigger a removeUpload this is usually triggered from
68 - */
69 - remove: function() {
70 - this.state = 'aborted';
71 - if ( this.deedPreview ) {
72 - this.deedPreview.remove();
73 - }
74 - if ( this.details && this.details.div ) {
75 - this.details.div.remove();
76 - }
77 - if ( this.thanksDiv ) {
78 - this.thanksDiv.remove();
79 - }
80 - // we signal to the wizard to update itself, which has to delete the final vestige of
81 - // this upload (the ui.div). We have to do this silly dance because we
82 - // trigger through the div. Triggering through objects doesn't always work.
83 - // TODO v.1.1 fix, don't need to use the div any more -- this now works in jquery 1.4.2
84 - $j( this.ui.div ).trigger( 'removeUploadEvent' );
85 - },
86 -
87 -
88 - /**
89 - * Wear our current progress, for observing processes to see
90 - * @param fraction
91 - */
92 - setTransportProgress: function ( fraction ) {
93 - var _this = this;
94 - _this.state = 'transporting';
95 - _this.transportProgress = fraction;
96 - $j( _this.ui.div ).trigger( 'transportProgressEvent' );
97 - },
98 -
99 - /**
100 - * Queue some warnings for possible later consumption
101 - */
102 - addWarning: function( code, info ) {
103 - if ( !mw.isDefined( this.warnings ) ) {
104 - this.warnings = [];
105 - }
106 - this.warnings.push( [ code, info ] );
107 - },
108 -
109 - /**
110 - * Stop the upload -- we have failed for some reason
111 - */
112 - setError: function( code, info ) {
113 - this.state = 'error';
114 - this.transportProgress = 0;
115 - this.ui.showError( code, info );
116 - },
117 -
118 - /**
119 - * To be executed when an individual upload finishes. Processes the result and updates step 2's details
120 - * @param result the API result in parsed JSON form
121 - */
122 - setTransported: function( result ) {
123 - var _this = this;
124 - if ( _this.state == 'aborted' ) {
125 - return;
126 - }
127 -
128 - // default error state
129 - var code = 'unknown';
130 - var info = 'unknown';
131 -
132 - if ( result.upload && result.upload.warnings ) {
133 - if ( result.upload.warnings['exists'] ) {
134 - // the filename we uploaded is in use already. Not a problem since we stashed it under a temporary name anyway
135 - // potentially we could indicate to the upload that it should set the Title field to error state now, but we'll let them deal with that later.
136 - // however, we don't get imageinfo, so let's try to get it and pretend that we did
137 - var existsFileName = result.upload.warnings.exists;
138 - try {
139 - code = 'exists';
140 - info = new mw.Title( existsFileName, 'file' ).getUrl();
141 - } catch ( e ) {
142 - code = 'unknown';
143 - info = 'Warned about existing filename, but filename is unparseable: "' + existsFileName + "'";
144 - }
145 - _this.addWarning( code, info );
146 - _this.extractUploadInfo( result.upload );
147 - var success = function( imageinfo ) {
148 - if ( imageinfo === null ) {
149 - _this.setError( 'noimageinfo' );
150 - } else {
151 - result.upload.stashimageinfo = imageinfo;
152 - _this.setSuccess( result );
153 - }
154 - };
155 - _this.getStashImageInfo( success, [ 'timestamp', 'url', 'size', 'dimensions', 'sha1', 'mime', 'metadata', 'bitdepth' ] );
156 - } else if ( result.upload.warnings['duplicate'] ) {
157 - code = 'duplicate';
158 - _this.setError( code, _this.duplicateErrorInfo( 'duplicate', result.upload.warnings['duplicate'] ) );
159 - } else if ( result.upload.warnings['duplicate-archive'] ) {
160 - code = 'duplicate-archive';
161 - _this.setError( code, _this.duplicateErrorInfo( 'duplicate-archive', result.upload.warnings['duplicate-archive'] ) );
162 - } else {
163 - // we have an unknown warning. Assume fatal
164 - code = 'unknown-warning';
165 - var warningInfo = [];
166 - $j.each( result.upload.warnings, function( k, v ) {
167 - warningInfo.push( k + ': ' + v );
168 - } );
169 - info = warningInfo.join( ', ' );
170 - _this.setError( code, [ info ] );
171 - }
172 - } else if ( result.upload && result.upload.result === 'Success' ) {
173 - if ( result.upload.imageinfo ) {
174 - _this.setSuccess( result );
175 - } else {
176 - _this.setError( 'noimageinfo', info );
177 - }
178 - } else {
179 - if ( result.error ) {
180 - if ( result.error.code ) {
181 - code = result.error.code;
182 - }
183 - if ( result.error.info ) {
184 - info = result.error.info;
185 - }
186 - }
187 - _this.setError( code, info );
188 - }
189 -
190 -
191 - },
192 -
193 -
194 - /**
195 - * Helper function to generate duplicate errors with dialog box. Works with existing duplicates and deleted dupes.
196 - * @param {String} error code, should have matching strings in .i18n.php
197 - * @param {Object} portion of the API error result listing duplicates
198 - */
199 - duplicateErrorInfo: function( code, resultDuplicate ) {
200 - var _this = this;
201 - var duplicates;
202 - if ( typeof resultDuplicate === 'object' ) {
203 - duplicates = resultDuplicate;
204 - } else if ( typeof resultDuplicate === 'string' ) {
205 - duplicates = [ resultDuplicate ];
206 - }
207 - var $ul = $j( '<ul></ul>' );
208 - $j.each( duplicates, function( i, filename ) {
209 - var $a = $j( '<a/>' ).append( filename );
210 - try {
211 - var href = new mw.Title( filename, 'file' ).getUrl();
212 - $a.attr( { 'href': href, 'target': '_blank' } );
213 - } catch ( e ) {
214 - $a.click( function() { alert('could not parse filename=' + filename ); } );
215 - $a.attr( 'href', '#' );
216 - }
217 - $ul.append( $j( '<li></li>' ).append( $a ) );
218 - } );
219 - var dialogFn = function() {
220 - $j( '<div></div>' )
221 - .html( $ul )
222 - .dialog( {
223 - width: 500,
224 - zIndex: 200000,
225 - autoOpen: true,
226 - title: gM( 'mwe-upwiz-api-error-' + code + '-popup-title', duplicates.length ),
227 - modal: true
228 - } );
229 - };
230 - return [ duplicates.length, dialogFn ];
231 - },
232 -
233 -
234 - /**
235 - * Called from any upload success condition
236 - * @param {Mixed} result -- result of AJAX call
237 - */
238 - setSuccess: function( result ) {
239 - var _this = this;
240 - _this.state = 'transported';
241 - _this.transportProgress = 1;
242 -
243 - _this.ui.setStatus( 'mwe-upwiz-getting-metadata' );
244 - if ( result.upload ) {
245 - _this.extractUploadInfo( result.upload );
246 - _this.deedPreview.setup();
247 - _this.details.populate();
248 - _this.state = 'stashed';
249 - _this.ui.showStashed();
250 - $.publishReady( 'thumbnails.' + _this.index, 'api' );
251 - } else {
252 - _this.setError( 'noimageinfo' );
253 - }
254 -
255 - },
256 -
257 - /**
258 - * Called when the file is entered into the file input.
259 - * Checks for file validity, then extracts metadata.
260 - * Error out if filename or its contents are determined to be unacceptable
261 - * Proceed to thumbnail extraction and image info if acceptable
262 - * @param {HTMLFileInput} file input field
263 - * @param {Function()} callback when ok, and upload object is ready
264 - * @param {Function(String, Mixed)} callback when filename or contents in error. Signature of string code, mixed info
265 - */
266 - checkFile: function( fileInput, fileNameOk, fileNameErr ) {
267 - // check if local file is acceptable
268 -
269 - var _this = this;
270 -
271 - // Check if filename is acceptable
272 - // TODO sanitize filename
273 - var filename = fileInput.value;
274 - var basename = mw.UploadWizardUtil.getBasename( filename );
275 -
276 -
277 - // check to see if the file has already been selected for upload.
278 - var duplicate = false;
279 - $j.each( this.wizard.uploads, function ( i, upload ) {
280 - if ( _this !== upload && filename === upload.filename ) {
281 - duplicate = true;
282 - return false;
283 - }
284 - } );
285 -
286 - if( duplicate ) {
287 - fileNameErr( 'dup', basename );
288 - return false;
289 - }
290 -
291 - try {
292 - this.title = new mw.Title( basename.replace( /:/g, '_' ), 'file' );
293 - } catch ( e ) {
294 - fileNameErr( 'unparseable' );
295 - }
296 -
297 - // Check if extension is acceptable
298 - var extension = this.title.getExtension();
299 - if ( mw.isEmpty( extension ) ) {
300 - fileNameErr( 'noext' );
301 - } else {
302 - if ( $j.inArray( extension.toLowerCase(), mw.UploadWizard.config[ 'fileExtensions' ] ) === -1 ) {
303 - fileNameErr( 'ext', extension );
304 - } else {
305 -
306 - // extract more info via fileAPI
307 - if ( mw.fileApi.isAvailable() ) {
308 - if ( fileInput.files && fileInput.files.length ) {
309 - // TODO multiple files in an input
310 - this.file = fileInput.files[0];
311 - }
312 - // TODO check max upload size, alert user if too big
313 - this.transportWeight = this.file.size;
314 - if ( !mw.isDefined( this.imageinfo ) ) {
315 - this.imageinfo = {};
316 - }
317 -
318 - var binReader = new FileReader();
319 - binReader.onload = function() {
320 - var meta;
321 - try {
322 - meta = mw.libs.jpegmeta( binReader.result, _this.file.fileName );
323 - meta._binary_data = null;
324 - } catch ( e ) {
325 - meta = null;
326 - }
327 - _this.extractMetadataFromJpegMeta( meta );
328 - _this.filename = filename;
329 - fileNameOk();
330 - };
331 - binReader.readAsBinaryString( _this.file );
332 - } else {
333 - this.filename = filename;
334 - fileNameOk();
335 - }
336 -
337 - }
338 -
339 - }
340 -
341 - },
342 -
343 -
344 -
345 -
346 - /**
347 - * Map fields from jpegmeta's metadata return into our format (which is more like the imageinfo returned from the API
348 - * @param {Object} (as returned by jpegmeta)
349 - */
350 - extractMetadataFromJpegMeta: function( meta ) {
351 - if ( mw.isDefined( meta ) && meta !== null && typeof meta === 'object' ) {
352 - if ( !mw.isDefined( this.imageinfo ) ) {
353 - this.imageinfo = {};
354 - }
355 - if ( !mw.isDefined( this.imageinfo.metadata ) ) {
356 - this.imageinfo.metadata = {};
357 - }
358 - if ( meta.tiff && meta.tiff.Orientation ) {
359 - this.imageinfo.metadata.orientation = meta.tiff.Orientation.value;
360 - }
361 - if ( meta.general ) {
362 - var pixelHeightDim = 'height';
363 - var pixelWidthDim = 'width';
364 - // this must be called after orientation is set above. If no orientation set, defaults to 0
365 - var degrees = this.getOrientationDegrees();
366 - // jpegmeta reports pixelHeight & width
367 - if ( degrees == 90 || degrees == 270 ) {
368 - pixelHeightDim = 'width';
369 - pixelWidthDim = 'height';
370 - }
371 - if ( meta.general.pixelHeight ) {
372 - this.imageinfo[pixelHeightDim] = meta.general.pixelHeight.value;
373 - }
374 - if ( meta.general.pixelWidth ) {
375 - this.imageinfo[pixelWidthDim] = meta.general.pixelWidth.value;
376 - }
377 - }
378 - }
379 - },
380 -
381 - /**
382 - * Accept the result from a successful API upload transport, and fill our own info
383 - *
384 - * @param result The JSON object from a successful API upload result.
385 - */
386 - extractUploadInfo: function( resultUpload ) {
387 -
388 - if ( resultUpload.filekey ) {
389 - this.fileKey = resultUpload.filekey;
390 - }
391 -
392 - if ( resultUpload.imageinfo ) {
393 - this.extractImageInfo( resultUpload.imageinfo );
394 - } else if ( resultUpload.stashimageinfo ) {
395 - this.extractImageInfo( resultUpload.stashimageinfo );
396 - }
397 -
398 - },
399 -
400 - /**
401 - * Extract image info into our upload object
402 - * Image info is obtained from various different API methods
403 - * This may overwrite metadata obtained from FileReader.
404 - * @param imageinfo JSON object obtained from API result.
405 - */
406 - extractImageInfo: function( imageinfo ) {
407 - var _this = this;
408 - for ( var key in imageinfo ) {
409 - // we get metadata as list of key-val pairs; convert to object for easier lookup. Assuming that EXIF fields are unique.
410 - if ( key == 'metadata' ) {
411 - if ( !mw.isDefined( _this.imageinfo.metadata ) ) {
412 - _this.imageinfo.metadata = {};
413 - }
414 - if ( imageinfo.metadata && imageinfo.metadata.length ) {
415 - $j.each( imageinfo.metadata, function( i, pair ) {
416 - if ( pair !== undefined ) {
417 - _this.imageinfo.metadata[pair['name'].toLowerCase()] = pair['value'];
418 - }
419 - } );
420 - }
421 - } else {
422 - _this.imageinfo[key] = imageinfo[key];
423 - }
424 - }
425 -
426 - if ( _this.title.getExtension() === null ) {
427 - 1;
428 - // TODO v1.1 what if we don't have an extension? Should be impossible as it is currently impossible to upload without extension, but you
429 - // never know... theoretically there is no restriction on extensions if we are uploading to the stash, but the check is performed anyway.
430 - /*
431 - var extension = mw.UploadWizardUtil.getExtension( _this.imageinfo.url );
432 - if ( !extension ) {
433 - if ( _this.imageinfo.mimetype ) {
434 - if ( mw.UploadWizardUtil.mimetypeToExtension[ _this.imageinfo.mimetype ] ) {
435 - extension = mw.UploadWizardUtil.mimetypeToExtension[ _this.imageinfo.mimetype ];
436 - }
437 - }
438 - }
439 - */
440 - }
441 -
442 -
443 -
444 -
445 - },
446 -
447 - /**
448 - * Get information about stashed images
449 - * See API documentation for prop=stashimageinfo for what 'props' can contain
450 - * @param {Function} callback -- called with null if failure, with imageinfo data structure if success
451 - * @param {Array} properties to extract
452 - * @param {Number} optional, width of thumbnail. Will force 'url' to be added to props
453 - * @param {Number} optional, height of thumbnail. Will force 'url' to be added to props
454 - */
455 - getStashImageInfo: function( callback, props, width, height ) {
456 - var _this = this;
457 -
458 - if (!mw.isDefined( props ) ) {
459 - props = [];
460 - }
461 -
462 - var params = {
463 - 'prop': 'stashimageinfo',
464 - 'siifilekey': _this.fileKey,
465 - 'siiprop': props.join( '|' )
466 - };
467 -
468 - if ( mw.isDefined( width ) || mw.isDefined( height ) ) {
469 - if ( ! $j.inArray( 'url', props ) ) {
470 - props.push( 'url' );
471 - }
472 - if ( mw.isDefined( width ) ) {
473 - params['siiurlwidth'] = width;
474 - }
475 - if ( mw.isDefined( height ) ) {
476 - params['siiurlheight'] = height;
477 - }
478 - }
479 -
480 - var ok = function( data ) {
481 - if ( !data || !data.query || !data.query.stashimageinfo ) {
482 - mw.log("mw.UploadWizardUpload::getStashImageInfo> No data? ");
483 - callback( null );
484 - return;
485 - }
486 - callback( data.query.stashimageinfo );
487 - };
488 -
489 - var err = function( code, result ) {
490 - mw.log( 'mw.UploadWizardUpload::getStashImageInfo> error: ' + code, 'debug' );
491 - callback( null );
492 - };
493 -
494 - this.api.get( params, { ok: ok, err: err } );
495 - },
496 -
497 -
498 - /**
499 - * Get information about published images
500 - * (There is some overlap with getStashedImageInfo, but it's different at every stage so it's clearer to have separate functions)
501 - * See API documentation for prop=imageinfo for what 'props' can contain
502 - * @param {Function} callback -- called with null if failure, with imageinfo data structure if success
503 - * @param {Array} properties to extract
504 - * @param {Number} optional, width of thumbnail. Will force 'url' to be added to props
505 - * @param {Number} optional, height of thumbnail. Will force 'url' to be added to props
506 - */
507 - getImageInfo: function( callback, props, width, height ) {
508 - var _this = this;
509 - if (!mw.isDefined( props ) ) {
510 - props = [];
511 - }
512 - var requestedTitle = _this.title.getPrefixedText();
513 - var params = {
514 - 'prop': 'imageinfo',
515 - 'titles': requestedTitle,
516 - 'iiprop': props.join( '|' )
517 - };
518 -
519 - if ( mw.isDefined( width ) || mw.isDefined( height ) ) {
520 - if ( ! $j.inArray( 'url', props ) ) {
521 - props.push( 'url' );
522 - }
523 - if ( mw.isDefined( width ) ) {
524 - params['iiurlwidth'] = width;
525 - }
526 - if ( mw.isDefined( height ) ) {
527 - params['iiurlheight'] = height;
528 - }
529 - }
530 -
531 - var ok = function( data ) {
532 - if ( data && data.query && data.query.pages ) {
533 - var found = false;
534 - $j.each( data.query.pages, function( pageId, page ) {
535 - if ( page.title && page.title === requestedTitle && page.imageinfo ) {
536 - found = true;
537 - callback( page.imageinfo );
538 - return false;
539 - }
540 - } );
541 - if ( found ) {
542 - return;
543 - }
544 - }
545 - mw.log("mw.UploadWizardUpload::getImageInfo> No data matching " + requestedTitle + " ? ");
546 - callback( null );
547 - };
548 -
549 - var err = function( code, result ) {
550 - mw.log( 'mw.UploadWizardUpload::getImageInfo> error: ' + code, 'debug' );
551 - callback( null );
552 - };
553 -
554 - this.api.get( params, { ok: ok, err: err } );
555 - },
556 -
557 -
558 - /**
559 - * Get the upload handler per browser capabilities
560 - */
561 - getUploadHandler: function(){
562 - if( !this.uploadHandler ){
563 - if( mw.UploadWizard.config[ 'enableFirefogg' ]
564 - &&
565 - typeof( Firefogg ) != 'undefined'
566 - ) {
567 - mw.log("mw.UploadWizard::getUploadHandler> FirefoggHandler");
568 - this.uploadHandler = new mw.FirefoggHandler( this, this.api );
569 - } else if( mw.UploadWizard.config[ 'enableFormData' ] &&
570 - (($j.browser.mozilla && $j.browser.version >= '5.0') ||
571 - ($j.browser.webkit && $j.browser.version >= '534.28'))
572 - ) {
573 - mw.log("mw.UploadWizard::getUploadHandler> ApiUploadFormDataHandler");
574 - this.uploadHandler = new mw.ApiUploadFormDataHandler( this, this.api );
575 - } else {
576 - // By default use the apiUploadHandler
577 - mw.log("mw.UploadWizard::getUploadHandler> ApiUploadHandler");
578 - this.uploadHandler = new mw.ApiUploadHandler( this, this.api );
579 - }
580 - }
581 - return this.uploadHandler;
582 - },
583 -
584 - /**
585 - * Explicitly fetch a thumbnail for a stashed upload of the desired width.
586 - * Publishes to any event listeners that might have wanted it.
587 - *
588 - * @param width - desired width of thumbnail (height will scale to match)
589 - * @param height - (optional) maximum height of thumbnail
590 - */
591 - getAndPublishApiThumbnail: function( key, width, height ) {
592 - var _this = this;
593 -
594 - if ( mw.isEmpty( height ) ) {
595 - height = -1;
596 - }
597 -
598 - if ( !mw.isDefined( _this.thumbnailPublishers[key] ) ) {
599 - var thumbnailPublisher = function( thumbnails ) {
600 - if ( thumbnails === null ) {
601 - // the api call failed somehow, no thumbnail data.
602 - $j.publishReady( key, null );
603 - } else {
604 - // ok, the api callback has returned us information on where the thumbnail(s) ARE, but that doesn't mean
605 - // they are actually there yet. Keep trying to set the source ( which should trigger "error" or "load" event )
606 - // on the image. If it loads publish the event with the image. If it errors out too many times, give up and publish
607 - // the event with a null.
608 - $j.each( thumbnails, function( i, thumb ) {
609 - if ( thumb.thumberror || ( ! ( thumb.thumburl && thumb.thumbwidth && thumb.thumbheight ) ) ) {
610 - mw.log( "mw.UploadWizardUpload::getThumbnail> thumbnail error or missing information" );
611 - $j.publishReady( key, null );
612 - return;
613 - }
614 -
615 - // try to load this image with exponential backoff
616 - // if the delay goes past 8 seconds, it gives up and publishes the event with null
617 - var timeoutMs = 100;
618 - var image = document.createElement( 'img' );
619 - image.width = thumb.thumbwidth;
620 - image.height = thumb.thumbheight;
621 - $j( image )
622 - .load( function() {
623 - // cache this thumbnail
624 - _this.thumbnails[key] = image;
625 - // publish the image to anyone who wanted it
626 - $j.publishReady( key, image );
627 - } )
628 - .error( function() {
629 - // retry with exponential backoff
630 - if ( timeoutMs < 8000 ) {
631 - setTimeout( function() {
632 - timeoutMs = timeoutMs * 2 + Math.round( Math.random() * ( timeoutMs / 10 ) );
633 - setSrc();
634 - }, timeoutMs );
635 - } else {
636 - $j.publishReady( key, null );
637 - }
638 - } );
639 -
640 - // executing this should cause a .load() or .error() event on the image
641 - function setSrc() {
642 - image.src = thumb.thumburl;
643 - }
644 -
645 - // and, go!
646 - setSrc();
647 - } );
648 - }
649 - };
650 -
651 - _this.thumbnailPublishers[key] = thumbnailPublisher;
652 - if ( _this.state !== 'complete' ) {
653 - _this.getStashImageInfo( thumbnailPublisher, [ 'url' ], width, height );
654 - } else {
655 - _this.getImageInfo( thumbnailPublisher, [ 'url' ], width, height );
656 - }
657 -
658 - }
659 - },
660 -
661 - /**
662 - * Return the orientation of the image in degrees. Relies on metadata that
663 - * may have been extracted at filereader stage, or after the upload when we fetch metadata. Default returns 0.
664 - * @return {Integer} orientation in degrees: 0, 90, 180 or 270
665 - */
666 - getOrientationDegrees: function() {
667 - var orientation = 0;
668 - if ( this.imageinfo && this.imageinfo.metadata && this.imageinfo.metadata.orientation ) {
669 - switch ( this.imageinfo.metadata.orientation ) {
670 - case 8:
671 - orientation = 90; // 'top left' -> 'left bottom'
672 - break;
673 - case 3:
674 - orientation = 180; // 'top left' -> 'bottom right'
675 - break;
676 - case 6:
677 - orientation = 270; // 'top left' -> 'right top'
678 - break;
679 - case 1:
680 - default:
681 - orientation = 0; // 'top left' -> 'top left'
682 - break;
683 -
684 - }
685 - }
686 - return orientation;
687 - },
688 -
689 - /**
690 - * Fit an image into width & height constraints with scaling factor
691 - * @param {HTMLImageElement}
692 - * @param {Object} with width & height properties
693 - * @return {Number}
694 - */
695 - getScalingFromConstraints: function( image, constraints ) {
696 - var scaling = 1;
697 - $j.each( [ 'width', 'height' ], function( i, dim ) {
698 - if ( constraints[dim] && image[dim] > constraints[dim] ) {
699 - var s = constraints[dim] / image[dim];
700 - if ( s < scaling ) {
701 - scaling = s;
702 - }
703 - }
704 - } );
705 - return scaling;
706 - },
707 -
708 - /**
709 - * Given an image (already loaded), dimension constraints
710 - * return canvas object scaled & transformedi ( & rotated if metadata indicates it's needed )
711 - * @param {HTMLImageElement}
712 - * @param {Object} containing width & height constraints
713 - * @return {HTMLCanvasElement}
714 - */
715 - getTransformedCanvasElement: function( image, constraints ) {
716 -
717 - var rotation = 0;
718 -
719 - // if this wiki can rotate images to match their EXIF metadata,
720 - // we should do the same in our preview
721 - if ( mw.config.get( 'wgFileCanRotate' ) ) {
722 - var angle = this.getOrientationDegrees();
723 - rotation = angle ? 360 - angle : 0;
724 - }
725 -
726 - // swap scaling constraints if needed by rotation...
727 - var scaleConstraints;
728 - if ( rotation === 90 || rotation === 270 ) {
729 - scaleConstraints = {
730 - width: constraints.height,
731 - height: constraints.width
732 - };
733 - } else {
734 - scaleConstraints = {
735 - width: constraints.width,
736 - height: constraints.height
737 - };
738 - }
739 -
740 - var scaling = this.getScalingFromConstraints( image, constraints );
741 -
742 - var width = image.width * scaling;
743 - var height = image.height * scaling;
744 -
745 - // Determine the offset required to center the image
746 - var dx = (constraints.width - width) / 2;
747 - var dy = (constraints.height - height) / 2;
748 -
749 - switch ( rotation ) {
750 - // If a rotation is applied, the direction of the axis
751 - // changes as well. You can derive the values below by
752 - // drawing on paper an axis system, rotate it and see
753 - // where the positive axis direction is
754 - case 90:
755 - x = dx;
756 - y = dy - constraints.height;
757 - break;
758 - case 180:
759 - x = dx - constraints.width;
760 - y = dy - constraints.height;
761 - break;
762 - case 270:
763 - x = dx - constraints.width;
764 - y = dy;
765 - break;
766 - case 0:
767 - default:
768 - x = dx;
769 - y = dy;
770 - break;
771 - }
772 -
773 - var $canvas = $j( '<canvas></canvas>' ).attr( constraints );
774 - var ctx = $canvas[0].getContext( '2d' );
775 - ctx.clearRect( 0, 0, width, height );
776 - ctx.rotate( rotation / 180 * Math.PI );
777 - ctx.drawImage( image, x, y, width, height );
778 -
779 - return $canvas;
780 - },
781 -
782 - /**
783 - * Return a browser-scaled image element, given an image and constraints.
784 - * @param {HTMLImageElement}
785 - * @param {Object} with width and height properties
786 - * @return {HTMLImageElement} with same src, but different attrs
787 - */
788 - getBrowserScaledImageElement: function( image, constraints ) {
789 - var scaling = this.getScalingFromConstraints( image, constraints );
790 - return $j( '<img/>' )
791 - .attr( {
792 - width: parseInt( image.width * scaling, 10 ),
793 - height: parseInt( image.height * scaling, 10 ),
794 - src: image.src
795 - } )
796 - .css( {
797 - 'margin-top': ( parseInt( ( constraints.height - image.height * scaling ) / 2, 10 ) ).toString() + 'px'
798 - } );
799 - },
800 -
801 - /**
802 - * Return an element suitable for the preview of a certain size. Uses canvas when possible
803 - * @param {HTMLImageElement}
804 - * @param {Integer} width
805 - * @param {Integer} height
806 - * @return {HTMLCanvasElement|HTMLImageElement}
807 - */
808 - getScaledImageElement: function( image, width, height ) {
809 - if ( typeof width === 'undefined' || width === null || width <= 0 ) {
810 - width = mw.UploadWizard.config['thumbnailWidth'];
811 - }
812 - var constraints = {
813 - width: parseInt( width, 10 ),
814 - height: ( mw.isDefined( height ) ? parseInt( height, 10 ) : null )
815 - };
816 -
817 - return mw.canvas.isAvailable() ? this.getTransformedCanvasElement( image, constraints )
818 - : this.getBrowserScaledImageElement( image, constraints );
819 - },
820 -
821 - /**
822 - * Given a jQuery selector, subscribe to the "ready" event that fills the thumbnail
823 - * This will trigger if the thumbnail is added in the future or if it already has been
824 - *
825 - * @param selector
826 - * @param width Width constraint
827 - * @param height Height constraint (optional)
828 - * @param boolean add lightbox large preview when ready
829 - */
830 - setThumbnail: function( selector, width, height, isLightBox ) {
831 - var _this = this;
832 -
833 - /**
834 - * This callback will add an image to the selector, using in-browser scaling if necessary
835 - * @param {HTMLImageElement}
836 - */
837 - var placed = false;
838 - var placeImageCallback = function( image ) {
839 - if ( image === null ) {
840 - $j( selector ).addClass( 'mwe-upwiz-file-preview-broken' );
841 - _this.ui.setStatus( 'mwe-upwiz-thumbnail-failed' );
842 - return;
843 - }
844 - var elm = _this.getScaledImageElement( image, width, height );
845 - // add the image to the DOM, finally
846 - $j( selector )
847 - .css( { background: 'none' } )
848 - .html(
849 - $j( '<a/></a>' )
850 - .addClass( "mwe-upwiz-thumbnail-link" )
851 - .append( elm )
852 - );
853 - placed = true;
854 - };
855 -
856 - // Listen for even which says some kind of thumbnail is available.
857 - // The argument is an either an ImageHtmlElement ( if we could get the thumbnail locally ) or the string 'api' indicating you
858 - // now need to get the scaled thumbnail via the API
859 - $.subscribeReady(
860 - 'thumbnails.' + _this.index,
861 - function ( x ) {
862 - if ( isLightBox ) {
863 - _this.setLightBox( selector );
864 - }
865 - if ( !placed ) {
866 - if ( x === 'api' ) {
867 - // get the thumbnail via API. This also works with an async pub/sub model; if this thumbnail was already
868 - // fetched for some reason, we'll get it immediately
869 - var key = 'apiThumbnail.' + _this.index + ',width=' + width + ',height=' + height;
870 - $.subscribeReady( key, placeImageCallback );
871 - _this.getAndPublishApiThumbnail( key, width, height );
872 - } else if ( x instanceof HTMLImageElement ) {
873 - placeImageCallback( x );
874 - } else {
875 - // something else went wrong, place broken image
876 - mw.log( 'unexpected argument to thumbnails event: ' + x );
877 - placeImageCallback( null );
878 - }
879 - }
880 - }
881 - );
882 - },
883 -
884 - /**
885 - * set up lightbox behavior for non-complete thumbnails
886 - * TODO center this
887 - * @param selector
888 - */
889 - setLightBox: function( selector ) {
890 - var _this = this;
891 - var $imgDiv = $j( '<div></div>' ).css( 'text-align', 'center' );
892 - $j( selector )
893 - .click( function() {
894 - // get large preview image
895 - // open large preview in modal dialog box
896 - $j( '<div class="mwe-upwiz-lightbox"></div>' )
897 - .append( $imgDiv )
898 - .dialog( {
899 - 'width': mw.UploadWizard.config[ 'largeThumbnailWidth' ],
900 - 'height': mw.UploadWizard.config[ 'largeThumbnailMaxHeight' ],
901 - 'autoOpen': true,
902 - 'title': gM( 'mwe-upwiz-image-preview' ),
903 - 'modal': true,
904 - 'resizable': false
905 - } );
906 - _this.setThumbnail(
907 - $imgDiv,
908 - mw.UploadWizard.config[ 'largeThumbnailWidth' ],
909 - mw.UploadWizard.config[ 'largeThumbnailMaxHeight' ],
910 - false /* obviously the largeThumbnail doesn't have a lightbox itself! */
911 - );
912 - return false;
913 - } ); // close thumbnail click function
914 - }
915 -
916 -};
917 -
918 -
919 -
920 -
921 -/**
9223 * Object that reperesents the entire multi-step Upload Wizard
9234 */
 5+
 6+( function( $j ) {
 7+
9248 mw.UploadWizard = function( config ) {
9259
92610 this.uploads = [];
Index: trunk/extensions/UploadWizard/resources/mw.UploadWizardUpload.js
@@ -0,0 +1,917 @@
 2+/**
 3+ * Represents the upload -- in its local and remote state. (Possibly those could be separate objects too...)
 4+ * This is our 'model' object if we are thinking MVC. Needs to be better factored, lots of feature envy with the UploadWizard
 5+ * states:
 6+ * 'new' 'transporting' 'transported' 'metadata' 'stashed' 'details' 'submitting-details' 'complete' 'error'
 7+ * should fork this into two -- local and remote, e.g. filename
 8+ */
 9+( function( $j ) {
 10+
 11+mw.UploadWizardUpload = function( wizard, filesDiv ) {
 12+
 13+ this.index = mw.UploadWizardUpload.prototype.count;
 14+ mw.UploadWizardUpload.prototype.count++;
 15+
 16+ this.wizard = wizard;
 17+ this.api = wizard.api;
 18+ this.state = 'new';
 19+ this.thumbnails = {};
 20+ this.thumbnailPublishers = {};
 21+ this.imageinfo = {};
 22+ this.title = undefined;
 23+ this.mimetype = undefined;
 24+ this.extension = undefined;
 25+ this.filename = undefined;
 26+
 27+ this.fileKey = undefined;
 28+
 29+ // this should be moved to the interface, if we even keep this
 30+ this.transportWeight = 1; // default all same
 31+ this.detailsWeight = 1; // default all same
 32+
 33+ // details
 34+ this.ui = new mw.UploadWizardUploadInterface( this, filesDiv );
 35+
 36+ // handler -- usually ApiUploadHandler
 37+ // this.handler = new ( mw.UploadWizard.config[ 'uploadHandlerClass' ] )( this );
 38+ // this.handler = new mw.MockUploadHandler( this );
 39+ this.handler = this.getUploadHandler();
 40+
 41+
 42+};
 43+
 44+mw.UploadWizardUpload.prototype = {
 45+ // Upload handler
 46+ uploadHandler: null,
 47+
 48+ // increments with each upload
 49+ count: 0,
 50+
 51+ acceptDeed: function( deed ) {
 52+ var _this = this;
 53+ _this.deed.applyDeed( _this );
 54+ },
 55+
 56+ /**
 57+ * start
 58+ */
 59+ start: function() {
 60+ var _this = this;
 61+ _this.setTransportProgress(0.0);
 62+ //_this.ui.start();
 63+ _this.handler.start();
 64+ },
 65+
 66+ /**
 67+ * remove this upload. n.b. we trigger a removeUpload this is usually triggered from
 68+ */
 69+ remove: function() {
 70+ this.state = 'aborted';
 71+ if ( this.deedPreview ) {
 72+ this.deedPreview.remove();
 73+ }
 74+ if ( this.details && this.details.div ) {
 75+ this.details.div.remove();
 76+ }
 77+ if ( this.thanksDiv ) {
 78+ this.thanksDiv.remove();
 79+ }
 80+ // we signal to the wizard to update itself, which has to delete the final vestige of
 81+ // this upload (the ui.div). We have to do this silly dance because we
 82+ // trigger through the div. Triggering through objects doesn't always work.
 83+ // TODO v.1.1 fix, don't need to use the div any more -- this now works in jquery 1.4.2
 84+ $j( this.ui.div ).trigger( 'removeUploadEvent' );
 85+ },
 86+
 87+
 88+ /**
 89+ * Wear our current progress, for observing processes to see
 90+ * @param fraction
 91+ */
 92+ setTransportProgress: function ( fraction ) {
 93+ var _this = this;
 94+ _this.state = 'transporting';
 95+ _this.transportProgress = fraction;
 96+ $j( _this.ui.div ).trigger( 'transportProgressEvent' );
 97+ },
 98+
 99+ /**
 100+ * Queue some warnings for possible later consumption
 101+ */
 102+ addWarning: function( code, info ) {
 103+ if ( !mw.isDefined( this.warnings ) ) {
 104+ this.warnings = [];
 105+ }
 106+ this.warnings.push( [ code, info ] );
 107+ },
 108+
 109+ /**
 110+ * Stop the upload -- we have failed for some reason
 111+ */
 112+ setError: function( code, info ) {
 113+ this.state = 'error';
 114+ this.transportProgress = 0;
 115+ this.ui.showError( code, info );
 116+ },
 117+
 118+ /**
 119+ * To be executed when an individual upload finishes. Processes the result and updates step 2's details
 120+ * @param result the API result in parsed JSON form
 121+ */
 122+ setTransported: function( result ) {
 123+ var _this = this;
 124+ if ( _this.state == 'aborted' ) {
 125+ return;
 126+ }
 127+
 128+ // default error state
 129+ var code = 'unknown';
 130+ var info = 'unknown';
 131+
 132+ if ( result.upload && result.upload.warnings ) {
 133+ if ( result.upload.warnings['exists'] ) {
 134+ // the filename we uploaded is in use already. Not a problem since we stashed it under a temporary name anyway
 135+ // potentially we could indicate to the upload that it should set the Title field to error state now, but we'll let them deal with that later.
 136+ // however, we don't get imageinfo, so let's try to get it and pretend that we did
 137+ var existsFileName = result.upload.warnings.exists;
 138+ try {
 139+ code = 'exists';
 140+ info = new mw.Title( existsFileName, 'file' ).getUrl();
 141+ } catch ( e ) {
 142+ code = 'unknown';
 143+ info = 'Warned about existing filename, but filename is unparseable: "' + existsFileName + "'";
 144+ }
 145+ _this.addWarning( code, info );
 146+ _this.extractUploadInfo( result.upload );
 147+ var success = function( imageinfo ) {
 148+ if ( imageinfo === null ) {
 149+ _this.setError( 'noimageinfo' );
 150+ } else {
 151+ result.upload.stashimageinfo = imageinfo;
 152+ _this.setSuccess( result );
 153+ }
 154+ };
 155+ _this.getStashImageInfo( success, [ 'timestamp', 'url', 'size', 'dimensions', 'sha1', 'mime', 'metadata', 'bitdepth' ] );
 156+ } else if ( result.upload.warnings['duplicate'] ) {
 157+ code = 'duplicate';
 158+ _this.setError( code, _this.duplicateErrorInfo( 'duplicate', result.upload.warnings['duplicate'] ) );
 159+ } else if ( result.upload.warnings['duplicate-archive'] ) {
 160+ code = 'duplicate-archive';
 161+ _this.setError( code, _this.duplicateErrorInfo( 'duplicate-archive', result.upload.warnings['duplicate-archive'] ) );
 162+ } else {
 163+ // we have an unknown warning. Assume fatal
 164+ code = 'unknown-warning';
 165+ var warningInfo = [];
 166+ $j.each( result.upload.warnings, function( k, v ) {
 167+ warningInfo.push( k + ': ' + v );
 168+ } );
 169+ info = warningInfo.join( ', ' );
 170+ _this.setError( code, [ info ] );
 171+ }
 172+ } else if ( result.upload && result.upload.result === 'Success' ) {
 173+ if ( result.upload.imageinfo ) {
 174+ _this.setSuccess( result );
 175+ } else {
 176+ _this.setError( 'noimageinfo', info );
 177+ }
 178+ } else {
 179+ if ( result.error ) {
 180+ if ( result.error.code ) {
 181+ code = result.error.code;
 182+ }
 183+ if ( result.error.info ) {
 184+ info = result.error.info;
 185+ }
 186+ }
 187+ _this.setError( code, info );
 188+ }
 189+
 190+
 191+ },
 192+
 193+
 194+ /**
 195+ * Helper function to generate duplicate errors with dialog box. Works with existing duplicates and deleted dupes.
 196+ * @param {String} error code, should have matching strings in .i18n.php
 197+ * @param {Object} portion of the API error result listing duplicates
 198+ */
 199+ duplicateErrorInfo: function( code, resultDuplicate ) {
 200+ var _this = this;
 201+ var duplicates;
 202+ if ( typeof resultDuplicate === 'object' ) {
 203+ duplicates = resultDuplicate;
 204+ } else if ( typeof resultDuplicate === 'string' ) {
 205+ duplicates = [ resultDuplicate ];
 206+ }
 207+ var $ul = $j( '<ul></ul>' );
 208+ $j.each( duplicates, function( i, filename ) {
 209+ var $a = $j( '<a/>' ).append( filename );
 210+ try {
 211+ var href = new mw.Title( filename, 'file' ).getUrl();
 212+ $a.attr( { 'href': href, 'target': '_blank' } );
 213+ } catch ( e ) {
 214+ $a.click( function() { alert('could not parse filename=' + filename ); } );
 215+ $a.attr( 'href', '#' );
 216+ }
 217+ $ul.append( $j( '<li></li>' ).append( $a ) );
 218+ } );
 219+ var dialogFn = function() {
 220+ $j( '<div></div>' )
 221+ .html( $ul )
 222+ .dialog( {
 223+ width: 500,
 224+ zIndex: 200000,
 225+ autoOpen: true,
 226+ title: gM( 'mwe-upwiz-api-error-' + code + '-popup-title', duplicates.length ),
 227+ modal: true
 228+ } );
 229+ };
 230+ return [ duplicates.length, dialogFn ];
 231+ },
 232+
 233+
 234+ /**
 235+ * Called from any upload success condition
 236+ * @param {Mixed} result -- result of AJAX call
 237+ */
 238+ setSuccess: function( result ) {
 239+ var _this = this;
 240+ _this.state = 'transported';
 241+ _this.transportProgress = 1;
 242+
 243+ _this.ui.setStatus( 'mwe-upwiz-getting-metadata' );
 244+ if ( result.upload ) {
 245+ _this.extractUploadInfo( result.upload );
 246+ _this.deedPreview.setup();
 247+ _this.details.populate();
 248+ _this.state = 'stashed';
 249+ _this.ui.showStashed();
 250+ $.publishReady( 'thumbnails.' + _this.index, 'api' );
 251+ } else {
 252+ _this.setError( 'noimageinfo' );
 253+ }
 254+
 255+ },
 256+
 257+ /**
 258+ * Called when the file is entered into the file input.
 259+ * Checks for file validity, then extracts metadata.
 260+ * Error out if filename or its contents are determined to be unacceptable
 261+ * Proceed to thumbnail extraction and image info if acceptable
 262+ * @param {HTMLFileInput} file input field
 263+ * @param {Function()} callback when ok, and upload object is ready
 264+ * @param {Function(String, Mixed)} callback when filename or contents in error. Signature of string code, mixed info
 265+ */
 266+ checkFile: function( fileInput, fileNameOk, fileNameErr ) {
 267+ // check if local file is acceptable
 268+
 269+ var _this = this;
 270+
 271+ // Check if filename is acceptable
 272+ // TODO sanitize filename
 273+ var filename = fileInput.value;
 274+ var basename = mw.UploadWizardUtil.getBasename( filename );
 275+
 276+
 277+ // check to see if the file has already been selected for upload.
 278+ var duplicate = false;
 279+ $j.each( this.wizard.uploads, function ( i, upload ) {
 280+ if ( _this !== upload && filename === upload.filename ) {
 281+ duplicate = true;
 282+ return false;
 283+ }
 284+ } );
 285+
 286+ if( duplicate ) {
 287+ fileNameErr( 'dup', basename );
 288+ return false;
 289+ }
 290+
 291+ try {
 292+ this.title = new mw.Title( basename.replace( /:/g, '_' ), 'file' );
 293+ } catch ( e ) {
 294+ fileNameErr( 'unparseable' );
 295+ }
 296+
 297+ // Check if extension is acceptable
 298+ var extension = this.title.getExtension();
 299+ if ( mw.isEmpty( extension ) ) {
 300+ fileNameErr( 'noext' );
 301+ } else {
 302+ if ( $j.inArray( extension.toLowerCase(), mw.UploadWizard.config[ 'fileExtensions' ] ) === -1 ) {
 303+ fileNameErr( 'ext', extension );
 304+ } else {
 305+
 306+ // extract more info via fileAPI
 307+ if ( mw.fileApi.isAvailable() ) {
 308+ if ( fileInput.files && fileInput.files.length ) {
 309+ // TODO multiple files in an input
 310+ this.file = fileInput.files[0];
 311+ }
 312+ // TODO check max upload size, alert user if too big
 313+ this.transportWeight = this.file.size;
 314+ if ( !mw.isDefined( this.imageinfo ) ) {
 315+ this.imageinfo = {};
 316+ }
 317+
 318+ var binReader = new FileReader();
 319+ binReader.onload = function() {
 320+ var meta;
 321+ try {
 322+ meta = mw.libs.jpegmeta( binReader.result, _this.file.fileName );
 323+ meta._binary_data = null;
 324+ } catch ( e ) {
 325+ meta = null;
 326+ }
 327+ _this.extractMetadataFromJpegMeta( meta );
 328+ _this.filename = filename;
 329+ fileNameOk();
 330+ };
 331+ binReader.readAsBinaryString( _this.file );
 332+ } else {
 333+ this.filename = filename;
 334+ fileNameOk();
 335+ }
 336+
 337+ }
 338+
 339+ }
 340+
 341+ },
 342+
 343+
 344+
 345+
 346+ /**
 347+ * Map fields from jpegmeta's metadata return into our format (which is more like the imageinfo returned from the API
 348+ * @param {Object} (as returned by jpegmeta)
 349+ */
 350+ extractMetadataFromJpegMeta: function( meta ) {
 351+ if ( mw.isDefined( meta ) && meta !== null && typeof meta === 'object' ) {
 352+ if ( !mw.isDefined( this.imageinfo ) ) {
 353+ this.imageinfo = {};
 354+ }
 355+ if ( !mw.isDefined( this.imageinfo.metadata ) ) {
 356+ this.imageinfo.metadata = {};
 357+ }
 358+ if ( meta.tiff && meta.tiff.Orientation ) {
 359+ this.imageinfo.metadata.orientation = meta.tiff.Orientation.value;
 360+ }
 361+ if ( meta.general ) {
 362+ var pixelHeightDim = 'height';
 363+ var pixelWidthDim = 'width';
 364+ // this must be called after orientation is set above. If no orientation set, defaults to 0
 365+ var degrees = this.getOrientationDegrees();
 366+ // jpegmeta reports pixelHeight & width
 367+ if ( degrees == 90 || degrees == 270 ) {
 368+ pixelHeightDim = 'width';
 369+ pixelWidthDim = 'height';
 370+ }
 371+ if ( meta.general.pixelHeight ) {
 372+ this.imageinfo[pixelHeightDim] = meta.general.pixelHeight.value;
 373+ }
 374+ if ( meta.general.pixelWidth ) {
 375+ this.imageinfo[pixelWidthDim] = meta.general.pixelWidth.value;
 376+ }
 377+ }
 378+ }
 379+ },
 380+
 381+ /**
 382+ * Accept the result from a successful API upload transport, and fill our own info
 383+ *
 384+ * @param result The JSON object from a successful API upload result.
 385+ */
 386+ extractUploadInfo: function( resultUpload ) {
 387+
 388+ if ( resultUpload.filekey ) {
 389+ this.fileKey = resultUpload.filekey;
 390+ }
 391+
 392+ if ( resultUpload.imageinfo ) {
 393+ this.extractImageInfo( resultUpload.imageinfo );
 394+ } else if ( resultUpload.stashimageinfo ) {
 395+ this.extractImageInfo( resultUpload.stashimageinfo );
 396+ }
 397+
 398+ },
 399+
 400+ /**
 401+ * Extract image info into our upload object
 402+ * Image info is obtained from various different API methods
 403+ * This may overwrite metadata obtained from FileReader.
 404+ * @param imageinfo JSON object obtained from API result.
 405+ */
 406+ extractImageInfo: function( imageinfo ) {
 407+ var _this = this;
 408+ for ( var key in imageinfo ) {
 409+ // we get metadata as list of key-val pairs; convert to object for easier lookup. Assuming that EXIF fields are unique.
 410+ if ( key == 'metadata' ) {
 411+ if ( !mw.isDefined( _this.imageinfo.metadata ) ) {
 412+ _this.imageinfo.metadata = {};
 413+ }
 414+ if ( imageinfo.metadata && imageinfo.metadata.length ) {
 415+ $j.each( imageinfo.metadata, function( i, pair ) {
 416+ if ( pair !== undefined ) {
 417+ _this.imageinfo.metadata[pair['name'].toLowerCase()] = pair['value'];
 418+ }
 419+ } );
 420+ }
 421+ } else {
 422+ _this.imageinfo[key] = imageinfo[key];
 423+ }
 424+ }
 425+
 426+ if ( _this.title.getExtension() === null ) {
 427+ 1;
 428+ // TODO v1.1 what if we don't have an extension? Should be impossible as it is currently impossible to upload without extension, but you
 429+ // never know... theoretically there is no restriction on extensions if we are uploading to the stash, but the check is performed anyway.
 430+ /*
 431+ var extension = mw.UploadWizardUtil.getExtension( _this.imageinfo.url );
 432+ if ( !extension ) {
 433+ if ( _this.imageinfo.mimetype ) {
 434+ if ( mw.UploadWizardUtil.mimetypeToExtension[ _this.imageinfo.mimetype ] ) {
 435+ extension = mw.UploadWizardUtil.mimetypeToExtension[ _this.imageinfo.mimetype ];
 436+ }
 437+ }
 438+ }
 439+ */
 440+ }
 441+
 442+
 443+
 444+
 445+ },
 446+
 447+ /**
 448+ * Get information about stashed images
 449+ * See API documentation for prop=stashimageinfo for what 'props' can contain
 450+ * @param {Function} callback -- called with null if failure, with imageinfo data structure if success
 451+ * @param {Array} properties to extract
 452+ * @param {Number} optional, width of thumbnail. Will force 'url' to be added to props
 453+ * @param {Number} optional, height of thumbnail. Will force 'url' to be added to props
 454+ */
 455+ getStashImageInfo: function( callback, props, width, height ) {
 456+ var _this = this;
 457+
 458+ if (!mw.isDefined( props ) ) {
 459+ props = [];
 460+ }
 461+
 462+ var params = {
 463+ 'prop': 'stashimageinfo',
 464+ 'siifilekey': _this.fileKey,
 465+ 'siiprop': props.join( '|' )
 466+ };
 467+
 468+ if ( mw.isDefined( width ) || mw.isDefined( height ) ) {
 469+ if ( ! $j.inArray( 'url', props ) ) {
 470+ props.push( 'url' );
 471+ }
 472+ if ( mw.isDefined( width ) ) {
 473+ params['siiurlwidth'] = width;
 474+ }
 475+ if ( mw.isDefined( height ) ) {
 476+ params['siiurlheight'] = height;
 477+ }
 478+ }
 479+
 480+ var ok = function( data ) {
 481+ if ( !data || !data.query || !data.query.stashimageinfo ) {
 482+ mw.log("mw.UploadWizardUpload::getStashImageInfo> No data? ");
 483+ callback( null );
 484+ return;
 485+ }
 486+ callback( data.query.stashimageinfo );
 487+ };
 488+
 489+ var err = function( code, result ) {
 490+ mw.log( 'mw.UploadWizardUpload::getStashImageInfo> error: ' + code, 'debug' );
 491+ callback( null );
 492+ };
 493+
 494+ this.api.get( params, { ok: ok, err: err } );
 495+ },
 496+
 497+
 498+ /**
 499+ * Get information about published images
 500+ * (There is some overlap with getStashedImageInfo, but it's different at every stage so it's clearer to have separate functions)
 501+ * See API documentation for prop=imageinfo for what 'props' can contain
 502+ * @param {Function} callback -- called with null if failure, with imageinfo data structure if success
 503+ * @param {Array} properties to extract
 504+ * @param {Number} optional, width of thumbnail. Will force 'url' to be added to props
 505+ * @param {Number} optional, height of thumbnail. Will force 'url' to be added to props
 506+ */
 507+ getImageInfo: function( callback, props, width, height ) {
 508+ var _this = this;
 509+ if (!mw.isDefined( props ) ) {
 510+ props = [];
 511+ }
 512+ var requestedTitle = _this.title.getPrefixedText();
 513+ var params = {
 514+ 'prop': 'imageinfo',
 515+ 'titles': requestedTitle,
 516+ 'iiprop': props.join( '|' )
 517+ };
 518+
 519+ if ( mw.isDefined( width ) || mw.isDefined( height ) ) {
 520+ if ( ! $j.inArray( 'url', props ) ) {
 521+ props.push( 'url' );
 522+ }
 523+ if ( mw.isDefined( width ) ) {
 524+ params['iiurlwidth'] = width;
 525+ }
 526+ if ( mw.isDefined( height ) ) {
 527+ params['iiurlheight'] = height;
 528+ }
 529+ }
 530+
 531+ var ok = function( data ) {
 532+ if ( data && data.query && data.query.pages ) {
 533+ var found = false;
 534+ $j.each( data.query.pages, function( pageId, page ) {
 535+ if ( page.title && page.title === requestedTitle && page.imageinfo ) {
 536+ found = true;
 537+ callback( page.imageinfo );
 538+ return false;
 539+ }
 540+ } );
 541+ if ( found ) {
 542+ return;
 543+ }
 544+ }
 545+ mw.log("mw.UploadWizardUpload::getImageInfo> No data matching " + requestedTitle + " ? ");
 546+ callback( null );
 547+ };
 548+
 549+ var err = function( code, result ) {
 550+ mw.log( 'mw.UploadWizardUpload::getImageInfo> error: ' + code, 'debug' );
 551+ callback( null );
 552+ };
 553+
 554+ this.api.get( params, { ok: ok, err: err } );
 555+ },
 556+
 557+
 558+ /**
 559+ * Get the upload handler per browser capabilities
 560+ */
 561+ getUploadHandler: function(){
 562+ if( !this.uploadHandler ){
 563+ if( mw.UploadWizard.config[ 'enableFirefogg' ]
 564+ &&
 565+ typeof( Firefogg ) != 'undefined'
 566+ ) {
 567+ mw.log("mw.UploadWizard::getUploadHandler> FirefoggHandler");
 568+ this.uploadHandler = new mw.FirefoggHandler( this, this.api );
 569+ } else if( mw.UploadWizard.config[ 'enableFormData' ] &&
 570+ (($j.browser.mozilla && $j.browser.version >= '5.0') ||
 571+ ($j.browser.webkit && $j.browser.version >= '534.28'))
 572+ ) {
 573+ mw.log("mw.UploadWizard::getUploadHandler> ApiUploadFormDataHandler");
 574+ this.uploadHandler = new mw.ApiUploadFormDataHandler( this, this.api );
 575+ } else {
 576+ // By default use the apiUploadHandler
 577+ mw.log("mw.UploadWizard::getUploadHandler> ApiUploadHandler");
 578+ this.uploadHandler = new mw.ApiUploadHandler( this, this.api );
 579+ }
 580+ }
 581+ return this.uploadHandler;
 582+ },
 583+
 584+ /**
 585+ * Explicitly fetch a thumbnail for a stashed upload of the desired width.
 586+ * Publishes to any event listeners that might have wanted it.
 587+ *
 588+ * @param width - desired width of thumbnail (height will scale to match)
 589+ * @param height - (optional) maximum height of thumbnail
 590+ */
 591+ getAndPublishApiThumbnail: function( key, width, height ) {
 592+ var _this = this;
 593+
 594+ if ( mw.isEmpty( height ) ) {
 595+ height = -1;
 596+ }
 597+
 598+ if ( !mw.isDefined( _this.thumbnailPublishers[key] ) ) {
 599+ var thumbnailPublisher = function( thumbnails ) {
 600+ if ( thumbnails === null ) {
 601+ // the api call failed somehow, no thumbnail data.
 602+ $j.publishReady( key, null );
 603+ } else {
 604+ // ok, the api callback has returned us information on where the thumbnail(s) ARE, but that doesn't mean
 605+ // they are actually there yet. Keep trying to set the source ( which should trigger "error" or "load" event )
 606+ // on the image. If it loads publish the event with the image. If it errors out too many times, give up and publish
 607+ // the event with a null.
 608+ $j.each( thumbnails, function( i, thumb ) {
 609+ if ( thumb.thumberror || ( ! ( thumb.thumburl && thumb.thumbwidth && thumb.thumbheight ) ) ) {
 610+ mw.log( "mw.UploadWizardUpload::getThumbnail> thumbnail error or missing information" );
 611+ $j.publishReady( key, null );
 612+ return;
 613+ }
 614+
 615+ // try to load this image with exponential backoff
 616+ // if the delay goes past 8 seconds, it gives up and publishes the event with null
 617+ var timeoutMs = 100;
 618+ var image = document.createElement( 'img' );
 619+ image.width = thumb.thumbwidth;
 620+ image.height = thumb.thumbheight;
 621+ $j( image )
 622+ .load( function() {
 623+ // cache this thumbnail
 624+ _this.thumbnails[key] = image;
 625+ // publish the image to anyone who wanted it
 626+ $j.publishReady( key, image );
 627+ } )
 628+ .error( function() {
 629+ // retry with exponential backoff
 630+ if ( timeoutMs < 8000 ) {
 631+ setTimeout( function() {
 632+ timeoutMs = timeoutMs * 2 + Math.round( Math.random() * ( timeoutMs / 10 ) );
 633+ setSrc();
 634+ }, timeoutMs );
 635+ } else {
 636+ $j.publishReady( key, null );
 637+ }
 638+ } );
 639+
 640+ // executing this should cause a .load() or .error() event on the image
 641+ function setSrc() {
 642+ image.src = thumb.thumburl;
 643+ }
 644+
 645+ // and, go!
 646+ setSrc();
 647+ } );
 648+ }
 649+ };
 650+
 651+ _this.thumbnailPublishers[key] = thumbnailPublisher;
 652+ if ( _this.state !== 'complete' ) {
 653+ _this.getStashImageInfo( thumbnailPublisher, [ 'url' ], width, height );
 654+ } else {
 655+ _this.getImageInfo( thumbnailPublisher, [ 'url' ], width, height );
 656+ }
 657+
 658+ }
 659+ },
 660+
 661+ /**
 662+ * Return the orientation of the image in degrees. Relies on metadata that
 663+ * may have been extracted at filereader stage, or after the upload when we fetch metadata. Default returns 0.
 664+ * @return {Integer} orientation in degrees: 0, 90, 180 or 270
 665+ */
 666+ getOrientationDegrees: function() {
 667+ var orientation = 0;
 668+ if ( this.imageinfo && this.imageinfo.metadata && this.imageinfo.metadata.orientation ) {
 669+ switch ( this.imageinfo.metadata.orientation ) {
 670+ case 8:
 671+ orientation = 90; // 'top left' -> 'left bottom'
 672+ break;
 673+ case 3:
 674+ orientation = 180; // 'top left' -> 'bottom right'
 675+ break;
 676+ case 6:
 677+ orientation = 270; // 'top left' -> 'right top'
 678+ break;
 679+ case 1:
 680+ default:
 681+ orientation = 0; // 'top left' -> 'top left'
 682+ break;
 683+
 684+ }
 685+ }
 686+ return orientation;
 687+ },
 688+
 689+ /**
 690+ * Fit an image into width & height constraints with scaling factor
 691+ * @param {HTMLImageElement}
 692+ * @param {Object} with width & height properties
 693+ * @return {Number}
 694+ */
 695+ getScalingFromConstraints: function( image, constraints ) {
 696+ var scaling = 1;
 697+ $j.each( [ 'width', 'height' ], function( i, dim ) {
 698+ if ( constraints[dim] && image[dim] > constraints[dim] ) {
 699+ var s = constraints[dim] / image[dim];
 700+ if ( s < scaling ) {
 701+ scaling = s;
 702+ }
 703+ }
 704+ } );
 705+ return scaling;
 706+ },
 707+
 708+ /**
 709+ * Given an image (already loaded), dimension constraints
 710+ * return canvas object scaled & transformedi ( & rotated if metadata indicates it's needed )
 711+ * @param {HTMLImageElement}
 712+ * @param {Object} containing width & height constraints
 713+ * @return {HTMLCanvasElement}
 714+ */
 715+ getTransformedCanvasElement: function( image, constraints ) {
 716+
 717+ var rotation = 0;
 718+
 719+ // if this wiki can rotate images to match their EXIF metadata,
 720+ // we should do the same in our preview
 721+ if ( mw.config.get( 'wgFileCanRotate' ) ) {
 722+ var angle = this.getOrientationDegrees();
 723+ rotation = angle ? 360 - angle : 0;
 724+ }
 725+
 726+ // swap scaling constraints if needed by rotation...
 727+ var scaleConstraints;
 728+ if ( rotation === 90 || rotation === 270 ) {
 729+ scaleConstraints = {
 730+ width: constraints.height,
 731+ height: constraints.width
 732+ };
 733+ } else {
 734+ scaleConstraints = {
 735+ width: constraints.width,
 736+ height: constraints.height
 737+ };
 738+ }
 739+
 740+ var scaling = this.getScalingFromConstraints( image, constraints );
 741+
 742+ var width = image.width * scaling;
 743+ var height = image.height * scaling;
 744+
 745+ // Determine the offset required to center the image
 746+ var dx = (constraints.width - width) / 2;
 747+ var dy = (constraints.height - height) / 2;
 748+
 749+ switch ( rotation ) {
 750+ // If a rotation is applied, the direction of the axis
 751+ // changes as well. You can derive the values below by
 752+ // drawing on paper an axis system, rotate it and see
 753+ // where the positive axis direction is
 754+ case 90:
 755+ x = dx;
 756+ y = dy - constraints.height;
 757+ break;
 758+ case 180:
 759+ x = dx - constraints.width;
 760+ y = dy - constraints.height;
 761+ break;
 762+ case 270:
 763+ x = dx - constraints.width;
 764+ y = dy;
 765+ break;
 766+ case 0:
 767+ default:
 768+ x = dx;
 769+ y = dy;
 770+ break;
 771+ }
 772+
 773+ var $canvas = $j( '<canvas></canvas>' ).attr( constraints );
 774+ var ctx = $canvas[0].getContext( '2d' );
 775+ ctx.clearRect( 0, 0, width, height );
 776+ ctx.rotate( rotation / 180 * Math.PI );
 777+ ctx.drawImage( image, x, y, width, height );
 778+
 779+ return $canvas;
 780+ },
 781+
 782+ /**
 783+ * Return a browser-scaled image element, given an image and constraints.
 784+ * @param {HTMLImageElement}
 785+ * @param {Object} with width and height properties
 786+ * @return {HTMLImageElement} with same src, but different attrs
 787+ */
 788+ getBrowserScaledImageElement: function( image, constraints ) {
 789+ var scaling = this.getScalingFromConstraints( image, constraints );
 790+ return $j( '<img/>' )
 791+ .attr( {
 792+ width: parseInt( image.width * scaling, 10 ),
 793+ height: parseInt( image.height * scaling, 10 ),
 794+ src: image.src
 795+ } )
 796+ .css( {
 797+ 'margin-top': ( parseInt( ( constraints.height - image.height * scaling ) / 2, 10 ) ).toString() + 'px'
 798+ } );
 799+ },
 800+
 801+ /**
 802+ * Return an element suitable for the preview of a certain size. Uses canvas when possible
 803+ * @param {HTMLImageElement}
 804+ * @param {Integer} width
 805+ * @param {Integer} height
 806+ * @return {HTMLCanvasElement|HTMLImageElement}
 807+ */
 808+ getScaledImageElement: function( image, width, height ) {
 809+ if ( typeof width === 'undefined' || width === null || width <= 0 ) {
 810+ width = mw.UploadWizard.config['thumbnailWidth'];
 811+ }
 812+ var constraints = {
 813+ width: parseInt( width, 10 ),
 814+ height: ( mw.isDefined( height ) ? parseInt( height, 10 ) : null )
 815+ };
 816+
 817+ return mw.canvas.isAvailable() ? this.getTransformedCanvasElement( image, constraints )
 818+ : this.getBrowserScaledImageElement( image, constraints );
 819+ },
 820+
 821+ /**
 822+ * Given a jQuery selector, subscribe to the "ready" event that fills the thumbnail
 823+ * This will trigger if the thumbnail is added in the future or if it already has been
 824+ *
 825+ * @param selector
 826+ * @param width Width constraint
 827+ * @param height Height constraint (optional)
 828+ * @param boolean add lightbox large preview when ready
 829+ */
 830+ setThumbnail: function( selector, width, height, isLightBox ) {
 831+ var _this = this;
 832+
 833+ /**
 834+ * This callback will add an image to the selector, using in-browser scaling if necessary
 835+ * @param {HTMLImageElement}
 836+ */
 837+ var placed = false;
 838+ var placeImageCallback = function( image ) {
 839+ if ( image === null ) {
 840+ $j( selector ).addClass( 'mwe-upwiz-file-preview-broken' );
 841+ _this.ui.setStatus( 'mwe-upwiz-thumbnail-failed' );
 842+ return;
 843+ }
 844+ var elm = _this.getScaledImageElement( image, width, height );
 845+ // add the image to the DOM, finally
 846+ $j( selector )
 847+ .css( { background: 'none' } )
 848+ .html(
 849+ $j( '<a/></a>' )
 850+ .addClass( "mwe-upwiz-thumbnail-link" )
 851+ .append( elm )
 852+ );
 853+ placed = true;
 854+ };
 855+
 856+ // Listen for even which says some kind of thumbnail is available.
 857+ // The argument is an either an ImageHtmlElement ( if we could get the thumbnail locally ) or the string 'api' indicating you
 858+ // now need to get the scaled thumbnail via the API
 859+ $.subscribeReady(
 860+ 'thumbnails.' + _this.index,
 861+ function ( x ) {
 862+ if ( isLightBox ) {
 863+ _this.setLightBox( selector );
 864+ }
 865+ if ( !placed ) {
 866+ if ( x === 'api' ) {
 867+ // get the thumbnail via API. This also works with an async pub/sub model; if this thumbnail was already
 868+ // fetched for some reason, we'll get it immediately
 869+ var key = 'apiThumbnail.' + _this.index + ',width=' + width + ',height=' + height;
 870+ $.subscribeReady( key, placeImageCallback );
 871+ _this.getAndPublishApiThumbnail( key, width, height );
 872+ } else if ( x instanceof HTMLImageElement ) {
 873+ placeImageCallback( x );
 874+ } else {
 875+ // something else went wrong, place broken image
 876+ mw.log( 'unexpected argument to thumbnails event: ' + x );
 877+ placeImageCallback( null );
 878+ }
 879+ }
 880+ }
 881+ );
 882+ },
 883+
 884+ /**
 885+ * set up lightbox behavior for non-complete thumbnails
 886+ * TODO center this
 887+ * @param selector
 888+ */
 889+ setLightBox: function( selector ) {
 890+ var _this = this;
 891+ var $imgDiv = $j( '<div></div>' ).css( 'text-align', 'center' );
 892+ $j( selector )
 893+ .click( function() {
 894+ // get large preview image
 895+ // open large preview in modal dialog box
 896+ $j( '<div class="mwe-upwiz-lightbox"></div>' )
 897+ .append( $imgDiv )
 898+ .dialog( {
 899+ 'width': mw.UploadWizard.config[ 'largeThumbnailWidth' ],
 900+ 'height': mw.UploadWizard.config[ 'largeThumbnailMaxHeight' ],
 901+ 'autoOpen': true,
 902+ 'title': gM( 'mwe-upwiz-image-preview' ),
 903+ 'modal': true,
 904+ 'resizable': false
 905+ } );
 906+ _this.setThumbnail(
 907+ $imgDiv,
 908+ mw.UploadWizard.config[ 'largeThumbnailWidth' ],
 909+ mw.UploadWizard.config[ 'largeThumbnailMaxHeight' ],
 910+ false /* obviously the largeThumbnail doesn't have a lightbox itself! */
 911+ );
 912+ return false;
 913+ } ); // close thumbnail click function
 914+ }
 915+
 916+};
 917+
 918+} )( jQuery );
Property changes on: trunk/extensions/UploadWizard/resources/mw.UploadWizardUpload.js
___________________________________________________________________
Added: svn:eol-style
1919 + native

Status & tagging log