diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js index d950d39619..fa6099b226 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js @@ -1,455 +1,464 @@ -/** -* @ngdoc service -* @name umbraco.services.umbRequestHelper -* @description A helper object used for sending requests to the server -**/ -function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogService, notificationsService, eventsService) { - return { - - /** - * @ngdoc method - * @name umbraco.services.umbRequestHelper#convertVirtualToAbsolutePath - * @methodOf umbraco.services.umbRequestHelper - * @function - * - * @description - * This will convert a virtual path (i.e. ~/App_Plugins/Blah/Test.html ) to an absolute path - * - * @param {string} a virtual path, if this is already an absolute path it will just be returned, if this is a relative path an exception will be thrown - */ - convertVirtualToAbsolutePath: function(virtualPath) { - if (virtualPath.startsWith("/")) { - return virtualPath; - } - if (!virtualPath.startsWith("~/")) { - throw "The path " + virtualPath + " is not a virtual path"; - } - if (!Umbraco.Sys.ServerVariables.application.applicationPath) { - throw "No applicationPath defined in Umbraco.ServerVariables.application.applicationPath"; - } - return Umbraco.Sys.ServerVariables.application.applicationPath + virtualPath.trimStart("~/"); - }, - - /** - * @ngdoc method - * @name umbraco.services.umbRequestHelper#dictionaryToQueryString - * @methodOf umbraco.services.umbRequestHelper - * @function - * - * @description - * This will turn an array of key/value pairs or a standard dictionary into a query string - * - * @param {Array} queryStrings An array of key/value pairs - */ - dictionaryToQueryString: function (queryStrings) { - - if (angular.isArray(queryStrings)) { - return _.map(queryStrings, function (item) { - var key = null; - var val = null; - for (var k in item) { - key = k; - val = item[k]; - break; - } - if (key === null || val === null) { - throw "The object in the array was not formatted as a key/value pair"; - } - return encodeURIComponent(key) + "=" + encodeURIComponent(val); - }).join("&"); - } - else if (angular.isObject(queryStrings)) { - - //this allows for a normal object to be passed in (ie. a dictionary) - return decodeURIComponent($.param(queryStrings)); - } - - throw "The queryString parameter is not an array or object of key value pairs"; - }, - - /** - * @ngdoc method - * @name umbraco.services.umbRequestHelper#getApiUrl - * @methodOf umbraco.services.umbRequestHelper - * @function - * - * @description - * This will return the webapi Url for the requested key based on the servervariables collection - * - * @param {string} apiName The webapi name that is found in the servervariables["umbracoUrls"] dictionary - * @param {string} actionName The webapi action name - * @param {object} queryStrings Can be either a string or an array containing key/value pairs - */ - getApiUrl: function (apiName, actionName, queryStrings) { - if (!Umbraco || !Umbraco.Sys || !Umbraco.Sys.ServerVariables || !Umbraco.Sys.ServerVariables["umbracoUrls"]) { - throw "No server variables defined!"; - } - - if (!Umbraco.Sys.ServerVariables["umbracoUrls"][apiName]) { - throw "No url found for api name " + apiName; - } - - return Umbraco.Sys.ServerVariables["umbracoUrls"][apiName] + actionName + - (!queryStrings ? "" : "?" + (angular.isString(queryStrings) ? queryStrings : this.dictionaryToQueryString(queryStrings))); - - }, - - /** - * @ngdoc function - * @name umbraco.services.umbRequestHelper#resourcePromise - * @methodOf umbraco.services.umbRequestHelper - * @function - * - * @description - * This returns a promise with an underlying http call, it is a helper method to reduce - * the amount of duplicate code needed to query http resources and automatically handle any - * Http errors. See /docs/source/using-promises-resources.md - * - * @param {object} opts A mixed object which can either be a string representing the error message to be - * returned OR an object containing either: - * { success: successCallback, errorMsg: errorMessage } - * OR - * { success: successCallback, error: errorCallback } - * In both of the above, the successCallback must accept these parameters: data, status, headers, config - * If using the errorCallback it must accept these parameters: data, status, headers, config - * The success callback must return the data which will be resolved by the deferred object. - * The error callback must return an object containing: {errorMsg: errorMessage, data: originalData, status: status } - */ - resourcePromise: function (httpPromise, opts) { - var deferred = $q.defer(); - - /** The default success callback used if one is not supplied in the opts */ - function defaultSuccess(data, status, headers, config) { - //when it's successful, just return the data - return data; - } - - /** The default error callback used if one is not supplied in the opts */ - function defaultError(data, status, headers, config) { - return { - //NOTE: the default error message here should never be used based on the above docs! - errorMsg: (angular.isString(opts) ? opts : 'An error occurred!'), - data: data, - status: status - }; - } - - //create the callbacs based on whats been passed in. - var callbacks = { - success: ((!opts || !opts.success) ? defaultSuccess : opts.success), - error: ((!opts || !opts.error) ? defaultError : opts.error) - }; - - httpPromise.success(function (data, status, headers, config) { - - //invoke the callback - var result = callbacks.success.apply(this, [data, status, headers, config]); - - //when it's successful, just return the data - deferred.resolve(result); - - }).error(function (data, status, headers, config) { - - //invoke the callback - var result = callbacks.error.apply(this, [data, status, headers, config]); - - //when there's a 500 (unhandled) error show a YSOD overlay if debugging is enabled. - if (status >= 500 && status < 600) { - - //show a ysod dialog - if (Umbraco.Sys.ServerVariables["isDebuggingEnabled"] === true) { - eventsService.emit('app.ysod', - { - errorMsg: 'An error occured', - data: data - }); - } - else { - //show a simple error notification - notificationsService.error("Server error", "Contact administrator, see log for full details.
" + result.errorMsg + ""); - } - - } - - //return an error object including the error message for UI - deferred.reject({ - errorMsg: result.errorMsg, - data: result.data, - status: result.status - }); - - - }); - - return deferred.promise; - - }, - - /** Used for saving media/content specifically */ - postSaveContent: function (args) { - - if (!args.restApiUrl) { - throw "args.restApiUrl is a required argument"; - } - if (!args.content) { - throw "args.content is a required argument"; - } - if (!args.action) { - throw "args.action is a required argument"; - } - if (!args.files) { - throw "args.files is a required argument"; - } - if (!args.dataFormatter) { - throw "args.dataFormatter is a required argument"; - } - - - var deferred = $q.defer(); - - //save the active tab id so we can set it when the data is returned. - var activeTab = _.find(args.content.tabs, function (item) { - return item.active; - }); - var activeTabIndex = (activeTab === undefined ? 0 : _.indexOf(args.content.tabs, activeTab)); - - //save the data - this.postMultiPartRequest( - args.restApiUrl, - { key: "contentItem", value: args.dataFormatter(args.content, args.action) }, - function (data, formData) { - //now add all of the assigned files - for (var f in args.files) { - //each item has a property alias and the file object, we'll ensure that the alias is suffixed to the key - // so we know which property it belongs to on the server side - formData.append("file_" + args.files[f].alias, args.files[f].file); - } - - }, - function (data, status, headers, config) { - //success callback - - //reset the tabs and set the active one - if(data.tabs && data.tabs.length > 0) { - _.each(data.tabs, function (item) { - item.active = false; - }); - data.tabs[activeTabIndex].active = true; - } - - //the data returned is the up-to-date data so the UI will refresh - deferred.resolve(data); - }, - function (data, status, headers, config) { - //failure callback - - //when there's a 500 (unhandled) error show a YSOD overlay if debugging is enabled. - if (status >= 500 && status < 600) { - - //This is a bit of a hack to check if the error is due to a file being uploaded that is too large, - // we have to just check for the existence of a string value but currently that is the best way to - // do this since it's very hacky/difficult to catch this on the server - if (typeof data !== "undefined" && typeof data.indexOf === "function" && data.indexOf("Maximum request length exceeded") >= 0) { - notificationsService.error("Server error", "The uploaded file was too large, check with your site administrator to adjust the maximum size allowed"); - } - else if (Umbraco.Sys.ServerVariables["isDebuggingEnabled"] === true) { - //show a ysod dialog - eventsService.emit('app.ysod', - { - errorMsg: 'An error occured', - data: data - }); - } - else { - //show a simple error notification - notificationsService.error("Server error", "Contact administrator, see log for full details.
" + data.ExceptionMessage + ""); - } - - } - - //return an error object including the error message for UI - deferred.reject({ - errorMsg: 'An error occurred', - data: data, - status: status - }); - - - }); - - return deferred.promise; - }, - - /** Posts a multi-part mime request to the server */ - postMultiPartRequest: function (url, jsonData, transformCallback, successCallback, failureCallback) { - - //validate input, jsonData can be an array of key/value pairs or just one key/value pair. - if (!jsonData) { throw "jsonData cannot be null"; } - - if (angular.isArray(jsonData)) { - _.each(jsonData, function (item) { - if (!item.key || !item.value) { throw "jsonData array item must have both a key and a value property"; } - }); - } - else if (!jsonData.key || !jsonData.value) { throw "jsonData object must have both a key and a value property"; } - - - $http({ - method: 'POST', - url: url, - //IMPORTANT!!! You might think this should be set to 'multipart/form-data' but this is not true because when we are sending up files - // the request needs to include a 'boundary' parameter which identifies the boundary name between parts in this multi-part request - // and setting the Content-type manually will not set this boundary parameter. For whatever reason, setting the Content-type to 'false' - // will force the request to automatically populate the headers properly including the boundary parameter. - headers: { 'Content-Type': false }, - transformRequest: function (data) { - var formData = new FormData(); - //add the json data - if (angular.isArray(data)) { - _.each(data, function (item) { - formData.append(item.key, !angular.isString(item.value) ? angular.toJson(item.value) : item.value); - }); - } - else { - formData.append(data.key, !angular.isString(data.value) ? angular.toJson(data.value) : data.value); - } - - //call the callback - if (transformCallback) { - transformCallback.apply(this, [data, formData]); - } - - return formData; - }, - data: jsonData - }). - success(function (data, status, headers, config) { - if (successCallback) { - successCallback.apply(this, [data, status, headers, config]); - } - }). - error(function (data, status, headers, config) { - if (failureCallback) { - failureCallback.apply(this, [data, status, headers, config]); - } - }); - }, - - /** - * Downloads a file to the client using AJAX/XHR - * Based on an implementation here: web.student.tuwien.ac.at/~e0427417/jsdownload.html - * See https://stackoverflow.com/a/24129082/694494 - */ - downloadFile : function (httpPath) { - - var deferred = $q.defer(); - - // Use an arraybuffer - $http.get(httpPath, { responseType: 'arraybuffer' }) - .success(function (data, status, headers) { - - var octetStreamMime = 'application/octet-stream'; - var success = false; - - // Get the headers - headers = headers(); - - // Get the filename from the x-filename header or default to "download.bin" - var filename = headers['x-filename'] || 'download.bin'; - - // Determine the content type from the header or default to "application/octet-stream" - var contentType = headers['content-type'] || octetStreamMime; - - try { - // Try using msSaveBlob if supported - console.log("Trying saveBlob method ..."); - var blob = new Blob([data], { type: contentType }); - if (navigator.msSaveBlob) - navigator.msSaveBlob(blob, filename); - else { - // Try using other saveBlob implementations, if available - var saveBlob = navigator.webkitSaveBlob || navigator.mozSaveBlob || navigator.saveBlob; - if (saveBlob === undefined) throw "Not supported"; - saveBlob(blob, filename); - } - console.log("saveBlob succeeded"); - success = true; - } catch (ex) { - console.log("saveBlob method failed with the following exception:"); - console.log(ex); - } - - if (!success) { - // Get the blob url creator - var urlCreator = window.URL || window.webkitURL || window.mozURL || window.msURL; - if (urlCreator) { - // Try to use a download link - var link = document.createElement('a'); - if ('download' in link) { - // Try to simulate a click - try { - // Prepare a blob URL - console.log("Trying download link method with simulated click ..."); - var blob = new Blob([data], { type: contentType }); - var url = urlCreator.createObjectURL(blob); - link.setAttribute('href', url); - - // Set the download attribute (Supported in Chrome 14+ / Firefox 20+) - link.setAttribute("download", filename); - - // Simulate clicking the download link - var event = document.createEvent('MouseEvents'); - event.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null); - link.dispatchEvent(event); - console.log("Download link method with simulated click succeeded"); - success = true; - - } catch (ex) { - console.log("Download link method with simulated click failed with the following exception:"); - console.log(ex); - } - } - - if (!success) { - // Fallback to window.location method - try { - // Prepare a blob URL - // Use application/octet-stream when using window.location to force download - console.log("Trying download link method with window.location ..."); - var blob = new Blob([data], { type: octetStreamMime }); - var url = urlCreator.createObjectURL(blob); - window.location = url; - console.log("Download link method with window.location succeeded"); - success = true; - } catch (ex) { - console.log("Download link method with window.location failed with the following exception:"); - console.log(ex); - } - } - - } - } - - if (!success) { - // Fallback to window.open method - console.log("No methods worked for saving the arraybuffer, using last resort window.open"); - window.open(httpPath, '_blank', ''); - } - - deferred.resolve(); - }) - .error(function (data, status) { - console.log("Request failed with status: " + status); - - deferred.reject({ - errorMsg: "An error occurred downloading the file", - data: data, - status: status - }); - }); - - return deferred.promise; - } - }; -} -angular.module('umbraco.services').factory('umbRequestHelper', umbRequestHelper); +/** +* @ngdoc service +* @name umbraco.services.umbRequestHelper +* @description A helper object used for sending requests to the server +**/ +function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogService, notificationsService, eventsService) { + return { + + /** + * @ngdoc method + * @name umbraco.services.umbRequestHelper#convertVirtualToAbsolutePath + * @methodOf umbraco.services.umbRequestHelper + * @function + * + * @description + * This will convert a virtual path (i.e. ~/App_Plugins/Blah/Test.html ) to an absolute path + * + * @param {string} a virtual path, if this is already an absolute path it will just be returned, if this is a relative path an exception will be thrown + */ + convertVirtualToAbsolutePath: function(virtualPath) { + if (virtualPath.startsWith("/")) { + return virtualPath; + } + if (!virtualPath.startsWith("~/")) { + throw "The path " + virtualPath + " is not a virtual path"; + } + if (!Umbraco.Sys.ServerVariables.application.applicationPath) { + throw "No applicationPath defined in Umbraco.ServerVariables.application.applicationPath"; + } + return Umbraco.Sys.ServerVariables.application.applicationPath + virtualPath.trimStart("~/"); + }, + + /** + * @ngdoc method + * @name umbraco.services.umbRequestHelper#dictionaryToQueryString + * @methodOf umbraco.services.umbRequestHelper + * @function + * + * @description + * This will turn an array of key/value pairs or a standard dictionary into a query string + * + * @param {Array} queryStrings An array of key/value pairs + */ + dictionaryToQueryString: function (queryStrings) { + + if (angular.isArray(queryStrings)) { + return _.map(queryStrings, function (item) { + var key = null; + var val = null; + for (var k in item) { + key = k; + val = item[k]; + break; + } + if (key === null || val === null) { + throw "The object in the array was not formatted as a key/value pair"; + } + return encodeURIComponent(key) + "=" + encodeURIComponent(val); + }).join("&"); + } + else if (angular.isObject(queryStrings)) { + + //this allows for a normal object to be passed in (ie. a dictionary) + return decodeURIComponent($.param(queryStrings)); + } + + throw "The queryString parameter is not an array or object of key value pairs"; + }, + + /** + * @ngdoc method + * @name umbraco.services.umbRequestHelper#getApiUrl + * @methodOf umbraco.services.umbRequestHelper + * @function + * + * @description + * This will return the webapi Url for the requested key based on the servervariables collection + * + * @param {string} apiName The webapi name that is found in the servervariables["umbracoUrls"] dictionary + * @param {string} actionName The webapi action name + * @param {object} queryStrings Can be either a string or an array containing key/value pairs + */ + getApiUrl: function (apiName, actionName, queryStrings) { + if (!Umbraco || !Umbraco.Sys || !Umbraco.Sys.ServerVariables || !Umbraco.Sys.ServerVariables["umbracoUrls"]) { + throw "No server variables defined!"; + } + + if (!Umbraco.Sys.ServerVariables["umbracoUrls"][apiName]) { + throw "No url found for api name " + apiName; + } + + return Umbraco.Sys.ServerVariables["umbracoUrls"][apiName] + actionName + + (!queryStrings ? "" : "?" + (angular.isString(queryStrings) ? queryStrings : this.dictionaryToQueryString(queryStrings))); + + }, + + /** + * @ngdoc function + * @name umbraco.services.umbRequestHelper#resourcePromise + * @methodOf umbraco.services.umbRequestHelper + * @function + * + * @description + * This returns a promise with an underlying http call, it is a helper method to reduce + * the amount of duplicate code needed to query http resources and automatically handle any + * Http errors. See /docs/source/using-promises-resources.md + * + * @param {object} opts A mixed object which can either be a string representing the error message to be + * returned OR an object containing either: + * { success: successCallback, errorMsg: errorMessage } + * OR + * { success: successCallback, error: errorCallback } + * In both of the above, the successCallback must accept these parameters: data, status, headers, config + * If using the errorCallback it must accept these parameters: data, status, headers, config + * The success callback must return the data which will be resolved by the deferred object. + * The error callback must return an object containing: {errorMsg: errorMessage, data: originalData, status: status } + */ + resourcePromise: function (httpPromise, opts) { + var deferred = $q.defer(); + + /** The default success callback used if one is not supplied in the opts */ + function defaultSuccess(data, status, headers, config) { + //when it's successful, just return the data + return data; + } + + /** The default error callback used if one is not supplied in the opts */ + function defaultError(data, status, headers, config) { + + var err = { + //NOTE: the default error message here should never be used based on the above docs! + errorMsg: (angular.isString(opts) ? opts : 'An error occurred!'), + data: data, + status: status + }; + + // if "opts" is a promise, we set "err.errorMsg" to be that promise + if (typeof(opts) == "object" && typeof(opts.then) == "function") { + err.errorMsg = opts; + } + + return err; + + } + + //create the callbacs based on whats been passed in. + var callbacks = { + success: ((!opts || !opts.success) ? defaultSuccess : opts.success), + error: ((!opts || !opts.error) ? defaultError : opts.error) + }; + + httpPromise.success(function (data, status, headers, config) { + + //invoke the callback + var result = callbacks.success.apply(this, [data, status, headers, config]); + + //when it's successful, just return the data + deferred.resolve(result); + + }).error(function (data, status, headers, config) { + + //invoke the callback + var result = callbacks.error.apply(this, [data, status, headers, config]); + + //when there's a 500 (unhandled) error show a YSOD overlay if debugging is enabled. + if (status >= 500 && status < 600) { + + //show a ysod dialog + if (Umbraco.Sys.ServerVariables["isDebuggingEnabled"] === true) { + eventsService.emit('app.ysod', + { + errorMsg: 'An error occured', + data: data + }); + } + else { + //show a simple error notification + notificationsService.error("Server error", "Contact administrator, see log for full details.
" + result.errorMsg + ""); + } + + } + + //return an error object including the error message for UI + deferred.reject({ + errorMsg: result.errorMsg, + data: result.data, + status: result.status + }); + + + }); + + return deferred.promise; + + }, + + /** Used for saving media/content specifically */ + postSaveContent: function (args) { + + if (!args.restApiUrl) { + throw "args.restApiUrl is a required argument"; + } + if (!args.content) { + throw "args.content is a required argument"; + } + if (!args.action) { + throw "args.action is a required argument"; + } + if (!args.files) { + throw "args.files is a required argument"; + } + if (!args.dataFormatter) { + throw "args.dataFormatter is a required argument"; + } + + + var deferred = $q.defer(); + + //save the active tab id so we can set it when the data is returned. + var activeTab = _.find(args.content.tabs, function (item) { + return item.active; + }); + var activeTabIndex = (activeTab === undefined ? 0 : _.indexOf(args.content.tabs, activeTab)); + + //save the data + this.postMultiPartRequest( + args.restApiUrl, + { key: "contentItem", value: args.dataFormatter(args.content, args.action) }, + function (data, formData) { + //now add all of the assigned files + for (var f in args.files) { + //each item has a property alias and the file object, we'll ensure that the alias is suffixed to the key + // so we know which property it belongs to on the server side + formData.append("file_" + args.files[f].alias, args.files[f].file); + } + + }, + function (data, status, headers, config) { + //success callback + + //reset the tabs and set the active one + if(data.tabs && data.tabs.length > 0) { + _.each(data.tabs, function (item) { + item.active = false; + }); + data.tabs[activeTabIndex].active = true; + } + + //the data returned is the up-to-date data so the UI will refresh + deferred.resolve(data); + }, + function (data, status, headers, config) { + //failure callback + + //when there's a 500 (unhandled) error show a YSOD overlay if debugging is enabled. + if (status >= 500 && status < 600) { + + //This is a bit of a hack to check if the error is due to a file being uploaded that is too large, + // we have to just check for the existence of a string value but currently that is the best way to + // do this since it's very hacky/difficult to catch this on the server + if (typeof data !== "undefined" && typeof data.indexOf === "function" && data.indexOf("Maximum request length exceeded") >= 0) { + notificationsService.error("Server error", "The uploaded file was too large, check with your site administrator to adjust the maximum size allowed"); + } + else if (Umbraco.Sys.ServerVariables["isDebuggingEnabled"] === true) { + //show a ysod dialog + eventsService.emit('app.ysod', + { + errorMsg: 'An error occured', + data: data + }); + } + else { + //show a simple error notification + notificationsService.error("Server error", "Contact administrator, see log for full details.
" + data.ExceptionMessage + ""); + } + + } + + //return an error object including the error message for UI + deferred.reject({ + errorMsg: 'An error occurred', + data: data, + status: status + }); + + + }); + + return deferred.promise; + }, + + /** Posts a multi-part mime request to the server */ + postMultiPartRequest: function (url, jsonData, transformCallback, successCallback, failureCallback) { + + //validate input, jsonData can be an array of key/value pairs or just one key/value pair. + if (!jsonData) { throw "jsonData cannot be null"; } + + if (angular.isArray(jsonData)) { + _.each(jsonData, function (item) { + if (!item.key || !item.value) { throw "jsonData array item must have both a key and a value property"; } + }); + } + else if (!jsonData.key || !jsonData.value) { throw "jsonData object must have both a key and a value property"; } + + + $http({ + method: 'POST', + url: url, + //IMPORTANT!!! You might think this should be set to 'multipart/form-data' but this is not true because when we are sending up files + // the request needs to include a 'boundary' parameter which identifies the boundary name between parts in this multi-part request + // and setting the Content-type manually will not set this boundary parameter. For whatever reason, setting the Content-type to 'false' + // will force the request to automatically populate the headers properly including the boundary parameter. + headers: { 'Content-Type': false }, + transformRequest: function (data) { + var formData = new FormData(); + //add the json data + if (angular.isArray(data)) { + _.each(data, function (item) { + formData.append(item.key, !angular.isString(item.value) ? angular.toJson(item.value) : item.value); + }); + } + else { + formData.append(data.key, !angular.isString(data.value) ? angular.toJson(data.value) : data.value); + } + + //call the callback + if (transformCallback) { + transformCallback.apply(this, [data, formData]); + } + + return formData; + }, + data: jsonData + }). + success(function (data, status, headers, config) { + if (successCallback) { + successCallback.apply(this, [data, status, headers, config]); + } + }). + error(function (data, status, headers, config) { + if (failureCallback) { + failureCallback.apply(this, [data, status, headers, config]); + } + }); + }, + + /** + * Downloads a file to the client using AJAX/XHR + * Based on an implementation here: web.student.tuwien.ac.at/~e0427417/jsdownload.html + * See https://stackoverflow.com/a/24129082/694494 + */ + downloadFile : function (httpPath) { + + var deferred = $q.defer(); + + // Use an arraybuffer + $http.get(httpPath, { responseType: 'arraybuffer' }) + .success(function (data, status, headers) { + + var octetStreamMime = 'application/octet-stream'; + var success = false; + + // Get the headers + headers = headers(); + + // Get the filename from the x-filename header or default to "download.bin" + var filename = headers['x-filename'] || 'download.bin'; + + // Determine the content type from the header or default to "application/octet-stream" + var contentType = headers['content-type'] || octetStreamMime; + + try { + // Try using msSaveBlob if supported + console.log("Trying saveBlob method ..."); + var blob = new Blob([data], { type: contentType }); + if (navigator.msSaveBlob) + navigator.msSaveBlob(blob, filename); + else { + // Try using other saveBlob implementations, if available + var saveBlob = navigator.webkitSaveBlob || navigator.mozSaveBlob || navigator.saveBlob; + if (saveBlob === undefined) throw "Not supported"; + saveBlob(blob, filename); + } + console.log("saveBlob succeeded"); + success = true; + } catch (ex) { + console.log("saveBlob method failed with the following exception:"); + console.log(ex); + } + + if (!success) { + // Get the blob url creator + var urlCreator = window.URL || window.webkitURL || window.mozURL || window.msURL; + if (urlCreator) { + // Try to use a download link + var link = document.createElement('a'); + if ('download' in link) { + // Try to simulate a click + try { + // Prepare a blob URL + console.log("Trying download link method with simulated click ..."); + var blob = new Blob([data], { type: contentType }); + var url = urlCreator.createObjectURL(blob); + link.setAttribute('href', url); + + // Set the download attribute (Supported in Chrome 14+ / Firefox 20+) + link.setAttribute("download", filename); + + // Simulate clicking the download link + var event = document.createEvent('MouseEvents'); + event.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null); + link.dispatchEvent(event); + console.log("Download link method with simulated click succeeded"); + success = true; + + } catch (ex) { + console.log("Download link method with simulated click failed with the following exception:"); + console.log(ex); + } + } + + if (!success) { + // Fallback to window.location method + try { + // Prepare a blob URL + // Use application/octet-stream when using window.location to force download + console.log("Trying download link method with window.location ..."); + var blob = new Blob([data], { type: octetStreamMime }); + var url = urlCreator.createObjectURL(blob); + window.location = url; + console.log("Download link method with window.location succeeded"); + success = true; + } catch (ex) { + console.log("Download link method with window.location failed with the following exception:"); + console.log(ex); + } + } + + } + } + + if (!success) { + // Fallback to window.open method + console.log("No methods worked for saving the arraybuffer, using last resort window.open"); + window.open(httpPath, '_blank', ''); + } + + deferred.resolve(); + }) + .error(function (data, status) { + console.log("Request failed with status: " + status); + + deferred.reject({ + errorMsg: "An error occurred downloading the file", + data: data, + status: status + }); + }); + + return deferred.promise; + } + }; +} +angular.module('umbraco.services').factory('umbRequestHelper', umbRequestHelper);