1/* 2* jQuery File Download Plugin v1.3.0 3* 4* http://www.johnculviner.com 5* 6* Copyright (c) 2012 - John Culviner 7* 8* Licensed under the MIT license: 9* http://www.opensource.org/licenses/mit-license.php 10*/ 11 12 13$.extend({ 14 // 15 //$.fileDownload('/path/to/url/', options) 16 // see directly below for possible 'options' 17 fileDownload: function (fileUrl, options) { 18 19 var defaultFailCallback = function (responseHtml, url) { 20 alert("A file download error has occurred, please try again. " + responseHtml); 21 }; 22 23 //provide some reasonable defaults to any unspecified options below 24 var settings = $.extend({ 25 26 // 27 //Requires jQuery UI: provide a message to display to the user when the file download is being prepared before the browser's dialog appears 28 // 29 preparingMessageHtml: null, 30 31 // 32 //Requires jQuery UI: provide a message to display to the user when a file download fails 33 // 34 failMessageHtml: null, 35 36 // 37 //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 38 //specify a message here to display if a user tries with an android browser 39 //if jQuery UI is installed this will be a dialog, otherwise it will be an alert 40 // 41 androidPostUnsupportedMessageHtml: "Unfortunately your Android browser doesn't support this type of file download. Please try again with a different browser.", 42 43 // 44 //Requires jQuery UI: options to pass into jQuery UI Dialog 45 // 46 dialogOptions: { modal: true }, 47 48 // 49 //a function to call after a file download dialog/ribbon has appeared 50 //Args: 51 // url - the original url attempted 52 // 53 successCallback: function (url) { }, 54 55 // 56 //a function to call after a file download dialog/ribbon has appeared 57 //Args: 58 // responseHtml - the html that came back in response to the file download. this won't necessarily come back depending on the browser. 59 // in less than IE9 a cross domain error occurs because 500+ errors cause a cross domain issue due to IE subbing out the 60 // server's error message with a "helpful" IE built in message 61 // url - the original url attempted 62 // 63 failCallback: defaultFailCallback, 64 65 // 66 // the HTTP method to use. Defaults to "GET". 67 // 68 httpMethod: "GET", 69 70 // 71 // if specified will perform a "httpMethod" request to the specified 'fileUrl' using the specified data. 72 // data must be an object (which will be $.param serialized) or already a key=value param string 73 // 74 data: null, 75 76 // 77 //a period in milliseconds to poll to determine if a successful file download has occured or not 78 // 79 checkInterval: 100, 80 81 // 82 //the cookie name to indicate if a file download has occured 83 // 84 cookieName: "fileDownload", 85 86 // 87 //the cookie value for the above name to indicate that a file download has occured 88 // 89 cookieValue: "true", 90 91 // 92 //the cookie path for above name value pair 93 // 94 cookiePath: "/", 95 96 // 97 //the title for the popup second window as a download is processing in the case of a mobile browser 98 // 99 popupWindowTitle: "Initiating file download..." 100 101 }, options); 102 103 104 //Setup mobile browser detection: Partial credit: http://detectmobilebrowser.com/ 105 var userAgent = (navigator.userAgent || navigator.vendor || window.opera).toLowerCase(); 106 107 var isIos = false; //has full support of features in iOS 4.0+, uses a new window to accomplish this. 108 var isAndroid = false; //has full support of GET features in 4.0+ by using a new window. POST will resort to a POST on the current window. 109 var isOtherMobileBrowser = false; //there is no way to reliably guess here so all other mobile devices will GET and POST to the current window. 110 111 if (/ip(ad|hone|od)/.test(userAgent)) { 112 113 isIos = true; 114 115 } else if (userAgent.indexOf('android') != -1) { 116 117 isAndroid = true; 118 119 } else { 120 121 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)); 122 123 } 124 125 var httpMethodUpper = settings.httpMethod.toUpperCase(); 126 127 if (isAndroid && httpMethodUpper != "GET") { 128 //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 129 130 if ($().dialog) { 131 $("<div>").html(settings.androidPostUnsupportedMessageHtml).dialog(settings.dialogOptions); 132 } else { 133 alert(settings.androidPostUnsupportedMessageHtml); 134 } 135 136 return; 137 } 138 139 //wire up a jquery dialog to display the preparing message if specified 140 var $preparingDialog = null; 141 if (settings.preparingMessageHtml) { 142 143 $preparingDialog = $("<div>").html(settings.preparingMessageHtml).dialog(settings.dialogOptions); 144 145 } 146 147 var internalCallbacks = { 148 149 onSuccess: function (url) { 150 151 //remove the perparing message if it was specified 152 if ($preparingDialog) { 153 $preparingDialog.dialog('close'); 154 }; 155 156 settings.successCallback(url); 157 158 }, 159 160 onFail: function (responseHtml, url) { 161 162 //remove the perparing message if it was specified 163 if ($preparingDialog) { 164 $preparingDialog.dialog('close'); 165 }; 166 167 //wire up a jquery dialog to display the fail message if specified 168 if (settings.failMessageHtml) { 169 170 $("<div>").html(settings.failMessageHtml).dialog(settings.dialogOptions); 171 172 //only run the fallcallback if the developer specified something different than default 173 //otherwise we would see two messages about how the file download failed 174 if (settings.failCallback != defaultFailCallback) { 175 settings.failCallback(responseHtml, url); 176 } 177 178 } else { 179 180 settings.failCallback(responseHtml, url); 181 } 182 } 183 }; 184 185 186 //make settings.data a param string if it exists and isn't already 187 if (settings.data !== null && typeof settings.data !== "string") { 188 settings.data = $.param(settings.data); 189 } 190 191 192 var $iframe, 193 downloadWindow, 194 formDoc, 195 $form; 196 197 if (httpMethodUpper === "GET") { 198 199 if (settings.data !== null) { 200 //need to merge any fileUrl params with the data object 201 202 var qsStart = fileUrl.indexOf('?'); 203 204 if (qsStart != -1) { 205 //we have a querystring in the url 206 207 if (fileUrl.substring(fileUrl.length - 1) !== "&") { 208 fileUrl = fileUrl + "&"; 209 } 210 } else { 211 212 fileUrl = fileUrl + "?"; 213 } 214 215 fileUrl = fileUrl + settings.data; 216 } 217 218 if (isIos || isAndroid) { 219 220 downloadWindow = window.open(fileUrl); 221 downloadWindow.document.title = settings.popupWindowTitle; 222 window.focus(); 223 224 } else if (isOtherMobileBrowser) { 225 226 window.location(fileUrl); 227 228 } else { 229 //create a temporary iframe that is used to request the fileUrl as a GET request 230 $iframe = $("<iframe>") 231 .hide() 232 .attr("src", fileUrl) 233 .appendTo("body"); 234 } 235 236 } else { 237 238 var formInnerHtml = ""; 239 240 if (settings.data !== null) { 241 242 $.each(settings.data.replace(/\+/g, ' ').split("&"), function () { 243 244 var kvp = this.split("="); 245 var key = decodeURIComponent(kvp[0]); 246 if (!key) return; 247 var value = decodeURIComponent(kvp[1] || ''); 248 249 formInnerHtml += '<input type="hidden" name="' + key + '" value="' + value + '" />'; 250 }); 251 } 252 253 if (isOtherMobileBrowser) { 254 255 $form = $("<form>").appendTo("body"); 256 $form.hide() 257 .attr('method', settings.httpMethod) 258 .attr('action', fileUrl) 259 .html(formInnerHtml); 260 261 } else { 262 263 if (isIos) { 264 265 downloadWindow = window.open("about:blank"); 266 downloadWindow.document.title = settings.popupWindowTitle; 267 formDoc = downloadWindow.document; 268 window.focus(); 269 270 } else { 271 272 $iframe = $("<iframe style='display: none' src='about:blank'></iframe>").appendTo("body"); 273 formDoc = getiframeDocument($iframe); 274 } 275 276 formDoc.write("<html><head></head><body><form method='" + settings.httpMethod + "' action='" + fileUrl + "'>" + formInnerHtml + "</form>" + settings.popupWindowTitle + "</body></html>"); 277 $form = $(formDoc).find('form'); 278 } 279 280 $form.submit(); 281 } 282 283 //check if the file download has completed every checkInterval ms 284 setTimeout(checkFileDownloadComplete, settings.checkInterval); 285 286 function checkFileDownloadComplete() { 287 288 //has the cookie been written due to a file download occuring? 289 if (document.cookie.indexOf(settings.cookieName + "=" + settings.cookieValue) != -1) { 290 291 //execute specified callback 292 internalCallbacks.onSuccess(fileUrl); 293 294 //remove the cookie and iframe 295 var date = new Date(1000); 296 document.cookie = settings.cookieName + "=; expires=" + date.toUTCString() + "; path=" + settings.cookiePath; 297 298 cleanUp(); 299 300 return; 301 } 302 303 //has an error occured? 304 //if neither containers exist below then the file download is occuring on the current window 305 if (downloadWindow || $iframe) { 306 307 //has an error occured? 308 try { 309 310 var formDoc; 311 if (downloadWindow) { 312 formDoc = downloadWindow.document; 313 } else { 314 formDoc = getiframeDocument($iframe); 315 } 316 317 if (formDoc && formDoc.body != null && formDoc.body.innerHTML.length > 0) { 318 319 var isFailure = true; 320 321 if ($form && $form.length > 0) { 322 var $contents = $(formDoc.body).contents().first(); 323 324 if ($contents.length > 0 && $contents[0] === $form[0]) { 325 isFailure = false; 326 } 327 } 328 329 if (isFailure) { 330 internalCallbacks.onFail(formDoc.body.innerHTML, fileUrl); 331 332 cleanUp(true); 333 334 return; 335 } 336 337 } 338 339 } 340 catch (err) { 341 //500 error less than IE9 342 internalCallbacks.onFail('', fileUrl); 343 344 cleanUp(true); 345 346 return; 347 } 348 } 349 350 351 //keep checking... 352 setTimeout(checkFileDownloadComplete, settings.checkInterval); 353 } 354 355 //gets an iframes document in a cross browser compatible manner 356 function getiframeDocument($iframe) { 357 var iframeDoc = $iframe[0].contentWindow || $iframe[0].contentDocument; 358 if (iframeDoc.document) { 359 iframeDoc = iframeDoc.document; 360 } 361 return iframeDoc; 362 } 363 364 function cleanUp(isFailure) { 365 366 if ($iframe) { 367 $iframe.remove(); 368 } 369 370 if (downloadWindow) { 371 372 if (isAndroid) { 373 downloadWindow.close(); 374 } 375 376 if (isIos) { 377 if (isFailure) { 378 downloadWindow.focus(); //ios safari bug doesn't allow a window to be closed unless it is focused 379 downloadWindow.close(); 380 } else { 381 downloadWindow.focus(); 382 } 383 } 384 } 385 } 386 } 387}); 388