Repository: ambari Updated Branches: refs/heads/trunk cb662f494 -> 09c575a22
AMBARI-6792. FE work for download client configurations (alexantonenko) Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/09c575a2 Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/09c575a2 Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/09c575a2 Branch: refs/heads/trunk Commit: 09c575a2261bb4327527c90343fd7b745f68745a Parents: cb662f4 Author: Alex Antonenko <[email protected]> Authored: Fri Aug 8 20:25:09 2014 +0300 Committer: Alex Antonenko <[email protected]> Committed: Fri Aug 8 20:25:09 2014 +0300 ---------------------------------------------------------------------- ambari-web/app/assets/test/tests.js | 3 +- ambari-web/app/config.js | 3 +- ambari-web/app/controllers/main/host/details.js | 9 + ambari-web/app/controllers/main/service/item.js | 9 + ambari-web/app/messages.js | 2 + ambari-web/app/styles/application.less | 20 +- ambari-web/app/templates/main/host/details.hbs | 16 + ambari-web/app/utils/components.js | 17 + ambari-web/app/views/main/host/details.js | 4 + ambari-web/app/views/main/service/item.js | 8 + .../test/controllers/main/host/details_test.js | 20 + .../test/controllers/main/service/item_test.js | 26 ++ ambari-web/test/views/main/host/details_test.js | 111 +++++ .../vendor/scripts/jquery.fileDownload.js | 453 +++++++++++++++++++ 14 files changed, 698 insertions(+), 3 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/09c575a2/ambari-web/app/assets/test/tests.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/assets/test/tests.js b/ambari-web/app/assets/test/tests.js index b4547d5..f82b32b 100644 --- a/ambari-web/app/assets/test/tests.js +++ b/ambari-web/app/assets/test/tests.js @@ -153,6 +153,7 @@ var files = ['test/init_model_test', 'test/views/main/dashboard/widgets/links_widget_test', 'test/views/main/dashboard/widgets/pie_chart_widget_test', 'test/views/main/dashboard/widgets/namenode_cpu_test', + 'test/views/main/host/details_test', 'test/views/main/host/summary_test', 'test/views/main/host/details/host_component_view_test', 'test/views/main/host/details/host_component_views/decommissionable_test', @@ -212,4 +213,4 @@ describe('Ambari Web Unit tests', function() { } -}); \ No newline at end of file +}); http://git-wip-us.apache.org/repos/asf/ambari/blob/09c575a2/ambari-web/app/config.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/config.js b/ambari-web/app/config.js index 7888c99..7262fb9 100644 --- a/ambari-web/app/config.js +++ b/ambari-web/app/config.js @@ -81,7 +81,8 @@ App.supports = { flume: true, databaseConnection: true, configHistory: false, - serverRecommendValidate: false + serverRecommendValidate: false, + downloadClientConfigs: false }; if (App.enableExperimental) { http://git-wip-us.apache.org/repos/asf/ambari/blob/09c575a2/ambari-web/app/controllers/main/host/details.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/controllers/main/host/details.js b/ambari-web/app/controllers/main/host/details.js index 591d90b..d7359dc 100644 --- a/ambari-web/app/controllers/main/host/details.js +++ b/ambari-web/app/controllers/main/host/details.js @@ -1560,5 +1560,14 @@ App.MainHostDetailsController = Em.Controller.extend({ return App.showConfirmationPopup(function () { self.updateComponentPassiveState(event.context, state, message); }); + }, + + downloadClientConfigs: function (event) { + componentsUtils.downloadClientConfigs.call(this, { + hostName: event.context.get('hostName'), + componentName: event.context.get('componentName'), + displayName: event.context.get('displayName') + }); } + }); http://git-wip-us.apache.org/repos/asf/ambari/blob/09c575a2/ambari-web/app/controllers/main/service/item.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/controllers/main/service/item.js b/ambari-web/app/controllers/main/service/item.js index 32c8f5d..f0243de 100644 --- a/ambari-web/app/controllers/main/service/item.js +++ b/ambari-web/app/controllers/main/service/item.js @@ -616,6 +616,15 @@ App.MainServiceItemController = Em.Controller.extend({ ability_controller.enableRMHighAvailability(); }, + downloadClientConfigs: function () { + var component = this.get('content.hostComponents').findProperty('isClient'); + componentsUtils.downloadClientConfigs.call(this, { + serviceName: this.get('content.serviceName'), + componentName: component.get('componentName'), + displayName: component.get('displayName') + }); + }, + isPending:true }); http://git-wip-us.apache.org/repos/asf/ambari/blob/09c575a2/ambari-web/app/messages.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/messages.js b/ambari-web/app/messages.js index da90e6e..6e0c472 100644 --- a/ambari-web/app/messages.js +++ b/ambari-web/app/messages.js @@ -1173,6 +1173,8 @@ Em.I18n.translations = { 'services.service.summary.moreStats':'more stats here', 'services.service.summary.clientCount': '{0} Client Hosts', 'services.service.summary.historyServer': 'History Server Web UI', + 'services.service.actions.downloadClientConfigs':'Download Client Configs', + 'services.service.actions.downloadClientConfigs.fail.popup.body':'Generation of {0} configurations file has failed. Do you want to try again?', 'services.service.actions.run.rebalancer':'Run Rebalancer', 'services.service.actions.run.rebalanceHdfsNodes':'Run Rebalance HDFS nodes', 'services.service.actions.run.rebalanceHdfsNodes.title':'HDFS Rebalance NameNode', http://git-wip-us.apache.org/repos/asf/ambari/blob/09c575a2/ambari-web/app/styles/application.less ---------------------------------------------------------------------- diff --git a/ambari-web/app/styles/application.less b/ambari-web/app/styles/application.less index 6446985..cfc3e66 100644 --- a/ambari-web/app/styles/application.less +++ b/ambari-web/app/styles/application.less @@ -1406,6 +1406,7 @@ a:focus { /**********start styles for dropdown-menu, accordion-group ****************/ .dropdown-menu, .dropdown-submenu > .dropdown-menu, +.dropdown-menu-wrap > .dropdown-menu, .nav-pills .dropdown-menu, .nav-pills > li > a, .nav-tabs > li > a, @@ -1414,7 +1415,24 @@ a:focus { -webkit-border-radius: 0px; -moz-border-radius: 0px; } -/**********end styles for dropdown-menu ****************/ +.dropdown-submenu.submenu-left { + &> .dropdown-menu-wrap { + position: relative; + width: 100%; + left: -100%; + &> .dropdown-menu { + position: absolute; + top: 0px; + right: -10px; + margin-top: -32px; + } + } + &:hover { + .dropdown-menu { + display: inline-block; + } + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/09c575a2/ambari-web/app/templates/main/host/details.hbs ---------------------------------------------------------------------- diff --git a/ambari-web/app/templates/main/host/details.hbs b/ambari-web/app/templates/main/host/details.hbs index 3378a3e..41ea3e0 100644 --- a/ambari-web/app/templates/main/host/details.hbs +++ b/ambari-web/app/templates/main/host/details.hbs @@ -47,6 +47,22 @@ <a {{action "doAction" option target="controller" href=true}}><i {{bindAttr class="option.cssClass option.liClass"}}></i> {{option.label}}</a> </li> {{/each}} + {{#if App.supports.downloadClientConfigs}} + {{#if view.clients}} + <li class="dropdown-submenu submenu-left"> + <a><i class="icon-download-alt"></i> {{t services.service.actions.downloadClientConfigs}}</a> + <div class="dropdown-menu-wrap"> + <ul class="dropdown-menu"> + {{#each client in view.clients}} + <li> + <a {{action "downloadClientConfigs" client target="controller" href=true}}>{{client.service.displayName}}</a> + </li> + {{/each}} + </ul> + </div> + </li> + {{/if}} + {{/if}} </ul> </div> </div> http://git-wip-us.apache.org/repos/asf/ambari/blob/09c575a2/ambari-web/app/utils/components.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/utils/components.js b/ambari-web/app/utils/components.js index b485e7c..11c2eec 100644 --- a/ambari-web/app/utils/components.js +++ b/ambari-web/app/utils/components.js @@ -88,5 +88,22 @@ module.exports = { ajaxErrorCallback: function (request, ajaxOptions, error, opt, params) { console.log('error on change component host status'); App.ajax.defaultErrorHandler(request, opt.url, opt.method); + }, + + downloadClientConfigs: function (data) { + var isForHost = typeof data.hostName !== 'undefined'; + var url = App.get('apiPrefix') + '/clusters/' + App.router.getClusterName() + '/' + + (isForHost ? 'hosts/' + data.hostName + '/host_components/' : 'services/' + data.serviceName + '/components/') + + data.componentName + '?format=client_config_tar'; + var self = this; + $.fileDownload(url).fail(function () { + var popup = App.showConfirmationPopup(function () { + popup.hide(); + self.downloadClientConfigs({ + context: Em.Object.create(data) + }); + }, Em.I18n.t('services.service.actions.downloadClientConfigs.fail.popup.body').format(data.displayName)) + }); } + }; \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/09c575a2/ambari-web/app/views/main/host/details.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views/main/host/details.js b/ambari-web/app/views/main/host/details.js index 1e92569..b969459 100644 --- a/ambari-web/app/views/main/host/details.js +++ b/ambari-web/app/views/main/host/details.js @@ -31,6 +31,10 @@ App.MainHostDetailsView = Em.View.extend({ return App.router.get('mainHostDetailsController.content'); }.property('App.router.mainHostDetailsController.content'), + clients: function () { + return this.get('content.hostComponents').filterProperty('isClient'); + }.property('content.hostComponents.@each'), + isActive: function() { return this.get('controller.content.passiveState') === "OFF"; }.property('controller.content.passiveState'), http://git-wip-us.apache.org/repos/asf/ambari/blob/09c575a2/ambari-web/app/views/main/service/item.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views/main/service/item.js b/ambari-web/app/views/main/service/item.js index 6e9108a..8f0e811 100644 --- a/ambari-web/app/views/main/service/item.js +++ b/ambari-web/app/views/main/service/item.js @@ -99,6 +99,13 @@ App.MainServiceItemView = Em.View.extend({ label: Em.I18n.t('services.service.actions.run.rebalanceHdfsNodes'), cssClass: 'icon-refresh', disabled: false + }, + DOWNLOAD_CLIENT_CONFIGS: { + action: 'downloadClientConfigs', + label: Em.I18n.t('services.service.actions.downloadClientConfigs'), + cssClass: 'icon-download-alt', + isHidden: !(App.get('supports.downloadClientConfigs') && this.get('controller.content.hostComponents').findProperty('isClient')), + disabled: false } } }, @@ -199,6 +206,7 @@ App.MainServiceItemView = Em.View.extend({ options.push(item); }); } + options.push(actionMap.DOWNLOAD_CLIENT_CONFIGS); return options; }.property('controller.content', 'controller.isStopDisabled','controller.isClientsOnlyService', 'controller.content.isRestartRequired', 'isPassive'), http://git-wip-us.apache.org/repos/asf/ambari/blob/09c575a2/ambari-web/test/controllers/main/host/details_test.js ---------------------------------------------------------------------- diff --git a/ambari-web/test/controllers/main/host/details_test.js b/ambari-web/test/controllers/main/host/details_test.js index 189e483..01ae311 100644 --- a/ambari-web/test/controllers/main/host/details_test.js +++ b/ambari-web/test/controllers/main/host/details_test.js @@ -1369,4 +1369,24 @@ describe('App.MainHostDetailsController', function () { })])).to.be.true; }); }); + + describe('#downloadClientConfigs()', function () { + + beforeEach(function () { + sinon.spy($, 'fileDownload'); + }); + afterEach(function () { + $.fileDownload.restore(); + }); + + it('should launch $.fileDownload method', function () { + controller.downloadClientConfigs({ + context: Em.Object.create({ + componentName: 'name' + }) + }); + expect($.fileDownload.calledOnce).to.be.true; + }); + }); + }); \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/09c575a2/ambari-web/test/controllers/main/service/item_test.js ---------------------------------------------------------------------- diff --git a/ambari-web/test/controllers/main/service/item_test.js b/ambari-web/test/controllers/main/service/item_test.js index f052893..f9ee143 100644 --- a/ambari-web/test/controllers/main/service/item_test.js +++ b/ambari-web/test/controllers/main/service/item_test.js @@ -595,4 +595,30 @@ describe('App.MainServiceItemController', function () { }); }); + + describe('#downloadClientConfigs()', function () { + + var mainServiceItemController = App.MainServiceItemController.create({ + content: { + hostComponents: [ + Em.Object.create({ + isClient: true + }) + ] + } + }); + + beforeEach(function () { + sinon.spy($, 'fileDownload'); + }); + afterEach(function () { + $.fileDownload.restore(); + }); + + it('should launch $.fileDownload method', function () { + mainServiceItemController.downloadClientConfigs(); + expect($.fileDownload.calledOnce).to.be.true; + }); + }); + }); http://git-wip-us.apache.org/repos/asf/ambari/blob/09c575a2/ambari-web/test/views/main/host/details_test.js ---------------------------------------------------------------------- diff --git a/ambari-web/test/views/main/host/details_test.js b/ambari-web/test/views/main/host/details_test.js new file mode 100644 index 0000000..7728fa8 --- /dev/null +++ b/ambari-web/test/views/main/host/details_test.js @@ -0,0 +1,111 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var App = require('app'); +require('views/main/host/details'); + +var view, + activeCases = [ + { + passiveState: 'ON', + isActive: false, + label: 'Off' + }, + { + passiveState: 'OFF', + isActive: true, + label: 'On' + } + ]; + +describe('App.MainHostDetailsView', function () { + + beforeEach(function () { + view = App.MainHostDetailsView.create(); + }); + + describe('#content', function () { + it('should take content from controller', function () { + App.router.set('mainHostDetailsController.content', { + property: 'value' + }); + expect(view.get('content.property')).to.equal('value'); + }); + }); + + describe('#clients', function () { + it('should take clients from content', function () { + App.router.set('mainHostDetailsController.content', { + hostComponents: [ + { + isClient: true + }, + { + isClient: false + } + ] + }); + expect(view.get('clients')).to.have.length(1); + view.get('content.hostComponents').pushObject({ + isClient: true + }); + expect(view.get('clients')).to.have.length(2); + }); + }); + + describe('#isActive', function () { + activeCases.forEach(function (item) { + it('should be ' + item.isActive, function () { + view.set('controller', { + content: { + passiveState: item.passiveState + } + }); + expect(view.get('isActive')).to.equal(item.isActive); + }); + }); + }); + + describe('#maintenance', function () { + activeCases.forEach(function (item) { + it('passive state label should contain ' + item.label, function () { + view.set('controller', { + content: { + passiveState: item.passiveState + } + }); + expect(view.get('maintenance').findProperty('action', 'onOffPassiveModeForHost').label).to.contain(item.label); + }); + }); + }); + + describe('#didInsertElement()', function () { + it('isLoaded should be set as true', function () { + view.didInsertElement(); + expect(view.get('isLoaded')).to.be.true; + }); + it('router should reditect to main.hosts.index', function () { + App.router.set('mainHostDetailsController.content', { + isLoaded: false + }); + view.didInsertElement(); + expect(App.router.get('currentState.name')).to.equal('index'); + }); + }); + +}); http://git-wip-us.apache.org/repos/asf/ambari/blob/09c575a2/ambari-web/vendor/scripts/jquery.fileDownload.js ---------------------------------------------------------------------- diff --git a/ambari-web/vendor/scripts/jquery.fileDownload.js b/ambari-web/vendor/scripts/jquery.fileDownload.js new file mode 100644 index 0000000..d6c51e6 --- /dev/null +++ b/ambari-web/vendor/scripts/jquery.fileDownload.js @@ -0,0 +1,453 @@ +/* +* jQuery File Download Plugin v1.4.2 +* +* http://www.johnculviner.com +* +* Copyright (c) 2013 - John Culviner +* +* Licensed under the MIT license: +* http://www.opensource.org/licenses/mit-license.php +* +* !!!!NOTE!!!! +* You must also write a cookie in conjunction with using this plugin as mentioned in the orignal post: +* http://johnculviner.com/jquery-file-download-plugin-for-ajax-like-feature-rich-file-downloads/ +* !!!!NOTE!!!! +*/ + +(function($, window){ + // i'll just put them here to get evaluated on script load + var htmlSpecialCharsRegEx = /[<>&\r\n"']/gm; + var htmlSpecialCharsPlaceHolders = { + '<': 'lt;', + '>': 'gt;', + '&': 'amp;', + '\r': "#13;", + '\n': "#10;", + '"': 'quot;', + "'": '#39;' /*single quotes just to be safe, IE8 doesn't support ', so use ' instead */ + }; + +$.extend({ + // + //$.fileDownload('/path/to/url/', options) + // see directly below for possible 'options' + fileDownload: function (fileUrl, options) { + + //provide some reasonable defaults to any unspecified options below + var settings = $.extend({ + + // + //Requires jQuery UI: provide a message to display to the user when the file download is being prepared before the browser's dialog appears + // + preparingMessageHtml: null, + + // + //Requires jQuery UI: provide a message to display to the user when a file download fails + // + failMessageHtml: null, + + // + //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 + //specify a message here to display if a user tries with an android browser + //if jQuery UI is installed this will be a dialog, otherwise it will be an alert + // + androidPostUnsupportedMessageHtml: "Unfortunately your Android browser doesn't support this type of file download. Please try again with a different browser.", + + // + //Requires jQuery UI: options to pass into jQuery UI Dialog + // + dialogOptions: { modal: true }, + + // + //a function to call while the dowload is being prepared before the browser's dialog appears + //Args: + // url - the original url attempted + // + prepareCallback: function (url) { }, + + // + //a function to call after a file download dialog/ribbon has appeared + //Args: + // url - the original url attempted + // + successCallback: function (url) { }, + + // + //a function to call after a file download dialog/ribbon has appeared + //Args: + // responseHtml - the html that came back in response to the file download. this won't necessarily come back depending on the browser. + // in less than IE9 a cross domain error occurs because 500+ errors cause a cross domain issue due to IE subbing out the + // server's error message with a "helpful" IE built in message + // url - the original url attempted + // + failCallback: function (responseHtml, url) { }, + + // + // the HTTP method to use. Defaults to "GET". + // + httpMethod: "GET", + + // + // if specified will perform a "httpMethod" request to the specified 'fileUrl' using the specified data. + // data must be an object (which will be $.param serialized) or already a key=value param string + // + data: null, + + // + //a period in milliseconds to poll to determine if a successful file download has occured or not + // + checkInterval: 100, + + // + //the cookie name to indicate if a file download has occured + // + cookieName: "fileDownload", + + // + //the cookie value for the above name to indicate that a file download has occured + // + cookieValue: "true", + + // + //the cookie path for above name value pair + // + cookiePath: "/", + + // + //if specified it will be used when attempting to clear the above name value pair + //useful for when downloads are being served on a subdomain (e.g. downloads.example.com) + // + cookieDomain: null, + + // + //the title for the popup second window as a download is processing in the case of a mobile browser + // + popupWindowTitle: "Initiating file download...", + + // + //Functionality to encode HTML entities for a POST, need this if data is an object with properties whose values contains strings with quotation marks. + //HTML entity encoding is done by replacing all &,<,>,',",\r,\n characters. + //Note that some browsers will POST the string htmlentity-encoded whilst others will decode it before POSTing. + //It is recommended that on the server, htmlentity decoding is done irrespective. + // + encodeHTMLEntities: true + + }, options); + + var deferred = new $.Deferred(); + + //Setup mobile browser detection: Partial credit: http://detectmobilebrowser.com/ + var userAgent = (navigator.userAgent || navigator.vendor || window.opera).toLowerCase(); + + var isIos; //has full support of features in iOS 4.0+, uses a new window to accomplish this. + 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. + var isOtherMobileBrowser; //there is no way to reliably guess here so all other mobile devices will GET and POST to the current window. + + if (/ip(ad|hone|od)/.test(userAgent)) { + + isIos = true; + + } else if (userAgent.indexOf('android') !== -1) { + + isAndroid = true; + + } else { + + 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)); + + } + + var httpMethodUpper = settings.httpMethod.toUpperCase(); + + if (isAndroid && httpMethodUpper !== "GET") { + //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 + + if ($().dialog) { + $("<div>").html(settings.androidPostUnsupportedMessageHtml).dialog(settings.dialogOptions); + } else { + alert(settings.androidPostUnsupportedMessageHtml); + } + + return deferred.reject(); + } + + var $preparingDialog = null; + + var internalCallbacks = { + + onPrepare: function (url) { + + //wire up a jquery dialog to display the preparing message if specified + if (settings.preparingMessageHtml) { + + $preparingDialog = $("<div>").html(settings.preparingMessageHtml).dialog(settings.dialogOptions); + + } else if (settings.prepareCallback) { + + settings.prepareCallback(url); + + } + + }, + + onSuccess: function (url) { + + //remove the perparing message if it was specified + if ($preparingDialog) { + $preparingDialog.dialog('close'); + }; + + settings.successCallback(url); + + deferred.resolve(url); + }, + + onFail: function (responseHtml, url) { + + //remove the perparing message if it was specified + if ($preparingDialog) { + $preparingDialog.dialog('close'); + }; + + //wire up a jquery dialog to display the fail message if specified + if (settings.failMessageHtml) { + $("<div>").html(settings.failMessageHtml).dialog(settings.dialogOptions); + } + + settings.failCallback(responseHtml, url); + + deferred.reject(responseHtml, url); + } + }; + + internalCallbacks.onPrepare(fileUrl); + + //make settings.data a param string if it exists and isn't already + if (settings.data !== null && typeof settings.data !== "string") { + settings.data = $.param(settings.data); + } + + + var $iframe, + downloadWindow, + formDoc, + $form; + + if (httpMethodUpper === "GET") { + + if (settings.data !== null) { + //need to merge any fileUrl params with the data object + + var qsStart = fileUrl.indexOf('?'); + + if (qsStart !== -1) { + //we have a querystring in the url + + if (fileUrl.substring(fileUrl.length - 1) !== "&") { + fileUrl = fileUrl + "&"; + } + } else { + + fileUrl = fileUrl + "?"; + } + + fileUrl = fileUrl + settings.data; + } + + if (isIos || isAndroid) { + + downloadWindow = window.open(fileUrl); + downloadWindow.document.title = settings.popupWindowTitle; + window.focus(); + + } else if (isOtherMobileBrowser) { + + window.location(fileUrl); + + } else { + + //create a temporary iframe that is used to request the fileUrl as a GET request + $iframe = $("<iframe>") + .hide() + .prop("src", fileUrl) + .appendTo("body"); + } + + } else { + + var formInnerHtml = ""; + + if (settings.data !== null) { + + $.each(settings.data.replace(/\+/g, ' ').split("&"), function () { + + var kvp = this.split("="); + + var key = settings.encodeHTMLEntities ? htmlSpecialCharsEntityEncode(decodeURIComponent(kvp[0])) : decodeURIComponent(kvp[0]); + if (key) { + var value = settings.encodeHTMLEntities ? htmlSpecialCharsEntityEncode(decodeURIComponent(kvp[1])) : decodeURIComponent(kvp[1]); + formInnerHtml += '<input type="hidden" name="' + key + '" value="' + value + '" />'; + } + }); + } + + if (isOtherMobileBrowser) { + + $form = $("<form>").appendTo("body"); + $form.hide() + .prop('method', settings.httpMethod) + .prop('action', fileUrl) + .html(formInnerHtml); + + } else { + + if (isIos) { + + downloadWindow = window.open("about:blank"); + downloadWindow.document.title = settings.popupWindowTitle; + formDoc = downloadWindow.document; + window.focus(); + + } else { + + $iframe = $("<iframe style='display: none' src='about:blank'></iframe>").appendTo("body"); + formDoc = getiframeDocument($iframe); + } + + formDoc.write("<html><head></head><body><form method='" + settings.httpMethod + "' action='" + fileUrl + "'>" + formInnerHtml + "</form>" + settings.popupWindowTitle + "</body></html>"); + $form = $(formDoc).find('form'); + } + + $form.submit(); + } + + + //check if the file download has completed every checkInterval ms + setTimeout(checkFileDownloadComplete, settings.checkInterval); + + + function checkFileDownloadComplete() { + //has the cookie been written due to a file download occuring? + if (document.cookie.indexOf(settings.cookieName + "=" + settings.cookieValue) != -1) { + + //execute specified callback + internalCallbacks.onSuccess(fileUrl); + + //remove cookie + var cookieData = settings.cookieName + "=; path=" + settings.cookiePath + "; expires=" + new Date(0).toUTCString() + ";"; + if (settings.cookieDomain) cookieData += " domain=" + settings.cookieDomain + ";"; + document.cookie = cookieData; + + //remove iframe + cleanUp(false); + + return; + } + + //has an error occured? + //if neither containers exist below then the file download is occuring on the current window + if (downloadWindow || $iframe) { + + //has an error occured? + try { + + var formDoc = downloadWindow ? downloadWindow.document : getiframeDocument($iframe); + + if (formDoc && formDoc.body != null && formDoc.body.innerHTML.length) { + + var isFailure = true; + + if ($form && $form.length) { + var $contents = $(formDoc.body).contents().first(); + + try { + if ($contents.length && $contents[0] === $form[0]) { + isFailure = false; + } + } catch (e) { + if (e && e.number == -2146828218) { + // IE 8-10 throw a permission denied after the form reloads on the "$contents[0] === $form[0]" comparison + isFailure = true; + } else { + throw e; + } + } + } + + if (isFailure) { + // IE 8-10 don't always have the full content available right away, they need a litle bit to finish + setTimeout(function () { + internalCallbacks.onFail(formDoc.body.innerHTML, fileUrl); + cleanUp(true); + }, 100); + + return; + } + } + } + catch (err) { + + //500 error less than IE9 + internalCallbacks.onFail('', fileUrl); + + cleanUp(true); + + return; + } + } + + + //keep checking... + setTimeout(checkFileDownloadComplete, settings.checkInterval); + } + + //gets an iframes document in a cross browser compatible manner + function getiframeDocument($iframe) { + var iframeDoc = $iframe[0].contentWindow || $iframe[0].contentDocument; + if (iframeDoc.document) { + iframeDoc = iframeDoc.document; + } + return iframeDoc; + } + + function cleanUp(isFailure) { + + setTimeout(function() { + + if (downloadWindow) { + + if (isAndroid) { + downloadWindow.close(); + } + + if (isIos) { + if (downloadWindow.focus) { + downloadWindow.focus(); //ios safari bug doesn't allow a window to be closed unless it is focused + if (isFailure) { + downloadWindow.close(); + } + } + } + } + + //iframe cleanup appears to randomly cause the download to fail + //not doing it seems better than failure... + //if ($iframe) { + // $iframe.remove(); + //} + + }, 0); + } + + + function htmlSpecialCharsEntityEncode(str) { + return str.replace(htmlSpecialCharsRegEx, function(match) { + return '&' + htmlSpecialCharsPlaceHolders[match]; + }); + } + + return deferred.promise(); + } +}); + +})(jQuery, this);
