From a16fc2c14b3f18ab0c725b992a77f767ad5c4d08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 2 Feb 2011 13:38:31 +0100 Subject: [PATCH 001/303] allow empty "data-remote" attribute, e.g.
--- src/rails.js | 6 +++--- test/public/test/call-remote.js | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/rails.js b/src/rails.js index 401a9847..cbb348b1 100644 --- a/src/rails.js +++ b/src/rails.js @@ -106,7 +106,7 @@ var link = $(this); if (!allowAction(link)) return false; - if (link.attr('data-remote')) { + if (link.attr('data-remote') != undefined) { handleRemote(link); return false; } else if (link.attr('data-method')) { @@ -116,13 +116,13 @@ }); $('form').live('submit.rails', function(e) { - var form = $(this); + var form = $(this), remote = form.attr('data-remote') != undefined; if (!allowAction(form)) return false; // skip other logic when required values are missing, but don't cancel the event if (requiredValuesMissing(form)) return; - if (form.attr('data-remote')) { + if (remote) { handleRemote(form); return false; } else { diff --git a/test/public/test/call-remote.js b/test/public/test/call-remote.js index a0d4b1ff..90fd0154 100644 --- a/test/public/test/call-remote.js +++ b/test/public/test/call-remote.js @@ -75,4 +75,12 @@ asyncTest('accept application/json if "data-type" is json', 1, function() { }); }); +asyncTest('allow empty "data-remote" attribute', 1, function() { + var form = $('#qunit-fixture').append($('')).find('form'); + + submit(function() { + ok(true, 'form with empty "data-remote" attribute is also allowed'); + }); +}); + })(); From 6c996cba7ce0b1834eee35564a3af7c7523179a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 2 Feb 2011 13:40:08 +0100 Subject: [PATCH 002/303] fix remote form not submitting when required values missing --- src/rails.js | 4 ++-- test/public/test/call-remote-callbacks.js | 3 +++ test/public/test/settings.js | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/rails.js b/src/rails.js index cbb348b1..4dcb3779 100644 --- a/src/rails.js +++ b/src/rails.js @@ -119,8 +119,8 @@ var form = $(this), remote = form.attr('data-remote') != undefined; if (!allowAction(form)) return false; - // skip other logic when required values are missing, but don't cancel the event - if (requiredValuesMissing(form)) return; + // skip other logic when required values are missing + if (requiredValuesMissing(form)) return !remote; if (remote) { handleRemote(form); diff --git a/test/public/test/call-remote-callbacks.js b/test/public/test/call-remote-callbacks.js index 69870867..f0c00117 100644 --- a/test/public/test/call-remote-callbacks.js +++ b/test/public/test/call-remote-callbacks.js @@ -46,6 +46,9 @@ asyncTest('blank required form input field should abort request', 1, function() .bind('ajax:beforeSend', function() { ok(false, 'ajax:beforeSend should not run'); }) + .bind('iframe:loading', function() { + ok(false, 'form should not get submitted'); + }) .trigger('submit'); setTimeout(function() { diff --git a/test/public/test/settings.js b/test/public/test/settings.js index b1126cab..616ebc68 100644 --- a/test/public/test/settings.js +++ b/test/public/test/settings.js @@ -27,6 +27,8 @@ App.assert_request_path = function(request_env, path) { equal(request_env['PATH_INFO'], path, 'request should be sent to right url'); }; +// hijacks normal form submit; lets it submit to an iframe to prevent +// navigating away from the test suite $(document).bind('submit', function(e) { if (!e.isDefaultPrevented()) { var form = $(e.target), action = form.attr('action'), @@ -36,5 +38,6 @@ $(document).bind('submit', function(e) { if (action.indexOf('iframe') < 0) form.attr('action', action + '?iframe=true') form.attr('target', name); $('#qunit-fixture').append(iframe); + $.event.trigger('iframe:loading', { form: form }); } -}) +}); From 58e20328f68517e1c7c54628de6ec35ce7bd916c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 2 Feb 2011 13:41:44 +0100 Subject: [PATCH 003/303] change "data-method" test to remove unnecessary teardown block --- test/public/test/data-method.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/test/public/test/data-method.js b/test/public/test/data-method.js index 5823bc4b..d7be7f8e 100644 --- a/test/public/test/data-method.js +++ b/test/public/test/data-method.js @@ -1,18 +1,16 @@ (function(){ -module('data-method', { - teardown: function() { $(document).unbind('iframe:loaded') } -}); +module('data-method'); function submit(fn) { - $(document).bind('iframe:loaded', function(e, data) { - fn(data); - start(); - }); - $('#qunit-fixture'). append($('', { href: '/echo', 'data-method': 'delete', text: 'destroy!' })) - .find('a').trigger('click'); + .find('a') + .bind('iframe:loaded', function(e, data) { + fn(data); + start(); + }) + .trigger('click'); } asyncTest('link with "data-method" set to "delete"', 2, function() { From 9f1d6623f314e70a734594aad7620cd135d6a212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 2 Feb 2011 13:43:23 +0100 Subject: [PATCH 004/303] fix jQuery UI note closes #79 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e3fba57b..e75848cb 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ For automated installation, use the "jquery-rails" generator: # Gemfile gem 'jquery-rails', '>= 0.2.6' -And run this command (add `-ui` if you want jQuery UI): +And run this command (add `--ui` if you want jQuery UI): $ bundle install $ rails generate jquery:install From 8b8375bbcd1695ba8297e398a68dd743cdcdaea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 2 Feb 2011 14:16:47 +0100 Subject: [PATCH 005/303] thoroughly test arguments to "ajax:*" event handlers closes #83 --- test/public/test/call-remote-callbacks.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/test/public/test/call-remote-callbacks.js b/test/public/test/call-remote-callbacks.js index f0c00117..d1ae79fa 100644 --- a/test/public/test/call-remote-callbacks.js +++ b/test/public/test/call-remote-callbacks.js @@ -74,19 +74,32 @@ asyncTest('"ajax:beforeSend" can be observed and stopped with event delegation', }); }); -asyncTest('"ajax:beforeSend", "ajax:success" and "ajax:complete" are triggered', 3, function() { +asyncTest('"ajax:beforeSend", "ajax:success" and "ajax:complete" are triggered', 8, function() { submit(function(form) { - form.bind('ajax:beforeSend', function(arg) { ok(true, 'ajax:beforeSend') }); - form.bind('ajax:success', function(arg) { ok(true, 'ajax:success') }); + form.bind('ajax:beforeSend', function(e, xhr, settings) { + equal(typeof xhr.getResponseHeader, 'function', 'first argument to "ajax:beforeSend" should be an XHR object'); + equal(settings.url, '/echo', 'second argument to "ajax:beforeSend" should be a settings object'); + }); + form.bind('ajax:success', function(e, data, status, xhr) { + ok(data.REQUEST_METHOD, 'first argument to ajax:success should be a data object'); + equal(status, 'success', 'second argument to ajax:success should be a status string'); + equal(typeof xhr.getResponseHeader, 'function', 'third argument to "ajax:success" should be an XHR object'); + }); + form.bind('ajax:complete', function(e, xhr, status) { + equal(typeof xhr.getResponseHeader, 'function', 'first argument to "ajax:complete" should be an XHR object'); + equal(status, 'success', 'second argument to ajax:complete should be a status string'); + }); }); }); -asyncTest('"ajax:beforeSend", "ajax:error" and "ajax:complete" are triggered on error', 4, function() { +asyncTest('"ajax:beforeSend", "ajax:error" and "ajax:complete" are triggered on error', 6, function() { submit(function(form) { form.attr('action', '/error'); form.bind('ajax:beforeSend', function(arg) { ok(true, 'ajax:beforeSend') }); form.bind('ajax:error', function(e, xhr, status, error) { - ok(true, 'ajax:error'); + equal(typeof xhr.getResponseHeader, 'function', 'first argument to "ajax:error" should be an XHR object'); + equal(status, 'error', 'second argument to ajax:error should be a status string'); + equal(error, 'Forbidden', 'third argument to ajax:error should be an HTTP status response'); // Opera returns "0" for HTTP code equal(xhr.status, window.opera ? 0 : 403, 'status code should be 403'); }); From e4fabbaa192c8a350b223d74b2ab68d890de3663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 9 Feb 2011 20:12:33 +0100 Subject: [PATCH 006/303] jQuery 1.4 doesn't supply a third argument to "ajax:error" --- test/public/test/call-remote-callbacks.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/public/test/call-remote-callbacks.js b/test/public/test/call-remote-callbacks.js index d1ae79fa..5dddf2bc 100644 --- a/test/public/test/call-remote-callbacks.js +++ b/test/public/test/call-remote-callbacks.js @@ -99,7 +99,8 @@ asyncTest('"ajax:beforeSend", "ajax:error" and "ajax:complete" are triggered on form.bind('ajax:error', function(e, xhr, status, error) { equal(typeof xhr.getResponseHeader, 'function', 'first argument to "ajax:error" should be an XHR object'); equal(status, 'error', 'second argument to ajax:error should be a status string'); - equal(error, 'Forbidden', 'third argument to ajax:error should be an HTTP status response'); + if (jQuery().jquery.indexOf('1.4') === 0) strictEqual(error, undefined) + else equal(error, 'Forbidden', 'third argument to ajax:error should be an HTTP status response'); // Opera returns "0" for HTTP code equal(xhr.status, window.opera ? 0 : 403, 'status code should be 403'); }); From e9311550fdb3afeb2917bcb1fef39767bf715003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 9 Feb 2011 20:15:13 +0100 Subject: [PATCH 007/303] send the "X-CSRF-Token" header with every Ajax request More info: http://weblog.rubyonrails.org/2011/2/8/csrf-protection-bypass-in-ruby-on-rails --- src/rails.js | 20 ++++++++++++++++++++ test/public/test/call-remote.js | 9 +++++++++ test/public/test/data-method.js | 3 ++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/rails.js b/src/rails.js index 4dcb3779..d862490f 100644 --- a/src/rails.js +++ b/src/rails.js @@ -6,6 +6,26 @@ */ (function($) { + // Make sure that every Ajax request sends the CSRF token + function CSRFProtection(fn) { + var token = $('meta[name="csrf-token"]').attr('content'); + if (token) fn(function(xhr) { xhr.setRequestHeader('X-CSRF-Token', token) }); + } + if ($().jquery == '1.5') { // gruesome hack + var factory = $.ajaxSettings.xhr; + $.ajaxSettings.xhr = function() { + var xhr = factory(); + CSRFProtection(function(setHeader) { + var open = xhr.open; + xhr.open = function() { open.apply(this, arguments); setHeader(this) }; + }); + return xhr; + }; + } + else $(document).ajaxSend(function(e, xhr) { + CSRFProtection(function(setHeader) { setHeader(xhr) }); + }); + // Triggers an event on an element and returns the event result function fire(obj, name, data) { var event = new $.Event(name); diff --git a/test/public/test/call-remote.js b/test/public/test/call-remote.js index 90fd0154..8e342aa2 100644 --- a/test/public/test/call-remote.js +++ b/test/public/test/call-remote.js @@ -83,4 +83,13 @@ asyncTest('allow empty "data-remote" attribute', 1, function() { }); }); +asyncTest('sends CSRF token in custom header', 1, function() { + build_form({ method: 'post' }); + $('#qunit-fixture').append(''); + + submit(function(e, data, status, xhr) { + equal(data.HTTP_X_CSRF_TOKEN, 'cf50faa3fe97702ca1ae', 'X-CSRF-Token header should be sent'); + }); +}); + })(); diff --git a/test/public/test/data-method.js b/test/public/test/data-method.js index d7be7f8e..34c282e0 100644 --- a/test/public/test/data-method.js +++ b/test/public/test/data-method.js @@ -13,10 +13,11 @@ function submit(fn) { .trigger('click'); } -asyncTest('link with "data-method" set to "delete"', 2, function() { +asyncTest('link with "data-method" set to "delete"', 3, function() { submit(function(data) { equal(data.REQUEST_METHOD, 'DELETE'); strictEqual(data.params.authenticity_token, undefined); + strictEqual(data.HTTP_X_CSRF_TOKEN, undefined); }); }); From 764e4e45600ec48b312b4af4d417088384e6cdce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 10 Feb 2011 13:36:26 +0100 Subject: [PATCH 008/303] fix some of the tests for IE7 --- test/public/test/call-remote-callbacks.js | 8 ++++---- test/public/test/call-remote.js | 2 +- test/public/test/settings.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/public/test/call-remote-callbacks.js b/test/public/test/call-remote-callbacks.js index 5dddf2bc..b078b8f6 100644 --- a/test/public/test/call-remote-callbacks.js +++ b/test/public/test/call-remote-callbacks.js @@ -77,16 +77,16 @@ asyncTest('"ajax:beforeSend" can be observed and stopped with event delegation', asyncTest('"ajax:beforeSend", "ajax:success" and "ajax:complete" are triggered', 8, function() { submit(function(form) { form.bind('ajax:beforeSend', function(e, xhr, settings) { - equal(typeof xhr.getResponseHeader, 'function', 'first argument to "ajax:beforeSend" should be an XHR object'); + ok(xhr.setRequestHeader, 'first argument to "ajax:beforeSend" should be an XHR object'); equal(settings.url, '/echo', 'second argument to "ajax:beforeSend" should be a settings object'); }); form.bind('ajax:success', function(e, data, status, xhr) { ok(data.REQUEST_METHOD, 'first argument to ajax:success should be a data object'); equal(status, 'success', 'second argument to ajax:success should be a status string'); - equal(typeof xhr.getResponseHeader, 'function', 'third argument to "ajax:success" should be an XHR object'); + ok(xhr.getResponseHeader, 'third argument to "ajax:success" should be an XHR object'); }); form.bind('ajax:complete', function(e, xhr, status) { - equal(typeof xhr.getResponseHeader, 'function', 'first argument to "ajax:complete" should be an XHR object'); + ok(xhr.getResponseHeader, 'first argument to "ajax:complete" should be an XHR object'); equal(status, 'success', 'second argument to ajax:complete should be a status string'); }); }); @@ -97,7 +97,7 @@ asyncTest('"ajax:beforeSend", "ajax:error" and "ajax:complete" are triggered on form.attr('action', '/error'); form.bind('ajax:beforeSend', function(arg) { ok(true, 'ajax:beforeSend') }); form.bind('ajax:error', function(e, xhr, status, error) { - equal(typeof xhr.getResponseHeader, 'function', 'first argument to "ajax:error" should be an XHR object'); + ok(xhr.getResponseHeader, 'first argument to "ajax:error" should be an XHR object'); equal(status, 'error', 'second argument to ajax:error should be a status string'); if (jQuery().jquery.indexOf('1.4') === 0) strictEqual(error, undefined) else equal(error, 'Forbidden', 'third argument to ajax:error should be an HTTP status response'); diff --git a/test/public/test/call-remote.js b/test/public/test/call-remote.js index 8e342aa2..ce73a260 100644 --- a/test/public/test/call-remote.js +++ b/test/public/test/call-remote.js @@ -10,7 +10,7 @@ function build_form(attrs) { module('call-remote'); function submit(fn) { - $('form[data-remote]') + $('form') .bind('ajax:success', fn) .bind('ajax:complete', function() { start() }) .trigger('submit'); diff --git a/test/public/test/settings.js b/test/public/test/settings.js index 616ebc68..2961dfb7 100644 --- a/test/public/test/settings.js +++ b/test/public/test/settings.js @@ -33,7 +33,7 @@ $(document).bind('submit', function(e) { if (!e.isDefaultPrevented()) { var form = $(e.target), action = form.attr('action'), name = 'form-frame' + jQuery.guid++, - iframe = $('