jquery.fileDownload.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. /*
  2. * jQuery File Download Plugin v1.4.3
  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. (function($, window){
  17. // i'll just put them here to get evaluated on script load
  18. var htmlSpecialCharsRegEx = /[<>&\r\n"']/gm;
  19. var htmlSpecialCharsPlaceHolders = {
  20. '<': 'lt;',
  21. '>': 'gt;',
  22. '&': 'amp;',
  23. '\r': "#13;",
  24. '\n': "#10;",
  25. '"': 'quot;',
  26. "'": '#39;' /*single quotes just to be safe, IE8 doesn't support &apos;, so use &#39; instead */
  27. };
  28. $.extend({
  29. //
  30. //$.fileDownload('/path/to/url/', options)
  31. // see directly below for possible 'options'
  32. fileDownload: function (fileUrl, options) {
  33. //provide some reasonable defaults to any unspecified options below
  34. var settings = $.extend({
  35. //
  36. //Requires jQuery UI: provide a message to display to the user when the file download is being prepared before the browser's dialog appears
  37. //
  38. preparingMessageHtml: null,
  39. //
  40. //Requires jQuery UI: provide a message to display to the user when a file download fails
  41. //
  42. failMessageHtml: null,
  43. //
  44. //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
  45. //specify a message here to display if a user tries with an android browser
  46. //if jQuery UI is installed this will be a dialog, otherwise it will be an alert
  47. //Set to null to disable the message and attempt to download anyway
  48. //
  49. androidPostUnsupportedMessageHtml: "Unfortunately your Android browser doesn't support this type of file download. Please try again with a different browser.",
  50. //
  51. //Requires jQuery UI: options to pass into jQuery UI Dialog
  52. //
  53. dialogOptions: { modal: true },
  54. //
  55. //a function to call while the dowload is being prepared before the browser's dialog appears
  56. //Args:
  57. // url - the original url attempted
  58. //
  59. prepareCallback: function (url) { },
  60. //
  61. //a function to call after a file download dialog/ribbon has appeared
  62. //Args:
  63. // url - the original url attempted
  64. //
  65. successCallback: function (url) { },
  66. //
  67. //a function to call after a file download dialog/ribbon has appeared
  68. //Args:
  69. // responseHtml - the html that came back in response to the file download. this won't necessarily come back depending on the browser.
  70. // in less than IE9 a cross domain error occurs because 500+ errors cause a cross domain issue due to IE subbing out the
  71. // server's error message with a "helpful" IE built in message
  72. // url - the original url attempted
  73. //
  74. failCallback: function (responseHtml, url) { },
  75. //
  76. // the HTTP method to use. Defaults to "GET".
  77. //
  78. httpMethod: "GET",
  79. //
  80. // if specified will perform a "httpMethod" request to the specified 'fileUrl' using the specified data.
  81. // data must be an object (which will be $.param serialized) or already a key=value param string
  82. //
  83. data: null,
  84. //
  85. //a period in milliseconds to poll to determine if a successful file download has occured or not
  86. //
  87. checkInterval: 100,
  88. //
  89. //the cookie name to indicate if a file download has occured
  90. //
  91. cookieName: "fileDownload",
  92. //
  93. //the cookie value for the above name to indicate that a file download has occured
  94. //
  95. cookieValue: "true",
  96. //
  97. //the cookie path for above name value pair
  98. //
  99. cookiePath: "/",
  100. //
  101. //if specified it will be used when attempting to clear the above name value pair
  102. //useful for when downloads are being served on a subdomain (e.g. downloads.example.com)
  103. //
  104. cookieDomain: null,
  105. //
  106. //the title for the popup second window as a download is processing in the case of a mobile browser
  107. //
  108. popupWindowTitle: "Initiating file download...",
  109. //
  110. //Functionality to encode HTML entities for a POST, need this if data is an object with properties whose values contains strings with quotation marks.
  111. //HTML entity encoding is done by replacing all &,<,>,',",\r,\n characters.
  112. //Note that some browsers will POST the string htmlentity-encoded whilst others will decode it before POSTing.
  113. //It is recommended that on the server, htmlentity decoding is done irrespective.
  114. //
  115. encodeHTMLEntities: true
  116. }, options);
  117. var deferred = new $.Deferred();
  118. //Setup mobile browser detection: Partial credit: http://detectmobilebrowser.com/
  119. var userAgent = (navigator.userAgent || navigator.vendor || window.opera).toLowerCase();
  120. var isIos; //has full support of features in iOS 4.0+, uses a new window to accomplish this.
  121. 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.
  122. var isOtherMobileBrowser; //there is no way to reliably guess here so all other mobile devices will GET and POST to the current window.
  123. if (/ip(ad|hone|od)/.test(userAgent)) {
  124. isIos = true;
  125. } else if (userAgent.indexOf('android') !== -1) {
  126. isAndroid = true;
  127. } else {
  128. 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));
  129. }
  130. var httpMethodUpper = settings.httpMethod.toUpperCase();
  131. if (isAndroid && httpMethodUpper !== "GET" && settings.androidPostUnsupportedMessageHtml) {
  132. //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
  133. if ($().dialog) {
  134. $("<div>").html(settings.androidPostUnsupportedMessageHtml).dialog(settings.dialogOptions);
  135. } else {
  136. alert(settings.androidPostUnsupportedMessageHtml);
  137. }
  138. return deferred.reject();
  139. }
  140. var $preparingDialog = null;
  141. var internalCallbacks = {
  142. onPrepare: function (url) {
  143. //wire up a jquery dialog to display the preparing message if specified
  144. if (settings.preparingMessageHtml) {
  145. $preparingDialog = $("<div>").html(settings.preparingMessageHtml).dialog(settings.dialogOptions);
  146. } else if (settings.prepareCallback) {
  147. settings.prepareCallback(url);
  148. }
  149. },
  150. onSuccess: function (url) {
  151. //remove the perparing message if it was specified
  152. if ($preparingDialog) {
  153. $preparingDialog.dialog('close');
  154. };
  155. settings.successCallback(url);
  156. deferred.resolve(url);
  157. },
  158. onFail: function (responseHtml, url) {
  159. //remove the perparing message if it was specified
  160. if ($preparingDialog) {
  161. $preparingDialog.dialog('close');
  162. };
  163. //wire up a jquery dialog to display the fail message if specified
  164. if (settings.failMessageHtml) {
  165. $("<div>").html(settings.failMessageHtml).dialog(settings.dialogOptions);
  166. }
  167. settings.failCallback(responseHtml, url);
  168. deferred.reject(responseHtml, url);
  169. }
  170. };
  171. internalCallbacks.onPrepare(fileUrl);
  172. //make settings.data a param string if it exists and isn't already
  173. if (settings.data !== null && typeof settings.data !== "string") {
  174. settings.data = $.param(settings.data);
  175. }
  176. var $iframe,
  177. downloadWindow,
  178. formDoc,
  179. $form;
  180. if (httpMethodUpper === "GET") {
  181. if (settings.data !== null) {
  182. //need to merge any fileUrl params with the data object
  183. var qsStart = fileUrl.indexOf('?');
  184. if (qsStart !== -1) {
  185. //we have a querystring in the url
  186. if (fileUrl.substring(fileUrl.length - 1) !== "&") {
  187. fileUrl = fileUrl + "&";
  188. }
  189. } else {
  190. fileUrl = fileUrl + "?";
  191. }
  192. fileUrl = fileUrl + settings.data;
  193. }
  194. if (isIos || isAndroid) {
  195. downloadWindow = window.open(fileUrl);
  196. downloadWindow.document.title = settings.popupWindowTitle;
  197. window.focus();
  198. } else if (isOtherMobileBrowser) {
  199. window.location(fileUrl);
  200. } else {
  201. //create a temporary iframe that is used to request the fileUrl as a GET request
  202. $iframe = $("<iframe>")
  203. .hide()
  204. .prop("src", fileUrl)
  205. .appendTo("body");
  206. }
  207. } else {
  208. var formInnerHtml = "";
  209. if (settings.data !== null) {
  210. $.each(settings.data.replace(/\+/g, ' ').split("&"), function () {
  211. var kvp = this.split("=");
  212. var key = settings.encodeHTMLEntities ? htmlSpecialCharsEntityEncode(decodeURIComponent(kvp[0])) : decodeURIComponent(kvp[0]);
  213. if (key) {
  214. var value = settings.encodeHTMLEntities ? htmlSpecialCharsEntityEncode(decodeURIComponent(kvp[1])) : decodeURIComponent(kvp[1]);
  215. formInnerHtml += '<input type="hidden" name="' + key + '" value="' + value + '" />';
  216. }
  217. });
  218. }
  219. if (isOtherMobileBrowser) {
  220. $form = $("<form>").appendTo("body");
  221. $form.hide()
  222. .prop('method', settings.httpMethod)
  223. .prop('action', fileUrl)
  224. .html(formInnerHtml);
  225. } else {
  226. if (isIos) {
  227. downloadWindow = window.open("about:blank");
  228. downloadWindow.document.title = settings.popupWindowTitle;
  229. formDoc = downloadWindow.document;
  230. window.focus();
  231. } else {
  232. $iframe = $("<iframe style='display: none' src='about:blank'></iframe>").appendTo("body");
  233. formDoc = getiframeDocument($iframe);
  234. }
  235. formDoc.write("<html><head></head><body><form method='" + settings.httpMethod + "' action='" + fileUrl + "'>" + formInnerHtml + "</form>" + settings.popupWindowTitle + "</body></html>");
  236. $form = $(formDoc).find('form');
  237. }
  238. $form.submit();
  239. }
  240. //check if the file download has completed every checkInterval ms
  241. setTimeout(checkFileDownloadComplete, settings.checkInterval);
  242. function checkFileDownloadComplete() {
  243. //has the cookie been written due to a file download occuring?
  244. if (document.cookie.indexOf(settings.cookieName + "=" + settings.cookieValue) != -1) {
  245. //execute specified callback
  246. internalCallbacks.onSuccess(fileUrl);
  247. //remove cookie
  248. var cookieData = settings.cookieName + "=; path=" + settings.cookiePath + "; expires=" + new Date(0).toUTCString() + ";";
  249. if (settings.cookieDomain) cookieData += " domain=" + settings.cookieDomain + ";";
  250. document.cookie = cookieData;
  251. //remove iframe
  252. cleanUp(false);
  253. return;
  254. }
  255. //has an error occured?
  256. //if neither containers exist below then the file download is occuring on the current window
  257. if (downloadWindow || $iframe) {
  258. //has an error occured?
  259. try {
  260. var formDoc = downloadWindow ? downloadWindow.document : getiframeDocument($iframe);
  261. if (formDoc && formDoc.body != null && formDoc.body.innerHTML.length) {
  262. var isFailure = true;
  263. if ($form && $form.length) {
  264. var $contents = $(formDoc.body).contents().first();
  265. try {
  266. if ($contents.length && $contents[0] === $form[0]) {
  267. isFailure = false;
  268. }
  269. } catch (e) {
  270. if (e && e.number == -2146828218) {
  271. // IE 8-10 throw a permission denied after the form reloads on the "$contents[0] === $form[0]" comparison
  272. isFailure = true;
  273. } else {
  274. throw e;
  275. }
  276. }
  277. }
  278. if (isFailure) {
  279. // IE 8-10 don't always have the full content available right away, they need a litle bit to finish
  280. setTimeout(function () {
  281. internalCallbacks.onFail(formDoc.body.innerHTML, fileUrl);
  282. cleanUp(true);
  283. }, 100);
  284. return;
  285. }
  286. }
  287. }
  288. catch (err) {
  289. //500 error less than IE9
  290. internalCallbacks.onFail('', fileUrl);
  291. cleanUp(true);
  292. return;
  293. }
  294. }
  295. //keep checking...
  296. setTimeout(checkFileDownloadComplete, settings.checkInterval);
  297. }
  298. //gets an iframes document in a cross browser compatible manner
  299. function getiframeDocument($iframe) {
  300. var iframeDoc = $iframe[0].contentWindow || $iframe[0].contentDocument;
  301. if (iframeDoc.document) {
  302. iframeDoc = iframeDoc.document;
  303. }
  304. return iframeDoc;
  305. }
  306. function cleanUp(isFailure) {
  307. setTimeout(function() {
  308. if (downloadWindow) {
  309. if (isAndroid) {
  310. downloadWindow.close();
  311. }
  312. if (isIos) {
  313. if (downloadWindow.focus) {
  314. downloadWindow.focus(); //ios safari bug doesn't allow a window to be closed unless it is focused
  315. if (isFailure) {
  316. downloadWindow.close();
  317. }
  318. }
  319. }
  320. }
  321. //iframe cleanup appears to randomly cause the download to fail
  322. //not doing it seems better than failure...
  323. //if ($iframe) {
  324. // $iframe.remove();
  325. //}
  326. }, 0);
  327. }
  328. function htmlSpecialCharsEntityEncode(str) {
  329. return str.replace(htmlSpecialCharsRegEx, function(match) {
  330. return '&' + htmlSpecialCharsPlaceHolders[match];
  331. });
  332. }
  333. var promise = deferred.promise();
  334. promise.abort = function() {
  335. cleanUp();
  336. $iframe.remove();
  337. };
  338. return promise;
  339. }
  340. });
  341. })(jQuery, this);