From 4d0ad3ea2be9cf458800c232479694c8cac365ea Mon Sep 17 00:00:00 2001 From: Ivan Novakov Date: Mon, 4 Nov 2013 17:35:31 +0100 Subject: [PATCH 1/6] partial --- lib/upload/header/AbstractFilenameEncoder.js | 11 +++++++++++ lib/upload/header/Base64FilenameEncoder.js | 9 +++++++++ lib/upload/uploader/AbstractUploader.js | 20 +++++++++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 lib/upload/header/AbstractFilenameEncoder.js create mode 100644 lib/upload/header/Base64FilenameEncoder.js diff --git a/lib/upload/header/AbstractFilenameEncoder.js b/lib/upload/header/AbstractFilenameEncoder.js new file mode 100644 index 0000000..31168e8 --- /dev/null +++ b/lib/upload/header/AbstractFilenameEncoder.js @@ -0,0 +1,11 @@ +Ext.define('Ext.ux.upload.header.AbstractFilenameEncoder', { + + config : {}, + + contructor : function(config) { + this.initConfig(config); + }, + + encode : function(filename) { + } +}); \ No newline at end of file diff --git a/lib/upload/header/Base64FilenameEncoder.js b/lib/upload/header/Base64FilenameEncoder.js new file mode 100644 index 0000000..b3e4f72 --- /dev/null +++ b/lib/upload/header/Base64FilenameEncoder.js @@ -0,0 +1,9 @@ +Ext.define('Ext.ux.upload.header.Base64FilenameEncoder', { + extend: 'Ext.ux.upload.header.AbstractFilenameEncoder', + + config: {}, + + encode: function(filename) { + window.btoa(unescape(encodeURIComponent(filename))); + } +}); \ No newline at end of file diff --git a/lib/upload/uploader/AbstractUploader.js b/lib/upload/uploader/AbstractUploader.js index c3c88cb..a30c938 100644 --- a/lib/upload/uploader/AbstractUploader.js +++ b/lib/upload/uploader/AbstractUploader.js @@ -73,7 +73,15 @@ Ext.define('Ext.ux.upload.uploader.AbstractUploader', { * * Extra headers to be sent with the upload request. */ - extraHeaders : {} + extraHeaders : {}, + + /** + * @cfg {Object/String} + * + * Encoder object/class used to encode the filename header. Usually used, when the filename + * contains non-ASCII characters. + */ + filenameEncoder: null }, /** @@ -97,7 +105,10 @@ Ext.define('Ext.ux.upload.uploader.AbstractUploader', { */ initHeaders : function(item) { var headers = this.extraHeaders || {}; + headers[this.filenameHeader] = item.getFilename(); + // https://developer.mozilla.org/en-US/docs/Web/API/window.btoa?redirectlocale=en-US&redirectslug=DOM%2Fwindow.btoa + //console.log(window.btoa(unescape(encodeURIComponent(item.getFilename())))); headers[this.sizeHeader] = item.getSize(); headers[this.typeHeader] = item.getType(); @@ -122,6 +133,13 @@ Ext.define('Ext.ux.upload.uploader.AbstractUploader', { * **Implement in subclass** */ abortUpload : function() { + }, + + /** + * @protected + */ + encodeFilename: function(filename) { + } }); \ No newline at end of file From b739f332be8b1feb8c0e1fd8a00c6ac5d6ee6ec2 Mon Sep 17 00:00:00 2001 From: Ivan Novakov Date: Mon, 4 Nov 2013 21:53:07 +0100 Subject: [PATCH 2/6] added option to encode the filename header through a filename encoder object, simple base64 filename encoder added (fixes #15) --- .gitignore | 3 ++ lib/upload/Manager.js | 6 ++- lib/upload/Panel.js | 12 ++++- lib/upload/header/AbstractFilenameEncoder.js | 12 ++++- lib/upload/header/Base64FilenameEncoder.js | 20 ++++++--- lib/upload/uploader/AbstractUploader.js | 46 +++++++++++++------- public/app.js | 3 +- public/upload.php | 11 ++++- 8 files changed, 84 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index 239bf3e..bfb4100 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ public/extjs public/_config.php docs/generated +.buildpath +.project +.settings \ No newline at end of file diff --git a/lib/upload/Manager.js b/lib/upload/Manager.js index 2a9a840..000771c 100644 --- a/lib/upload/Manager.js +++ b/lib/upload/Manager.js @@ -14,7 +14,8 @@ Ext.define('Ext.ux.upload.Manager', { config : { uploader : null, uploaderOptions : null, - synchronous : true + synchronous : true, + filenameEncoder: null }, DEFAULT_UPLOADER_CLASS : 'Ext.ux.upload.uploader.ExtJsUploader', @@ -81,7 +82,8 @@ Ext.define('Ext.ux.upload.Manager', { Ext.applyIf(uploaderOptions, { success : this.onUploadSuccess, failure : this.onUploadFailure, - progress : this.onUploadProgress + progress : this.onUploadProgress, + filenameEncoder: this.getFilenameEncoder() }); this.uploader = Ext.create(uploaderClass, uploaderOptions); diff --git a/lib/upload/Panel.js b/lib/upload/Panel.js index 3d94aba..dd3f00a 100644 --- a/lib/upload/Panel.js +++ b/lib/upload/Panel.js @@ -99,6 +99,15 @@ Ext.define('Ext.ux.upload.Panel', { * The time after the upload request times out - in miliseconds. */ uploadTimeout : 60000, + + /** + * @cfg {Object/String} + * + * Encoder object/class used to encode the filename header. Usually used, when the filename + * contains non-ASCII characters. If an encoder is used, the server backend has to be + * modified accordingly to decode the value. + */ + filenameEncoder: null, // strings textOk : 'OK', @@ -236,7 +245,8 @@ Ext.define('Ext.ux.upload.Panel', { var uploadManager = Ext.create('Ext.ux.upload.Manager', { uploader : this.uploader, uploaderOptions : uploaderOptions, - synchronous: this.getSynchronous() + synchronous: this.getSynchronous(), + filenameEncoder: this.getFilenameEncoder() }); return uploadManager; diff --git a/lib/upload/header/AbstractFilenameEncoder.js b/lib/upload/header/AbstractFilenameEncoder.js index 31168e8..44f1fee 100644 --- a/lib/upload/header/AbstractFilenameEncoder.js +++ b/lib/upload/header/AbstractFilenameEncoder.js @@ -1,11 +1,19 @@ +/** + * Abstract filename encoder. + */ Ext.define('Ext.ux.upload.header.AbstractFilenameEncoder', { config : {}, + type : 'generic', + contructor : function(config) { this.initConfig(config); }, - encode : function(filename) { + encode : function(filename) {}, + + getType : function() { + return this.type; } -}); \ No newline at end of file +}); diff --git a/lib/upload/header/Base64FilenameEncoder.js b/lib/upload/header/Base64FilenameEncoder.js index b3e4f72..5cfaf42 100644 --- a/lib/upload/header/Base64FilenameEncoder.js +++ b/lib/upload/header/Base64FilenameEncoder.js @@ -1,9 +1,15 @@ +/** + * Base64 filename encoder - uses the built-in function window.btoa(). + * @see https://developer.mozilla.org/en-US/docs/Web/API/Window.btoa + */ Ext.define('Ext.ux.upload.header.Base64FilenameEncoder', { - extend: 'Ext.ux.upload.header.AbstractFilenameEncoder', - - config: {}, - - encode: function(filename) { - window.btoa(unescape(encodeURIComponent(filename))); + extend : 'Ext.ux.upload.header.AbstractFilenameEncoder', + + config : {}, + + type : 'base64', + + encode : function(filename) { + return window.btoa(unescape(encodeURIComponent(filename))); } -}); \ No newline at end of file +}); diff --git a/lib/upload/uploader/AbstractUploader.js b/lib/upload/uploader/AbstractUploader.js index a30c938..db49c87 100644 --- a/lib/upload/uploader/AbstractUploader.js +++ b/lib/upload/uploader/AbstractUploader.js @@ -74,14 +74,16 @@ Ext.define('Ext.ux.upload.uploader.AbstractUploader', { * Extra headers to be sent with the upload request. */ extraHeaders : {}, - + /** * @cfg {Object/String} * * Encoder object/class used to encode the filename header. Usually used, when the filename * contains non-ASCII characters. */ - filenameEncoder: null + filenameEncoder : null, + + filenameEncoderHeader : 'X-Filename-Encoder' }, /** @@ -104,11 +106,19 @@ Ext.define('Ext.ux.upload.uploader.AbstractUploader', { * @protected */ initHeaders : function(item) { - var headers = this.extraHeaders || {}; - - headers[this.filenameHeader] = item.getFilename(); - // https://developer.mozilla.org/en-US/docs/Web/API/window.btoa?redirectlocale=en-US&redirectslug=DOM%2Fwindow.btoa - //console.log(window.btoa(unescape(encodeURIComponent(item.getFilename())))); + var headers = this.extraHeaders || {}, + filename = item.getFilename(); + + /* + * If there is a filename encoder defined - use it to encode the filename + * in the header and set the type of the encoder as an additional header. + */ + var filenameEncoder = this.initFilenameEncoder(); + if (filenameEncoder) { + filename = filenameEncoder.encode(filename); + headers[this.filenameEncoderHeader] = filenameEncoder.getType(); + } + headers[this.filenameHeader] = filename; headers[this.sizeHeader] = item.getSize(); headers[this.typeHeader] = item.getType(); @@ -123,8 +133,7 @@ Ext.define('Ext.ux.upload.uploader.AbstractUploader', { * * @param {Ext.ux.upload.Item} item */ - uploadItem : function(item) { - }, + uploadItem : function(item) {}, /** * @abstract @@ -132,14 +141,21 @@ Ext.define('Ext.ux.upload.uploader.AbstractUploader', { * Aborts the current upload. * **Implement in subclass** */ - abortUpload : function() { - }, - + abortUpload : function() {}, + /** * @protected */ - encodeFilename: function(filename) { - + initFilenameEncoder : function() { + if (Ext.isString(this.filenameEncoder)) { + this.filenameEncoder = Ext.create(this.filenameEncoder); + } + + if (Ext.isObject(this.filenameEncoder)) { + return this.filenameEncoder; + } + + return null; } -}); \ No newline at end of file +}); diff --git a/public/app.js b/public/app.js index 776af32..8fb5f07 100644 --- a/public/app.js +++ b/public/app.js @@ -51,7 +51,8 @@ Ext.application({ var uploadPanel = Ext.create('Ext.ux.upload.Panel', { uploaderOptions : { url : 'upload.php' - } + }, + filenameEncoder: 'Ext.ux.upload.header.Base64FilenameEncoder' }); var uploadDialog = Ext.create('Ext.ux.upload.Dialog', { diff --git a/public/upload.php b/public/upload.php index cba9a0d..0bc54bc 100644 --- a/public/upload.php +++ b/public/upload.php @@ -17,9 +17,18 @@ /* * You should check these values for XSS or SQL injection. */ +if (!isset($_SERVER['HTTP_X_FILE_NAME'])) { + _error('Unknown file name'); +} +$fileName = $_SERVER['HTTP_X_FILE_NAME']; +if (isset($_SERVER['HTTP_X_FILENAME_ENCODER']) && 'base64' == $_SERVER['HTTP_X_FILENAME_ENCODER']) { + $fileName = base64_decode($fileName); +} +$fileName = htmlspecialchars($fileName); + $mimeType = htmlspecialchars($_SERVER['HTTP_X_FILE_TYPE']); $size = intval($_SERVER['HTTP_X_FILE_SIZE']); -$fileName = htmlspecialchars($_SERVER['HTTP_X_FILE_NAME']); + $inputStream = fopen('php://input', 'r'); $outputFilename = $config['upload_dir'] . '/' . $fileName; From 67274c1f2616103b6214566d2c38ec99422dd189 Mon Sep 17 00:00:00 2001 From: Ivan Novakov Date: Mon, 4 Nov 2013 22:33:33 +0100 Subject: [PATCH 3/6] replaced "id" with "itemId" (fixes #14) --- lib/upload/ItemGridPanel.js | 1 - lib/upload/Panel.js | 29 +++++++++++++++++------------ lib/upload/StatusBar.js | 2 +- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/upload/ItemGridPanel.js b/lib/upload/ItemGridPanel.js index 1e2ee65..e9b6e44 100644 --- a/lib/upload/ItemGridPanel.js +++ b/lib/upload/ItemGridPanel.js @@ -52,7 +52,6 @@ Ext.define('Ext.ux.upload.ItemGridPanel', { xtype : 'rownumberer', width : 50 }, { - id : 'filename', dataIndex : 'filename', header : this.textFilename, flex : 1 diff --git a/lib/upload/Panel.js b/lib/upload/Panel.js index dd3f00a..ff49279 100644 --- a/lib/upload/Panel.js +++ b/lib/upload/Panel.js @@ -262,19 +262,20 @@ Ext.define('Ext.ux.upload.Panel', { getTopToolbarConfig : function() { this.browseButton = Ext.create('Ext.ux.upload.BrowseButton', { - id : 'button_browse', + itemId : 'button_browse', buttonText : this.buttonText }); this.browseButton.on('fileselected', this.onFileSelection, this); return { xtype : 'toolbar', + itemId: 'topToolbar', dock : 'top', items : [ this.browseButton, '-', { - id : 'button_upload', + itemId : 'button_upload', text : this.textUpload, iconCls : 'ux-mu-icon-action-upload', scope : this, @@ -282,7 +283,7 @@ Ext.define('Ext.ux.upload.Panel', { }, '-', { - id : 'button_abort', + itemId : 'button_abort', text : this.textAbort, iconCls : 'ux-mu-icon-action-abort', scope : this, @@ -291,7 +292,7 @@ Ext.define('Ext.ux.upload.Panel', { }, '->', { - id : 'button_remove_selected', + itemId : 'button_remove_selected', text : this.textRemoveSelected, iconCls : 'ux-mu-icon-action-remove', scope : this, @@ -299,7 +300,7 @@ Ext.define('Ext.ux.upload.Panel', { }, '-', { - id : 'button_remove_all', + itemId : 'button_remove_all', text : this.textRemoveAll, iconCls : 'ux-mu-icon-action-remove', scope : this, @@ -431,19 +432,23 @@ Ext.define('Ext.ux.upload.Panel', { this.statusBar.setSelectionMessage(this.queue.getCount(), this.queue.getTotalBytes()); }, - getButton : function(id) { - return Ext.ComponentMgr.get(id); + getButton : function(itemId) { + var topToolbar = this.getDockedComponent('topToolbar'); + if (topToolbar) { + return topToolbar.getComponent(itemId); + } + return null; }, switchButtons : function(info) { - var id; - for (id in info) { - this.switchButton(id, info[id]); + var itemId; + for (itemId in info) { + this.switchButton(itemId, info[itemId]); } }, - switchButton : function(id, on) { - var button = this.getButton(id); + switchButton : function(itemId, on) { + var button = this.getButton(itemId); if (button) { if (on) { diff --git a/lib/upload/StatusBar.js b/lib/upload/StatusBar.js index 2152173..8ad6747 100644 --- a/lib/upload/StatusBar.js +++ b/lib/upload/StatusBar.js @@ -25,7 +25,7 @@ Ext.define('Ext.ux.upload.StatusBar', { items : [ { xtype : 'tbtext', - id : this.textComponentId, + itemId : this.textComponentId, text : ' ' } ] From 6694b7b5426f394970967609f786acf8fe3b1577 Mon Sep 17 00:00:00 2001 From: Ivan Novakov Date: Mon, 4 Nov 2013 22:51:26 +0100 Subject: [PATCH 4/6] option to re-upload files in case of error (fixes #9) --- lib/upload/Panel.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/upload/Panel.js b/lib/upload/Panel.js index ff49279..a0bff6c 100644 --- a/lib/upload/Panel.js +++ b/lib/upload/Panel.js @@ -99,7 +99,7 @@ Ext.define('Ext.ux.upload.Panel', { * The time after the upload request times out - in miliseconds. */ uploadTimeout : 60000, - + /** * @cfg {Object/String} * @@ -107,7 +107,7 @@ Ext.define('Ext.ux.upload.Panel', { * contains non-ASCII characters. If an encoder is used, the server backend has to be * modified accordingly to decode the value. */ - filenameEncoder: null, + filenameEncoder : null, // strings textOk : 'OK', @@ -245,8 +245,8 @@ Ext.define('Ext.ux.upload.Panel', { var uploadManager = Ext.create('Ext.ux.upload.Manager', { uploader : this.uploader, uploaderOptions : uploaderOptions, - synchronous: this.getSynchronous(), - filenameEncoder: this.getFilenameEncoder() + synchronous : this.getSynchronous(), + filenameEncoder : this.getFilenameEncoder() }); return uploadManager; @@ -269,7 +269,7 @@ Ext.define('Ext.ux.upload.Panel', { return { xtype : 'toolbar', - itemId: 'topToolbar', + itemId : 'topToolbar', dock : 'top', items : [ this.browseButton, @@ -342,7 +342,11 @@ Ext.define('Ext.ux.upload.Panel', { onUploadComplete : function(manager, queue, errorCount) { this.finishUpload(); - this.stateInit(); + if (errorCount) { + this.stateQueue(); + } else { + this.stateInit(); + } this.fireEvent('uploadcomplete', this, manager, queue.getUploadedItems(), errorCount); manager.resetUpload(); }, @@ -433,11 +437,11 @@ Ext.define('Ext.ux.upload.Panel', { }, getButton : function(itemId) { - var topToolbar = this.getDockedComponent('topToolbar'); - if (topToolbar) { - return topToolbar.getComponent(itemId); - } - return null; + var topToolbar = this.getDockedComponent('topToolbar'); + if (topToolbar) { + return topToolbar.getComponent(itemId); + } + return null; }, switchButtons : function(info) { @@ -499,4 +503,4 @@ Ext.define('Ext.ux.upload.Panel', { }); } -}); \ No newline at end of file +}); From 9b5e7aff0bc1d6c0102b7013dcbd8f8a029feff1 Mon Sep 17 00:00:00 2001 From: Ivan Novakov Date: Mon, 4 Nov 2013 23:12:29 +0100 Subject: [PATCH 5/6] reset the browse button with the multiple attribute (fixes #10) --- lib/upload/BrowseButton.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/upload/BrowseButton.js b/lib/upload/BrowseButton.js index c72666c..47473e1 100644 --- a/lib/upload/BrowseButton.js +++ b/lib/upload/BrowseButton.js @@ -28,14 +28,16 @@ Ext.define('Ext.ux.upload.BrowseButton', { * Fixing the issue when adding an icon to the button - the text does not render properly. OBSOLETE - from * ExtJS v4.1 the internal implementation has changed, there is no button object anymore. */ + /* if (this.iconCls) { // this.button.removeCls('x-btn-icon'); // var width = this.button.getWidth(); // this.setWidth(width); } + */ // Allow picking multiple files at once. - this.fileInputEl.dom.setAttribute('multiple', '1'); + this.setMultipleInputAttribute(); }, this); @@ -49,10 +51,23 @@ Ext.define('Ext.ux.upload.BrowseButton', { this.callParent(arguments); }, + reset : function() { + this.callParent(arguments); + this.setMultipleInputAttribute(); + }, + + setMultipleInputAttribute : function(inputEl) { + inputEl = inputEl || this.fileInputEl; + inputEl.dom.setAttribute('multiple', '1'); + } + // OBSOLETE - the method is not used by the superclass anymore + /* createFileInput : function() { this.callParent(arguments); this.fileInputEl.dom.setAttribute('multiple', '1'); } + */ -}); \ No newline at end of file +} +); From aaa396ae82b3762da6ef041c56906a41eb2df241 Mon Sep 17 00:00:00 2001 From: Ivan Novakov Date: Tue, 5 Nov 2013 10:02:39 +0100 Subject: [PATCH 6/6] added the dummy uploader to the example (closes #11) --- public/app.js | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/public/app.js b/public/app.js index 8fb5f07..c3aa445 100644 --- a/public/app.js +++ b/public/app.js @@ -52,7 +52,7 @@ Ext.application({ uploaderOptions : { url : 'upload.php' }, - filenameEncoder: 'Ext.ux.upload.header.Base64FilenameEncoder' + filenameEncoder : 'Ext.ux.upload.header.Base64FilenameEncoder' }); var uploadDialog = Ext.create('Ext.ux.upload.Dialog', { @@ -94,6 +94,30 @@ Ext.application({ } }, this); + uploadDialog.show(); + } + }, '-', { + xtype : 'button', + text : 'Dummy upload', + scope : appPanel, + handler : function() { + + var uploadPanel = Ext.create('Ext.ux.upload.Panel', { + uploader : 'Ext.ux.upload.uploader.DummyUploader' + }); + + var uploadDialog = Ext.create('Ext.ux.upload.Dialog', { + dialogTitle : 'My Upload Dialog', + panel : uploadPanel + }); + + this.mon(uploadDialog, 'uploadcomplete', function(uploadPanel, manager, items, errorCount) { + this.uploadComplete(items); + if (!errorCount) { + uploadDialog.close(); + } + }, this); + uploadDialog.show(); } }