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.
@@ -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
+ });