1/* 2* jQuery File Download Plugin v1.4.2 3* 4* http://www.johnculviner.com 5* 6* Copyright (c) 2013 - John Culviner 7* 8* Licensed under the MIT license: 9* http://www.opensource.org/licenses/mit-license.php 10* 11* !!!!NOTE!!!! 12* You must also write a cookie in conjunction with using this plugin as mentioned in the orignal post: 13* http://johnculviner.com/jquery-file-download-plugin-for-ajax-like-feature-rich-file-downloads/ 14* !!!!NOTE!!!! 15*/ 16 17(function($, window){ 18 // i'll just put them here to get evaluated on script load 19 var htmlSpecialCharsRegEx = /[<>&\r\n"']/gm; 20 var htmlSpecialCharsPlaceHolders = { 21 '<': 'lt;', 22 '>': 'gt;', 23 '&': 'amp;', 24 '\r': "#13;", 25 '\n': "#10;", 26 '"': 'quot;', 27 "'": 'apos;' /*single quotes just to be safe*/ 28 }; 29 30$.extend({ 31 // 32 //$.fileDownload('/path/to/url/', options) 33 // see directly below for possible 'options' 34 fileDownload: function (fileUrl, options) { 35 36 //provide some reasonable defaults to any unspecified options below 37 var settings = $.extend({ 38 39 // 40 //Requires jQuery UI: provide a message to display to the user when the file download is being prepared before the browser's dialog appears 41 // 42 preparingMessageHtml: null, 43 44 // 45 //Requires jQuery UI: provide a message to display to the user when a file download fails 46 // 47 failMessageHtml: null, 48 49 // 50 //the stock android browser straight up doesn't support file downloads initiated by a non GET: http://code.google.com/p/android/issues/detail?id=1780 51 //specify a message here to display if a user tries with an android browser 52 //if jQuery UI is installed this will be a dialog, otherwise it will be an alert 53 // 54 androidPostUnsupportedMessageHtml: "Unfortunately your Android browser doesn't support this type of file download. Please try again with a different browser.", 55 56 // 57 //Requires jQuery UI: options to pass into jQuery UI Dialog 58 // 59 dialogOptions: { modal: true }, 60 61 // 62 //a function to call while the dowload is being prepared before the browser's dialog appears 63 //Args: 64 // url - the original url attempted 65 // 66 prepareCallback: function (url) { }, 67 68 // 69 //a function to call after a file download dialog/ribbon has appeared 70 //Args: 71 // url - the original url attempted 72 // 73 successCallback: function (url) { }, 74 75 // 76 //a function to call after a file download dialog/ribbon has appeared 77 //Args: 78 // responseHtml - the html that came back in response to the file download. this won't necessarily come back depending on the browser. 79 // in less than IE9 a cross domain error occurs because 500+ errors cause a cross domain issue due to IE subbing out the 80 // server's error message with a "helpful" IE built in message 81 // url - the original url attempted 82 // 83 failCallback: function (responseHtml, url) { }, 84 85 // 86 // the HTTP method to use. Defaults to "GET". 87 // 88 httpMethod: "GET", 89 90 // 91 // if specified will perform a "httpMethod" request to the specified 'fileUrl' using the specified data. 92 // data must be an object (which will be $.param serialized) or already a key=value param string 93 // 94 data: null, 95 96 // 97 //a period in milliseconds to poll to determine if a successful file download has occured or not 98 // 99 checkInterval: 100, 100 101 // 102 //the cookie name to indicate if a file download has occured 103 // 104 cookieName: "fileDownload", 105 106 // 107 //the cookie value for the above name to indicate that a file download has occured 108 // 109 cookieValue: "true", 110 111 // 112 //the cookie path for above name value pair 113 // 114 cookiePath: "/", 115 116 // 117 //the title for the popup second window as a download is processing in the case of a mobile browser 118 // 119 popupWindowTitle: "Initiating file download...", 120 121 // 122 //Functionality to encode HTML entities for a POST, need this if data is an object with properties whose values contains strings with quotation marks. 123 //HTML entity encoding is done by replacing all &,<,>,',",\r,\n characters. 124 //Note that some browsers will POST the string htmlentity-encoded whilst others will decode it before POSTing. 125 //It is recommended that on the server, htmlentity decoding is done irrespective. 126 // 127 encodeHTMLEntities: true 128 129 }, options); 130 131 var deferred = new $.Deferred(); 132 133 //Setup mobile browser detection: Partial credit: http://detectmobilebrowser.com/ 134 var userAgent = (navigator.userAgent || navigator.vendor || window.opera).toLowerCase(); 135 136 var isIos; //has full support of features in iOS 4.0+, uses a new window to accomplish this. 137 var isAndroid; //has full support of GET features in 4.0+ by using a new window. Non-GET is completely unsupported by the browser. See above for specifying a message. 138 var isOtherMobileBrowser; //there is no way to reliably guess here so all other mobile devices will GET and POST to the current window. 139 140 if (/ip(ad|hone|od)/.test(userAgent)) { 141 142 isIos = true; 143 144 } else if (userAgent.indexOf('android') !== -1) { 145 146 isAndroid = true; 147 148 } else { 149 150 isOtherMobileBrowser = /avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|playbook|silk|iemobile|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(userAgent) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|e\-|e\/|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(di|rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|xda(\-|2|g)|yas\-|your|zeto|zte\-/i.test(userAgent.substr(0, 4)); 151 152 } 153 154 var httpMethodUpper = settings.httpMethod.toUpperCase(); 155 156 if (isAndroid && httpMethodUpper !== "GET") { 157 //the stock android browser straight up doesn't support file downloads initiated by non GET requests: http://code.google.com/p/android/issues/detail?id=1780 158 159 if ($().dialog) { 160 $("<div>").html(settings.androidPostUnsupportedMessageHtml).dialog(settings.dialogOptions); 161 } else { 162 alert(settings.androidPostUnsupportedMessageHtml); 163 } 164 165 return deferred.reject(); 166 } 167 168 var $preparingDialog = null; 169 170 var internalCallbacks = { 171 172 onPrepare: function (url) { 173 174 //wire up a jquery dialog to display the preparing message if specified 175 if (settings.preparingMessageHtml) { 176 177 $preparingDialog = $("<div>").html(settings.preparingMessageHtml).dialog(settings.dialogOptions); 178 179 } else if (settings.prepareCallback) { 180 181 settings.prepareCallback(url); 182 183 } 184 185 }, 186 187 onSuccess: function (url) { 188 189 //remove the perparing message if it was specified 190 if ($preparingDialog) { 191 $preparingDialog.dialog('close'); 192 } 193 194 settings.successCallback(url); 195 196 deferred.resolve(url); 197 }, 198 199 onFail: function (responseHtml, url) { 200 201 //remove the perparing message if it was specified 202 if ($preparingDialog) { 203 $preparingDialog.dialog('close'); 204 } 205 206 //wire up a jquery dialog to display the fail message if specified 207 if (settings.failMessageHtml) { 208 $("<div>").html(settings.failMessageHtml).dialog(settings.dialogOptions); 209 } 210 211 settings.failCallback(responseHtml, url); 212 213 deferred.reject(responseHtml, url); 214 } 215 }; 216 217 internalCallbacks.onPrepare(fileUrl); 218 219 //make settings.data a param string if it exists and isn't already 220 if (settings.data !== null && typeof settings.data !== "string") { 221 settings.data = $.param(settings.data); 222 } 223 224 225 var $iframe, 226 downloadWindow, 227 formDoc, 228 $form; 229 230 if (httpMethodUpper === "GET") { 231 232 if (settings.data !== null) { 233 //need to merge any fileUrl params with the data object 234 235 var qsStart = fileUrl.indexOf('?'); 236 237 if (qsStart !== -1) { 238 //we have a querystring in the url 239 240 if (fileUrl.substring(fileUrl.length - 1) !== "&") { 241 fileUrl = fileUrl + "&"; 242 } 243 } else { 244 245 fileUrl = fileUrl + "?"; 246 } 247 248 fileUrl = fileUrl + settings.data; 249 } 250 251 if (isIos || isAndroid) { 252 253 downloadWindow = window.open(fileUrl); 254 downloadWindow.document.title = settings.popupWindowTitle; 255 window.focus(); 256 257 } else if (isOtherMobileBrowser) { 258 259 window.location(fileUrl); 260 261 } else { 262 263 //create a temporary iframe that is used to request the fileUrl as a GET request 264 $iframe = $("<iframe>") 265 .hide() 266 .prop("src", fileUrl) 267 .appendTo("body"); 268 } 269 270 } else { 271 272 var formInnerHtml = ""; 273 274 if (settings.data !== null) { 275 276 $.each(settings.data.replace(/\+/g, ' ').split("&"), function () { 277 278 var kvp = this.split("="); 279 280 var key = settings.encodeHTMLEntities ? htmlSpecialCharsEntityEncode(decodeURIComponent(kvp[0])) : decodeURIComponent(kvp[0]); 281 if (key) { 282 var value = settings.encodeHTMLEntities ? htmlSpecialCharsEntityEncode(decodeURIComponent(kvp[1])) : decodeURIComponent(kvp[1]); 283 formInnerHtml += '<input type="hidden" name="' + key + '" value="' + value + '" />'; 284 } 285 }); 286 } 287 288 if (isOtherMobileBrowser) { 289 290 $form = $("<form>").appendTo("body"); 291 $form.hide() 292 .prop('method', settings.httpMethod) 293 .prop('action', fileUrl) 294 .html(formInnerHtml); 295 296 } else { 297 298 if (isIos) { 299 300 downloadWindow = window.open("about:blank"); 301 downloadWindow.document.title = settings.popupWindowTitle; 302 formDoc = downloadWindow.document; 303 window.focus(); 304 305 } else { 306 307 $iframe = $("<iframe style='display: none' src='about:blank'></iframe>").appendTo("body"); 308 formDoc = getiframeDocument($iframe); 309 } 310 311 formDoc.write("<html><head></head><body><form method='" + settings.httpMethod + "' action='" + fileUrl + "'>" + formInnerHtml + "</form>" + settings.popupWindowTitle + "</body></html>"); 312 $form = $(formDoc).find('form'); 313 } 314 315 $form.submit(); 316 } 317 318 319 //check if the file download has completed every checkInterval ms 320 setTimeout(checkFileDownloadComplete, settings.checkInterval); 321 322 323 function checkFileDownloadComplete() { 324 325 //has the cookie been written due to a file download occuring? 326 if (document.cookie.indexOf(settings.cookieName + "=" + settings.cookieValue) != -1) { 327 328 //execute specified callback 329 internalCallbacks.onSuccess(fileUrl); 330 331 //remove the cookie and iframe 332 document.cookie = settings.cookieName + "=; expires=" + new Date(1000).toUTCString() + "; path=" + settings.cookiePath; 333 334 cleanUp(false); 335 336 return; 337 } 338 339 //has an error occured? 340 //if neither containers exist below then the file download is occuring on the current window 341 if (downloadWindow || $iframe) { 342 343 //has an error occured? 344 try { 345 346 var formDoc = downloadWindow ? downloadWindow.document : getiframeDocument($iframe); 347 348 if (formDoc && formDoc.body != null && formDoc.body.innerHTML.length) { 349 350 var isFailure = true; 351 352 if ($form && $form.length) { 353 var $contents = $(formDoc.body).contents().first(); 354 355 try { 356 if ($contents.length && $contents[0] === $form[0]) { 357 isFailure = false; 358 } 359 } catch (e) { 360 if (e && e.number == -2146828218) { 361 // IE 8-10 throw a permission denied after the form reloads on the "$contents[0] === $form[0]" comparison 362 isFailure = true; 363 } else { 364 throw e; 365 } 366 } 367 } 368 369 if (isFailure) { 370 // IE 8-10 don't always have the full content available right away, they need a litle bit to finish 371 setTimeout(function () { 372 internalCallbacks.onFail(formDoc.body.innerHTML, fileUrl); 373 cleanUp(true); 374 }, 100); 375 376 return; 377 } 378 } 379 } 380 catch (err) { 381 382 //500 error less than IE9 383 internalCallbacks.onFail('', fileUrl); 384 385 cleanUp(true); 386 387 return; 388 } 389 } 390 391 392 //keep checking... 393 setTimeout(checkFileDownloadComplete, settings.checkInterval); 394 } 395 396 //gets an iframes document in a cross browser compatible manner 397 function getiframeDocument($iframe) { 398 var iframeDoc = $iframe[0].contentWindow || $iframe[0].contentDocument; 399 if (iframeDoc.document) { 400 iframeDoc = iframeDoc.document; 401 } 402 return iframeDoc; 403 } 404 405 function cleanUp(isFailure) { 406 407 setTimeout(function() { 408 409 if (downloadWindow) { 410 411 if (isAndroid) { 412 downloadWindow.close(); 413 } 414 415 if (isIos) { 416 if (downloadWindow.focus) { 417 downloadWindow.focus(); //ios safari bug doesn't allow a window to be closed unless it is focused 418 if (isFailure) { 419 downloadWindow.close(); 420 } 421 } 422 } 423 } 424 425 //iframe cleanup appears to randomly cause the download to fail 426 //not doing it seems better than failure... 427 //if ($iframe) { 428 // $iframe.remove(); 429 //} 430 431 }, 0); 432 } 433 434 435 function htmlSpecialCharsEntityEncode(str) { 436 return str.replace(htmlSpecialCharsRegEx, function(match) { 437 return '&' + htmlSpecialCharsPlaceHolders[match]; 438 }); 439 } 440 441 return deferred.promise(); 442 } 443}); 444 445})(jQuery, this); 446