diff --git a/ng-upload.js b/ng-upload.js index 29d5e8d..1fec8a5 100644 --- a/ng-upload.js +++ b/ng-upload.js @@ -58,8 +58,8 @@ angular.module('ngUpload', []) } }; }]) - .directive('ngUpload', ["$log", "$parse", "$document", - function ($log, $parse, $document) { + .directive('ngUpload', ["$log", "$parse", "$document", "$browser", "$http", + function ($log, $parse, $document, $browser, $http) { var iframeID = 1; // Utility function to get meta tag with a given name attribute function getMetaTagWithName(name) { @@ -75,6 +75,10 @@ angular.module('ngUpload', []) return angular.element(match); } + function getCsrfTokenValue() { + return $browser.cookies()[$http.defaults.xsrfCookieName || 'X-XSRF-TOKEN']; + } + return { restrict: 'AC', link: function (scope, element, attrs) { @@ -110,6 +114,11 @@ angular.module('ngUpload', []) options.beforeSubmit = $parse(attrs.uploadOptionsBeforeSubmit); } + if ( attrs.hasOwnProperty( "uploadOptionsEnableCsrf" ) ) { + // allow for blank or true + options.enableCsrf = attrs.uploadOptionsEnableCsrf != "false"; + } + element.attr({ 'target': 'upload-iframe-' + iframeID, 'method': 'post', @@ -133,6 +142,17 @@ angular.module('ngUpload', []) element.append(input); } + + if ( options.enableCsrf ) { + var input = angular.element(""); + input.attr("class", "upload-csrf-token"); + input.attr("type", "hidden"); + input.attr("name", attrs.uploadOptionsCsrfParam || 'CSRFToken'); + input.val(getCsrfTokenValue()); + + element.append(input); + } + element.after(iframe); setLoadingState(false); diff --git a/ng-upload.min.js b/ng-upload.min.js index 92fc2a5..e87b194 100644 --- a/ng-upload.min.js +++ b/ng-upload.min.js @@ -1 +1 @@ -angular.module("ngUpload",[]).directive("uploadSubmit",["$parse",function(){function n(t,e){t=angular.element(t);var a=t.parent();return e=e.toLowerCase(),a&&a[0].tagName.toLowerCase()===e?a:a?n(a,e):null}return{restrict:"AC",link:function(t,e){e.bind("click",function(t){if(t&&(t.preventDefault(),t.stopPropagation()),!e.attr("disabled")){var a=n(e,"form");a.triggerHandler("submit"),a[0].submit()}})}}}]).directive("ngUpload",["$log","$parse","$document",function(n,t,e){function a(n){var t,a=e.find("head");return angular.forEach(a.find("meta"),function(e){e.getAttribute("name")===n&&(t=e)}),angular.element(t)}var r=1;return{restrict:"AC",link:function(e,o,i){function l(n){e.$isUploading=n}function p(){c.unbind("load"),e.$$phase?l(!1):e.$apply(function(){l(!1)});try{var t,a=(c[0].contentDocument||c[0].contentWindow.document).body;try{t=angular.fromJson(a.innerText||a.textContent),e.$$phase?d(e,{content:t}):e.$apply(function(){d(e,{content:t})})}catch(r){t=a.innerHTML;var o="ng-upload: Response is not valid JSON";n.warn(o),f&&(e.$$phase?f(e,{error:o}):e.$apply(function(){f(e,{error:o})}))}}catch(o){n.warn("ng-upload: Server error"),f&&(e.$$phase?f(e,{error:o}):e.$apply(function(){f(e,{error:o})}))}}r++;var u={},d=i.ngUpload?t(i.ngUpload):null,f=i.errorCatcher?t(i.errorCatcher):null,s=i.ngUploadLoading?t(i.ngUploadLoading):null;i.hasOwnProperty("uploadOptionsConvertHidden")&&(u.convertHidden="false"!=i.uploadOptionsConvertHidden),i.hasOwnProperty("uploadOptionsEnableRailsCsrf")&&(u.enableRailsCsrf="false"!=i.uploadOptionsEnableRailsCsrf),i.hasOwnProperty("uploadOptionsBeforeSubmit")&&(u.beforeSubmit=t(i.uploadOptionsBeforeSubmit)),o.attr({target:"upload-iframe-"+r,method:"post",enctype:"multipart/form-data",encoding:"multipart/form-data"});var c=angular.element('');if(u.enableRailsCsrf){var m=angular.element("");m.attr("class","upload-csrf-token"),m.attr("type","hidden"),m.attr("name",a("csrf-param").attr("content")),m.val(a("csrf-token").attr("content")),o.append(m)}o.after(c),l(!1),o.bind("submit",function(n){var t=e[i.name];return t&&t.$invalid?(n.preventDefault(),!1):u.beforeSubmit&&u.beforeSubmit(e,{})===!1?(e.$$phase||e.$apply(),n.preventDefault(),!1):(c.bind("load",p),u.convertHidden&&angular.forEach(o.find("input"),function(n){var t=angular.element(n);t.attr("ng-model")&&t.attr("type")&&"hidden"==t.attr("type")&&t.attr("value",e.$eval(t.attr("ng-model")))}),e.$$phase?(s&&s(e),l(!0)):e.$apply(function(){s&&s(e),l(!0)}),void 0)})}}}]); \ No newline at end of file +angular.module("ngUpload",[]).directive("uploadSubmit",["$parse",function(){function n(e,t){e=angular.element(e);var a=e.parent();return t=t.toLowerCase(),a&&a[0].tagName.toLowerCase()===t?a:a?n(a,t):null}return{restrict:"AC",link:function(e,t){t.bind("click",function(e){if(e&&(e.preventDefault(),e.stopPropagation()),!t.attr("disabled")){var a=n(t,"form");a.triggerHandler("submit"),a[0].submit()}})}}}]).directive("ngUpload",["$log","$parse","$document","$browser","$http",function(n,e,t,a,r){function o(n){var e,a=t.find("head");return angular.forEach(a.find("meta"),function(t){t.getAttribute("name")===n&&(e=t)}),angular.element(e)}function i(){return a.cookies()[r.defaults.xsrfCookieName||"X-XSRF-TOKEN"]}var l=1;return{restrict:"AC",link:function(t,a,r){function p(n){t.$isUploading=n}function u(){m.unbind("load"),t.$$phase?p(!1):t.$apply(function(){p(!1)});try{var e,a=(m[0].contentDocument||m[0].contentWindow.document).body;try{e=angular.fromJson(a.innerText||a.textContent),t.$$phase?s(t,{content:e}):t.$apply(function(){s(t,{content:e})})}catch(r){e=a.innerHTML;var o="ng-upload: Response is not valid JSON";n.warn(o),f&&(t.$$phase?f(t,{error:o}):t.$apply(function(){f(t,{error:o})}))}}catch(o){n.warn("ng-upload: Server error"),f&&(t.$$phase?f(t,{error:o}):t.$apply(function(){f(t,{error:o})}))}}l++;var d={},s=r.ngUpload?e(r.ngUpload):null,f=r.errorCatcher?e(r.errorCatcher):null,c=r.ngUploadLoading?e(r.ngUploadLoading):null;r.hasOwnProperty("uploadOptionsConvertHidden")&&(d.convertHidden="false"!=r.uploadOptionsConvertHidden),r.hasOwnProperty("uploadOptionsEnableRailsCsrf")&&(d.enableRailsCsrf="false"!=r.uploadOptionsEnableRailsCsrf),r.hasOwnProperty("uploadOptionsBeforeSubmit")&&(d.beforeSubmit=e(r.uploadOptionsBeforeSubmit)),r.hasOwnProperty("uploadOptionsEnableCsrf")&&(d.enableCsrf="false"!=r.uploadOptionsEnableCsrf),a.attr({target:"upload-iframe-"+l,method:"post",enctype:"multipart/form-data",encoding:"multipart/form-data"});var m=angular.element('');if(d.enableRailsCsrf){var g=angular.element("");g.attr("class","upload-csrf-token"),g.attr("type","hidden"),g.attr("name",o("csrf-param").attr("content")),g.val(o("csrf-token").attr("content")),a.append(g)}if(d.enableCsrf){var g=angular.element("");g.attr("class","upload-csrf-token"),g.attr("type","hidden"),g.attr("name",r.uploadOptionsCsrfParam||"CSRFToken"),g.val(i()),a.append(g)}a.after(m),p(!1),a.bind("submit",function(n){var e=t[r.name];return e&&e.$invalid?(n.preventDefault(),!1):d.beforeSubmit&&d.beforeSubmit(t,{})===!1?(t.$$phase||t.$apply(),n.preventDefault(),!1):(m.bind("load",u),d.convertHidden&&angular.forEach(a.find("input"),function(n){var e=angular.element(n);e.attr("ng-model")&&e.attr("type")&&"hidden"==e.attr("type")&&e.attr("value",t.$eval(e.attr("ng-model")))}),t.$$phase?(c&&c(t),p(!0)):t.$apply(function(){c&&c(t),p(!0)}),void 0)})}}}]); \ No newline at end of file diff --git a/readme.md b/readme.md index 33643a1..98da328 100644 --- a/readme.md +++ b/readme.md @@ -149,6 +149,10 @@ In order, for ngUpload to respond correctly for IE, your server needs to return * `upload-options-enable-rails-csrf`: Turns on support for [Rails' CSRF](http://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf) by adding a hidden form field with the csrf token. +* `upload-options-enable-csrf`: Turns on support for CSRF by adding a hidden form field with the csrf token fetched from cookies as configured in [$httpProvider.defaults.xsrfCookieName](https://docs.angularjs.org/api/ng/provider/$httpProvider). + +* `upload-options-csrf-param`: Name of the csrf parameter to be sent while submitting form (Default value: "CSRFToken"). Only usable with `upload-options-enable-csrf` option. + * `upload-options-before-submit`: function that gets triggered before the upload starts and if the function returns false it will cancel the submit. ### uploadSubmit diff --git a/test/ngUploadCsrfSpec.js b/test/ngUploadCsrfSpec.js new file mode 100644 index 0000000..4903e85 --- /dev/null +++ b/test/ngUploadCsrfSpec.js @@ -0,0 +1,71 @@ +// ngUpload as directive and input submit +describe('ngUpload', function() { + var scope, $compile, $http, $browser; + beforeEach(module('ngUpload')); + beforeEach(inject(function($rootScope, _$compile_, _$browser_, _$http_) { + $browser = _$browser_; + $browser.cookies('X-XSRF-TOKEN', 'the-token'); + scope = $rootScope; + $compile = _$compile_; + $http = _$http_; + })); + + function submit(element) { + element[0].getElementsByClassName('submit-button')[0].click(); + } + + function compile(template) { + var elm = angular.element(template); + $compile(elm)(scope); + return elm; + } + + function getHiddenField(element) { + return element[0].getElementsByClassName('upload-csrf-token')[0]; + } + + it('should set csrf hidden field with csrf token from cookie', function() { + var form = compile( + '' + + '' + + '' + + '' + + '' + + ''); + submit(form); + + var hiddenField = getHiddenField(form); + expect(hiddenField).toBeDefined(); + expect(hiddenField.getAttribute('value')).toBe('the-token'); + expect(hiddenField.getAttribute('name')).toBe('CSRFToken'); + }); + + it('should get csrf param name from a specific attribute', function() { + var form = compile( + '' + + '' + + '' + + '' + + '' + + ''); + submit(form); + + expect(getHiddenField(form).getAttribute('name')).toBe('csrf-token-parameter'); + }); + + it('should get csrf token from cookies according to configured xsrfCookieName', function() { + $http.defaults.xsrfCookieName = 'another.cookie.name'; + $browser.cookies('another.cookie.name', 'another-token'); + var form = compile( + '' + + '' + + '' + + '' + + '' + + ''); + submit(form); + + expect(getHiddenField(form).getAttribute('value')).toBe('another-token'); + }); + +}); \ No newline at end of file