├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── bower.json
├── package.json
├── test
├── spec
│ ├── xdm.noConflict.spec.js
│ ├── xdm.checkAcl.spec.js
│ ├── xdm.spec.js
│ └── xdm.Rpc.spec.js
└── fixture
│ └── guest.html
├── .jshintrc
├── LICENSE.md
├── karma.conf.js
├── README.md
└── xdm.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | components
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.10"
4 | - "0.8"
5 | before_script:
6 | - export DISPLAY=:99.0
7 | - sh -e /etc/init.d/xvfb start
8 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | === HEAD
2 |
3 | === 1.0.1 (October 11, 2013)
4 |
5 | * Remove loadrunner export.
6 | * Update to Karma 0.10.x.
7 | * Unit tests for `noConflict`.
8 | * Unit tests for `checkAcl`.
9 | * Exit early if no `postMessage` support.
10 |
11 | === 1.0.0 (June 24, 2013)
12 |
13 | * Initial release.
14 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "xdm.js",
3 | "description": "JSON-RPC 2.0 cross-domain messaging over postMessage",
4 | "version": "1.0.1",
5 | "main": "xdm.js",
6 | "ignore": [
7 | "test",
8 | ".*",
9 | "CHANGELOG.md",
10 | "karma.conf.js",
11 | "package.json"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "xdm.js",
3 | "version": "1.0.1",
4 | "devDependencies": {
5 | "karma": "~0.10.1",
6 | "karma-jasmine": "~0.1.0",
7 | "karma-chrome-launcher": "~0.1.0",
8 | "karma-ie-launcher": "~0.1.1",
9 | "karma-firefox-launcher": "~0.1.0",
10 | "karma-phantomjs-launcher": "~0.1.0",
11 | "karma-safari-launcher": "~0.1.1"
12 | },
13 | "scripts": {
14 | "test": "./node_modules/.bin/karma start --browsers Firefox --single-run"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/test/spec/xdm.noConflict.spec.js:
--------------------------------------------------------------------------------
1 | describe('xdm.noConflict', function () {
2 | 'use strict';
3 |
4 | var xdmCache = window.xdm;
5 | var namespace = null;
6 | var i = 0;
7 | var expectedMessage = ++i + '_abcd1234%@¤/';
8 |
9 | beforeEach(function () {
10 | namespace = {
11 | xdm: window.xdm.noConflict('modules')
12 | };
13 | });
14 |
15 | afterEach(function () {
16 | window.xdm = xdmCache;
17 | namespace = null;
18 | });
19 |
20 | it('releases window.xdm', function () {
21 | expect(window.xdm).toBeUndefined();
22 | expect(namespace.xdm).toBe(xdmCache);
23 | });
24 | });
25 |
26 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "browser": true,
3 | "esnext": true,
4 | "bitwise": false,
5 | "curly": false,
6 | "eqeqeq": true,
7 | "eqnull": true,
8 | "immed": true,
9 | "latedef": false,
10 | "laxcomma": true,
11 | "newcap": true,
12 | "noarg": true,
13 | "undef": true,
14 | "strict": true,
15 | "trailing": true,
16 | "smarttabs": true,
17 | "white": true,
18 | "indent": 4,
19 | "predef": [
20 | "describe",
21 | "it",
22 | "expect",
23 | "runs",
24 | "waitsFor",
25 | "beforeEach",
26 | "afterEach",
27 | "module",
28 | "provide",
29 | "define"
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/test/spec/xdm.checkAcl.spec.js:
--------------------------------------------------------------------------------
1 | describe('xdm.checkAcl', function () {
2 | 'use strict';
3 |
4 | var acl = [
5 | 'http://www.domain.invalid',
6 | '*.domaina.com',
7 | 'http://dom?inb.com',
8 | '^http://domc{3}ain\\.com$'
9 | ];
10 |
11 | it('matches a complete string', function () {
12 | expect(xdm.checkAcl(acl, 'http://www.domain.invalid')).toBe(true);
13 | });
14 |
15 | it('matches *', function () {
16 | expect(xdm.checkAcl(acl, 'http://www.domaina.com')).toBe(true);
17 | });
18 |
19 | it('matches ?', function () {
20 | expect(xdm.checkAcl(acl, 'http://domainb.com')).toBe(true);
21 | });
22 |
23 | it('matches RegExp', function () {
24 | expect(xdm.checkAcl(acl, 'http://domcccain.com')).toBe(true);
25 | });
26 |
27 | it('does not match', function () {
28 | expect(xdm.checkAcl(acl, 'http://foo.com')).toBe(false);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/test/spec/xdm.spec.js:
--------------------------------------------------------------------------------
1 | describe('the xdm object', function () {
2 | 'use strict';
3 |
4 | it('is defined', function () {
5 | expect(xdm).toBeDefined();
6 | });
7 |
8 | it('exposes xdm.query', function () {
9 | expect(xdm.query).toBeDefined();
10 | });
11 |
12 | it('exposes xdm.version', function () {
13 | expect(xdm.version).toBeDefined();
14 | });
15 |
16 | it('exposes xdm.checkAcl', function () {
17 | expect(xdm.checkAcl).toBeDefined();
18 | });
19 |
20 | it('exposes xdm.stack', function () {
21 | expect(xdm.stack).toBeDefined();
22 | });
23 |
24 | it('exposes xdm.stack.PostMessageTransport', function () {
25 | expect(xdm.stack.PostMessageTransport).toBeDefined();
26 | });
27 |
28 | it('exposes xdm.stack.QueueBehavior', function () {
29 | expect(xdm.stack.QueueBehavior).toBeDefined();
30 | });
31 |
32 | it('exposes xdm.stack.RpcBehavior', function () {
33 | expect(xdm.stack.RpcBehavior).toBeDefined();
34 | });
35 |
36 | it('exposes xdm.Rpc', function () {
37 | expect(xdm.Rpc).toBeDefined();
38 | });
39 | });
40 |
41 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) Nicolas Gallagher
2 | Copyright (c) 2009-2011 Øyvind Sean Kinsey, oyvind@kinsey.no
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of
5 | this software and associated documentation files (the "Software"), to deal in
6 | the Software without restriction, including without limitation the rights to
7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
8 | of the Software, and to permit persons to whom the Software is furnished to do
9 | so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 |
--------------------------------------------------------------------------------
/test/fixture/guest.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Guest window
5 |
6 |
7 |
8 | Guest window
9 |
10 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file
2 | //
3 | // For all available config options and default values, see:
4 | // https://github.com/karma-runner/karma/blob/stable/lib/config.js#L54
5 |
6 | module.exports = function (config) {
7 | 'use strict';
8 |
9 | config.set({
10 | // base path, that will be used to resolve files and exclude
11 | basePath: '',
12 |
13 | frameworks: [
14 | 'jasmine'
15 | ],
16 |
17 | // list of files / patterns to load in the browser
18 |
19 | files: [
20 | 'xdm.js',
21 | {pattern: 'test/fixture/**/*.html', watched: true, served: true, included: false},
22 | 'test/spec/**/*.js'
23 | ],
24 |
25 | // use dots reporter, as travis terminal does not support escaping sequences
26 | // possible values: 'dots', 'progress', 'junit', 'teamcity'
27 | // CLI --reporters progress
28 | reporters: ['dots'],
29 |
30 | // enable / disable watching file and executing tests whenever any file changes
31 | // CLI --auto-watch --no-auto-watch
32 | autoWatch: true,
33 |
34 | // start these browsers
35 | // CLI --browsers Chrome,Firefox,Safari
36 | browsers: [
37 | 'Chrome',
38 | 'Firefox'
39 | ],
40 |
41 | // If browser does not capture in given timeout [ms], kill it
42 | // CLI --capture-timeout 5000
43 | captureTimeout: 20000,
44 |
45 | // Auto run tests on start (when browsers are captured) and exit
46 | // CLI --single-run --no-single-run
47 | singleRun: false,
48 |
49 | plugins: [
50 | 'karma-jasmine',
51 | 'karma-requirejs',
52 | 'karma-chrome-launcher',
53 | 'karma-firefox-launcher',
54 | 'karma-ie-launcher',
55 | 'karma-safari-launcher'
56 | ]
57 | });
58 | };
59 |
--------------------------------------------------------------------------------
/test/spec/xdm.Rpc.spec.js:
--------------------------------------------------------------------------------
1 | describe('an instance of xdm.Rpc', function () {
2 | 'use strict';
3 |
4 | var expectedMessage;
5 | var guest;
6 | var i = 0;
7 | var isReady = false;
8 | var isComplete = false;
9 | var message;
10 |
11 | beforeEach(function () {
12 | // create a unique message
13 | expectedMessage = ++i + "_abcd1234%@¤/";
14 |
15 | // set up the cross-domain messaging channel
16 | guest = new xdm.Rpc({
17 | remote: '/base/test/fixture/guest.html',
18 | container: document.body,
19 | props: {
20 | height: '50px'
21 | },
22 | onReady: function () {
23 | isReady = true;
24 | }
25 | }, {
26 | remote: {
27 | voidMethod: {},
28 | asyncMethod: {},
29 | regularMethod: {},
30 | errorMethod: {},
31 | nonexistent: {},
32 | namedParamsMethod: {
33 | namedParams: true
34 | }
35 | },
36 | local: {
37 | voidCallback: function (msg) {
38 | message = msg;
39 | isComplete = true;
40 | }
41 | }
42 | });
43 |
44 | // wait until the onReady callback has been triggered
45 | waitsFor(function () {
46 | return isReady;
47 | });
48 | });
49 |
50 | afterEach(function () {
51 | isReady = false;
52 | isComplete = false;
53 | message = null;
54 |
55 | guest.destroy();
56 | });
57 |
58 | it('creates an iframe', function () {
59 | expect(document.body.innerHTML).toContain('iframe');
60 | });
61 |
62 | it('has a reference to the iframe', function () {
63 | expect(guest.iframe.nodeName).toEqual('IFRAME');
64 | });
65 |
66 | it('triggers onReady', function () {
67 | expect(isReady).toBe(true);
68 | });
69 |
70 | it('can remove the iframe', function () {
71 | guest.destroy();
72 | expect(document.getElementsByTagName('iframe').length).toBe(0);
73 | });
74 |
75 | it('can remove the channel', function () {
76 | guest.destroy();
77 | expect(guest.voidMethod).not.toBeDefined();
78 | });
79 |
80 | /**
81 | * Remote procedure call tests
82 | */
83 |
84 | describe('remote method call', function () {
85 | it('supports void methods', function () {
86 | runs(function () {
87 | guest.voidMethod(expectedMessage);
88 | });
89 |
90 | waitsFor(function () {
91 | return isComplete;
92 | });
93 |
94 | runs(function () {
95 | expect(message).toEqual(expectedMessage);
96 | });
97 | });
98 |
99 | it('supports async methods', function () {
100 | runs(function () {
101 | guest.asyncMethod(expectedMessage, function (msg) {
102 | message = msg;
103 | isComplete = true;
104 | });
105 | });
106 |
107 | waitsFor(function () {
108 | return isComplete;
109 | });
110 |
111 | runs(function () {
112 | expect(message).toEqual(expectedMessage);
113 | });
114 | });
115 |
116 | it('supports regular methods', function () {
117 | runs(function () {
118 | guest.regularMethod(expectedMessage, function (msg) {
119 | message = msg;
120 | isComplete = true;
121 | });
122 | });
123 |
124 | waitsFor(function () {
125 | return isComplete;
126 | });
127 |
128 | runs(function () {
129 | expect(message).toEqual(expectedMessage);
130 | });
131 | });
132 |
133 | it('handles errors by calling the error callback', function () {
134 | var isError = false;
135 |
136 | runs(function () {
137 | guest.errorMethod(expectedMessage, function (msg) {
138 | isComplete = true;
139 | }, function (err) {
140 | isError = true;
141 | isComplete = true;
142 | });
143 | });
144 |
145 | waitsFor(function () {
146 | return isComplete;
147 | });
148 |
149 | runs(function () {
150 | expect(isError).toBe(true);
151 | });
152 | });
153 |
154 | it('throws an error on nonexistent methods', function () {
155 | var isError = false;
156 |
157 | runs(function () {
158 | guest.nonexistent(expectedMessage, function (msg) {
159 | isComplete = true;
160 | }, function (err) {
161 | isError = true;
162 | isComplete = true;
163 | });
164 | });
165 |
166 | waitsFor(function () {
167 | return isComplete;
168 | });
169 |
170 | runs(function () {
171 | expect(isError).toBe(true);
172 | });
173 | });
174 |
175 | it('supports messages with named parameters', function () {
176 | runs(function () {
177 | guest.namedParamsMethod({
178 | msg: expectedMessage
179 | }, function (msg) {
180 | message = msg;
181 | isComplete = true;
182 | });
183 | });
184 |
185 | waitsFor(function () {
186 | return isComplete;
187 | });
188 |
189 | runs(function () {
190 | expect(message).toEqual(expectedMessage);
191 | });
192 | });
193 | });
194 | });
195 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # xdm.js
2 |
3 | 
4 |
5 | [](http://travis-ci.org/necolas/xdm.js)
6 |
7 | Cross-domain messaging over postMessage, based on the [JSON-RPC
8 | 2.0](http://www.jsonrpc.org/specification) protocol. It is a stripped down and
9 | slightly modified version of [easyXDM](https://github.com/oyvindkinsey/easyXDM/).
10 |
11 | ## Installation
12 |
13 | Install with [Bower](http://bower.io):
14 |
15 | ```
16 | bower install --save xdm.js
17 | ```
18 |
19 | The component can be used as a Common JS module, an AMD module, or a browser
20 | global.
21 |
22 |
23 | ## API
24 |
25 | The xdm.js library must be included in both the host and guest application. It
26 | will dynamically create the guest application's iframe from the host
27 | application.
28 |
29 | ### xdm.version
30 |
31 | The version of the library.
32 |
33 | ### xdm.Rpc(xdmConfig, rpcConfig);
34 |
35 | The cross-domain RPC constructor.
36 |
37 | Creates a proxy object that can be used to call methods implemented on the
38 | remote end of the channel, and also to provide the implementation of methods to
39 | be called from the remote end.
40 |
41 | Creates an iframe to the remote window.
42 |
43 | ```js
44 | // connect to the host application
45 | var host = xdm.Rpc(xdmConfig, rpcConfig);
46 | ```
47 |
48 | #### xdmConfig.onReady (optional)
49 |
50 | Specify a function to call when communication has been established.
51 |
52 | #### xdmConfig.container (host application only)
53 |
54 | The DOM node to append the generated iframe to.
55 |
56 | #### xdmConfig.remote (host application only)
57 |
58 | The path to the local or remote window. Set to 'about:blank' if using
59 | `config.html`.
60 |
61 | #### xdmConfig.html (optional; host application only)
62 |
63 | The HTML to be injected into a sourceless iframe.
64 |
65 | #### xdmConfig.props (optional; host application only)
66 |
67 | The additional attributes to set on the iframe. Can contain nested objects, e.g.,
68 | `'style': { 'border': '1px solid black' }`.
69 |
70 | #### xdmConfig.acl (optional; guest application only)
71 |
72 | Add domains to an Access Control List. The ACL can contain `*` and `?` as
73 | wildcards, or can be regular expressions. If regular expressions they need to
74 | begin with `^` and end with `$`.
75 |
76 | #### rpcConfig.local (optional)
77 |
78 | All the methods you which to expose to the remote window.
79 |
80 | ```js
81 | var rpcConfig = {};
82 |
83 | rpcConfig.local = {
84 | namedMethod: function (data, success, error) { /* ... */ }
85 | };
86 | ```
87 |
88 | The function will receive the passed arguments followed by the callback
89 | functions `success` and `error`. To send a successful result back you can
90 | use:
91 |
92 | `return foo` or `success(foo)`
93 |
94 | To return an error you can use:
95 |
96 | `throw new Error('foo error')` or `error('foo error')`
97 |
98 | #### rpcConfig.remote (optional)
99 |
100 | All the remote methods you want to use from the remote window when it's ready.
101 | These are just stubs.
102 |
103 | ```js
104 | rpcConfig.remote = {
105 | remoteMethod: {}
106 | };
107 | ```
108 |
109 | #### Example
110 |
111 | ```js
112 | // host application creates new connection with a guest widget
113 |
114 | var isReady = false;
115 |
116 | var widget = new xdm.Rpc({
117 | remote: 'http://another.domain.com/widget.html',
118 | container: document.body,
119 | props: {
120 | 'height': '300px',
121 | 'style': {
122 | 'border': '2px solid red'
123 | }
124 | },
125 | onReady: function () {
126 | isReady = true;
127 | }
128 | }, {
129 | remote: {
130 | methodInWidget: {}
131 | },
132 | local: {
133 | methodInHost: function (data) {
134 | return data;
135 | }
136 | }
137 | });
138 |
139 | widget.methodInWidget('message');
140 |
141 | // guest application creates new connection with host application
142 |
143 | var isReady = false;
144 |
145 | var host = new xdm.Rpc({
146 | acl: [
147 | 'http://example.com',
148 | 'http://sub.example.com'
149 | ],
150 | onReady: function () {
151 | isReady = true;
152 | }
153 | }, {
154 | remote: {
155 | methodInHost: {}
156 | },
157 | local: {
158 | methodInWidget: function (data) {
159 | return data;
160 | }
161 | }
162 | });
163 |
164 | host.methodInHost('message')
165 | ```
166 |
167 | ### xdm.Rpc.iframe;
168 |
169 | A reference to the instance's iframe.
170 |
171 | ### xdm.Rpc.destroy();
172 |
173 | Teardown the communication channel and remove the iframe from the DOM.
174 |
175 | ### proxy.remoteMethod(arg1, arg2, successCallback, errorCallback)
176 |
177 | Remote method calls can take any number of arguments.
178 |
179 | The second last argument is a success callback.
180 | The last argument is an error callback.
181 |
182 | ```js
183 | // from within the widget, request that the host resize the iframe
184 | host.resizeIframe({
185 | height: calculatedHeight
186 | }, successCallback, errorCallback);
187 | ```
188 |
189 | When called with no callback a JSON-RPC 2.0 notification will be executed.
190 | Be aware that you will not be notified of any errors with this method.
191 |
192 | ## Development
193 |
194 | Install [Node](http://nodejs.org) (comes with npm).
195 |
196 | From the repo root, install the project's development dependencies:
197 |
198 | ```
199 | npm install
200 | ```
201 |
202 | Testing relies on the Karma test-runner. If you'd like to use Karma to
203 | automatically watch and re-run the test file during development, it's easiest
204 | to globally install Karma and run it from the CLI.
205 |
206 | ```
207 | npm install -g karma
208 | karma start
209 | ```
210 |
211 | To run the tests in Firefox, just once, as Travis CI does:
212 |
213 | ```
214 | npm test
215 | ```
216 |
217 | ## Browser support
218 |
219 | * Google Chrome
220 | * Firefox 4+
221 | * Internet Explorer 8+
222 | * Safari 5+
223 | * Opera (latest)
224 |
--------------------------------------------------------------------------------
/xdm.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * xdm.js – Nicolas Gallagher – MIT License
3 | * easyXDM – Copyright(c) 2009-2011, Øyvind Sean Kinsey, oyvind@kinsey.no – MIT License
4 | */
5 |
6 | (function (window) {
7 | 'use strict';
8 |
9 | if (!window.postMessage) {
10 | return;
11 | }
12 |
13 | // stores namespace under which the 'xdm' object is stored on the page
14 | // (empty if object is global)
15 | var namespace = "";
16 | var xdm = {};
17 | // map over global xdm in case of overwrite
18 | var _xdm = window.xdm;
19 | var IFRAME_PREFIX = 'xdm_';
20 |
21 | var iframe = document.createElement('IFRAME');
22 | // randomize the initial id in case multiple closures are loaded
23 | var channelId = Math.floor(Math.random() * 10000);
24 | var emptyFn = Function.prototype;
25 | // returns groups for protocol (2), domain (3) and port (4)
26 | var reURI = /^((http.?:)\/\/([^:\/\s]+)(:\d+)*)/;
27 | // matches a foo/../ expression
28 | var reParent = /[\-\w]+\/\.\.\//;
29 | // matches `//` anywhere but in the protocol
30 | var reDoubleSlash = /([^:])\/\//g;
31 |
32 | /**
33 | * Helper for adding and removing cross-browser event listeners
34 | */
35 |
36 | var addEvent = (function () {
37 | if (window.addEventListener) {
38 | return function (target, type, listener) {
39 | target.addEventListener(type, listener, false);
40 | };
41 | }
42 | return function (target, type, listener) {
43 | target.attachEvent('on' + type, listener);
44 | };
45 | }());
46 |
47 | var removeEvent = (function () {
48 | if (window.removeEventListener) {
49 | return function (target, type, listener) {
50 | target.removeEventListener(type, listener, false);
51 | };
52 | }
53 | return function (target, type, listener) {
54 | target.detachEvent('on' + type, listener);
55 | };
56 | }());
57 |
58 | /**
59 | * Build the query object from location.hash
60 | */
61 |
62 | var query = (function (input) {
63 | input = input.substring(1, input.length).split('&');
64 | var data = {}, pair, i = input.length;
65 | while (i--) {
66 | pair = input[i].split('=');
67 | data[pair[0]] = decodeURIComponent(pair[1]);
68 | }
69 | return data;
70 | }(location.hash));
71 |
72 | // -------------------------------------------------------------------------
73 |
74 | /**
75 | * xdm object setup
76 | */
77 |
78 | xdm.version = "1.0.1";
79 | xdm.stack = {};
80 | xdm.query = query;
81 | xdm.checkAcl = checkAcl;
82 |
83 | /**
84 | * Removes the `xdm` variable from the global scope. It also returns control
85 | * of the `xdm` variable to the code that used it before.
86 | *
87 | * @param {String} ns A string representation of an object that will hold
88 | * an instance of xdm.
89 | * @return {xdm}
90 | */
91 |
92 | xdm.noConflict = function (ns) {
93 | window.xdm = _xdm;
94 | namespace = ns;
95 | if (namespace) {
96 | IFRAME_PREFIX = 'xdm_' + namespace.replace('.', '_') + '_';
97 | }
98 | return xdm;
99 | };
100 |
101 | // -------------------------------------------------------------------------
102 |
103 | /**
104 | * Helper for testing if an object is an array
105 | *
106 | * @param {Object} obj The object to test
107 | * @return {Boolean}
108 | */
109 |
110 | function isArray(obj) {
111 | return Object.prototype.toString.call(obj) === '[object Array]';
112 | }
113 |
114 | /**
115 | * Helper for testing if a variable/property is undefined
116 | *
117 | * @param {Object} variable The variable to test
118 | * @return {Boolean}
119 | */
120 |
121 | function undef(variable) {
122 | return typeof variable === 'undefined';
123 | }
124 |
125 | /**
126 | * Returns a string containing the schema, domain and if present the port
127 | *
128 | * @param {String} url The url to extract the location from
129 | * @return {String} The location part of the url
130 | */
131 |
132 | function getLocation(url) {
133 | if (!url) {
134 | throw new Error('url is undefined or empty');
135 | }
136 | if (/^file/.test(url)) {
137 | throw new Error('The file:// protocol is not supported');
138 | }
139 |
140 | var m = url.toLowerCase().match(reURI);
141 | if (m) {
142 | var proto = m[2], domain = m[3], port = m[4] || '';
143 | if ((proto === 'http:' && port === ':80') || (proto === 'https:' && port === ':443')) {
144 | port = '';
145 | }
146 | return proto + '//' + domain + port;
147 | }
148 |
149 | return url;
150 | }
151 |
152 | /**
153 | * Resolves a relative url into an absolute one.
154 | *
155 | * @param {String} url The path to resolve.
156 | * @return {String} The resolved url.
157 | */
158 | function resolveUrl(url) {
159 | if (!url) {
160 | throw new Error('url is undefined or empty');
161 | }
162 |
163 | // replace all `//` except the one in proto with `/`
164 | url = url.replace(reDoubleSlash, '$1/');
165 |
166 | // if the url is a valid url we do nothing
167 | if (!url.match(/^(http||https):\/\//)) {
168 | // If this is a relative path
169 | var path = (url.substring(0, 1) === '/') ? '' : location.pathname;
170 | if (path.substring(path.length - 1) !== '/') {
171 | path = path.substring(0, path.lastIndexOf('/') + 1);
172 | }
173 |
174 | url = location.protocol + '//' + location.host + path + url;
175 | }
176 |
177 | // reduce all 'xyz/../' to just ''
178 | while (reParent.test(url)) {
179 | url = url.replace(reParent, '');
180 | }
181 |
182 | return url;
183 | }
184 |
185 | /**
186 | * Applies properties from the source object to the target object.
187 | *
188 | * @param {Object} destination The target of the properties.
189 | * @param {Object} source The source of the properties.
190 | * @param {Boolean} noOverwrite Set to True to only set non-existing properties.
191 | */
192 |
193 | function merge(destination, source, noOverwrite) {
194 | var member;
195 | for (var prop in source) {
196 | if (source.hasOwnProperty(prop)) {
197 | if (prop in destination) {
198 | member = source[prop];
199 | if (typeof member === 'object') {
200 | merge(destination[prop], member, noOverwrite);
201 | }
202 | else if (!noOverwrite) {
203 | destination[prop] = source[prop];
204 | }
205 | }
206 | else {
207 | destination[prop] = source[prop];
208 | }
209 | }
210 | }
211 | return destination;
212 | }
213 |
214 | /**
215 | * GUEST ONLY
216 | * Check whether a host domain is allowed using an Access Control List.
217 | * The ACL can contain `*` and `?` as wildcards, or can be regular expressions.
218 | * If regular expressions they need to begin with `^` and end with `$`.
219 | *
220 | * @param {Array/String} acl The list of allowed domains
221 | * @param {String} domain The domain to test.
222 | * @return {Boolean} True if the domain is allowed, false if not.
223 | */
224 |
225 | function checkAcl(acl, domain) {
226 | // normalize into an array
227 | if (typeof acl === 'string') {
228 | acl = [acl];
229 | }
230 | var re;
231 | var i = acl.length;
232 | while (i--) {
233 | re = acl[i];
234 | re = new RegExp(re.substr(0, 1) === '^' ? re : ('^' + re.replace(/(\*)/g, '.$1').replace(/\?/g, '.') + '$'));
235 | if (re.test(domain)) {
236 | return true;
237 | }
238 | }
239 | return false;
240 | }
241 |
242 | /**
243 | * HOST ONLY
244 | * Appends the parameters to the given url.
245 | * The base url can contain existing query parameters.
246 | *
247 | * @param {String} url The base url.
248 | * @param {Object} parameters The parameters to add.
249 | * @return {String} A new valid url with the parameters appended.
250 | */
251 |
252 | function appendQueryParameters(url, parameters) {
253 | if (!parameters) {
254 | throw new Error('parameters is undefined or null');
255 | }
256 |
257 | var indexOf = url.indexOf('#');
258 | var q = [];
259 | for (var key in parameters) {
260 | if (parameters.hasOwnProperty(key)) {
261 | q.push(key + '=' + encodeURIComponent(parameters[key]));
262 | }
263 | }
264 |
265 | return url + (indexOf === -1 ? '#' : '&') + q.join('&');
266 | }
267 |
268 | /**
269 | * HOST ONLY
270 | * Creates an iframe and appends it to the DOM.
271 | *
272 | * @param {Object} config This iframe configuration object
273 | * @return {Element} The frames DOM Element
274 | */
275 |
276 | function createFrame(config) {
277 | var frame = iframe.cloneNode(false);
278 |
279 | // merge the defaults with the configuration properties
280 | merge(config.props, {
281 | frameBorder: 0,
282 | allowTransparency: true,
283 | scrolling: 'no',
284 | width: '100%',
285 | src: appendQueryParameters(config.remote, {
286 | xdm_e: getLocation(location.href),
287 | xdm_c: config.channel,
288 | xdm_p: 1
289 | }),
290 | name: IFRAME_PREFIX + config.channel + '_provider',
291 | style: {
292 | margin: 0,
293 | padding: 0,
294 | border: 0
295 | }
296 | });
297 |
298 | frame.id = config.props.name;
299 | delete config.props.name;
300 |
301 | // if no container, then we cannot proceed
302 | if (!config.container) {
303 | throw new Error('xdm.Rpc() configuration object missing a DOM "container" property');
304 | }
305 |
306 | // merge config properties into the frame
307 | merge(frame, config.props);
308 |
309 | config.container.appendChild(frame);
310 |
311 | if (config.onLoad) {
312 | addEvent(frame, 'load', config.onLoad);
313 | }
314 |
315 | // if we're injecting HTML directly into the iframe
316 | if (config.html) {
317 | frame.contentWindow.document.open();
318 | frame.contentWindow.document.write(config.html);
319 | frame.contentWindow.document.close();
320 | }
321 |
322 | // pass a reference to the frame around so that it can be exposed on the
323 | // rpc object
324 | config.iframe = frame;
325 |
326 | return frame;
327 | }
328 |
329 | /**
330 | * Prepares an array of stack-elements suitable for the current configuration
331 | *
332 | * @param {Object} config The Transports configuration.
333 | * @return {Array} An array of stack-elements with the TransportElement at index 0.
334 | */
335 |
336 | function prepareTransportStack(config) {
337 | var stackEls;
338 |
339 | config.isHost = config.isHost || undef(query.xdm_p);
340 | config.props = config.props || {};
341 |
342 | if (!config.isHost) {
343 | config.channel = query.xdm_c.replace(/["'<>\\]/g, '');
344 | config.remote = query.xdm_e.replace(/["'<>\\]/g, '');
345 |
346 | if (config.acl && !checkAcl(config.acl, config.remote)) {
347 | throw new Error('Access denied for ' + config.remote);
348 | }
349 | }
350 | else {
351 | config.remote = resolveUrl(config.remote);
352 | config.channel = config.channel || 'default' + channelId++;
353 | }
354 |
355 | stackEls = [new xdm.stack.PostMessageTransport(config)];
356 | // this behavior is responsible for buffering outgoing messages
357 | stackEls.push(new xdm.stack.QueueBehavior(true));
358 |
359 | return stackEls;
360 | }
361 |
362 | /**
363 | * Chains all the separate stack elements into a single usable stack.
364 | * If an element is missing a necessary method then it will have a pass-through method applied.
365 | *
366 | * @param {Array} stackElements An array of stack elements to be linked.
367 | * @return {xdm.stack.StackElement} The last element in the chain.
368 | */
369 |
370 | function chainStack(stackElements) {
371 | var stackEl;
372 | var i;
373 | var len = stackElements.length;
374 |
375 | var defaults = {
376 | incoming: function (message, origin) {
377 | this.up.incoming(message, origin);
378 | },
379 | outgoing: function (message, recipient) {
380 | this.down.outgoing(message, recipient);
381 | },
382 | callback: function (success) {
383 | this.up.callback(success);
384 | },
385 | init: function () {
386 | this.down.init();
387 | },
388 | destroy: function () {
389 | this.down.destroy();
390 | }
391 | };
392 |
393 | for (i = 0; i < len; i++) {
394 | stackEl = stackElements[i];
395 | merge(stackEl, defaults, true);
396 |
397 | if (i !== 0) {
398 | stackEl.down = stackElements[i - 1];
399 | }
400 |
401 | if (i !== len - 1) {
402 | stackEl.up = stackElements[i + 1];
403 | }
404 | }
405 |
406 | return stackEl;
407 | }
408 |
409 | /**
410 | * This will remove a stackelement from its stack while leaving the stack functional.
411 | *
412 | * @param {Object} element The elment to remove from the stack.
413 | */
414 |
415 | function removeFromStack(element) {
416 | element.up.down = element.down;
417 | element.down.up = element.up;
418 | element.up = element.down = null;
419 | }
420 |
421 | /**
422 | * xdm.Rpc
423 | *
424 | * Creates a proxy object that can be used to call methods implemented on the
425 | * remote end of the channel, and also to provide the implementation of methods
426 | * to be called from the remote end.
427 | *
428 | * The instantiated object will have methods matching those specified in
429 | * `config.remote`.
430 | *
431 | * @param {Object} config The underlying transport configuration.
432 | * remote: The remote window's location
433 | * html: HTML to inject into a sourceless iframe
434 | * container: The iframe's container DOM element
435 | * props: Object of the properties that should be set on the frame
436 | * acl: An array of domains to add to the Access Control List
437 | * onReady: A function to call when communication has been established
438 | * onLoad: A function to called – with the iframe's `contentWindow` as
439 | * the argument – when the frame is fully loaded
440 | * @param {Object} jsonRpcConfig The description of the interface to implement.
441 | */
442 |
443 | xdm.Rpc = function (config, jsonRpcConfig) {
444 | var member;
445 |
446 | // expand shorthand notation
447 | if (jsonRpcConfig.local) {
448 | for (var method in jsonRpcConfig.local) {
449 | if (jsonRpcConfig.local.hasOwnProperty(method)) {
450 | member = jsonRpcConfig.local[method];
451 | if (typeof member === 'function') {
452 | jsonRpcConfig.local[method] = {
453 | method: member
454 | };
455 | }
456 | }
457 | }
458 | }
459 |
460 | // create the stack
461 | var stack = chainStack(prepareTransportStack(config).concat([
462 | new xdm.stack.RpcBehavior(this, jsonRpcConfig), {
463 | callback: function (success) {
464 | if (config.onReady) config.onReady(success);
465 | }
466 | }
467 | ]));
468 |
469 | // set the origin
470 | this.origin = getLocation(config.remote);
471 |
472 | // initiates the destruction of the stack.
473 | this.destroy = function () {
474 | stack.destroy();
475 | };
476 |
477 | stack.init();
478 |
479 | // store a reference to the iframe that is created
480 | this.iframe = config.iframe;
481 | };
482 |
483 | /**
484 | * xdm.stack.PostMessageTransport
485 | *
486 | * PostMessageTransport uses HTML5 postMessage for communication.
487 | *
488 | * @param {Object} config The transports configuration.
489 | */
490 |
491 | xdm.stack.PostMessageTransport = function (config) {
492 | var pub;
493 | var frame;
494 | var callerWindow;
495 | var targetOrigin;
496 |
497 | /**
498 | * This is the main implementation for the onMessage event.
499 | * It checks the validity of the origin and passes the message on if appropriate.
500 | *
501 | * @param {Object} event The message event
502 | */
503 |
504 | function _windowOnMessage(event) {
505 | var origin = getLocation(event.origin);
506 | var dataIsString = (typeof event.data === 'string');
507 | if (origin === targetOrigin && dataIsString && event.data.substring(0, config.channel.length + 1) === config.channel + ' ') {
508 | pub.up.incoming(event.data.substring(config.channel.length + 1), origin);
509 | }
510 | }
511 |
512 | pub = {
513 | outgoing: function (message, domain, fn) {
514 | callerWindow.postMessage(config.channel + ' ' + message, domain || targetOrigin);
515 | if (fn) {
516 | fn();
517 | }
518 | },
519 |
520 | destroy: function () {
521 | removeEvent(window, 'message', _windowOnMessage);
522 | if (frame) {
523 | callerWindow = null;
524 | frame.parentNode.removeChild(frame);
525 | frame = null;
526 | }
527 | },
528 |
529 | init: function () {
530 | targetOrigin = getLocation(config.remote);
531 | if (config.isHost) {
532 | // add the event handler for listening
533 | var waitForReady = function (event) {
534 | if (event.data === config.channel + '-ready') {
535 | if ('postMessage' in frame.contentWindow) {
536 | callerWindow = frame.contentWindow;
537 | }
538 | else {
539 | callerWindow = frame.contentWindow.document;
540 | }
541 |
542 | // replace the eventlistener
543 | removeEvent(window, 'message', waitForReady);
544 | addEvent(window, 'message', _windowOnMessage);
545 |
546 | setTimeout(function () {
547 | pub.up.callback(true);
548 | }, 0);
549 | }
550 | };
551 |
552 | addEvent(window, 'message', waitForReady);
553 | frame = createFrame(config);
554 | }
555 | else {
556 | // add the event handler for listening
557 | addEvent(window, 'message', _windowOnMessage);
558 | if ('postMessage' in window.parent) {
559 | callerWindow = window.parent;
560 | }
561 | else {
562 | callerWindow = window.parent.document;
563 | }
564 |
565 | callerWindow.postMessage(config.channel + '-ready', targetOrigin);
566 |
567 | setTimeout(function () {
568 | pub.up.callback(true);
569 | }, 0);
570 | }
571 | }
572 | };
573 |
574 | return pub;
575 | };
576 |
577 | /**
578 | * xdm.stack.QueueBehavior
579 | *
580 | * This is a behavior that enables queueing of messages.
581 | * It will buffer incoming messages and dispach these as fast as the underlying transport allows.
582 | *
583 | * @param {Boolean} remove If true, it will remove from the stack
584 | */
585 |
586 | xdm.stack.QueueBehavior = function (remove) {
587 | var pub;
588 | var queue = [];
589 | var waiting = true;
590 | var incoming = '';
591 | var destroying;
592 |
593 | function dispatch() {
594 | var message;
595 |
596 | if (remove === true && queue.length === 0) {
597 | removeFromStack(pub);
598 | return;
599 | }
600 | if (waiting || queue.length === 0 || destroying) {
601 | return;
602 | }
603 |
604 | waiting = true;
605 | message = queue.shift();
606 |
607 | pub.down.outgoing(message.data, message.origin, function (success) {
608 | waiting = false;
609 | if (message.callback) {
610 | setTimeout(function () {
611 | message.callback(success);
612 | }, 0);
613 | }
614 | dispatch();
615 | });
616 | }
617 |
618 | pub = {
619 | init: function () {
620 | pub.down.init();
621 | },
622 |
623 | callback: function (success) {
624 | waiting = false;
625 | // in case dispatch calls removeFromStack
626 | var up = pub.up;
627 | dispatch();
628 | up.callback(success);
629 | },
630 |
631 | incoming: function (message, origin) {
632 | pub.up.incoming(message, origin);
633 | },
634 |
635 | outgoing: function (message, origin, fn) {
636 | queue.push({
637 | data: message,
638 | origin: origin,
639 | callback: fn
640 | });
641 |
642 | dispatch();
643 | },
644 |
645 | destroy: function () {
646 | destroying = true;
647 | pub.down.destroy();
648 | }
649 | };
650 |
651 | return pub;
652 | };
653 |
654 | /**
655 | * xdm.stack.RpcBehavior
656 | *
657 | * This uses JSON-RPC 2.0 to expose local methods and to invoke remote methods
658 | * and have responses returned over the the string based transport stack.
659 | *
660 | * Exposed methods can return values synchronously, asynchronously, or not at all.
661 | *
662 | * @param {Object} proxy The object to apply the methods to.
663 | * @param {Object} config The definition of the local and remote interface to implement.
664 | * local: The local interface to expose.
665 | * remote: The remote methods to expose through the proxy.
666 | */
667 |
668 | xdm.stack.RpcBehavior = function (proxy, config) {
669 | var pub;
670 | var _callbackCounter = 0;
671 | var _callbacks = {};
672 |
673 | /**
674 | * Serializes and sends the message
675 | *
676 | * @param {Object} data The JSON-RPC message to be sent. The jsonrpc property will be added.
677 | */
678 |
679 | function _send(data) {
680 | data.jsonrpc = '2.0';
681 | pub.down.outgoing(JSON.stringify(data));
682 | }
683 |
684 | /**
685 | * Creates a method that implements the given definition
686 | *
687 | * @param {Object} definition The method configuration
688 | * @param {String} method The name of the method
689 | * @return {Function} A stub capable of proxying the requested method call
690 | */
691 |
692 | function _createMethod(definition, method) {
693 | var slice = Array.prototype.slice;
694 |
695 | return function () {
696 | var l = arguments.length;
697 | var callback;
698 | var message = {
699 | method: method
700 | };
701 |
702 | if (l > 0 && typeof arguments[l - 1] === 'function') {
703 | // one callback, procedure
704 | if (l > 1 && typeof arguments[l - 2] === 'function') {
705 | // two callbacks, success and error
706 | callback = {
707 | success: arguments[l - 2],
708 | error: arguments[l - 1]
709 | };
710 | message.params = slice.call(arguments, 0, l - 2);
711 | }
712 | else {
713 | // single callback, success
714 | callback = {
715 | success: arguments[l - 1]
716 | };
717 | message.params = slice.call(arguments, 0, l - 1);
718 | }
719 | _callbacks['' + (++_callbackCounter)] = callback;
720 | message.id = _callbackCounter;
721 | }
722 | else {
723 | // no callbacks, a notification
724 | message.params = slice.call(arguments, 0);
725 | }
726 |
727 | if (definition.namedParams && message.params.length === 1) {
728 | message.params = message.params[0];
729 | }
730 | // Send the method request
731 | _send(message);
732 | };
733 | }
734 |
735 | /**
736 | * Executes the exposed method
737 | *
738 | * @param {String} method The name of the method
739 | * @param {Number} id The callback id to use
740 | * @param {Function} fn The exposed implementation
741 | * @param {Array} params The parameters supplied by the remote end
742 | */
743 |
744 | function _executeMethod(method, id, fn, params) {
745 | if (!fn) {
746 | if (id) {
747 | _send({
748 | id: id,
749 | error: {
750 | code: -32601,
751 | message: 'Procedure not found.'
752 | }
753 | });
754 | }
755 | return;
756 | }
757 |
758 | var success;
759 | var error;
760 |
761 | if (id) {
762 | success = function (result) {
763 | success = emptyFn;
764 | _send({
765 | id: id,
766 | result: result
767 | });
768 | };
769 |
770 | error = function (message, data) {
771 | error = emptyFn;
772 | var msg = {
773 | id: id,
774 | error: {
775 | code: -32099,
776 | message: message
777 | }
778 | };
779 |
780 | if (data) {
781 | msg.error.data = data;
782 | }
783 | _send(msg);
784 | };
785 | }
786 | else {
787 | success = error = emptyFn;
788 | }
789 |
790 | // call local method
791 | if (!isArray(params)) {
792 | params = [params];
793 | }
794 |
795 | try {
796 | var result = fn.method.apply(fn.scope, params.concat([success, error]));
797 | if (!undef(result)) {
798 | success(result);
799 | }
800 | }
801 | catch (ex1) {
802 | error(ex1.message);
803 | }
804 | }
805 |
806 | pub = {
807 | incoming: function (message, origin) {
808 | var data = JSON.parse(message);
809 | var callback;
810 |
811 | if (data.method) {
812 | // a method call from the remote end
813 | if (config.handle) {
814 | config.handle(data, _send);
815 | }
816 | else {
817 | _executeMethod(data.method, data.id, config.local[data.method], data.params);
818 | }
819 | }
820 | else {
821 | // a method response from the other end
822 | callback = _callbacks[data.id];
823 | if (data.error && callback.error) {
824 | callback.error(data.error);
825 | }
826 | else if (callback.success) {
827 | callback.success(data.result);
828 | }
829 | delete _callbacks[data.id];
830 | }
831 | },
832 |
833 | init: function () {
834 | if (config.remote) {
835 | // implement the remote sides exposed methods
836 | for (var method in config.remote) {
837 | if (config.remote.hasOwnProperty(method)) {
838 | proxy[method] = _createMethod(config.remote[method], method);
839 | }
840 | }
841 | }
842 | pub.down.init();
843 | },
844 |
845 | destroy: function () {
846 | for (var method in config.remote) {
847 | if (config.remote.hasOwnProperty(method) && proxy.hasOwnProperty(method)) {
848 | delete proxy[method];
849 | }
850 | }
851 | pub.down.destroy();
852 | }
853 | };
854 |
855 | return pub;
856 | };
857 |
858 | // commonjs export
859 | if (typeof exports === 'object') {
860 | module.exports = xdm;
861 | }
862 | // amd export
863 | else if (typeof define === 'function' && define.amd) {
864 | define(function () {
865 | return xdm;
866 | });
867 | }
868 | // browser global
869 | else {
870 | window.xdm = xdm;
871 | }
872 |
873 | }(window));
874 |
--------------------------------------------------------------------------------