From e05a81cc9f019a2c8c9d3432cba087c6e0d5e907 Mon Sep 17 00:00:00 2001 From: Ray Nicholus Date: Tue, 16 Sep 2014 22:15:05 -0500 Subject: [PATCH] fix($ios8): uploads not possible in iOS8 Safari No way to work around this, so we just alert the user. #1284 --- client/js/button.js | 6 +- client/js/uploader.basic.api.js | 24 ++- client/js/uploader.basic.js | 9 +- client/js/uploader.js | 5 +- client/js/util.js | 12 ++ docs/api/options.jmd | 9 + docs/browser-support.jmd | 1 + docs/index.jmd | 7 + test/unit/basic.js | 57 ++---- test/unit/button.js | 8 +- test/unit/workarounds.js | 297 ++++++++++++++++++++++++++++++++ 11 files changed, 387 insertions(+), 48 deletions(-) create mode 100644 test/unit/workarounds.js diff --git a/client/js/button.js b/client/js/button.js index 1b8018d38..455e6aeb4 100644 --- a/client/js/button.js +++ b/client/js/button.js @@ -38,6 +38,8 @@ qq.UploadButton = function(o) { // Called when the browser invokes the onchange handler on the `` onChange: function(input) {}, + ios8BrowserCrashWorkaround: true, + // **This option will be removed** in the future as the :hover CSS pseudo-class is available on all supported browsers hoverClass: "qq-upload-button-hover", @@ -147,10 +149,10 @@ qq.UploadButton = function(o) { setMultiple: function(isMultiple, opt_input) { var input = this.getInput() || opt_input; - // Temporary workaround for bug in in iOS8 Chrome that causes the browser to crash + // Temporary workaround for bug in in iOS8 UIWebView that causes the browser to crash // before the file chooser appears if the file input doesn't contain a multiple attribute. // See #1283. - if (qq.iosChrome() && !qq.ios6() && !qq.ios7()) { + if (options.ios8BrowserCrashWorkaround && qq.ios8() && (qq.iosChrome() || qq.iosSafariWebView())) { input.setAttribute("multiple", ""); } diff --git a/client/js/uploader.basic.api.js b/client/js/uploader.basic.api.js index 465dd7fa2..18868a110 100644 --- a/client/js/uploader.basic.api.js +++ b/client/js/uploader.basic.api.js @@ -11,6 +11,8 @@ throw new qq.Error("Blob uploading is not supported in this browser!"); } + this._maybeHandleIos8SafariWorkaround(); + if (blobDataOrArray) { var blobDataArray = [].concat(blobDataOrArray), verifiedBlobDataList = [], @@ -46,6 +48,8 @@ }, addFiles: function(filesOrInputs, params, endpoint) { + this._maybeHandleIos8SafariWorkaround(); + var verifiedFilesOrInputs = [], batchId = this._storedIds.length === 0 ? qq.getUniqueId() : this._currentBatchId, fileOrInputIndex, fileOrInput, fileIndex; @@ -563,7 +567,11 @@ function allowMultiple() { if (qq.supportedFeatures.ajaxUploading) { // Workaround for bug in iOS7+ (see #1039) - if (qq.ios() && !qq.ios6() && self._isAllowedExtension(allowedExtensions, ".mov")) { + if (self._options.workarounds.iosEmptyVideos && + qq.ios() && + !qq.ios6() && + self._isAllowedExtension(allowedExtensions, ".mov")) { + return false; } @@ -587,7 +595,8 @@ self._onInputChange(input); }, hoverClass: this._options.classes.buttonHover, - focusClass: this._options.classes.buttonFocus + focusClass: this._options.classes.buttonFocus, + ios8BrowserCrashWorkaround: this._options.workarounds.ios8BrowserCrash }); this._disposeSupport.addDisposer(function() { @@ -1176,6 +1185,17 @@ } }, + _maybeHandleIos8SafariWorkaround: function() { + var self = this; + + if (this._options.workarounds.ios8SafariUploads && qq.ios8() && qq.iosSafari()) { + setTimeout(function() { + window.alert(self._options.messages.unsupportedBrowserIos8Safari); + }, 0); + throw new qq.Error(this._options.messages.unsupportedBrowserIos8Safari); + } + }, + _maybeParseAndSendUploadError: function(id, name, response, xhr) { // Assuming no one will actually set the response code to something other than 200 // and still set 'success' to true... diff --git a/client/js/uploader.basic.js b/client/js/uploader.basic.js index 9e2f68e9a..fe6897e44 100644 --- a/client/js/uploader.basic.js +++ b/client/js/uploader.basic.js @@ -78,7 +78,8 @@ minHeightImageError: "Image is not tall enough.", minWidthImageError: "Image is not wide enough.", retryFailTooManyItems: "Retry failed - you have reached your file limit.", - onLeave: "The files are being uploaded, if you leave now the upload will be canceled." + onLeave: "The files are being uploaded, if you leave now the upload will be canceled.", + unsupportedBrowserIos8Safari: "Unrecoverable error - this browser does not permit file uploading of any kind due to serious bugs in iOS8 Safari. Please use iOS8 Chrome until Apple fixes these issues." }, retry: { @@ -211,6 +212,12 @@ // metadata about each requested scaled version sizes: [] + }, + + workarounds: { + iosEmptyVideos: true, + ios8SafariUploads: true, + ios8BrowserCrash: true } }; diff --git a/client/js/uploader.js b/client/js/uploader.js index 5249fe6ad..64e3ee969 100644 --- a/client/js/uploader.js +++ b/client/js/uploader.js @@ -127,7 +127,10 @@ qq.FineUploader = function(o, namespace) { text: this._options.text }); - if (!qq.supportedFeatures.uploading || (this._options.cors.expected && !qq.supportedFeatures.uploadCors)) { + if (this._options.workarounds.ios8SafariUploads && qq.ios8() && qq.iosSafari()) { + this._templating.renderFailure(this._options.messages.unsupportedBrowserIos8Safari); + } + else if (!qq.supportedFeatures.uploading || (this._options.cors.expected && !qq.supportedFeatures.uploadCors)) { this._templating.renderFailure(this._options.messages.unsupportedBrowser); } else { diff --git a/client/js/util.js b/client/js/util.js index 75e88287e..10c6ee3ee 100644 --- a/client/js/util.js +++ b/client/js/util.js @@ -522,6 +522,10 @@ var qq = function(element) { return qq.ios() && navigator.userAgent.indexOf(" OS 7_") !== -1; }; + qq.ios8 = function() { + return qq.ios() && navigator.userAgent.indexOf(" OS 8_") !== -1; + }; + qq.ios = function() { /*jshint -W014 */ return navigator.userAgent.indexOf("iPad") !== -1 @@ -533,6 +537,14 @@ var qq = function(element) { return qq.ios() && navigator.userAgent.indexOf("CriOS") !== -1; }; + qq.iosSafari = function() { + return qq.ios() && !qq.iosChrome() && navigator.userAgent.indexOf("Safari") !== -1; + }; + + qq.iosSafariWebView = function() { + return qq.ios() && !qq.iosChrome() && !qq.iosSafari(); + }; + // // Events diff --git a/docs/api/options.jmd b/docs/api/options.jmd index 7dedc0db6..a8eb3e615 100644 --- a/docs/api/options.jmd +++ b/docs/api/options.jmd @@ -228,6 +228,15 @@ alert("The `chunking.success.endpoint` option **only** applies to traditional up ) ) }} + +{{ api_parent_option("workarounds", "workarounds", "Flags that enable or disable workarounds for browser-specific bugs.", + ( + ("workarounds.iosEmptyVideos", "iosEmptyVideos", "Ensures all `` elements tracked by Fine Uploader do NOT contain a `multiple` attribute to work around an issue present in iOS7 & 8 that otherwise results in 0-sized uploaded videos.", "Boolean", "true",), + ("workarounds.ios8BrowserCrash", "ios8BrowserCrash", "Ensures all `` elements tracked by Fine Uploader always have a `multiple` attribute present. This only applies to iOS8 Chrome and iOS8 UIWebView, and is put in place to work around an issue that causes the browser to crash when a file input element does not contain a `multiple` attribute inside of a UIWebView container created by an iOS8 app compiled with and iOS7 SDK.", "Boolean", "true",), + ("workarounds.ios8SafariUploads", "ios8SafariUploads", "Disables Fine Uploader and displays a message to the user in iOS8 Safari. Due to serious bugs in iOS8 Safari, uploading is not possible.", "Boolean", "true",) + ) +) +}} diff --git a/docs/browser-support.jmd b/docs/browser-support.jmd index 6e147fc8d..0a96b92db 100644 --- a/docs/browser-support.jmd +++ b/docs/browser-support.jmd @@ -359,6 +359,7 @@ Note: Any features not listed here are supported in all browsers. * - iOS browsers are unable to upload multiple files when video files are allowed to be uploaded due to a long-standing iOS bug. See case #990 on our bug tracker for more details. +* - iOS8 Safari is unable to upload any files due to bugs in Apple's code. Please see [the blog post on this topic for more details](http://blog.fineuploader.com/2014/09/10/ios8-presents-serious-issues-that-prevent-file-uploading/). ### Upload size Limitations diff --git a/docs/index.jmd b/docs/index.jmd index 893707432..3d55b1ec7 100644 --- a/docs/index.jmd +++ b/docs/index.jmd @@ -3,6 +3,13 @@ {% block content %} {% markdown %} +{{ alert( +"""iOS8 contains some serious bugs that prevent uploading in some cases. Workarounds are included +in Fine Uploader in an attempt to work around these issues. Unfortunately, there are no known +workarounds for iOS8 Safari and uploading in that browser is not possible at this time +Please see [the blog post on this topic for more details](http://blog.fineuploader.com/2014/09/10/ios8-presents-serious-issues-that-prevent-file-uploading/).""" +)}} + {{ alert( """Version 5.0 brings some breaking changes. See the [upgrading to 5.x page](upgrading-to-5.html) for help on upgrading from a 4.x version.""" diff --git a/test/unit/basic.js b/test/unit/basic.js index e9ca13ebe..e87db7607 100644 --- a/test/unit/basic.js +++ b/test/unit/basic.js @@ -2,8 +2,7 @@ describe("uploader.basic.js", function () { "use strict"; - var $fineUploader, $button, $extraButton, $extraButton2, $extraButton3, - isIos8 = qq.ios() && navigator.userAgent.indexOf(" OS 8_") !== -1; + var $fineUploader, $button, $extraButton, $extraButton2, $extraButton3; function getFileInput($containerEl) { return $containerEl.find("INPUT")[0]; @@ -26,22 +25,6 @@ describe("uploader.basic.js", function () { $extraButton3 = $fixture.find("#test-button4"); }); - it("Excludes the multiple attribute on the file input element by default only in iOS7+ AND when MOV files can be submitted", function() { - var uploader = new qq.FineUploaderBasic({ - element: $fixture[0], - button: $button[0], - validation: { - allowedExtensions: ["gif", "mov"] - } - }); - - var multipleExpected = (!qq.ios() && qq.supportedFeatures.ajaxUploading) || - qq.ios6() || - (isIos8 && qq.iosChrome()); - - assert.equal(qq(getFileInput($button)).hasAttribute("multiple"), multipleExpected); - }); - it("Includes the multiple attribute on the file input element by default in all supported browsers (even iOS7+) when MOV files cannot be submitted", function() { var uploader = new qq.FineUploaderBasic({ element: $fixture[0], @@ -54,35 +37,37 @@ describe("uploader.basic.js", function () { assert.equal(qq(getFileInput($button)).hasAttribute("multiple"), qq.supportedFeatures.ajaxUploading); }); - it("Includes the multiple attribute on the file input element by default (where supported) except in iOS7+ with the default alloweExtensions value", function() { + it("Includes the multiple attribute on the file input element by default (where supported)", function() { var uploader = new qq.FineUploaderBasic({ element: $fixture[0], button: $button[0] }); - var multipleExpected = (!qq.ios() && qq.supportedFeatures.ajaxUploading) || - qq.ios6() || - (isIos8 && qq.iosChrome()); - assert.equal(qq(getFileInput($button)).hasAttribute("multiple"), qq.supportedFeatures.ajaxUploading && !qq.ios7()); }); - it("Excludes the multiple attribute on the file input element if requested, unless iOS8 Chrome", function() { + it("Excludes the multiple attribute on the file input element if requested", function() { var uploader = new qq.FineUploaderBasic({ element: $fixture[0], button: $button[0], - multiple: false + multiple: false, + workarounds: { + ios8BrowserCrash: false, + iosEmptyVideos: false + } }); - var multipleExpected = isIos8 && qq.iosChrome(); - - assert.equal(qq(getFileInput($button)).hasAttribute("multiple"), multipleExpected); + assert.ok(!qq(getFileInput($button)).hasAttribute("multiple")); }); - it("Excludes or includes the multiple attribute on 'extra' file input elements appropriately, taking OS and extraButton properties into consideration", function() { + it("Excludes or includes the multiple attribute on 'extra' file input elements appropriately, taking extraButton properties into consideration", function() { var uploader = new qq.FineUploaderBasic({ element: $fixture[0], button: $button[0], + workarounds: { + ios8BrowserCrash: false, + iosEmptyVideos: false + }, validation: { allowedExtensions: ["gif", "mov"] }, @@ -103,16 +88,8 @@ describe("uploader.basic.js", function () { ] }); - var multipleExpectedForBtn1 = (!qq.ios() && qq.supportedFeatures.ajaxUploading) || - qq.ios6() || - (isIos8 && qq.iosChrome()); - - var multipleExpectedForBtn2 = isIos8 && qq.iosChrome(); - - var multipleExpectedForBtn3 = qq.supportedFeatures.ajaxUploading; - - assert.equal(qq(getFileInput($extraButton)).hasAttribute("multiple"), multipleExpectedForBtn1); - assert.equal(qq(getFileInput($extraButton2)).hasAttribute("multiple"), multipleExpectedForBtn2); - assert.equal(qq(getFileInput($extraButton3)).hasAttribute("multiple"), multipleExpectedForBtn3); + assert.equal(qq(getFileInput($extraButton)).hasAttribute("multiple"), true); + assert.equal(qq(getFileInput($extraButton2)).hasAttribute("multiple"), false); + assert.equal(qq(getFileInput($extraButton3)).hasAttribute("multiple"), true); }); }); diff --git a/test/unit/button.js b/test/unit/button.js index dddb760fe..7666ce175 100644 --- a/test/unit/button.js +++ b/test/unit/button.js @@ -86,11 +86,15 @@ describe("button.js", function () { var input; var button = new qq.UploadButton({ element: $button[0], - multiple: false + multiple: false, + workarounds: { + ios8BrowserCrash: false, + iosEmptyVideos: false + } }); input = button.getInput(); - assert.equal(input.hasAttribute("multiple"), qq.iosChrome() && !qq.ios6() && !qq.ios7()); + assert.ok(!input.hasAttribute("multiple")); button.setMultiple(true); assert.ok(input.hasAttribute("multiple")); diff --git a/test/unit/workarounds.js b/test/unit/workarounds.js new file mode 100644 index 000000000..03ba969db --- /dev/null +++ b/test/unit/workarounds.js @@ -0,0 +1,297 @@ +/* globals describe, beforeEach, afterEach, qq, assert, it, $fixture */ +describe("browser-specific workarounds", function() { + "use strict"; + + var $fineUploader, $button, $extraButton, $extraButton2, $extraButton3; + + function getFileInput($containerEl) { + return $containerEl.find("INPUT")[0]; + } + + beforeEach(function () { + $fixture.append("
"); + $fineUploader = $fixture.find("#fine-uploader"); + + $fixture.append("
"); + $button = $fixture.find("#test-button"); + }); + + describe("iOS8 WebView & Chrome browser crash", function() { + var origIos8 = qq.ios8, + origIosChrome = qq.iosChrome, + origSafariWebView = qq.iosSafariWebView; + + beforeEach(function() { + qq.ios8 = function() {return true;}; + }); + + afterEach(function() { + qq.ios8 = origIos8; + qq.iosChrome = origIosChrome; + qq.iosSafariWebView = origSafariWebView; + }); + + it("ensures the file input always contains a multiple attr in iOS8 Chrome", function() { + qq.iosChrome = function() {return true;}; + + var uploader = new qq.FineUploaderBasic({ + element: $fixture[0], + button: $button[0], + multiple: false, + workarounds: { + ios8BrowserCrash: true, + iosEmptyVideos: false + } + }); + + assert.equal(qq(getFileInput($button)).hasAttribute("multiple"), true); + }); + + it("ensures the file input does not have a multiple attr if the multiple option is not set in iOS8 Chrome & the workaround is disabled", function() { + qq.iosChrome = function() {return true;}; + + var uploader = new qq.FineUploaderBasic({ + element: $fixture[0], + button: $button[0], + multiple: false, + workarounds: { + ios8BrowserCrash: false, + iosEmptyVideos: false + } + }); + + assert.equal(qq(getFileInput($button)).hasAttribute("multiple"), false); + }); + + it("ensures the file input always contains a multiple attr in iOS8 UIWebView", function() { + qq.iosSafariWebView = function() {return true;}; + + var uploader = new qq.FineUploaderBasic({ + element: $fixture[0], + button: $button[0], + multiple: false, + workarounds: { + ios8BrowserCrash: true, + iosEmptyVideos: false + } + }); + + assert.equal(qq(getFileInput($button)).hasAttribute("multiple"), true); + }); + + it("ensures the file input does not have a multiple attr if the multiple option is not set in iOS8 UIWebView & the workaround is disabled", function() { + qq.iosSafariWebView = function() {return true;}; + + var uploader = new qq.FineUploaderBasic({ + element: $fixture[0], + button: $button[0], + multiple: false, + workarounds: { + ios8BrowserCrash: false, + iosEmptyVideos: false + } + }); + + assert.equal(qq(getFileInput($button)).hasAttribute("multiple"), false); + }); + }); + + describe("iOS7+ 0-sized videos", function() { + var origIos = qq.ios, + origIos6 = qq.ios6; + + beforeEach(function() { + qq.ios = function() {return true;}; + }); + + afterEach(function() { + qq.ios = origIos; + qq.ios6 = origIos6; + }); + + it("ensures the file input never contains a multiple attr in iOS7 or 8", function() { + qq.ios6 = function() {return false;}; + + var uploader = new qq.FineUploaderBasic({ + element: $fixture[0], + button: $button[0], + multiple: true, + workarounds: { + ios8BrowserCrash: false, + iosEmptyVideos: true + } + }); + + assert.equal(qq(getFileInput($button)).hasAttribute("multiple"), false); + }); + + it("ensures the file input does have a multiple attr if the multiple option is set in iOS6", function() { + qq.ios6 = function() {return true;}; + + var uploader = new qq.FineUploaderBasic({ + element: $fixture[0], + button: $button[0], + multiple: true, + workarounds: { + ios8BrowserCrash: false, + iosEmptyVideos: false + } + }); + + assert.equal(qq(getFileInput($button)).hasAttribute("multiple"), true); + }); + + it("ensures the file input does have a multiple attr if the multiple option is set in iOS8 & the workaround is disabled", function() { + qq.ios6 = function() {return false;}; + + var uploader = new qq.FineUploaderBasic({ + element: $fixture[0], + button: $button[0], + multiple: true, + workarounds: { + ios8BrowserCrash: false, + iosEmptyVideos: false + } + }); + + assert.equal(qq(getFileInput($button)).hasAttribute("multiple"), true); + }); + }); + + describe("iOS7+ 0-sized videos & Chrome browser crash", function() { + var origIos8 = qq.ios8, + origIosChrome = qq.iosChrome, + origSafariWebView = qq.iosSafariWebView, + origIos = qq.ios, + origIosSafari = qq.iosSafari, + origIos7 = qq.ios7; + + beforeEach(function() { + qq.ios = function() {return true;}; + qq.ios8 = function() {return true;}; + }); + + afterEach(function() { + qq.ios8 = origIos8; + qq.ios7 = origIos7; + qq.iosChrome = origIosChrome; + qq.iosSafari = origIosSafari; + qq.iosSafariWebView = origSafariWebView; + qq.ios = origIos; + }); + + it("ensures the file input always contains a multiple attr in iOS8 Chrome", function() { + qq.iosChrome = function() {return true;}; + + var uploader = new qq.FineUploaderBasic({ + element: $fixture[0], + button: $button[0], + multiple: false, + workarounds: { + ios8BrowserCrash: true, + iosEmptyVideos: true + } + }); + + assert.equal(qq(getFileInput($button)).hasAttribute("multiple"), true); + }); + + it("ensures the file input always contains a multiple attr in iOS8 UIWebView", function() { + qq.iosSafariWebView = function() {return true;}; + + var uploader = new qq.FineUploaderBasic({ + element: $fixture[0], + button: $button[0], + multiple: false, + workarounds: { + ios8BrowserCrash: true, + iosEmptyVideos: true + } + }); + + assert.equal(qq(getFileInput($button)).hasAttribute("multiple"), true); + }); + + it("ensures the file input never contains a multiple attr in iOS7", function() { + qq.ios8 = function() {return false;}; + qq.ios7 = function() {return true;}; + + var uploader = new qq.FineUploaderBasic({ + element: $fixture[0], + button: $button[0], + multiple: true, + workarounds: { + ios8BrowserCrash: true, + iosEmptyVideos: true + } + }); + + assert.equal(qq(getFileInput($button)).hasAttribute("multiple"), false); + }); + + it("ensures the file input never contains a multiple attr in iOS8 Safari", function() { + qq.iosSafari = function() {return true;}; + + var uploader = new qq.FineUploaderBasic({ + element: $fixture[0], + button: $button[0], + multiple: true, + workarounds: { + ios8BrowserCrash: true, + iosEmptyVideos: true + } + }); + + assert.equal(qq(getFileInput($button)).hasAttribute("multiple"), false); + }); + }); + + describe("iOS8 Safari uploads impossible", function() { + var origIos8 = qq.ios8, + origIosSafari = qq.iosSafari, + origWindowAlert = window.alert; + + beforeEach(function() { + qq.ios8 = function() {return true;}; + qq.iosSafari = function() {return true;}; + }); + + afterEach(function() { + qq.ios8 = origIos8; + qq.iosSafari = origIosSafari; + window.alert = origWindowAlert; + }); + + it("throws an error and pops up an alert if addFiles or addBlobs is called in iOS8 Safari", function(done) { + var uploader = new qq.FineUploaderBasic({ + element: $fixture[0], + button: $button[0], + multiple: true, + workarounds: { + ios8SafariUploads: true + } + }), + alertCalled = 0; + + window.alert = function() { + alertCalled++; + + if (alertCalled === 2) { + done(); + } + }; + + assert.throws( + function() { + uploader.addBlobs(); + }, qq.Error + ); + + assert.throws( + function() { + uploader.addFiles(); + }, qq.Error + ); + }); + }); +});