jquery.fileDownload.js 18 KB

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