Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for race condition while uploading multiple files with uploadimage plugin #1216

Merged
merged 6 commits into from
Nov 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Fixed Issues:
* [#1057](https://github.com/ckeditor/ckeditor-dev/issues/1057): Fixed: The [Notification](https://ckeditor.com/addon/notification) plugin overwrites Web Notifications API due to leakage to the global scope.
* [#1068](https://github.com/ckeditor/ckeditor-dev/issues/1068): Fixed: Upload widget paste listener ignores changes to the [`uploadWidgetDefinition`](https://docs.ckeditor.com/ckeditor4/docs/#!/api/CKEDITOR.fileTools.uploadWidgetDefinition).
* [#921](https://github.com/ckeditor/ckeditor-dev/issues/921): Fixed: [Edge] CKEditor erroneously perceives internal copy and paste as type "external".
* [#1213](https://github.com/ckeditor/ckeditor-dev/issues/1213): Fixed: Multiple images uploaded using [Upload Image](https://ckeditor.com/cke4/addon/uploadimage) plugin are randomly duplicated or mangled.

API Changes:

Expand Down
43 changes: 35 additions & 8 deletions plugins/uploadimage/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@
'use strict';

( function() {
var uniqueNameCounter = 0,
// Black rectangle which is shown before image is loaded.
loadingImage = 'data:image/gif;base64,R0lGODlhDgAOAIAAAAAAAP///yH5BAAAAAAALAAAAAAOAA4AAAIMhI+py+0Po5y02qsKADs=';

// Returns number as a string. If a number has 1 digit only it returns it prefixed with an extra 0.
function padNumber( input ) {
if ( input <= 9 ) {
input = '0' + input;
}

return String( input );
}

// Returns an unique image file name.
function getUniqueImageFileName( type ) {
var date = new Date(),
dateParts = [ date.getFullYear(), date.getMonth() + 1, date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds() ];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't we use simply timestamp here, returned from Date.now()/Date.prototype.getTime()?

Copy link
Contributor Author

@mlewand mlewand Nov 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to give it some human readability, at the same time not making the name too long. So we'd like to stick to YYYYmmddhhmmss schema.


uniqueNameCounter += 1;

return 'image-' + CKEDITOR.tools.array.map( dateParts, padNumber ).join( '' ) + '-' + uniqueNameCounter + '.' + type;
}

CKEDITOR.plugins.add( 'uploadimage', {
requires: 'uploadwidget',

Expand Down Expand Up @@ -89,13 +112,22 @@
for ( i = 0; i < imgs.count(); i++ ) {
img = imgs.getItem( i );

// Image have to contain src=data:...
var isDataInSrc = img.getAttribute( 'src' ) && img.getAttribute( 'src' ).substring( 0, 5 ) == 'data:',
// Assign src once, as it might be a big string, so there's no point in duplicating it all over the place.
var imgSrc = img.getAttribute( 'src' ),
// Image have to contain src=data:...
isDataInSrc = imgSrc && imgSrc.substring( 0, 5 ) == 'data:',
isRealObject = img.data( 'cke-realelement' ) === null;

// We are not uploading images in non-editable blocs and fake objects (https://dev.ckeditor.com/ticket/13003).
if ( isDataInSrc && isRealObject && !img.data( 'cke-upload-id' ) && !img.isReadOnly( 1 ) ) {
var loader = editor.uploadRepository.create( img.getAttribute( 'src' ) );
// Note normally we'd extract this logic into a separate function, but we should not duplicate this string, as it might
// be large.
var imgFormat = imgSrc.match( /image\/([a-z]+?);/i ),
loader;

imgFormat = ( imgFormat && imgFormat[ 1 ] ) || 'jpg';

loader = editor.uploadRepository.create( imgSrc, getUniqueImageFileName( imgFormat ) );
loader.upload( uploadUrl );

fileTools.markElement( img, 'uploadimage', loader.id );
Expand All @@ -109,11 +141,6 @@
}
} );

// jscs:disable maximumLineLength
// Black rectangle which is shown before image is loaded.
var loadingImage = 'data:image/gif;base64,R0lGODlhDgAOAIAAAAAAAP///yH5BAAAAAAALAAAAAAOAA4AAAIMhI+py+0Po5y02qsKADs=';
// jscs:enable maximumLineLength

/**
* The URL where images should be uploaded.
*
Expand Down
30 changes: 28 additions & 2 deletions tests/plugins/uploadimage/uploadimage.js
Original file line number Diff line number Diff line change
Expand Up @@ -538,15 +538,41 @@

'test prevent upload fake elements (https://dev.ckeditor.com/ticket/13003)': function() {
var editor = this.editors.inline,
createspy = sinon.spy( editor.uploadRepository, 'create' );
createSpy = sinon.spy( editor.uploadRepository, 'create' );

editor.fire( 'paste', {
dataValue: '<img src="data:image/gif;base64,aw==" alt="nothing" data-cke-realelement="some" />'
} );

editor.once( 'afterPaste', function() {
resume( function() {
assert.isTrue( createspy.notCalled );
createSpy.restore();
assert.isTrue( createSpy.notCalled );
} );
} );

wait();
},

'test uploads generate unique names (#1213)': function() {
var editor = this.editors.inline,
createSpy = sinon.spy( editor.uploadRepository, 'create' );

editor.fire( 'paste', {
dataValue: '<img src="data:image/gif;base64,aw==" alt="gif" />' +
'<img src="data:image/gif;base64,aw==" alt="gif" />' +
'<img src="data:image/png;base64,aw==" alt="png" />'
} );

editor.once( 'afterPaste', function() {
resume( function() {
createSpy.restore();
assert.areSame( 3, createSpy.callCount, 'create call count' );

assert.isMatching( /image-\d+-\d+\.gif/, createSpy.args[ 0 ][ 1 ], 'file name passed to first call' );
assert.isMatching( /image-\d+-\d+\.gif/, createSpy.args[ 1 ][ 1 ], 'file name passed to second call' );
assert.areNotSame( createSpy.args[ 0 ][ 1 ], createSpy.args[ 1 ][ 1 ], 'first and second call names are different' );
assert.isMatching( /image-\d+-\d+\.png/, createSpy.args[ 2 ][ 1 ], 'png type is recognized' );
} );
} );

Expand Down