vidibus-xss 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +21 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +29 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/app/controllers/xss_controller.rb +7 -0
- data/config/routes.rb +3 -0
- data/lib/vidibus/xss/extensions/controller.rb +295 -0
- data/lib/vidibus/xss/extensions/string.rb +22 -0
- data/lib/vidibus/xss/extensions/view.rb +15 -0
- data/lib/vidibus/xss/extensions.rb +9 -0
- data/lib/vidibus/xss/mime_type.rb +1 -0
- data/lib/vidibus/xss.rb +2 -0
- data/lib/vidibus-xss.rb +7 -0
- data/public/javascripts/jquery.ba-bbq.js +1137 -0
- data/public/javascripts/vidibus.js +193 -0
- data/public/javascripts/vidibus.xss.js +335 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +11 -0
- data/vidibus-xss.gemspec +77 -0
- metadata +174 -0
@@ -0,0 +1,193 @@
|
|
1
|
+
// if (typeof console == undefined) {
|
2
|
+
// console = {};
|
3
|
+
// }
|
4
|
+
|
5
|
+
|
6
|
+
var vidibus = {};
|
7
|
+
|
8
|
+
// Basic loader for stylesheets and javascripts.
|
9
|
+
vidibus.loader = {
|
10
|
+
|
11
|
+
complete: true, // indicates that loading has been finished
|
12
|
+
queue: [], // holds resources that are queued to load
|
13
|
+
loading: undefined, // holds resource that is currently being loaded
|
14
|
+
preloaded: undefined, // holds resources that are included in consumer base file
|
15
|
+
loaded: {}, // holds resources that are currently loaded
|
16
|
+
unused: {}, // holds resources that are loaded, but not required anymore
|
17
|
+
|
18
|
+
/**
|
19
|
+
* Load resources.
|
20
|
+
*/
|
21
|
+
load: function(resources, scope) {
|
22
|
+
this.initStaticResources();
|
23
|
+
this.complete = false;
|
24
|
+
this.unused = jQuery.extend({}, this.loaded); // clone
|
25
|
+
|
26
|
+
$(resources).each(function() {
|
27
|
+
var resource = this,
|
28
|
+
src = resource.src,
|
29
|
+
name = vidibus.loader.resourceName(src);
|
30
|
+
|
31
|
+
resource.name = name;
|
32
|
+
resource.scopes = {}
|
33
|
+
resource.scopes[scope] = true;
|
34
|
+
|
35
|
+
// remove current file, because it is used
|
36
|
+
delete vidibus.loader.unused[name];
|
37
|
+
|
38
|
+
// skip files that have already been loaded
|
39
|
+
if (vidibus.loader.loaded[name]) {
|
40
|
+
vidibus.loader.loaded[name].scopes[scope] = true; // add current scope
|
41
|
+
return; // continue
|
42
|
+
} else if (vidibus.loader.preloaded[name]) {
|
43
|
+
return; // continue
|
44
|
+
}
|
45
|
+
|
46
|
+
vidibus.loader.loaded[name] = resource;
|
47
|
+
switch (resource.type) {
|
48
|
+
|
49
|
+
// load css file directly
|
50
|
+
case 'text/css':
|
51
|
+
var element = document.createElement("link");
|
52
|
+
element.rel = 'stylesheet';
|
53
|
+
element.href = src;
|
54
|
+
element.media = resource.media || 'all';
|
55
|
+
element.type = 'text/css';
|
56
|
+
vidibus.loader.appendToHead(element);
|
57
|
+
break;
|
58
|
+
|
59
|
+
// push script file to loading queue
|
60
|
+
case 'text/javascript':
|
61
|
+
vidibus.loader.queue.push(resource);
|
62
|
+
break;
|
63
|
+
|
64
|
+
default: console.log('vidibus.loader.load: unsupported resource type: '+resource.type);
|
65
|
+
}
|
66
|
+
});
|
67
|
+
|
68
|
+
this.loadQueue(true);
|
69
|
+
this.unloadUnused(scope);
|
70
|
+
},
|
71
|
+
|
72
|
+
/**
|
73
|
+
* Returns file name of resource.
|
74
|
+
*/
|
75
|
+
resourceName: function(url) {
|
76
|
+
return url.match(/\/([^\/\?]+)(\?.*)*$/)[1];
|
77
|
+
},
|
78
|
+
|
79
|
+
/**
|
80
|
+
* Returns list of static resources.
|
81
|
+
*/
|
82
|
+
initStaticResources: function() {
|
83
|
+
if (vidibus.loader.preloaded == undefined) {
|
84
|
+
vidibus.loader.preloaded = {};
|
85
|
+
var $resource, src, name;
|
86
|
+
$('script[src],link[href]',$('head')).each(function() {
|
87
|
+
$resource = $(this);
|
88
|
+
src = $resource.attr('src') || $resource.attr('href');
|
89
|
+
name = vidibus.loader.resourceName(src);
|
90
|
+
vidibus.loader.preloaded[name] = src;
|
91
|
+
});
|
92
|
+
}
|
93
|
+
},
|
94
|
+
|
95
|
+
/**
|
96
|
+
* Loads resources in queue.
|
97
|
+
*/
|
98
|
+
loadQueue: function(start) {
|
99
|
+
|
100
|
+
// Reduce queue if this method is called as callback.
|
101
|
+
if(start != true) {
|
102
|
+
vidibus.loader.queue.shift();
|
103
|
+
}
|
104
|
+
|
105
|
+
var resource = vidibus.loader.queue[0];
|
106
|
+
|
107
|
+
// return if file is currently loading
|
108
|
+
if (resource) {
|
109
|
+
if (resource == vidibus.loader.loading) {
|
110
|
+
// console.log('CURRENTLY LOADING: '+resource.src);
|
111
|
+
return;
|
112
|
+
}
|
113
|
+
vidibus.loader.loading = resource;
|
114
|
+
vidibus.loader.loadScript(resource.src, vidibus.loader.loadQueue);
|
115
|
+
} else {
|
116
|
+
vidibus.loader.loading = undefined;
|
117
|
+
vidibus.loader.complete = true;
|
118
|
+
}
|
119
|
+
},
|
120
|
+
|
121
|
+
/**
|
122
|
+
* Loads script src.
|
123
|
+
*/
|
124
|
+
loadScript: function(src, callback) {
|
125
|
+
var element = document.createElement("script");
|
126
|
+
if (element.addEventListener) {
|
127
|
+
element.addEventListener("load", callback, false);
|
128
|
+
} else {
|
129
|
+
// IE
|
130
|
+
element.onreadystatechange = function() {
|
131
|
+
if (this.readyState == 'loaded') callback.call(this);
|
132
|
+
}
|
133
|
+
}
|
134
|
+
element.type = 'text/javascript';
|
135
|
+
element.src = src;
|
136
|
+
vidibus.loader.appendToHead(element);
|
137
|
+
element = null;
|
138
|
+
},
|
139
|
+
|
140
|
+
/**
|
141
|
+
* Detects unused resources and removes them.
|
142
|
+
*/
|
143
|
+
unloadUnused: function(scope) {
|
144
|
+
var name, resources = [];
|
145
|
+
for(name in vidibus.loader.unused) {
|
146
|
+
|
147
|
+
// Remove dependency for given scope.
|
148
|
+
if (vidibus.loader.unused[name].scopes[scope]) {
|
149
|
+
delete vidibus.loader.unused[name].scopes[scope];
|
150
|
+
}
|
151
|
+
|
152
|
+
// Unload resource if it has no dependencies left.
|
153
|
+
if ($.isEmptyObject(vidibus.loader.unused[name].scopes)) {
|
154
|
+
resources.push(vidibus.loader.unused[name]);
|
155
|
+
}
|
156
|
+
}
|
157
|
+
vidibus.loader.unload(resources);
|
158
|
+
vidibus.loader.unused = {}
|
159
|
+
},
|
160
|
+
|
161
|
+
/**
|
162
|
+
* Removes resources given in list.
|
163
|
+
*/
|
164
|
+
unload: function(resources) {
|
165
|
+
var src, data, resource;
|
166
|
+
$(resources).each(function() {
|
167
|
+
resource = this;
|
168
|
+
src = resource.src;
|
169
|
+
|
170
|
+
// console.log('REMOVE UNUSED RESOURCE: '+src);
|
171
|
+
|
172
|
+
switch (resource.type) {
|
173
|
+
case "text/css":
|
174
|
+
$('link[href="'+src+'"]').remove();
|
175
|
+
break;
|
176
|
+
|
177
|
+
case "text/javascript":
|
178
|
+
$('script[src="'+src+'"]').remove();
|
179
|
+
break;
|
180
|
+
|
181
|
+
default: console.log('vidibus.loader.unload: unsupported resource type: '+resource.type);
|
182
|
+
}
|
183
|
+
delete vidibus.loader.loaded[resource.name];
|
184
|
+
});
|
185
|
+
},
|
186
|
+
|
187
|
+
/**
|
188
|
+
* Appends given element to document head.
|
189
|
+
*/
|
190
|
+
appendToHead: function(element) {
|
191
|
+
document.getElementsByTagName("head")[0].appendChild(element);
|
192
|
+
}
|
193
|
+
};
|
@@ -0,0 +1,335 @@
|
|
1
|
+
// TODO: Support cross-domain AJAX requests in IE:
|
2
|
+
// if ($.browser.msie && window.XDomainRequest) {
|
3
|
+
// // Use Microsoft XDR
|
4
|
+
// var xdr = new XDomainRequest();
|
5
|
+
// xdr.open('get', url);
|
6
|
+
// xdr.onload = function() {
|
7
|
+
// // XDomainRequest doesn't provide responseXml, so if you need it:
|
8
|
+
// var dom = new ActiveXObject('Microsoft.XMLDOM');
|
9
|
+
// dom.async = false;
|
10
|
+
// dom.loadXML(xdr.responseText);
|
11
|
+
// };
|
12
|
+
// xdr.send();
|
13
|
+
// } else {
|
14
|
+
// $.ajax({...});
|
15
|
+
// }
|
16
|
+
|
17
|
+
|
18
|
+
vidibus.xss = {
|
19
|
+
initialized: {}, // holds true for every scope that has been initialized
|
20
|
+
fileExtension: 'xss', // use 'xss' as file extension
|
21
|
+
loadedUrls: {}, // store urls currently loaded in each scope
|
22
|
+
|
23
|
+
/**
|
24
|
+
* Detects scope of script block to be executed.
|
25
|
+
* Must be called from embedding page.
|
26
|
+
*/
|
27
|
+
detectScope: function() {
|
28
|
+
document.write('<div id="scopeDetector"></div>');
|
29
|
+
var $detector = $("#scopeDetector");
|
30
|
+
var $scope = $detector.parent();
|
31
|
+
$detector.remove();
|
32
|
+
return $scope;
|
33
|
+
},
|
34
|
+
|
35
|
+
/**
|
36
|
+
* Usage:
|
37
|
+
* vidibus.xss.embed('<div>Some HTML</div>', $('#scope'), 'http://host.url/');
|
38
|
+
*/
|
39
|
+
embed: function(html, $scope, host) {
|
40
|
+
html = this.transformPaths(html, $scope); // Transform local paths before embedding html into page!
|
41
|
+
$scope.html(html);
|
42
|
+
this.setUrls($scope);
|
43
|
+
this.setActions($scope);
|
44
|
+
},
|
45
|
+
|
46
|
+
/**
|
47
|
+
* Calls given path for given scope. If a third parameter is set to true,
|
48
|
+
* the location will be loaded, even if it is currently loaded.
|
49
|
+
*
|
50
|
+
* Usage:
|
51
|
+
* vidibus.xss.get('path', $('#scope') [, true]);
|
52
|
+
*
|
53
|
+
* If no host is provided, host of scope will be used.
|
54
|
+
*/
|
55
|
+
get: function(path, $scope, reload) {
|
56
|
+
// Escape query parts
|
57
|
+
path = path.replace("?", "%3F").replace("&", "%26").replace("=", "%3D");
|
58
|
+
|
59
|
+
var scopeId = $scope[0].id,
|
60
|
+
params = scopeId+'='+this.getPath(path),
|
61
|
+
keepCurrentLocation = this.initialized[scopeId] ? 0 : 1,
|
62
|
+
location = $.param.fragment(String(document.location), params, keepCurrentLocation),
|
63
|
+
reloadScope = {};
|
64
|
+
|
65
|
+
this.initialized[scopeId] = true;
|
66
|
+
window.location.href = location; // write history
|
67
|
+
reloadScope[scopeId] = reload;
|
68
|
+
this.loadUrl(location, reloadScope);
|
69
|
+
},
|
70
|
+
|
71
|
+
/**
|
72
|
+
* Redirect to given path while forcing reloading.
|
73
|
+
*/
|
74
|
+
redirect: function(path, $scope) {
|
75
|
+
this.get(path, $scope, true)
|
76
|
+
},
|
77
|
+
|
78
|
+
/**
|
79
|
+
* Handles callback action.
|
80
|
+
*
|
81
|
+
* Requires data.status:
|
82
|
+
* redirect, TODO: ok, error
|
83
|
+
*
|
84
|
+
* Accepts actions depending on status:
|
85
|
+
* (redirect) to: Performs redirect to location.
|
86
|
+
*/
|
87
|
+
callback: function(data, $scope) {
|
88
|
+
if (data.status == 'redirect') {
|
89
|
+
this.redirect(data.to, $scope);
|
90
|
+
}
|
91
|
+
},
|
92
|
+
|
93
|
+
/**
|
94
|
+
* Sets host for given scope.
|
95
|
+
*/
|
96
|
+
setHost: function(host, $scope) {
|
97
|
+
$scope.attr('data-host', host);
|
98
|
+
},
|
99
|
+
|
100
|
+
/**
|
101
|
+
* Returns host for given scope.
|
102
|
+
*/
|
103
|
+
getHost: function($scope) {
|
104
|
+
return $scope.attr('data-host');
|
105
|
+
},
|
106
|
+
|
107
|
+
/**
|
108
|
+
* Turns given path into an absolute url.
|
109
|
+
*/
|
110
|
+
getUrl: function(path, host) {
|
111
|
+
if (path.match(/https?:\/\//)) { return path }
|
112
|
+
return host + path;
|
113
|
+
},
|
114
|
+
|
115
|
+
/**
|
116
|
+
* Returns relative path from url.
|
117
|
+
*/
|
118
|
+
getPath: function(url) {
|
119
|
+
return url.replace(/https?:\/\/[^\/]+/,'')
|
120
|
+
},
|
121
|
+
|
122
|
+
/**
|
123
|
+
* Rewrites links to absolute urls.
|
124
|
+
*/
|
125
|
+
setUrls: function($scope) {
|
126
|
+
var host = this.getHost($scope);
|
127
|
+
|
128
|
+
// Rewrite links
|
129
|
+
$('a[href]:not([href^=http])', $scope).each(function(e) {
|
130
|
+
var href = $(this).attr('href');
|
131
|
+
$(this).attr('href', vidibus.xss.getUrl(href, host));
|
132
|
+
});
|
133
|
+
|
134
|
+
// Rewrite forms
|
135
|
+
$('form[action]', $scope).each(function(e) {
|
136
|
+
var action = $(this).attr('action');
|
137
|
+
$(this).attr('href', vidibus.xss.getUrl(action, host));
|
138
|
+
});
|
139
|
+
},
|
140
|
+
|
141
|
+
/**
|
142
|
+
* Set xss actions for interactive elements.
|
143
|
+
*/
|
144
|
+
setActions: function($scope) {
|
145
|
+
var host = this.getHost($scope);
|
146
|
+
|
147
|
+
// Set action for GET links
|
148
|
+
// TODO: Allow links to be flagged as "external"
|
149
|
+
$('a[href^='+host+']:not([data-method],[data-remote])', $scope).bind('click.xss', function(e) {
|
150
|
+
var href = $(this).attr('href');
|
151
|
+
vidibus.xss.get(href, $scope);
|
152
|
+
e.preventDefault();
|
153
|
+
});
|
154
|
+
|
155
|
+
// Set action non-GET links
|
156
|
+
// TODO: Remove bindings from links that match current host only: a[data-method][href^='+host+']:not([data-remote])
|
157
|
+
$('a[data-method]:not([data-remote])').die('click').unbind('click'); // remove bindings
|
158
|
+
$('a[data-method][href^='+host+']:not([data-remote])', $scope).click(function(e) {
|
159
|
+
var $link = $(this),
|
160
|
+
path = $link.attr('href'),
|
161
|
+
url = vidibus.xss.buildUrl(path, $scope);
|
162
|
+
$(this).callAjax(url);
|
163
|
+
e.preventDefault();
|
164
|
+
});
|
165
|
+
|
166
|
+
// Set form action
|
167
|
+
$('form[action]').die('submit').unbind('submit') // remove bindings
|
168
|
+
$('form[action][href^='+host+']', $scope).submit(function(e) {
|
169
|
+
var $form = $(this),
|
170
|
+
path = $form.attr('action');
|
171
|
+
url = vidibus.xss.buildUrl(path, $scope);
|
172
|
+
$(this).callAjax(url);
|
173
|
+
return false;
|
174
|
+
});
|
175
|
+
},
|
176
|
+
|
177
|
+
/**
|
178
|
+
* Modifies paths within given html string.
|
179
|
+
* This method must be called before embedding html snippet into the page
|
180
|
+
* to avoid loading errors from invalid relative paths.
|
181
|
+
*/
|
182
|
+
transformPaths: function(html, $scope) {
|
183
|
+
var match, url;
|
184
|
+
while (match = html.match(/src="((?!http)[^"]+)"/)) {
|
185
|
+
url = vidibus.xss.buildUrl(match[1], $scope);
|
186
|
+
html = html.replace(match[0], 'src="'+url+'"')
|
187
|
+
}
|
188
|
+
return html;
|
189
|
+
},
|
190
|
+
|
191
|
+
/**
|
192
|
+
* Load XSS sources from given url.
|
193
|
+
* If url is empty, the current location will be used.
|
194
|
+
*/
|
195
|
+
loadUrl: function(url, reload) {
|
196
|
+
var scope, $scope, path, loaded, params = $.deparam.fragment();
|
197
|
+
for(scope in params) {
|
198
|
+
path = params[scope];
|
199
|
+
|
200
|
+
// don't reload locations that have already been loaded
|
201
|
+
loaded = this.loadedUrls[scope];
|
202
|
+
if((!reload || !reload[scope]) && loaded && loaded == path) {
|
203
|
+
continue;
|
204
|
+
}
|
205
|
+
|
206
|
+
$scope = $('#'+scope);
|
207
|
+
if($scope[0] == undefined) {
|
208
|
+
console.log('Scope not found: '+scope);
|
209
|
+
} else {
|
210
|
+
this.loadedUrls[scope] = path;
|
211
|
+
this.loadData(path, $scope);
|
212
|
+
}
|
213
|
+
}
|
214
|
+
},
|
215
|
+
|
216
|
+
/**
|
217
|
+
* Load relative path into given scope.
|
218
|
+
* Transforms scope host and path into a XSS location.
|
219
|
+
*/
|
220
|
+
loadData: function(path, $scope) {
|
221
|
+
var url = this.buildUrl(path, $scope, true);
|
222
|
+
$.ajax({
|
223
|
+
url: url,
|
224
|
+
data: [],
|
225
|
+
dataType: 'script',
|
226
|
+
type: 'GET'
|
227
|
+
});
|
228
|
+
},
|
229
|
+
|
230
|
+
/**
|
231
|
+
* Tranforms local paths to absolute ones.
|
232
|
+
*/
|
233
|
+
buildUrl: function(path, $scope, uncached) {
|
234
|
+
var parts = path.split("?");
|
235
|
+
path = parts[0];
|
236
|
+
params = parts[1];
|
237
|
+
if(params) {
|
238
|
+
params = params.split("&");
|
239
|
+
} else {
|
240
|
+
params = []
|
241
|
+
}
|
242
|
+
|
243
|
+
var host = this.getHost($scope),
|
244
|
+
scope = $scope.attr('id'),
|
245
|
+
url = this.getUrl(path, host)
|
246
|
+
|
247
|
+
if (!url.match(/\.[a-z]+((\?|\#).*)?$/)) url += '.xss';
|
248
|
+
if (url.indexOf('.xss') > -1 && params.toString().indexOf('scope='+scope) == -1) {
|
249
|
+
params.push('scope='+scope);
|
250
|
+
}
|
251
|
+
|
252
|
+
// add cache buster
|
253
|
+
if (uncached) {
|
254
|
+
var d = new Date();
|
255
|
+
params.push(d.getTime());
|
256
|
+
}
|
257
|
+
|
258
|
+
// append params
|
259
|
+
if (params.length) {
|
260
|
+
if (url.indexOf('?') == -1) url += '?'
|
261
|
+
url += params.join('&');
|
262
|
+
}
|
263
|
+
|
264
|
+
return url;
|
265
|
+
}
|
266
|
+
};
|
267
|
+
|
268
|
+
/**
|
269
|
+
* Detect changes to document's location.
|
270
|
+
*
|
271
|
+
*/
|
272
|
+
$(function($){
|
273
|
+
|
274
|
+
// Detect changes of document.location and trigger loading.
|
275
|
+
$(window).bind('hashchange', function(e) {
|
276
|
+
if (vidibus.loader.complete) {
|
277
|
+
vidibus.xss.loadUrl();
|
278
|
+
}
|
279
|
+
return false;
|
280
|
+
});
|
281
|
+
|
282
|
+
// Since the event is only triggered when the hash changes, we need
|
283
|
+
// to trigger the event now, to handle the hash the page may have
|
284
|
+
// loaded with.
|
285
|
+
$(window).trigger('hashchange');
|
286
|
+
});
|
287
|
+
|
288
|
+
/**
|
289
|
+
* Implement ajax handler.
|
290
|
+
* This is the default handler provided in rails.js extended to accept delete method.
|
291
|
+
*/
|
292
|
+
$(function($) {
|
293
|
+
$.fn.extend({
|
294
|
+
|
295
|
+
/**
|
296
|
+
* Handles execution of remote calls firing overridable events along the way.
|
297
|
+
*/
|
298
|
+
callAjax: function(url) {
|
299
|
+
var el = this,
|
300
|
+
method = el.attr('method') || el.attr('data-method') || 'GET',
|
301
|
+
dataType = el.attr('data-type') || 'script';
|
302
|
+
if (!url) url = el.attr('action') || el.attr('href');
|
303
|
+
if (url === undefined) {
|
304
|
+
throw "No URL specified for remote call (action or href must be present).";
|
305
|
+
} else {
|
306
|
+
if (el.triggerAndReturn('ajax:before')) {
|
307
|
+
var data = el.is('form') ? el.serializeArray() : {};
|
308
|
+
if (method == 'delete') {
|
309
|
+
data['_method'] = method;
|
310
|
+
method = 'POST';
|
311
|
+
}
|
312
|
+
$.ajax({
|
313
|
+
url: url,
|
314
|
+
data: data,
|
315
|
+
dataType: dataType,
|
316
|
+
type: method.toUpperCase(),
|
317
|
+
beforeSend: function(xhr) {
|
318
|
+
el.trigger('ajax:loading', xhr);
|
319
|
+
},
|
320
|
+
success: function(data, status, xhr) {
|
321
|
+
el.trigger('ajax:success', [data, status, xhr]);
|
322
|
+
},
|
323
|
+
complete: function(xhr) {
|
324
|
+
el.trigger('ajax:complete', xhr);
|
325
|
+
},
|
326
|
+
error: function(xhr, status, error) {
|
327
|
+
el.trigger('ajax:failure', [xhr, status, error]);
|
328
|
+
}
|
329
|
+
});
|
330
|
+
}
|
331
|
+
el.trigger('ajax:after');
|
332
|
+
}
|
333
|
+
}
|
334
|
+
});
|
335
|
+
});
|