diff --git a/ghost/admin/app/components/gh-file-uploader.js b/ghost/admin/app/components/gh-file-uploader.js index 7aff070130..45bb95328b 100644 --- a/ghost/admin/app/components/gh-file-uploader.js +++ b/ghost/admin/app/components/gh-file-uploader.js @@ -198,16 +198,10 @@ export default Component.extend({ }, _defaultValidator(file) { - let accept = this._accept; + let [, extension] = (/(?:\.([^.]+))?$/).exec(file.name); + let extensions = this._extensions; - if (isBlank(file.type)) { - let [, extension] = (/(?:\.([^.]+))?$/).exec(file.name); - let extensions = this._extensions; - - if (!extension || extensions.indexOf(extension.toLowerCase()) === -1) { - return new UnsupportedMediaTypeError(); - } - } else if (!isBlank(file.type) && !isBlank(accept) && file && accept.indexOf(file.type) === -1) { + if (!extension || extensions.indexOf(extension.toLowerCase()) === -1) { return new UnsupportedMediaTypeError(); } diff --git a/ghost/admin/app/components/gh-image-uploader.js b/ghost/admin/app/components/gh-image-uploader.js index b1bf5abb1c..199a3af937 100644 --- a/ghost/admin/app/components/gh-image-uploader.js +++ b/ghost/admin/app/components/gh-image-uploader.js @@ -3,6 +3,7 @@ import computed from 'ember-computed'; import injectService from 'ember-service/inject'; import {htmlSafe} from 'ember-string'; import {isBlank} from 'ember-utils'; +import {isEmberArray} from 'ember-array/utils'; import run from 'ember-runloop'; import {invokeAction} from 'ember-invoke-action'; @@ -24,6 +25,7 @@ export default Component.extend({ altText: '', saveButton: true, accept: 'image/gif,image/jpg,image/jpeg,image/png,image/svg+xml', + extensions: ['gif', 'jpg', 'jpeg', 'png', 'svg'], validate: null, dragClass: null, @@ -212,9 +214,14 @@ export default Component.extend({ }, _defaultValidator(file) { - let accept = this.get('accept'); + let extensions = this.get('extensions'); + let [, extension] = (/(?:\.([^.]+))?$/).exec(file.name); - if (!isBlank(accept) && file && accept.indexOf(file.type) === -1) { + if (!isEmberArray(extensions)) { + extensions = extensions.split(','); + } + + if (!extension || extensions.indexOf(extension.toLowerCase()) === -1) { return new UnsupportedMediaTypeError(); } diff --git a/ghost/admin/app/components/modals/upload-theme.js b/ghost/admin/app/components/modals/upload-theme.js index 1c162b1e89..184d7e6607 100644 --- a/ghost/admin/app/components/modals/upload-theme.js +++ b/ghost/admin/app/components/modals/upload-theme.js @@ -6,13 +6,13 @@ import { UnsupportedMediaTypeError, isThemeValidationError } from 'ghost-admin/services/ajax'; -import {isBlank} from 'ember-utils'; import run from 'ember-runloop'; import injectService from 'ember-service/inject'; export default ModalComponent.extend({ accept: ['application/zip', 'application/x-zip-compressed'], + extensions: ['zip'], availableThemes: null, closeDisabled: false, file: null, @@ -47,13 +47,15 @@ export default ModalComponent.extend({ actions: { validateTheme(file) { - let accept = this.get('accept'); let themeName = file.name.replace(/\.zip$/, ''); let availableThemeNames = this.get('availableThemeNames'); this.set('file', file); - if (!isBlank(accept) && file && accept.indexOf(file.type) === -1) { + let [, extension] = (/(?:\.([^.]+))?$/).exec(file.name); + let extensions = this.get('extensions'); + + if (!extension || extensions.indexOf(extension.toLowerCase()) === -1) { return new UnsupportedMediaTypeError(); } diff --git a/ghost/admin/tests/acceptance/subscribers-test.js b/ghost/admin/tests/acceptance/subscribers-test.js index c076461a4f..839b9ceb53 100644 --- a/ghost/admin/tests/acceptance/subscribers-test.js +++ b/ghost/admin/tests/acceptance/subscribers-test.js @@ -246,7 +246,7 @@ describe('Acceptance: Subscribers', function() { }); click('.btn:contains("Import CSV")'); - fileUpload('.fullscreen-modal input[type="file"]', ['test'], {type: 'text/csv'}); + fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'test.csv'}); andThen(function () { // modal title changes diff --git a/ghost/admin/tests/acceptance/version-mismatch-test.js b/ghost/admin/tests/acceptance/version-mismatch-test.js index d08e8e3bcf..51292f68b3 100644 --- a/ghost/admin/tests/acceptance/version-mismatch-test.js +++ b/ghost/admin/tests/acceptance/version-mismatch-test.js @@ -104,7 +104,7 @@ describe('Acceptance: Version Mismatch', function() { visit('/subscribers'); click('.btn:contains("Import CSV")'); - fileUpload('.fullscreen-modal input[type="file"]', ['test'], {type: 'text/csv'}); + fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'test.csv'}); andThen(() => { // alert is shown diff --git a/ghost/admin/tests/integration/components/gh-file-uploader-test.js b/ghost/admin/tests/integration/components/gh-file-uploader-test.js index 51d78f9eb1..3f49fa2770 100644 --- a/ghost/admin/tests/integration/components/gh-file-uploader-test.js +++ b/ghost/admin/tests/integration/components/gh-file-uploader-test.js @@ -91,7 +91,7 @@ describeComponent( stubSuccessfulUpload(server); this.render(hbs`{{gh-file-uploader url=uploadUrl}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'text/csv'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'}); wait().then(() => { expect(server.handledRequests.length).to.equal(1); @@ -107,7 +107,7 @@ describeComponent( stubSuccessfulUpload(server); this.render(hbs`{{gh-file-uploader url=uploadUrl uploadSuccess=(action uploadSuccess)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'text/csv'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'}); wait().then(() => { expect(uploadSuccess.calledOnce).to.be.true; @@ -123,7 +123,7 @@ describeComponent( stubFailedUpload(server, 500); this.render(hbs`{{gh-file-uploader url=uploadUrl uploadSuccess=(action uploadSuccess)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'text/csv'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'}); wait().then(() => { expect(uploadSuccess.calledOnce).to.be.false; @@ -138,7 +138,7 @@ describeComponent( stubSuccessfulUpload(server); this.render(hbs`{{gh-file-uploader url=uploadUrl fileSelected=(action fileSelected)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'text/csv'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'}); wait().then(() => { expect(fileSelected.calledOnce).to.be.true; @@ -154,7 +154,7 @@ describeComponent( stubSuccessfulUpload(server); this.render(hbs`{{gh-file-uploader url=uploadUrl uploadStarted=(action uploadStarted)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'text/csv'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'}); wait().then(() => { expect(uploadStarted.calledOnce).to.be.true; @@ -169,7 +169,7 @@ describeComponent( stubSuccessfulUpload(server); this.render(hbs`{{gh-file-uploader url=uploadUrl uploadFinished=(action uploadFinished)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'text/csv'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'}); wait().then(() => { expect(uploadFinished.calledOnce).to.be.true; @@ -184,7 +184,7 @@ describeComponent( stubFailedUpload(server); this.render(hbs`{{gh-file-uploader url=uploadUrl uploadFinished=(action uploadFinished)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'text/csv'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'}); wait().then(() => { expect(uploadFinished.calledOnce).to.be.true; @@ -195,7 +195,7 @@ describeComponent( it('displays invalid file type error', function (done) { stubFailedUpload(server, 415, 'UnsupportedMediaTypeError'); this.render(hbs`{{gh-file-uploader url=uploadUrl}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'text/csv'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'}); wait().then(() => { expect(this.$('.failed').length, 'error message is displayed').to.equal(1); @@ -209,7 +209,7 @@ describeComponent( it('displays file too large for server error', function (done) { stubFailedUpload(server, 413, 'RequestEntityTooLargeError'); this.render(hbs`{{gh-file-uploader url=uploadUrl}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'text/csv'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'}); wait().then(() => { expect(this.$('.failed').length, 'error message is displayed').to.equal(1); @@ -223,7 +223,7 @@ describeComponent( return [413, {}, '']; }); this.render(hbs`{{gh-file-uploader url=uploadUrl}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'text/csv'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'}); wait().then(() => { expect(this.$('.failed').length, 'error message is displayed').to.equal(1); @@ -235,7 +235,7 @@ describeComponent( it('displays other server-side error with message', function (done) { stubFailedUpload(server, 400, 'UnknownError'); this.render(hbs`{{gh-file-uploader url=uploadUrl}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'text/csv'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'}); wait().then(() => { expect(this.$('.failed').length, 'error message is displayed').to.equal(1); @@ -249,7 +249,7 @@ describeComponent( return [500, {'Content-Type': 'application/json'}, '']; }); this.render(hbs`{{gh-file-uploader url=uploadUrl}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'text/csv'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'}); wait().then(() => { expect(this.$('.failed').length, 'error message is displayed').to.equal(1); @@ -265,7 +265,7 @@ describeComponent( stubFailedUpload(server, 400, 'VersionMismatchError'); this.render(hbs`{{gh-file-uploader url=uploadUrl}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'text/csv'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'}); wait().then(() => { expect(showAPIError.calledOnce).to.be.true; @@ -279,7 +279,7 @@ describeComponent( stubFailedUpload(server, 400, 'UnknownError'); this.render(hbs`{{gh-file-uploader url=uploadUrl}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'text/csv'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'}); wait().then(() => { expect(showAPIError.called).to.be.false; @@ -290,7 +290,7 @@ describeComponent( it('can be reset after a failed upload', function (done) { stubFailedUpload(server, 400, 'UnknownError'); this.render(hbs`{{gh-file-uploader url=uploadUrl}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'text/csv'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'}); wait().then(() => { run(() => { @@ -311,7 +311,7 @@ describeComponent( stubSuccessfulUpload(server, 150); this.render(hbs`{{gh-file-uploader url=uploadUrl uploadFinished=(action done)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'text/csv'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'}); // after 75ms we should have had one progress event run.later(this, function () { @@ -347,7 +347,7 @@ describeComponent( let uploadSuccess = sinon.spy(); let drop = $.Event('drop', { dataTransfer: { - files: [createFile(['test'], {type: 'text/csv'})] + files: [createFile(['test'], {name: 'test.csv'})] } }); @@ -367,7 +367,7 @@ describeComponent( }); }); - it('validates "accept" mime type by default', function (done) { + it('validates extension by default', function (done) { let uploadSuccess = sinon.spy(); let uploadFailed = sinon.spy(); @@ -381,7 +381,7 @@ describeComponent( uploadSuccess=(action uploadSuccess) uploadFailed=(action uploadFailed)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'application/plain'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.txt'}); wait().then(() => { expect(uploadSuccess.called).to.be.false; @@ -406,7 +406,7 @@ describeComponent( uploadSuccess=(action uploadSuccess) validate=(action validate)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'text/csv'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'}); wait().then(() => { expect(validate.calledOnce).to.be.true; @@ -432,7 +432,7 @@ describeComponent( uploadFailed=(action uploadFailed) validate=(action validate)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'text/csv'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.csv'}); wait().then(() => { expect(validate.calledOnce).to.be.true; diff --git a/ghost/admin/tests/integration/components/gh-image-uploader-test.js b/ghost/admin/tests/integration/components/gh-image-uploader-test.js index d5f0db8a33..21591a1208 100644 --- a/ghost/admin/tests/integration/components/gh-image-uploader-test.js +++ b/ghost/admin/tests/integration/components/gh-image-uploader-test.js @@ -145,7 +145,7 @@ describeComponent( stubSuccessfulUpload(server); this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'image/png'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'}); wait().then(() => { expect(server.handledRequests.length).to.equal(1); @@ -161,7 +161,7 @@ describeComponent( this.get('sessionService').set('isAuthenticated', true); this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'image/png'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'}); wait().then(() => { let [request] = server.handledRequests; @@ -177,7 +177,7 @@ describeComponent( stubSuccessfulUpload(server); this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'image/png'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'}); wait().then(() => { expect(update.calledOnce).to.be.true; @@ -193,7 +193,7 @@ describeComponent( stubFailedUpload(server, 500); this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'image/png'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'}); wait().then(() => { expect(update.calledOnce).to.be.false; @@ -208,7 +208,7 @@ describeComponent( stubSuccessfulUpload(server); this.render(hbs`{{gh-image-uploader url=image fileSelected=(action fileSelected) update=(action update)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'image/png'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'}); wait().then(() => { expect(fileSelected.calledOnce).to.be.true; @@ -224,7 +224,7 @@ describeComponent( stubSuccessfulUpload(server); this.render(hbs`{{gh-image-uploader image=image uploadStarted=(action uploadStarted) update=(action update)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'image/png'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'}); wait().then(() => { expect(uploadStarted.calledOnce).to.be.true; @@ -239,7 +239,7 @@ describeComponent( stubSuccessfulUpload(server); this.render(hbs`{{gh-image-uploader image=image uploadFinished=(action uploadFinished) update=(action update)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'image/png'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'}); wait().then(() => { expect(uploadFinished.calledOnce).to.be.true; @@ -254,7 +254,7 @@ describeComponent( stubFailedUpload(server); this.render(hbs`{{gh-image-uploader image=image uploadFinished=(action uploadFinished) update=(action update)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'image/png'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'}); wait().then(() => { expect(uploadFinished.calledOnce).to.be.true; @@ -265,7 +265,7 @@ describeComponent( it('displays invalid file type error', function (done) { stubFailedUpload(server, 415, 'UnsupportedMediaTypeError'); this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'image/png'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'}); wait().then(() => { expect(this.$('.failed').length, 'error message is displayed').to.equal(1); @@ -279,7 +279,7 @@ describeComponent( it('displays file too large for server error', function (done) { stubFailedUpload(server, 413, 'RequestEntityTooLargeError'); this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'image/png'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'}); wait().then(() => { expect(this.$('.failed').length, 'error message is displayed').to.equal(1); @@ -293,7 +293,7 @@ describeComponent( return [413, {}, '']; }); this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'image/png'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'}); wait().then(() => { expect(this.$('.failed').length, 'error message is displayed').to.equal(1); @@ -305,7 +305,7 @@ describeComponent( it('displays other server-side error with message', function (done) { stubFailedUpload(server, 400, 'UnknownError'); this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'image/png'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'}); wait().then(() => { expect(this.$('.failed').length, 'error message is displayed').to.equal(1); @@ -319,7 +319,7 @@ describeComponent( return [500, {'Content-Type': 'application/json'}, '']; }); this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'image/png'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'}); wait().then(() => { expect(this.$('.failed').length, 'error message is displayed').to.equal(1); @@ -335,7 +335,7 @@ describeComponent( stubFailedUpload(server, 400, 'VersionMismatchError'); this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'image/png'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'}); wait().then(() => { expect(showAPIError.calledOnce).to.be.true; @@ -349,7 +349,7 @@ describeComponent( stubFailedUpload(server, 400, 'UnknownError'); this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'image/png'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'}); wait().then(() => { expect(showAPIError.called).to.be.false; @@ -360,7 +360,7 @@ describeComponent( it('can be reset after a failed upload', function (done) { stubFailedUpload(server, 400, 'UnknownError'); this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'image/png'}); + fileUpload(this.$('input[type="file"]'), ['test'], {type: 'test.png'}); wait().then(() => { run(() => { @@ -381,7 +381,7 @@ describeComponent( stubSuccessfulUpload(server, 150); this.render(hbs`{{gh-image-uploader image=image uploadFinished=(action done) update=(action update)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'image/png'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'}); // after 75ms we should have had one progress event run.later(this, function () { @@ -419,7 +419,7 @@ describeComponent( let uploadSuccess = sinon.spy(); let drop = $.Event('drop', { dataTransfer: { - files: [createFile(['test'], {type: 'image/png'})] + files: [createFile(['test'], {name: 'test.png'})] } }); @@ -439,7 +439,7 @@ describeComponent( }); }); - it('validates "accept" mime type by default', function (done) { + it('validates extension by default', function (done) { let uploadSuccess = sinon.spy(); let uploadFailed = sinon.spy(); @@ -452,7 +452,7 @@ describeComponent( uploadSuccess=(action uploadSuccess) uploadFailed=(action uploadFailed)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'application/plain'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.json'}); wait().then(() => { expect(uploadSuccess.called).to.be.false; @@ -476,7 +476,7 @@ describeComponent( uploadSuccess=(action uploadSuccess) validate=(action validate)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'application/plain'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.txt'}); wait().then(() => { expect(validate.calledOnce).to.be.true; @@ -501,7 +501,7 @@ describeComponent( uploadFailed=(action uploadFailed) validate=(action validate)}}`); - fileUpload(this.$('input[type="file"]'), ['test'], {type: 'image/png'}); + fileUpload(this.$('input[type="file"]'), ['test'], {name: 'test.png'}); wait().then(() => { expect(validate.calledOnce).to.be.true;