├── .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 | ![unmaintained](http://img.shields.io/badge/status-unmaintained-red.png) 4 | 5 | [![Build Status](https://secure.travis-ci.org/necolas/xdm.js.png?branch=master)](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 | --------------------------------------------------------------------------------